class Fastlane::SocketServer

Constants

COMMAND_EXECUTION_STATE

Attributes

command_executor[RW]
return_value_processor[RW]

Public Class Methods

new( command_executor: nil, return_value_processor: nil, connection_timeout: 5, stay_alive: false, port: 2000 ) click to toggle source
# File fastlane/lib/fastlane/server/socket_server.rb, line 18
def initialize(
  command_executor: nil,
  return_value_processor: nil,
  connection_timeout: 5,
  stay_alive: false,
  port: 2000
)
  if return_value_processor.nil?
    return_value_processor = JSONReturnValueProcessor.new
  end

  @command_executor = command_executor
  @return_value_processor = return_value_processor
  @connection_timeout = connection_timeout.to_i
  @stay_alive = stay_alive
  @port = port.to_i
end

Public Instance Methods

start() click to toggle source

this is the public API, don't call anything else

# File fastlane/lib/fastlane/server/socket_server.rb, line 37
def start
  listen

  while @stay_alive
    UI.important("stay_alive is set to true, restarting server")
    listen
  end
end

Private Instance Methods

execute_action_command(command: nil) click to toggle source

execute fastlane action command

# File fastlane/lib/fastlane/server/socket_server.rb, line 180
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

  return_object = return_value_processor.prepare_object(
    return_value: return_object,
    return_value_type: return_value_type
  )

  if closure_arg.nil?
    closure_arg = closure_arg.to_s
  else
    closure_arg = return_value_processor.prepare_object(
      return_value: closure_arg,
      return_value_type: :string # always assume string for closure error_callback
    )
  end

  Thread.current[:exception] = nil

  payload = {
    payload: {
      status: "ready_for_next",
      return_object: return_object,
      closure_argument_value: closure_arg
    }
  }
  return JSON.generate(payload)
rescue StandardError => e
  Thread.current[:exception] = e

  exception_array = []
  exception_array << "#{e.class}:"
  exception_array << e.backtrace

  ec = e.class

  em = e.message

  while e.respond_to?("cause") && (e = e.cause)
    exception_array << "cause: #{e.class}"
    exception_array << e.backtrace
  end

  payload = {
    payload: {
      status: "failure",
      failure_information: exception_array.flatten,
      failure_class: ec,
      failure_message: em
    }
  }
  return JSON.generate(payload)
end
handle_action_command(command) click to toggle source

execute and send back response to client

# File fastlane/lib/fastlane/server/socket_server.rb, line 122
def handle_action_command(command)
  response_json = process_action_command(command: command)
  return send_response(response_json)
end
handle_control_command(command) click to toggle source

we got a server control command from the client to do something like shutdown

# File fastlane/lib/fastlane/server/socket_server.rb, line 94
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
handle_disconnect(error: false, exit_reason: :error) click to toggle source
# File fastlane/lib/fastlane/server/socket_server.rb, line 161
def handle_disconnect(error: false, exit_reason: :error)
  Thread.current[:exit_reason] = exit_reason

  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
listen() click to toggle source
# File fastlane/lib/fastlane/server/socket_server.rb, line 139
def listen
  @server = TCPServer.open('localhost', @port) # Socket to listen on port 2000
  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
    Timeout.timeout(@connection_timeout) do
      @client = @server.accept # Wait for a client to connect
    end
  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.verbose("Client connected")

  # this loops forever
  receive_and_process_commands
end
parse_and_execute_command(command_string: nil) click to toggle source
# File fastlane/lib/fastlane/server/socket_server.rb, line 80
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
process_action_command(command: nil) click to toggle source

record fastlane action command and then execute it

# File fastlane/lib/fastlane/server/socket_server.rb, line 174
def process_action_command(command: nil)
  UI.verbose("received command:#{command.inspect}")
  return execute_action_command(command: command)
end
receive_and_process_commands() click to toggle source
# File fastlane/lib/fastlane/server/socket_server.rb, line 48
def receive_and_process_commands
  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]

    command_string = nil
    begin
      command_string = @client.recv(1_048_576) # 1024 * 1024
    rescue Errno::ECONNRESET => e
      UI.verbose(e)
      execution_state = COMMAND_EXECUTION_STATE[:error]
    end

    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

    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
send_response(json) click to toggle source

send json back to client

# File fastlane/lib/fastlane/server/socket_server.rb, line 128
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