Class: Tracks

Inherits:
Object
  • Object
show all
Includes:
HTTPTools::Builder
Defined in:
lib/tracks.rb

Overview

Tracks is a bare-bones HTTP server that talks Rack and uses a thread per connection model of concurrency.

The simplest way to get up and running with Tracks is via rackup, in the same directory as your application’s config.ru run

rackup -rtracks -stracks

Alternately you can alter your config.ru, adding to the top

require "tracks"
#\ --server tracks

If you need to start up Tracks from code, the simplest way to go is

require "tracks"
Tracks.run(app, :host => host, :port => port)

Where app is a Rack app, responding to #call. The ::run method will block till the server quits. To stop all running Tracks servers in the current process call ::shutdown. You may want to setup a signal handler for this, like so

trap(:INT) {Tracks.shutdown}

This will allow Tracks to gracefully shutdown when your program is quit with Ctrl-C. The signal handler must be setup before the call to ::run.

A slightly more generic version of the above looks like

server = Tracks.new(app, :host => host, :port => port)
trap(:INT) {server.shutdown}
server.listen

To start a server listening on a Unix domain socket, an instance of UNIXServer can be given to #listen

require "socket"
server = Tracks.new(app)
server.listen(UNIXServer.new("/tmp/tracks.sock"))

If you have an already accepted socket you can use Tracks to handle the connection like so

server = Tracks.new(app)
server.on_connection(socket)

A specific use case for this would be an inetd handler, which would look like

STDERR.reopen(File.new("/dev/null", "w"))
server = Tracks.new(app)
server.on_connection(TCPSocket.for_fd(STDIN.fileno))

Defined Under Namespace

Classes: Input

Constant Summary collapse

ENV_CONSTANTS =

:nodoc:

{"rack.multithread" => true}

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ Tracks

:call-seq: Tracks.new(rack_app[, options]) -> server

Create a new Tracks server. rack_app should be a rack application, responding to #call. options should be a hash, with the following optional keys, as symbols

:host

the host to listen on, defaults to 0.0.0.0

:port

the port to listen on, defaults to 9292

:read_timeout

the maximum amount of time, in seconds, to wait on idle connections, defaults to 30

:shutdown_timeout

the maximum amount of time, in seconds, to wait for in process requests to complete when signalled to shut down, defaults to 30



180
181
182
183
184
185
186
187
# File 'lib/tracks.rb', line 180

def initialize(app, options={})
  @host = options[:host] || options[:Host] || "0.0.0.0"
  @port = (options[:port] || options[:Port] || "9292").to_s
  @read_timeout = options[:read_timeout] || 30
  @shutdown_timeout = options[:shutdown_timeout] || 30
  @app = app
  @shutdown_signal, @signal_shutdown = IO.pipe
end

Class Attribute Details

.runningObject

class accessor, array of currently running instances



69
70
71
# File 'lib/tracks.rb', line 69

def running
  @running
end

Class Method Details

.run(app, options = {}) ⇒ Object

:call-seq: Tracks.run(rack_app[, options]) -> nil

Equivalent to Tracks.new(rack_app, options).listen



193
194
195
# File 'lib/tracks.rb', line 193

def self.run(app, options={})
  new(app, options).listen
end

.shutdownObject

:call-seq: Tracks.shutdown -> nil

Signal all running Tracks servers to shutdown.



201
202
203
# File 'lib/tracks.rb', line 201

def self.shutdown
  running.dup.each {|s| s.shutdown} && nil
end

Instance Method Details

#listen(server = TCPServer.new(@host, @port)) ⇒ Object

:call-seq: server.listen() -> bool

Start listening for/accepting connections on socket_server. socket_server defaults to a TCP server listening on the host and port supplied to ::new.

An alternate socket server can be supplied as an argument, such as an instance of UNIXServer to listen on a unix domain socket.

This method will block until #shutdown is called. The socket_server will be closed when this method returns.

A return value of false indicates there were threads left running after shutdown_timeout had expired which were forcibly killed. This may leave resources in an inconsistant state, and it is advised you exit the process in this case (likely what you were planning anyway).



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/tracks.rb', line 231

