Your IP : 3.144.91.130
require 'tmpdir'
require 'fileutils'
require 'thread'
PhusionPassenger.require_passenger_lib 'utils'
PhusionPassenger.require_passenger_lib 'utils/json'
PhusionPassenger.require_passenger_lib 'message_channel'
module PhusionPassenger
class AppProcess
attr_reader :pid, :input, :work_dir, :properties
def initialize(pid, input, work_dir, properties)
@pid = pid
@input = input
@work_dir = work_dir
@properties = properties
end
def close
if @input && !@input.closed?
@input.close
end
if @pid
# Give process some time to exit
sleep 0.1
begin
Process.kill('TERM', @pid)
rescue Errno::ESRCH
end
begin
Process.waitpid(@pid)
rescue Errno::ECHILD
end
end
FileUtils.rm_rf(@work_dir)
end
def connect_and_send_request(headers)
socket = Utils.connect_to_server(find_socket_accepting_http_requests['address'])
channel = MessageChannel.new(socket)
data = ""
headers["REQUEST_METHOD"] ||= "GET"
headers["REQUEST_URI"] ||= headers["PATH_INFO"]
headers["QUERY_STRING"] ||= ""
headers["SCRIPT_NAME"] ||= ""
headers["CONTENT_LENGTH"] ||= "0"
headers.each_pair do |key, value|
data << "#{key}\0#{value}\0"
end
channel.write_scalar(data)
socket
end
def sockets
@sockets ||= @properties['sockets']
end
def find_socket_accepting_http_requests
sockets.find do |socket|
socket['accept_http_requests']
end
end
def find_socket_with_protocol(protocol)
sockets.find do |socket|
socket['protocol'] == protocol
end
end
end
class SpawnError < StandardError
attr_accessor :status, :summary
def initialize(status, details)
super(details[:summary])
@status = status
@summary = details[:summary]
end
end
class Loader
attr_reader :command, :app_root
def initialize(command, app_root)
@command = command
@app_root = app_root
end
def spawn(options = {})
work_dir = create_work_dir
begin
write_startup_arguments(work_dir, options)
pid, input = create_process(@command, work_dir, options)
status = wait_for_finish(pid, work_dir)
if status == :success
properties = read_properties(work_dir)
AppProcess.new(pid, input, work_dir, properties)
else
SpawnError.new(status, read_errors(work_dir))
end
rescue Exception => e
FileUtils.rm_rf(work_dir)
raise e
end
end
private
def create_work_dir
work_dir = Dir.mktmpdir
begin
create_work_dir_contents(work_dir)
work_dir
rescue Exception => e
FileUtils.rm_rf(work_dir)
raise e
end
end
def create_work_dir_contents(work_dir)
Dir.mkdir("#{work_dir}/args")
Dir.mkdir("#{work_dir}/envdump")
Dir.mkdir("#{work_dir}/response")
Dir.mkdir("#{work_dir}/response/error")
Dir.mkdir("#{work_dir}/response/steps")
Dir.mkdir("#{work_dir}/response/steps/subprocess_exec_wrapper")
Dir.mkdir("#{work_dir}/response/steps/subprocess_wrapper_preparation")
Dir.mkdir("#{work_dir}/response/steps/subprocess_app_load_or_exec")
Dir.mkdir("#{work_dir}/response/steps/subprocess_listen")
Dir.mkdir("#{work_dir}/response/steps/preloader_preparation")
Dir.mkdir("#{work_dir}/response/steps/preloader_fork_subprocess")
Dir.mkdir("#{work_dir}/response/steps/preloader_send_response")
Dir.mkdir("#{work_dir}/response/steps/preloader_finish")
Dir.mkdir("#{work_dir}/response/steps/subprocess_prepare_after_forking_from_preloader")
if !system("mkfifo", "#{work_dir}/response/finish")
raise "'mkfifo #{work_dir}/response/finish' failed"
end
end
def create_process(command, work_dir, options = {})
a, b = IO.pipe
pid = fork do
STDIN.reopen(a)
if options[:quiet]
STDOUT.reopen('/dev/null', 'w')
STDERR.reopen(STDOUT)
STDOUT.sync = STDERR.sync = true
end
b.close
Dir.chdir(@app_root)
ENV['RAILS_ENV'] = ENV['RACK_ENV'] = ENV['PASSENGER_ENV'] = 'production'
ENV['PASSENGER_SPAWN_WORK_DIR'] = work_dir
exec(*command)
end
a.close
[pid, b]
end
def write_startup_arguments(work_dir, options)
real_options = {
:passenger_root => PhusionPassenger.install_spec,
:ruby_libdir => PhusionPassenger.ruby_libdir,
:app_root => File.expand_path(@app_root),
:app_group_name => @app_root
}
real_options[:log_level] = 7 if DEBUG
real_options.merge!(options)
File.open("#{work_dir}/args.json", 'w') do |f|
f.write(PhusionPassenger::Utils::JSON.generate(real_options))
end
real_options.each_pair do |key, value|
File.open("#{work_dir}/args/#{key}", 'w') do |f|
f.write(value)
end
end
end
def wait_for_finish(pid, work_dir)
mutex = Mutex.new
cond = ConditionVariable.new
result = nil
finish_signal_thr = Thread.new do
content = File.open("#{work_dir}/response/finish", 'r') do |f|
f.read(1)
end
mutex.synchronize do
if result.nil?
result = { :finish_signal => content }
cond.signal
end
end
end
process_exit_thr = Thread.new do
while true
if Utils.process_is_alive?(pid)
begin
wait_result = Process.waitpid(pid, Process::WNOHANG)
rescue Errno::ECHILD
# The process is not a direct child of ours.
# We don't know whether it is a zombie,
# but let's assume that it isn't and
# let's assume that its actual parent
# does a proper job of reaping zombies.
wait_result = false
end
if wait_result
break
else
sleep 0.1
end
else
break
end
end
mutex.synchronize do
if result.nil?
result = { :process_exited => true }
cond.signal
end
end
end
begin
mutex.synchronize do
while result.nil?
cond.wait(mutex)
end
end
if result[:finish_signal]
if result[:finish_signal] == '1'
:success
else
:error
end
else
:premature_exit
end
ensure
finish_signal_thr.kill
process_exit_thr.kill
finish_signal_thr.join
process_exit_thr.join
end
end
def read_properties(work_dir)
PhusionPassenger::Utils::JSON.parse(
File.read("#{work_dir}/response/properties.json"))
end
def read_errors(work_dir)
{
:summary => File.read("#{work_dir}/response/error/summary")
}
end
end
class Preloader < Loader
attr_reader :command, :app_root, :preloader_process
def initialize(command, app_root)
@command = command
@app_root = app_root
end
def close
@preloader_process.close if @preloader_process
end
def start(options = {})
work_dir = create_work_dir
begin
write_startup_arguments(work_dir, options)
pid, input = create_process(@command, work_dir, options)
status = wait_for_finish(pid, work_dir)
if status == :success
properties = read_properties(work_dir)
@preloader_process = AppProcess.new(pid, input, work_dir, properties)
else
raise SpawnError.new(status, read_errors(work_dir))
end
rescue Exception => e
FileUtils.rm_rf(work_dir)
raise e
end
end
def spawn(options = {})
socket = Utils.connect_to_server(
@preloader_process.find_socket_with_protocol('preloader')['address'])
work_dir = create_work_dir
begin
if !system("mkfifo", "#{work_dir}/stdin")
raise "'mkfifo #{work_dir}/stdin' failed"
end
write_startup_arguments(work_dir, options)
write_spawn_request(socket, work_dir)
pid = read_spawn_response(socket)
begin
input = File.open("#{work_dir}/stdin", 'w')
status = wait_for_finish(pid, work_dir)
if status == :success
AppProcess.new(pid, input, work_dir, read_properties(work_dir))
else
raise SpawnError.new(status, read_errors(work_dir))
end
rescue Exception => e
begin
Process.kill('TERM', pid)
rescue Errno::ESRCH
end
raise e
end
rescue Exception => e
FileUtils.rm_rf(work_dir)
raise e
end
end
private
def write_spawn_request(socket, work_dir)
socket.puts(PhusionPassenger::Utils::JSON.generate(
:command => 'spawn',
:work_dir => work_dir
))
end
def read_spawn_response(socket)
doc = PhusionPassenger::Utils::JSON.parse(socket.readline)
if doc['result'] != 'ok'
raise "Spawn failed: #{doc.inspect}"
end
doc['pid']
end
def open_std_channels_async(work_dir)
state = {
:mutex => Mutex.new,
:cond => ConditionVariable.new,
:stdin => nil,
:stdout_and_err => nil,
:stdin_open_thread => nil,
:stdout_and_err_open_thread => nil
}
state[:stdin_open_thread] = Thread.new do
begin
f = File.open("#{work_dir}/stdin", 'w')
rescue Exception => e
state[:mutex].synchronize do
state[:stdin] = e
state[:cond].signal
end
else
state[:mutex].synchronize do
state[:stdin] = f
state[:cond].signal
end
end
end
state[:stdout_and_err_open_thread] = Thread.new do
begin
f = File.open("#{work_dir}/stdout_and_err", 'w')
rescue Exception => e
state[:mutex].synchronize do
state[:stdout_and_err] = e
state[:cond].signal
end
else
state[:mutex].synchronize do
state[:stdout_and_err] = f
state[:cond].signal
end
end
end
state
end
end
module LoaderSpecHelper
def self.included(klass)
klass.before(:each) do
@stubs = []
end
klass.after(:each) do
begin
@process.close if @process && @process.is_a?(AppProcess)
@preloader.close if @preloader
ensure
@stubs.each do |stub|
stub.destroy
end
end
end
end
def before_start(code)
@before_start = code
end
def after_start(code)
@after_start = code
end
def register_stub(stub)
@stubs << stub
File.prepend(stub.startup_file, "#{@before_start}\n")
File.append(stub.startup_file, "\n#{@after_start}")
return stub
end
def register_app(app)
@apps << app
return app
end
def start!(options = {})
result = start(options)
if !result.is_a?(AppProcess)
raise "Loader failed to start; error:\n#{result.summary}"
end
end
def perform_request(headers)
socket = @process.connect_and_send_request(headers)
headers = {}
line = socket.readline
headers["Status"] = line.split(" ")[1]
while line != "\r\n"
key, value = line.strip.split(/ *: */, 2)
headers[key] = value
line = socket.readline
end
body = socket.read
socket.close
return [headers, body]
end
end
RSpec.shared_examples_for 'a loader' do
it 'works' do
start
expect(@process).to be_an_instance_of(AppProcess)
headers, body = perform_request(
"REQUEST_METHOD" => "GET",
"PATH_INFO" => "/"
)
expect(headers['Status']).to eq('200')
expect(body).to eq('front page')
end
end
end # module PhusionPassenger