#!/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 = "/run/github-fast-env/github-fast-envd.sock" $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 begin $control_socket = UNIXSocket.new(control_socket_path) rescue StandardError => error log "error", "could not connect to github-fast-envd socket" exit 1 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