Initial commit
This commit is contained in:
commit
86b2f92511
161
github-fast-env.rb
Executable file
161
github-fast-env.rb
Executable file
@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require "base64"
|
||||||
|
require "optparse"
|
||||||
|
require "socket"
|
||||||
|
|
||||||
|
usage_output = "USAGE:\n github-fast-env SCRIPT_PATH [options]"
|
||||||
|
|
||||||
|
$options = {:verbose => false}
|
||||||
|
OptionParser.new do |options|
|
||||||
|
options.banner = usage_output
|
||||||
|
options.on("-v", "--verbose", "Show verbose output") do
|
||||||
|
$options[:verbose] = true
|
||||||
|
end
|
||||||
|
end.parse!
|
||||||
|
|
||||||
|
if not ARGV or ARGV.empty?
|
||||||
|
$stderr.puts "error: missing argument (script path)\n\n"
|
||||||
|
$stderr.puts usage_output
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if ARGV.length > 1
|
||||||
|
$stderr.puts "error: only one script expected\n\n"
|
||||||
|
$stderr.puts usage_output
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
script_path = File.realpath(ARGV[0])
|
||||||
|
|
||||||
|
control_socket_path = "/tmp/github-fast-envd.sock"
|
||||||
|
|
||||||
|
$control_socket = UNIXSocket.new(control_socket_path)
|
||||||
|
|
||||||
|
def log(level, message)
|
||||||
|
if level == "error" or $options[:verbose]
|
||||||
|
$stderr.puts "[github-fast-env] #{level}: #{message}"
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
if command == "script_error"
|
||||||
|
if response.length < 2
|
||||||
|
log "error", "malformed script error response from github-fast-envd"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
error_message = Base64.decode64(response[1])
|
||||||
|
|
||||||
|
$stderr.puts error_message
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
arguments = response[1].nil? ? [] : response[1].split(" ")
|
||||||
|
|
||||||
|
return command, arguments
|
||||||
|
end
|
||||||
|
|
||||||
|
log "info", "connected to control socket"
|
||||||
|
|
||||||
|
encoded_script_path = Base64.encode64(script_path).delete("\n")
|
||||||
|
$control_socket.puts "new v1 #{encoded_script_path}"
|
||||||
|
|
||||||
|
pipes = {"stdin" => nil, "stdout" => nil, "stderr" => nil}
|
||||||
|
|
||||||
|
while true
|
||||||
|
command, arguments = read_command($control_socket)
|
||||||
|
|
||||||
|
if command == "ready"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ["stdin", "stdout", "stderr"].include?(command) or arguments.empty?
|
||||||
|
log "error", "malformed response"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
pipe_path = arguments[0]
|
||||||
|
|
||||||
|
log "info", "connecting to #{pipe_path}"
|
||||||
|
|
||||||
|
if command == "stdin"
|
||||||
|
pipes[command] = File::open(pipe_path, "w")
|
||||||
|
else
|
||||||
|
pipes[command] = File::open(pipe_path, "r")
|
||||||
|
end
|
||||||
|
|
||||||
|
pipes[command].sync = true
|
||||||
|
|
||||||
|
log "info", "connected"
|
||||||
|
end
|
||||||
|
|
||||||
|
log "info", "ready"
|
||||||
|
$control_socket.puts "ready"
|
||||||
|
|
||||||
|
read_ios = [$control_socket, $stdin, pipes["stdout"], pipes["stderr"]]
|
||||||
|
|
||||||
|
while read_ios.include?($control_socket) or read_ios.include?(pipes["stdout"]) or read_ios.include?(pipes["stderr"])
|
||||||
|
log "trace", read_ios.inspect
|
||||||
|
|
||||||
|
ready_read_ios, _, _ = IO.select(read_ios, [], [])
|
||||||
|
|
||||||
|
log "trace", "ready read IOs: #{ready_read_ios.inspect}"
|
||||||
|
|
||||||
|
ready_read_ios.each do |ready_read_io|
|
||||||
|
begin
|
||||||
|
if ready_read_io.equal? $stdin
|
||||||
|
log "trace", "writing to stdin"
|
||||||
|
pipes["stdin"].write ready_read_io.readpartial(4096)
|
||||||
|
elsif ready_read_io.equal? pipes["stdout"]
|
||||||
|
log "trace", "reading from stdout"
|
||||||
|
$stdout.write ready_read_io.readpartial(4096)
|
||||||
|
elsif ready_read_io.equal? pipes["stderr"]
|
||||||
|
log "trace", "reading from stderr"
|
||||||
|
$stderr.write ready_read_io.readpartial(4096)
|
||||||
|
elsif ready_read_io.equal? $control_socket
|
||||||
|
log "trace", "reading from control socket"
|
||||||
|
command, arguments = read_command($control_socket)
|
||||||
|
|
||||||
|
if command != "done"
|
||||||
|
log "error", "malformed response from github-fast-envd"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log "warn", "received input from unknown stream"
|
||||||
|
end
|
||||||
|
rescue EOFError
|
||||||
|
log "trace", "closing stream"
|
||||||
|
ready_read_io.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
read_ios = read_ios.select {|x| not x.closed?}
|
||||||
|
end
|
204
github-fast-envd.rb
Executable file
204
github-fast-envd.rb
Executable file
@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require "base64"
|
||||||
|
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
|
||||||
|
|
||||||
|
control_socket_path = "/tmp/github-fast-envd.sock"
|
||||||
|
|
||||||
|
if File.exist?(control_socket_path) and File.socket?(control_socket_path)
|
||||||
|
File.unlink(control_socket_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
control_server = UNIXServer.new(control_socket_path)
|
||||||
|
File.chmod 0700, control_socket_path
|
||||||
|
|
||||||
|
#at_exit do
|
||||||
|
# control_server.close
|
||||||
|
#end
|
||||||
|
|
||||||
|
$stderr.puts "serving control socket"
|
||||||
|
|
||||||
|
connection_id = 0
|
||||||
|
|
||||||
|
def open_pipe(connection_id, name)
|
||||||
|
pipe_path = "/tmp/github-fast-envd.#{connection_id}.#{name}"
|
||||||
|
|
||||||
|
if File.exist?(pipe_path) and File.pipe?(pipe_path)
|
||||||
|
File.unlink(pipe_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
File.mkfifo(pipe_path, mode = 0600)
|
||||||
|
|
||||||
|
pipe_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
|
||||||
|
|
||||||
|
while true
|
||||||
|
control_socket = control_server.accept
|
||||||
|
$stderr.puts "- new connection"
|
||||||
|
|
||||||
|
begin
|
||||||
|
command, arguments = read_command(control_socket)
|
||||||
|
|
||||||
|
if command != "new"
|
||||||
|
raise ClientError.new "unexpected command"
|
||||||
|
end
|
||||||
|
|
||||||
|
if arguments.length != 2
|
||||||
|
raise ClientError.new "malformed command"
|
||||||
|
end
|
||||||
|
|
||||||
|
if arguments[0] != "v1"
|
||||||
|
raise ClientError.new "unsupported protocol version"
|
||||||
|
end
|
||||||
|
|
||||||
|
connection_id += 1
|
||||||
|
|
||||||
|
child_process = fork {
|
||||||
|
original_stdin = $stdin.dup
|
||||||
|
original_stdout = $stdout.dup
|
||||||
|
original_stderr = $stderr.dup
|
||||||
|
|
||||||
|
begin
|
||||||
|
stdin = open_pipe(connection_id, "stdin")
|
||||||
|
control_socket.puts "stdin #{stdin}"
|
||||||
|
stdin = File::open(stdin, "r")
|
||||||
|
stdin.sync = true
|
||||||
|
|
||||||
|
stdout = open_pipe(connection_id, "stdout")
|
||||||
|
control_socket.puts "stdout #{stdout}"
|
||||||
|
stdout = File::open(stdout, "w")
|
||||||
|
stdout.sync = true
|
||||||
|
|
||||||
|
stderr = open_pipe(connection_id, "stderr")
|
||||||
|
control_socket.puts "stderr #{stderr}"
|
||||||
|
stderr = File::open(stderr, "w")
|
||||||
|
stderr.sync = true
|
||||||
|
|
||||||
|
$stderr.puts " set up pipes"
|
||||||
|
control_socket.puts "ready"
|
||||||
|
|
||||||
|
response = control_socket.readline.strip
|
||||||
|
|
||||||
|
if response != "ready"
|
||||||
|
raise ClientError.new "invalid command"
|
||||||
|
Kernel.exit!
|
||||||
|
end
|
||||||
|
|
||||||
|
script_path = Base64.decode64(arguments[1])
|
||||||
|
$stderr.puts " executing script " + script_path
|
||||||
|
|
||||||
|
$stdin.reopen(stdin)
|
||||||
|
$stdout.reopen(stdout)
|
||||||
|
$stderr.reopen(stderr)
|
||||||
|
|
||||||
|
begin
|
||||||
|
load script_path, true
|
||||||
|
rescue => error
|
||||||
|
$stdin = original_stdin
|
||||||
|
$stdout = original_stdout
|
||||||
|
$stderr = original_stderr
|
||||||
|
|
||||||
|
raise ClientScriptError.new error
|
||||||
|
end
|
||||||
|
rescue ClientScriptError => error
|
||||||
|
# TODO: Restore pipes to make sure that syntax errors are caught
|
||||||
|
encoded_error_output = Base64.encode64(error.source.full_message).delete("\n")
|
||||||
|
original_stderr.puts " error executing script, ignoring request"
|
||||||
|
# TODO: if the begin/rescue blog has syntax errors, these go unnoticed
|
||||||
|
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"
|
||||||
|
rescue
|
||||||
|
end
|
||||||
|
control_socket.close
|
||||||
|
|
||||||
|
original_stderr.puts " finished handling request"
|
||||||
|
|
||||||
|
Kernel.exit!
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.detach(child_process)
|
||||||
|
rescue ClientError => error
|
||||||
|
$stderr.puts " error communicating with client, ignoring request (#{error})"
|
||||||
|
begin
|
||||||
|
control_socket.puts "error #{error}"
|
||||||
|
rescue
|
||||||
|
end
|
||||||
|
rescue StandardError => error
|
||||||
|
$stderr.puts " error, ignoring request (#{error})"
|
||||||
|
begin
|
||||||
|
control_socket.puts "error internal server error"
|
||||||
|
rescue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
control_socket.close
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user