You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
5.8 KiB
258 lines
5.8 KiB
#!/usr/bin/env ruby |
|
|
|
require "base64" |
|
require "io/console" |
|
require "optparse" |
|
require "socket" |
|
|
|
usage_output = "USAGE:\n github-fast-env SCRIPT_PATH [options]" |
|
|
|
$options = {:verbose => false, :interactive => false} |
|
OptionParser.new do |options| |
|
options.banner = usage_output |
|
options.on("-v", "--verbose", "Show verbose output") do |
|
$options[:verbose] = true |
|
end |
|
options.on("-i", "--interactive", "Run an interactive session using a pseudoterminal") do |
|
$options[:interactive] = 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 |
|
|
|
# TODO: support arguments |
|
script_path = File.realpath(ARGV[0]) |
|
|
|
control_socket_path = "/tmp/github-fast-envd.sock" |
|
|
|
$control_socket = UNIXSocket.new(control_socket_path) |
|
|
|
$original_stdin = $stdin.dup |
|
$original_stdout = $stdout.dup |
|
$original_stderr = $stderr.dup |
|
|
|
def log(level, message) |
|
if level == "error" or $options[:verbose] |
|
$original_stderr.puts "[github-fast-env] #{level}: #{message}" |
|
end |
|
end |
|
|
|
$remote_process_id = nil |
|
|
|
Signal.trap("HUP") do |
|
if $remote_process_id |
|
Process.kill("HUP", $remote_process_id) |
|
else |
|
Signal.trap("HUP", "DEFAULT") |
|
Process.kill("HUP", 0) |
|
end |
|
end |
|
|
|
Signal.trap("INT") do |
|
if $remote_process_id |
|
Process.kill("INT", $remote_process_id) |
|
else |
|
Signal.trap("INT", "DEFAULT") |
|
Process.kill("INT", 0) |
|
end |
|
end |
|
|
|
Signal.trap("QUIT") do |
|
if $remote_process_id |
|
Process.kill("QUIT", $remote_process_id) |
|
else |
|
Signal.trap("QUIT", "DEFAULT") |
|
Process.kill("QUIT", 0) |
|
end |
|
end |
|
|
|
Signal.trap("TERM") do |
|
if $remote_process_id |
|
Process.kill("TERM", $remote_process_id) |
|
else |
|
Signal.trap("TERM", "DEFAULT") |
|
Process.kill("TERM", 0) |
|
end |
|
end |
|
|
|
Signal.trap("TSTP") do |
|
if $remote_process_id |
|
Process.kill("TSTP", $remote_process_id) |
|
end |
|
|
|
Signal.trap("TSTP", "DEFAULT") |
|
Process.kill("TSTP", 0) |
|
end |
|
|
|
Signal.trap("CONT") do |
|
if $remote_process_id |
|
Process.kill("CONT", $remote_process_id) |
|
end |
|
|
|
Signal.trap("CONT", "DEFAULT") |
|
Process.kill("CONT", 0) |
|
end |
|
|
|
def read_command |
|
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") |
|
|
|
read_ios = [$control_socket] |
|
|
|
if $options[:interactive] |
|
pseudoterminal_path = File.readlink("/proc/self/fd/0") |
|
encoded_pseudoterminal_path = Base64.encode64(pseudoterminal_path).delete("\n") |
|
|
|
$control_socket.puts "new v1 pseudoterminal #{encoded_pseudoterminal_path} #{encoded_script_path}" |
|
else |
|
$control_socket.puts "new v1 named-pipes #{encoded_script_path}" |
|
end |
|
|
|
pipes = {"stdin" => nil, "stdout" => nil, "stderr" => nil} |
|
|
|
while true |
|
command, arguments = read_command |
|
|
|
if command == "pid" |
|
if arguments.empty? |
|
log "error", "malformed response" |
|
exit 1 |
|
end |
|
|
|
$remote_process_id = arguments[0].to_i |
|
log "trace", "remote process ID: #{$remote_process_id}" |
|
elsif command == "ready" |
|
break |
|
elsif command == "named-pipes" |
|
if arguments.empty? |
|
log "error", "malformed response" |
|
exit 1 |
|
end |
|
|
|
pipe_base_path = Base64.decode64(arguments[0]) |
|
|
|
log "info", "connecting to named pipes at #{pipe_base_path}" |
|
|
|
pipes["stdin"] = File.open("#{pipe_base_path}.stdin", "w") |
|
pipes["stdin"].sync = true |
|
pipes["stdout"] = File.open("#{pipe_base_path}.stdout", "r") |
|
pipes["stdout"].sync = true |
|
pipes["stderr"] = File.open("#{pipe_base_path}.stderr", "r") |
|
pipes["stderr"].sync = true |
|
|
|
read_ios += [$stdin, pipes["stdout"], pipes["stderr"]] |
|
|
|
log "info", "connected via named pipes" |
|
|
|
log "info", "ready" |
|
$control_socket.puts "ready" |
|
else |
|
log "error", "malformed response" |
|
exit 1 |
|
end |
|
end |
|
|
|
exit_code = "unknown" |
|
|
|
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 |
|
|
|
if command != "done" |
|
log "error", "malformed response from github-fast-envd" |
|
end |
|
|
|
if !arguments.empty? |
|
exit_code = arguments[0] |
|
end |
|
else |
|
log "warn", "received input from unknown stream" |
|
end |
|
rescue EOFError |
|
log "trace", "closing stream #{ready_read_io}" |
|
ready_read_io.close |
|
end |
|
end |
|
|
|
read_ios = read_ios.select {|x| not x.closed?} |
|
end |
|
|
|
exit_code_is_numeric = Integer(exit_code) != nil rescue false |
|
|
|
if exit_code_is_numeric |
|
exit Integer(exit_code) |
|
end
|
|
|