280 lines
6.3 KiB
Ruby
280 lines
6.3 KiB
Ruby
#!/usr/bin/env ruby
|
|
|
|
require "base64"
|
|
require "io/console"
|
|
require "socket"
|
|
|
|
class ClientError < StandardError
|
|
def initialize(message)
|
|
super(message)
|
|
end
|
|
end
|
|
|
|
class ClientScriptError < StandardError
|
|
def initialize(source)
|
|
super(source.message)
|
|
@source = source
|
|
end
|
|
|
|
def source
|
|
@source
|
|
end
|
|
end
|
|
|
|
$original_stdin = $stdin.dup
|
|
$original_stdout = $stdout.dup
|
|
$original_stderr = $stderr.dup
|
|
|
|
$run_base_path = "/run/github-fast-env"
|
|
|
|
unless File.directory?($run_base_path)
|
|
FileUtils.mkdir_p($run_base_path, :mode => 0700)
|
|
end
|
|
|
|
control_socket_path = "#{$run_base_path}/github-fast-envd.sock"
|
|
|
|
if File.exist?(control_socket_path) and File.socket?(control_socket_path)
|
|
File.unlink(control_socket_path)
|
|
end
|
|
|
|
$original_stderr.puts "creating control socket"
|
|
|
|
control_server = UNIXServer.new(control_socket_path)
|
|
File.chmod 0700, control_socket_path
|
|
|
|
connection_id = 0
|
|
|
|
def open_pipe(path)
|
|
if File.exist?(path) and File.pipe?(path)
|
|
File.unlink(path)
|
|
end
|
|
|
|
File.mkfifo(path, mode = 0600)
|
|
|
|
path
|
|
end
|
|
|
|
def read_command(control_socket)
|
|
ready_ios = IO.select([control_socket], [], [], 10)
|
|
|
|
if not ready_ios
|
|
log "error", "timeout while communicating with github-fast-envd"
|
|
exit 1
|
|
end
|
|
|
|
response = control_socket.readline.strip.split(" ", 2)
|
|
|
|
if response.empty?
|
|
log "error", "malformed response from github-fast-envd"
|
|
exit 1
|
|
end
|
|
|
|
command = response[0]
|
|
|
|
if command == "error"
|
|
if response.length < 2
|
|
log "error", "malformed error response from github-fast-envd"
|
|
exit 1
|
|
end
|
|
|
|
error_message = response[1]
|
|
|
|
log "error", "#{error_message}"
|
|
exit 1
|
|
end
|
|
|
|
arguments = response[1].nil? ? [] : response[1].split(" ")
|
|
|
|
return command, arguments
|
|
end
|
|
|
|
def set_up_named_pipes(control_socket, connection_id)
|
|
pipe_base_path = "/#{$run_base_path}/github-fast-envd.#{connection_id}"
|
|
pipe_base_path_encoded = Base64.encode64(pipe_base_path).delete("\n")
|
|
|
|
stdin = open_pipe("#{pipe_base_path}.stdin")
|
|
stdout = open_pipe("#{pipe_base_path}.stdout")
|
|
stderr = open_pipe("#{pipe_base_path}.stderr")
|
|
|
|
# TODO: support script arguments
|
|
control_socket.puts "named-pipes #{pipe_base_path_encoded}"
|
|
|
|
stdin = File.open(stdin, "r")
|
|
stdin.sync = true
|
|
stdout = File.open(stdout, "w")
|
|
stdout.sync = true
|
|
stderr = File.open(stderr, "w")
|
|
stderr.sync = true
|
|
|
|
$original_stderr.puts " set up named pipes"
|
|
|
|
control_socket.puts "ready"
|
|
|
|
response = control_socket.readline.strip
|
|
|
|
if response != "ready"
|
|
raise ClientError.new "invalid command"
|
|
Kernel.exit!
|
|
end
|
|
|
|
$stdin.reopen(stdin)
|
|
$stdout.reopen(stdout)
|
|
$stderr.reopen(stderr)
|
|
end
|
|
|
|
def clean_up_named_pipes(control_socket, connection_id)
|
|
pipe_base_path = "/#{$run_base_path}/github-fast-envd.#{connection_id}"
|
|
|
|
if File.exist?("#{pipe_base_path}.stdin")
|
|
File.delete("#{pipe_base_path}.stdin")
|
|
end
|
|
if File.exist?("#{pipe_base_path}.stdout")
|
|
File.delete("#{pipe_base_path}.stdout")
|
|
end
|
|
if File.exist?("#{pipe_base_path}.stderr")
|
|
File.delete("#{pipe_base_path}.stderr")
|
|
end
|
|
end
|
|
|
|
def set_up_pseudoterminal(control_socket, pseudoterminal_path)
|
|
pseudoterminal_io = File.open(pseudoterminal_path, File::RDWR | File::NOCTTY)
|
|
|
|
$original_stderr.puts " connecting to pseudoterminal #{pseudoterminal_path}"
|
|
|
|
$stdin.reopen(pseudoterminal_io)
|
|
$stdout.reopen(pseudoterminal_io)
|
|
$stderr.reopen(pseudoterminal_io)
|
|
|
|
$original_stderr.puts " connected to pseudoterminal #{pseudoterminal_path}"
|
|
|
|
control_socket.puts "ready"
|
|
end
|
|
|
|
$original_stderr.puts "preloading common modules"
|
|
|
|
load "/usr/lib/github-fast-env/preload.rb"
|
|
|
|
$original_stderr.puts "ready to serve requests"
|
|
|
|
while true
|
|
control_socket = control_server.accept
|
|
$original_stderr.puts "- new connection"
|
|
|
|
begin
|
|
command, arguments = read_command(control_socket)
|
|
|
|
if command != "new"
|
|
raise ClientError.new "unexpected command"
|
|
end
|
|
|
|
if arguments.empty?
|
|
raise ClientError.new "malformed command"
|
|
end
|
|
|
|
protocol_version = arguments[0]
|
|
|
|
if protocol_version != "v1"
|
|
raise ClientError.new "unsupported protocol version (#{protocol_version})"
|
|
end
|
|
|
|
mode = arguments[1]
|
|
|
|
if mode == "named-pipes"
|
|
if arguments.length < 3
|
|
raise ClientError.new "malformed command"
|
|
end
|
|
elsif mode == "pseudoterminal"
|
|
if arguments.length < 4
|
|
raise ClientError.new "malformed command"
|
|
end
|
|
|
|
pseudoterminal_path = Base64.decode64(arguments[2])
|
|
else
|
|
raise ClientError.new "unknown mode (#{mode})"
|
|
end
|
|
|
|
script_path = Base64.decode64(arguments.last)
|
|
|
|
connection_id += 1
|
|
|
|
child_process = fork {
|
|
process_id = Process.pid
|
|
control_socket.puts "pid #{process_id}"
|
|
|
|
exit_code = "unknown"
|
|
|
|
if mode == "named-pipes"
|
|
set_up_named_pipes(control_socket, connection_id)
|
|
else
|
|
set_up_pseudoterminal(control_socket, pseudoterminal_path)
|
|
end
|
|
|
|
$original_stderr.puts " executing script #{script_path} (#{process_id})"
|
|
|
|
begin
|
|
begin
|
|
load script_path, true
|
|
rescue SystemExit => error
|
|
$original_stderr.puts " exit code: #{error.status}"
|
|
exit_code = error.status
|
|
rescue StandardError => error
|
|
$stdin = $original_stdin
|
|
$stdout = $original_stdout
|
|
$stderr = $original_stderr
|
|
|
|
raise ClientScriptError.new error
|
|
end
|
|
rescue ClientScriptError => error
|
|
encoded_error_output = Base64.encode64(error.source.full_message).delete("\n")
|
|
$original_stderr.puts " error executing script, ignoring request"
|
|
begin
|
|
control_socket.puts "script_error #{encoded_error_output}"
|
|
rescue
|
|
end
|
|
rescue ClientError => error
|
|
$original_stderr.puts " error communicating with client, ignoring request (#{error})"
|
|
begin
|
|
control_socket.puts "error #{error}"
|
|
rescue
|
|
end
|
|
rescue StandardError => error
|
|
$original_stderr.puts " error, ignoring request (#{error})"
|
|
begin
|
|
control_socket.puts "error internal server error"
|
|
rescue
|
|
end
|
|
end
|
|
|
|
begin
|
|
control_socket.puts "done #{exit_code}"
|
|
rescue
|
|
end
|
|
control_socket.close
|
|
|
|
if mode == "named-pipes"
|
|
clean_up_named_pipes(control_socket, connection_id)
|
|
end
|
|
|
|
$original_stderr.puts " finished handling request (#{process_id})"
|
|
|
|
Kernel.exit!
|
|
}
|
|
|
|
Process.detach(child_process)
|
|
rescue ClientError => error
|
|
$original_stderr.puts " error communicating with client, ignoring request (#{error})"
|
|
begin
|
|
control_socket.puts "error #{error}"
|
|
rescue
|
|
end
|
|
rescue StandardError => error
|
|
$original_stderr.puts " error, ignoring request (#{error})"
|
|
begin
|
|
control_socket.puts "error internal server error"
|
|
rescue
|
|
end
|
|
end
|
|
|
|
control_socket.close
|
|
end
|