Class: Tracks
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
-
.running ⇒ Object
class accessor, array of currently running instances.
Class Method Summary collapse
-
.run(app, options = {}) ⇒ Object
:call-seq: Tracks.run(rack_app[, options]) -> nil.
-
.shutdown ⇒ Object
:call-seq: Tracks.shutdown -> nil.
Instance Method Summary collapse
-
#initialize(app, options = {}) ⇒ Tracks
constructor
:call-seq: Tracks.new(rack_app[, options]) -> server.
-
#listen(server = TCPServer.new(@host, @port)) ⇒ Object
:call-seq: server.listen() -> bool.
-
#on_connection(socket) ⇒ Object
:call-seq: server.on_connection(socket) -> nil.
-
#shutdown ⇒ Object
:call-seq: server.shutdown -> nil.
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, ={}) @host = [:host] || [:Host] || "0.0.0.0" @port = ([:port] || [:Port] || "9292").to_s @read_timeout = [:read_timeout] || 30 @shutdown_timeout = [:shutdown_timeout] || 30 @app = app @shutdown_signal, @signal_shutdown = IO.pipe end |
Class Attribute Details
.running ⇒ Object
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, ={}) new(app, ).listen end |
.shutdown ⇒ Object
: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.} #{e.backtrace.join("\n")}") ensure socket.close end |
#shutdown ⇒ Object
: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 |