fastlane/lib/fastlane/server/socket_server.rb in fastlane-2.73.0 vs fastlane/lib/fastlane/server/socket_server.rb in fastlane-2.74.0.beta.20180106010004

- old
+ new

@@ -1,12 +1,19 @@ -require 'fastlane/server/command.rb' require 'fastlane/server/command_executor.rb' +require 'fastlane/server/command_parser.rb' +require 'fastlane/server/json_return_value_processor.rb' require 'socket' require 'json' module Fastlane class SocketServer + COMMAND_EXECUTION_STATE = { + ready: :ready, + already_shutdown: :already_shutdown, + error: :error + } + attr_accessor :command_executor attr_accessor :return_value_processor def initialize( command_executor: nil, @@ -22,57 +29,116 @@ @return_value_processor = return_value_processor @connection_timeout = connection_timeout.to_i @stay_alive = stay_alive end - # This is the public API, don't call anything else + # this is the public API, don't call anything else def start - while listen - # Loop for-ev-er + listen + + while @stay_alive + UI.important("stay_alive is set to true, restarting server") + listen end end private def receive_and_process_commands - # We'll break out of the infinite loop somehow, either error or 'done' message - ended_loop_due_to_error = true + loop do # no idea how many commands are coming, so we loop until an error or the done command is sent + execution_state = COMMAND_EXECUTION_STATE[:ready] - loop do # No idea how many commands are coming, so we loop until an error or the done command is sent - str = nil - + command_string = nil begin - str = @client.recv(1_048_576) # 1024 * 1024 + command_string = @client.recv(1_048_576) # 1024 * 1024 rescue Errno::ECONNRESET => e UI.verbose(e) - break + execution_state = COMMAND_EXECUTION_STATE[:error] end - if str == 'done' - time = Time.new - UI.verbose("[#{time.usec}]: received done signal, shutting down") - ended_loop_due_to_error = false - break + if execution_state == COMMAND_EXECUTION_STATE[:ready] + # Ok, all is good, let's see what command we have + execution_state = parse_and_execute_command(command_string: command_string) end - response_json = process_command(command_json: str) - time = Time.new - UI.verbose("[#{time.usec}]: sending #{response_json}") - begin - @client.puts(response_json) # Send some json to the client - rescue Errno::EPIPE => e - UI.verbose(e) + case execution_state + when COMMAND_EXECUTION_STATE[:ready] + # command executed successfully, let's setup for the next command + next + when COMMAND_EXECUTION_STATE[:already_shutdown] + # we shutdown in response to a command, nothing left to do but exit break + when COMMAND_EXECUTION_STATE[:error] + # we got an error somewhere, let's shutdown and exit + handle_disconnect(error: true, exit_reason: :error) + break end end + end - return handle_disconnect(error: ended_loop_due_to_error) + def parse_and_execute_command(command_string: nil) + command = CommandParser.parse(json: command_string) + case command + when ControlCommand + return handle_control_command(command) + when ActionCommand + return handle_action_command(command) + end + + # catch all + raise "Command #{command} not supported" end + # we got a server control command from the client to do something like shutdown + def handle_control_command(command) + exit_reason = nil + if command.cancel_signal? + UI.verbose("received cancel signal shutting down, reason: #{command.reason}") + + # send an ack to the client to let it know we're shutting down + cancel_response = '{"payload":{"status":"cancelled"}}' + send_response(cancel_response) + + exit_reason = :cancelled + elsif command.done_signal? + UI.verbose("received done signal shutting down") + + # client is already in the process of shutting down, no need to ack + exit_reason = :done + end + + # if the command came in with a user-facing message, display it + if command.user_message + UI.important(command.user_message) + end + + # currently all control commands should trigger a disconnect and shutdown + handle_disconnect(error: false, exit_reason: exit_reason) + return COMMAND_EXECUTION_STATE[:already_shutdown] + end + + # execute and send back response to client + def handle_action_command(command) + response_json = process_action_command(command: command) + return send_response(response_json) + end + + # send json back to client + def send_response(json) + UI.verbose("sending #{json}") + begin + @client.puts(json) # Send some json to the client + rescue Errno::EPIPE => e + UI.verbose(e) + return COMMAND_EXECUTION_STATE[:error] + end + return COMMAND_EXECUTION_STATE[:ready] + end + def listen @server = TCPServer.open('localhost', 2000) # Socket to listen on port 2000 - UI.message("Waiting for #{@connection_timeout} seconds for a connection from FastlaneRunner") + UI.verbose("Waiting for #{@connection_timeout} seconds for a connection from FastlaneRunner") # set thread local to ready so we can check it Thread.current[:ready] = true @client = nil begin @@ -82,38 +148,36 @@ rescue Timeout::Error UI.user_error!("fastlane failed to receive a connection from the FastlaneRunner binary after #{@connection_timeout} seconds, shutting down") rescue StandardError => e UI.user_error!("Something went wrong while waiting for a connection from the FastlaneRunner binary, shutting down\n#{e}") end - UI.message("Client connected") + UI.verbose("Client connected") + # this loops forever receive_and_process_commands end - def handle_disconnect(error: false) - UI.important("Client disconnected, or a pipe broke") if error - if @stay_alive - UI.important("stay_alive is set to true, restarting server") - # clean up before restart - @client.close - @client = nil + def handle_disconnect(error: false, exit_reason: :error) + Thread.current[:exit_reason] = exit_reason - @server.close - @server = nil - return true # Restart server - end - return false # Don't restart server + UI.important("Client disconnected, a pipe broke, or received malformed data") if exit_reason == :error + # clean up + @client.close + @client = nil + + @server.close + @server = nil end - def process_command(command_json: nil) - time = Time.new - UI.verbose("[#{time.usec}]: received command:#{command_json}") - return execute_command(command_json: command_json) + # record fastlane action command and then execute it + def process_action_command(command: nil) + UI.verbose("received command:#{command.inspect}") + return execute_action_command(command: command) end - def execute_command(command_json: nil) - command = Command.new(json: command_json) + # execute fastlane action command + def execute_action_command(command: nil) command_return = @command_executor.execute(command: command, target_object: nil) ## probably need to just return Strings, or ready_for_next with object isn't String return_object = command_return.return_value return_value_type = command_return.return_value_type closure_arg = command_return.closure_argument_value @@ -131,87 +195,22 @@ return_value_type: :string # always assume string for closure error_callback ) closure_arg = ', "closure_argument_value": ' + closure_arg end + Thread.current[:exception] = nil return '{"payload":{"status":"ready_for_next", "return_object":' + return_object + closure_arg + '}}' rescue StandardError => e + Thread.current[:exception] = e + exception_array = [] exception_array << "#{e.class}:" exception_array << e.backtrace while e.respond_to?("cause") && (e = e.cause) exception_array << "cause: #{e.class}" exception_array << backtrace end return "{\"payload\":{\"status\":\"failure\",\"failure_information\":#{exception_array.flatten}}}" - end - end - - class JSONReturnValueProcessor - def prepare_object(return_value: nil, return_value_type: nil) - case return_value_type - when nil - UI.verbose("return_value_type is nil value: #{return_value}") - return process_value_as_string(return_value: return_value) - when :string - return process_value_as_string(return_value: return_value) - when :int - return process_value_as_int(return_value: return_value) - when :bool - return process_value_as_bool(return_value: return_value) - when :array_of_strings - return process_value_as_array_of_strings(return_value: return_value) - when :hash_of_strings - return process_value_as_hash_of_strings(return_value: return_value) - else - UI.verbose("Unknown return type defined: #{return_value_type} for value: #{return_value}") - return process_value_as_string(return_value: return_value) - end - end - - def process_value_as_string(return_value: nil) - if return_value.nil? - return_value = "" - end - - # quirks_mode because sometimes the built-in library is used for some folks and that needs quirks_mode: true - return JSON.generate(return_value.to_s, quirks_mode: true) - end - - def process_value_as_array_of_strings(return_value: nil) - if return_value.nil? - return_value = [] - end - - # quirks_mode shouldn't be required for real objects - return JSON.generate(return_value) - end - - def process_value_as_hash_of_strings(return_value: nil) - if return_value.nil? - return_value = {} - end - - # quirks_mode shouldn't be required for real objects - return JSON.generate(return_value) - end - - def process_value_as_bool(return_value: nil) - if return_value.nil? - return_value = false - end - - # quirks_mode because sometimes the built-in library is used for some folks and that needs quirks_mode: true - return JSON.generate(return_value.to_s, quirks_mode: true) - end - - def process_value_as_int(return_value: nil) - if return_value.nil? - return_value = 0 - end - - # quirks_mode because sometimes the built-in library is used for some folks and that needs quirks_mode: true - return JSON.generate(return_value.to_s, quirks_mode: true) end end end