From 86b2f925112eae59c29cf939d86c4313e854fc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20L=C3=BChne?= Date: Sun, 24 May 2020 04:30:04 +0200 Subject: [PATCH] Initial commit --- github-fast-env.rb | 161 ++++++++++++++++++++++++++++++++++ github-fast-envd.rb | 204 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100755 github-fast-env.rb create mode 100755 github-fast-envd.rb diff --git a/github-fast-env.rb b/github-fast-env.rb new file mode 100755 index 0000000..a6a561a --- /dev/null +++ b/github-fast-env.rb @@ -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 diff --git a/github-fast-envd.rb b/github-fast-envd.rb new file mode 100755 index 0000000..6bd2a27 --- /dev/null +++ b/github-fast-envd.rb @@ -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