class Soba::Server

Attributes

app[R]
host[R]
logger[R]
port[R]

Public Class Methods

new(app, host:, port:, **options) click to toggle source
# File lib/soba/server.rb, line 16
def initialize(app, host:, port:, **options)
  @app = app
  @host, @port = host, port
  @server = nil
  @logger = Logger.new(STDOUT, level: is_true?(options[:debug]) ? Logger::DEBUG : Logger::INFO)
  @options = options
end

Public Instance Methods

error_stream() click to toggle source
# File lib/soba/server.rb, line 10
def error_stream
  STDERR
end
process_request(socket) click to toggle source
# File lib/soba/server.rb, line 55
def process_request(socket)
  parser = Parser.new(socket)
  env = parser.env.merge(rack_env)
  env["rack.url_scheme"] = parser.request_schema
  env["rack.input"] = parser.body
  env["rack.errors"] = error_stream
  env["SERVER_NAME"] = env[HTTP_HOST].split(":")[0]

  keep_alive = env[HTTP_CONNECTION].to_s.downcase == KEEP_ALIVE

  begin
    status, headers, res_body = app.call(env)
    logger.debug("response: #{status} #{headers.inspect} #{res_body.inspect}")
  rescue Exception => e
    status = 500
    headers = {'Content-Type' => 'text/plain'}
    logger.error("Internal Server Error: #{e}:\n#{e.backtrace.join("\n")}")
  end

  nobody = parser.env[REQUEST_METHOD] == HEAD || STATUS_WITH_NO_ENTITY_BODY[status]

  outbuf = ''
  status_line = "#{parser.server_protocol} #{status} #{status_text(status)}\n"
  outbuf << status_line

  set_content_length = false
  headers.each do |header, vs|
    case header.downcase
      when CONTENT_LENGTH2, CONTENT_LENGTH
        set_content_length = true
    end
    outbuf << "#{header}#{COLON}#{vs}#{LINE_END}"
  end

  chunked = !set_content_length
  outbuf << TRANSFER_ENCODING_CHUNKED if chunked

  connection_header = keep_alive ? CONNECTION_KEEP_ALIVE : CONNECTION_CLOSE
  outbuf << connection_header
  outbuf << "\n"

  if nobody
    socket << outbuf
    socket.flush
    return
  end

  res_body.each do |part|
    if chunked
      next if part.bytesize.zero?
      outbuf << part.bytesize.to_s(16)
      outbuf << LINE_END
      outbuf << part
      outbuf << LINE_END
    else
      outbuf << part
    end
  end

  if chunked
    outbuf << CLOSE_CHUNKED
  end
  # write is very slow, don't know why
  socket << outbuf
  socket.flush
  res_body.close if res_body.respond_to?(:close)
ensure
  socket.close unless socket.closed? || keep_alive
end
rack_env() click to toggle source
# File lib/soba/server.rb, line 41
def rack_env
  @rack_env ||= {
    "rack.version" => Rack::VERSION,
    "rack.multithread" => true,
    "rack.multiprocess" => false,
    "rack.run_once" => false,
    "rack.hijack?" => false,
    "rack.hijack" => nil,
    "rack.hijack_io" => nil,
    "rack.logger" => logger,
    "SERVER_PORT" => port.to_s,
  }
end
run() click to toggle source
# File lib/soba/server.rb, line 24
def run
  setup
  while (socket = @server.accept)
    _, port, host = socket.peeraddr
    logger.debug "accept connection from #{host}:#{port}"

    LightIO::Beam.new(socket) do |socket|
      begin
        process_request(socket) until socket.closed?
      rescue StandardError => e
        logger.info("Exception: #{e}")
        raise
      end
    end
  end
end

Private Instance Methods

is_true?(v) click to toggle source
# File lib/soba/server.rb, line 145
def is_true?(v)
  %w{true on 1}.include?(v.to_s)
end
setup() click to toggle source
# File lib/soba/server.rb, line 130
def setup
  monkey_patch = LightIO::Monkey.patched?(IO)
  logger.info "Soba #{Soba::VERSION}"
  logger.info "ruby #{RUBY_VERSION}"
  if monkey_patch
    logger.info "Run in Green thread(monkey patch) mode, engine: #{NIO.engine}, see https://github.com/socketry/lightio"
    logger.info "Current backend: #{LightIO::IOloop.current.backend}, available backends: #{NIO::Selector.backends} (set `LIGHTIO_BACKEND` env to choose)"
  else
    logger.info "Run in normal mode"
  end
  logger.info "Server start listen #{host}:#{port}"

  @server = LightIO::TCPServer.new(host, port)
end
status_text(status) click to toggle source
# File lib/soba/server.rb, line 126
def status_text(status)
  HTTP_STATUS_CODES[status.to_i]
end