def listen(server=TCPServer.new(@host, @port))
  @shutdown = false
  server.listen(1024) if server.respond_to?(:listen)
  @port, @host = server.addr[1,2].map{|e| e.to_s} if server.respond_to?(:addr)
  servers = [server, @shutdown_signal]
  threads = ThreadGroup.new
  self.class.running << self
  puts "Tracks HTTP server available at #{@host}:#{@port}"
  while select(servers, nil, nil) && !@shutdown
    threads.add(Thread.new(server.accept) {|sock| on_connection(sock)})
  end
  server.close
  wait = @shutdown_timeout
  wait -= sleep 1 until threads.list.empty? || wait <= 0
  @shutdown_signal.sysread(1)
  threads.list.each {|thread| thread.kill}.empty?
end

#on_connection(socket) ⇒ Object

:call-seq: server.on_connection(socket) -> nil

Handle HTTP messages on socket, dispatching them to the rack_app supplied to ::new.

This method will return when socket has reached EOF or has been idle for the read_timeout supplied to ::new. The socket will be closed when this method returns.

Errors encountered in this method will be printed to stderr, but not raised.



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/tracks.rb', line 260

def on_connection(socket)
  parser = HTTPTools::Parser.new
  buffer = ""
  sockets = [socket, @shutdown_signal]
  idle = false
  reader = Proc.new do
    readable, = select(sockets, nil, nil, @read_timeout)
    return unless readable
    sockets.delete(@shutdown_signal) if @shutdown
    return if idle && @shutdown
    idle = false
    begin
      socket.sysread(16384, buffer)
      parser << buffer
    rescue HTTPTools::ParseError
      socket << response(400, CONNECTION => CLOSE)
      return
    rescue EOFError
      return
    end
  end
  input = Input.new(reader)
  parser.on(:stream) {|chunk| input.recieve_chunk(chunk)}
  parser.on(:finish) {input.finished = true}
  
  remote_family, remote_port, remote_host, remote_addr = socket.peeraddr
  while true
    reader.call until parser.header?
    env = {SERVER_NAME => @host, SERVER_PORT => @port}.merge!(parser.env
      ).merge!(HTTP_VERSION => parser.version, REMOTE_ADDR => remote_addr,
      RACK_INPUT => Rack::RewindableInput.new(input)).merge!(ENV_CONSTANTS)
    input.first_read {socket << response(100)} if env[HTTP_EXPECT] == CONTINUE
    
    status, header, body = @app.call(env)
    
    header = Rack::Utils::HeaderHash.new(header)
    connection_header = header[CONNECTION] || env[HTTP_CONNECTION]
    keep_alive = ((parser.version.casecmp(HTTP_1_1) == 0 &&
      (!connection_header || connection_header.casecmp(CLOSE) != 0)) ||
      (connection_header && connection_header.casecmp(KEEP_ALIVE) == 0)) &&
      !@shutdown && (header.key?(CONTENT_LENGTH) ||
      header.key?(TRANSFER_ENCODING) || HTTPTools::NO_BODY[status.to_i])
    header[CONNECTION] = keep_alive ? KEEP_ALIVE : CLOSE
    
    socket << response(status, header)
    body.each {|chunk| socket << chunk}
    body.close if body.respond_to?(:close)
    
    if keep_alive && !@shutdown
      reader.call until parser.finished?
      input.reset
      remainder = parser.rest.lstrip
      parser.reset << remainder
      idle = true
    else
      break
    end
  end
  
rescue StandardError, LoadError, SyntaxError => e
  STDERR.puts("#{e.class}: #{e.message} #{e.backtrace.join("\n")}")
ensure
  socket.close
end

#shutdownObject

:call-seq: server.shutdown -> nil

Signal the server to shut down.



209
210
211
212
213
# File 'lib/tracks.rb', line 209

def shutdown
  @shutdown = true
  self.class.running.delete(self)
  @signal_shutdown << "x" && nil
end