tracks 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +69 -0
- data/lib/tracks.rb +320 -0
- metadata +74 -0
data/README.rdoc
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
= Tracks
|
2
|
+
|
3
|
+
Tiny/Threaded Rack Server.
|
4
|
+
|
5
|
+
Tracks is a bare-bones HTTP server that talks Rack and uses a thread per
|
6
|
+
connection model of concurrency, written entirely in Ruby.
|
7
|
+
|
8
|
+
* bare-bones: by doing only what it must, Tracks is kept small and fast
|
9
|
+
* Rack based: Tracks natively uses the Rack api you already know, add missing
|
10
|
+
features with Rack middleware
|
11
|
+
* threaded: use Tracks for those problems where threads are the solution
|
12
|
+
* pure Ruby: install and run anywhere Ruby is available, without need for a C
|
13
|
+
compiler. Debug the whole stack. Runs great on Ruby 1.8, 1.9, JRuby,
|
14
|
+
Rubinius, and MacRuby
|
15
|
+
|
16
|
+
== Features
|
17
|
+
|
18
|
+
* handles concurrent requests
|
19
|
+
* stream requests in and responses out
|
20
|
+
* keep-alive support
|
21
|
+
* on 100-continue automatically sends continue response when #read is called on
|
22
|
+
rack.input
|
23
|
+
* graceful shutdown
|
24
|
+
* easily extensible, see
|
25
|
+
examples[http://github.com/matsadler/tracks/tree/master/examples]
|
26
|
+
|
27
|
+
== Install
|
28
|
+
|
29
|
+
gem install tracks
|
30
|
+
|
31
|
+
== Usage
|
32
|
+
|
33
|
+
Add <tt>require 'tracks'</tt> to your config.ru, like so:
|
34
|
+
|
35
|
+
require 'tracks'
|
36
|
+
use Rack::ContentLength
|
37
|
+
|
38
|
+
run(Proc.new do |env|
|
39
|
+
[200, {"Content-Type" => "text/plain"}, ["Hello world!\n"]]
|
40
|
+
end)
|
41
|
+
|
42
|
+
and start it up with <tt>rackup -stracks</tt>
|
43
|
+
|
44
|
+
you can also use the 'magic comment' <tt>#\ --server tracks</tt> in your
|
45
|
+
config.ru to default to Tracks.
|
46
|
+
|
47
|
+
== Licence
|
48
|
+
|
49
|
+
(The MIT License)
|
50
|
+
|
51
|
+
Copyright (c) 2011, 2012 Matthew Sadler
|
52
|
+
|
53
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
54
|
+
of this software and associated documentation files (the "Software"), to deal
|
55
|
+
in the Software without restriction, including without limitation the rights
|
56
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
57
|
+
copies of the Software, and to permit persons to whom the Software is
|
58
|
+
furnished to do so, subject to the following conditions:
|
59
|
+
|
60
|
+
The above copyright notice and this permission notice shall be included in
|
61
|
+
all copies or substantial portions of the Software.
|
62
|
+
|
63
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
64
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
65
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
66
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
67
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
68
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
69
|
+
THE SOFTWARE.
|
data/lib/tracks.rb
ADDED
@@ -0,0 +1,320 @@
|
|
1
|
+
%W{socket http_tools rack rack/rewindable_input rack/utils}.each {|l| require l}
|
2
|
+
|
3
|
+
Rack::Handler.register('tracks', 'Tracks')
|
4
|
+
|
5
|
+
# Tracks is a bare-bones HTTP server that talks Rack and uses a thread per
|
6
|
+
# connection model of concurrency.
|
7
|
+
#
|
8
|
+
# The simplest way to get up and running with Tracks is via rackup, in the same
|
9
|
+
# directory as your application's config.ru run
|
10
|
+
#
|
11
|
+
# rackup -rtracks -stracks
|
12
|
+
#
|
13
|
+
# Alternately you can alter your config.ru, adding to the top
|
14
|
+
#
|
15
|
+
# require "tracks"
|
16
|
+
# #\ --server tracks
|
17
|
+
#
|
18
|
+
# If you need to start up Tracks from code, the simplest way to go is
|
19
|
+
#
|
20
|
+
# require "tracks"
|
21
|
+
# Tracks.run(app, :host => host, :port => port)
|
22
|
+
#
|
23
|
+
# Where app is a Rack app, responding to #call. The ::run method will block till
|
24
|
+
# the server quits. To stop all running Tracks servers in the current process
|
25
|
+
# call ::shutdown. You may want to setup a signal handler for this, like so
|
26
|
+
#
|
27
|
+
# trap(:INT) {Tracks.shutdown}
|
28
|
+
#
|
29
|
+
# This will allow Tracks to gracefully shutdown when your program is quit with
|
30
|
+
# Ctrl-C. The signal handler must be setup before the call to ::run.
|
31
|
+
#
|
32
|
+
# A slightly more generic version of the above looks like
|
33
|
+
#
|
34
|
+
# server = Tracks.new(app, :host => host, :port => port)
|
35
|
+
# trap(:INT) {server.shutdown}
|
36
|
+
# server.listen
|
37
|
+
#
|
38
|
+
# To start a server listening on a Unix domain socket, an instance of
|
39
|
+
# UNIXServer can be given to #listen
|
40
|
+
#
|
41
|
+
# require "socket"
|
42
|
+
# server = Tracks.new(app)
|
43
|
+
# server.listen(UNIXServer.new("/tmp/tracks.sock"))
|
44
|
+
#
|
45
|
+
# If you have an already accepted socket you can use Tracks to handle the
|
46
|
+
# connection like so
|
47
|
+
#
|
48
|
+
# server = Tracks.new(app)
|
49
|
+
# server.on_connection(socket)
|
50
|
+
#
|
51
|
+
# A specific use case for this would be an inetd handler, which would look like
|
52
|
+
#
|
53
|
+
# STDERR.reopen(File.new("/dev/null", "w"))
|
54
|
+
# server = Tracks.new(app)
|
55
|
+
# server.on_connection(TCPSocket.for_fd(STDIN.fileno))
|
56
|
+
#
|
57
|
+
class Tracks
|
58
|
+
%W{rack.input HTTP_VERSION REMOTE_ADDR Connection HTTP_CONNECTION Keep-Alive
|
59
|
+
close HTTP/1.1 HTTP_EXPECT 100-continue SERVER_NAME SERVER_PORT
|
60
|
+
Content-Length Transfer-Encoding}.map do |str|
|
61
|
+
const_set(str.upcase.sub(/^[^A-Z]+/, "").gsub(/[^A-Z0-9]/, "_"), str.freeze)
|
62
|
+
end
|
63
|
+
ENV_CONSTANTS = {"rack.multithread" => true} # :nodoc:
|
64
|
+
include HTTPTools::Builder
|
65
|
+
|
66
|
+
@running = []
|
67
|
+
class << self
|
68
|
+
# class accessor, array of currently running instances
|
69
|
+
attr_accessor :running
|
70
|
+
end
|
71
|
+
|
72
|
+
# Tracks::Input is used to defer reading of the input stream. It is used
|
73
|
+
# internally to Tracks, and should not need to be created outside of Tracks.
|
74
|
+
#
|
75
|
+
# Tracks::Input is not rewindable, so will always come wrapped by a
|
76
|
+
# Rack::RewindableInput. The #read method conforms to the Rack input spec for
|
77
|
+
# #read.
|
78
|
+
#
|
79
|
+
# On initialisation the Tracks::Input instance is given an object which it
|
80
|
+
# will use to notify it's creator when input is required. This will be done
|
81
|
+
# by calling the #call method on the passed object. This call method should
|
82
|
+
# block until after a chunk of input has been fed to the Tracks::Input
|
83
|
+
# instance's #recieve_chunk method.
|
84
|
+
#
|
85
|
+
class Input
|
86
|
+
# set true when the end of the stream has been read into the internal buffer
|
87
|
+
attr_accessor :finished
|
88
|
+
|
89
|
+
# :call-seq: Input.new(reader) -> input
|
90
|
+
#
|
91
|
+
# Create a new Input instance.
|
92
|
+
#
|
93
|
+
def initialize(reader)
|
94
|
+
@reader = reader
|
95
|
+
reset
|
96
|
+
end
|
97
|
+
|
98
|
+
# :call-seq: input.read([length[, buffer]])
|
99
|
+
#
|
100
|
+
# Read at most length bytes from the input stream.
|
101
|
+
#
|
102
|
+
# Conforms to the Rack spec for the input stream's #read method:
|
103
|
+
#
|
104
|
+
# If given, length must be an non-negative Integer (>= 0) or nil, and
|
105
|
+
# buffer must be a String and may not be nil. If length is given and not
|
106
|
+
# nil, then this method reads at most length bytes from the input stream.
|
107
|
+
# If length is not given or nil, then this method reads all data until EOF.
|
108
|
+
# When EOF is reached, this method returns nil if length is given and not
|
109
|
+
# nil, or "" if length is not given or is nil. If buffer is given, then the
|
110
|
+
# read data will be placed into buffer instead of a newly created String
|
111
|
+
# object.
|
112
|
+
#
|
113
|
+
def read(length=nil, output="")
|
114
|
+
@on_initial_read.call if @on_initial_read && !@started
|
115
|
+
@started = true
|
116
|
+
|
117
|
+
if length && (@buffer || fill_buffer)
|
118
|
+
fill_buffer until @buffer.length >= length || @finished
|
119
|
+
output.replace(@buffer.slice!(0, length))
|
120
|
+
@buffer = nil if @buffer.empty?
|
121
|
+
elsif length
|
122
|
+
output = nil
|
123
|
+
elsif !@finished
|
124
|
+
fill_buffer until @finished
|
125
|
+
output.replace(@buffer || "")
|
126
|
+
@buffer = nil
|
127
|
+
end
|
128
|
+
output
|
129
|
+
end
|
130
|
+
|
131
|
+
# :call-seq: input.recieve_chunk(string) -> string
|
132
|
+
#
|
133
|
+
# Append string to the internal buffer.
|
134
|
+
#
|
135
|
+
def recieve_chunk(chunk)
|
136
|
+
if @buffer then @buffer << chunk else @buffer = chunk end
|
137
|
+
end
|
138
|
+
|
139
|
+
# :call-seq: input.first_read { block } -> block
|
140
|
+
#
|
141
|
+
# Setup a callback to be executed on when #read is first called. Only one
|
142
|
+
# callback can be set, with subsequent calls to this method overriding the
|
143
|
+
# previous. Used internally to Tracks for automatic 100-continue support.
|
144
|
+
#
|
145
|
+
def first_read(&block)
|
146
|
+
@on_initial_read = block
|
147
|
+
end
|
148
|
+
|
149
|
+
# :call-seq: input.reset -> nil
|
150
|
+
#
|
151
|
+
# Reset input, allowing it to be reused.
|
152
|
+
#
|
153
|
+
def reset
|
154
|
+
@started = false
|
155
|
+
@finished = false
|
156
|
+
@buffer = nil
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
def fill_buffer
|
161
|
+
@reader.call unless @finished
|
162
|
+
@buffer
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# :call-seq: Tracks.new(rack_app[, options]) -> server
|
167
|
+
#
|
168
|
+
# Create a new Tracks server. rack_app should be a rack application,
|
169
|
+
# responding to #call. options should be a hash, with the following optional
|
170
|
+
# keys, as symbols
|
171
|
+
#
|
172
|
+
# [:host] the host to listen on, defaults to 0.0.0.0
|
173
|
+
# [:port] the port to listen on, defaults to 9292
|
174
|
+
# [:read_timeout] the maximum amount of time, in seconds, to wait on idle
|
175
|
+
# connections, defaults to 30
|
176
|
+
#
|
177
|
+
def initialize(app, options={})
|
178
|
+
@host = options[:host] || options[:Host] || "0.0.0.0"
|
179
|
+
@port = (options[:port] || options[:Port] || "9292").to_s
|
180
|
+
@read_timeout = options[:read_timeout] || 30
|
181
|
+
@app = app
|
182
|
+
@shutdown_signal, @signal_shutdown = IO.pipe
|
183
|
+
@threads = ThreadGroup.new
|
184
|
+
end
|
185
|
+
|
186
|
+
# :call-seq: Tracks.run(rack_app[, options]) -> nil
|
187
|
+
#
|
188
|
+
# Equivalent to Tracks.new(rack_app, options).listen
|
189
|
+
#
|
190
|
+
def self.run(app, options={})
|
191
|
+
new(app, options).listen
|
192
|
+
end
|
193
|
+
|
194
|
+
# :call-seq: Tracks.shutdown([wait_time]) -> bool
|
195
|
+
#
|
196
|
+
# Shut down all running Tracks servers, after waiting wait_time seconds for
|
197
|
+
# each to gracefully shutdown. See #shutdown for more.
|
198
|
+
#
|
199
|
+
def self.shutdown(wait=30)
|
200
|
+
running.dup.inject(true) {|memo, s| s.shutdown(wait) && memo}
|
201
|
+
end
|
202
|
+
|
203
|
+
# :call-seq: server.shutdown(wait_time) -> bool
|
204
|
+
#
|
205
|
+
# Shut down the server, after waiting wait_time seconds for graceful
|
206
|
+
# shutdown. Returns true on graceful shutdown, false otherwise.
|
207
|
+
#
|
208
|
+
# wait_time defaults to 30 seconds.
|
209
|
+
#
|
210
|
+
# A return value of false indicates there were threads left running after
|
211
|
+
# wait_time had expired which were forcibly killed. This may leave resources
|
212
|
+
# in an inconsistant state, and it is advised you exit the process in this
|
213
|
+
# case (likely what you were planning anyway).
|
214
|
+
#
|
215
|
+
def shutdown(wait=30)
|
216
|
+
@shutdown = true
|
217
|
+
self.class.running.delete(self)
|
218
|
+
@signal_shutdown << "x"
|
219
|
+
wait -= sleep 1 until @threads.list.empty? || wait <= 0
|
220
|
+
@threads.list.each {|thread| thread.kill}.empty?
|
221
|
+
end
|
222
|
+
|
223
|
+
# :call-seq: server.listen([socket_server]) -> nil
|
224
|
+
#
|
225
|
+
# Start listening for/accepting connections on socket_server. socket_server
|
226
|
+
# defaults to a TCP server listening on the host and port supplied to ::new.
|
227
|
+
#
|
228
|
+
# An alternate socket server can be supplied as an argument, such as an
|
229
|
+
# instance of UNIXServer to listen on a unix domain socket.
|
230
|
+
#
|
231
|
+
# This method will block until #shutdown is called. The socket_server will
|
232
|
+
# be closed when this method returns.
|
233
|
+
#
|
234
|
+
def listen(server=TCPServer.new(@host, @port))
|
235
|
+
@shutdown = false
|
236
|
+
server.listen(1024) if server.respond_to?(:listen)
|
237
|
+
@port, @host = server.addr[1,2].map{|e| e.to_s} if server.respond_to?(:addr)
|
238
|
+
servers = [server, @shutdown_signal]
|
239
|
+
self.class.running << self
|
240
|
+
puts "Tracks HTTP server available at #{@host}:#{@port}"
|
241
|
+
while select(servers, nil, nil) && !@shutdown
|
242
|
+
@threads.add(Thread.new(server.accept) {|sock| on_connection(sock)})
|
243
|
+
end
|
244
|
+
@shutdown_signal.sysread(1) && nil
|
245
|
+
ensure
|
246
|
+
server.close
|
247
|
+
end
|
248
|
+
|
249
|
+
# :call-seq: server.on_connection(socket) -> nil
|
250
|
+
#
|
251
|
+
# Handle HTTP messages on socket, dispatching them to the rack_app supplied to
|
252
|
+
# ::new.
|
253
|
+
#
|
254
|
+
# This method will return when socket has reached EOF or has been idle for
|
255
|
+
# the read_timeout supplied to ::new. The socket will be closed when this
|
256
|
+
# method returns.
|
257
|
+
#
|
258
|
+
# Errors encountered in this method will be printed to stderr, but not raised.
|
259
|
+
#
|
260
|
+
def on_connection(socket)
|
261
|
+
parser = HTTPTools::Parser.new
|
262
|
+
buffer = ""
|
263
|
+
sockets = [socket]
|
264
|
+
reader = Proc.new do
|
265
|
+
readable, = select(sockets, nil, nil, @read_timeout)
|
266
|
+
return unless readable
|
267
|
+
begin
|
268
|
+
socket.sysread(16384, buffer)
|
269
|
+
parser << buffer
|
270
|
+
rescue HTTPTools::ParseError
|
271
|
+
socket << response(400, CONNECTION => CLOSE)
|
272
|
+
return
|
273
|
+
rescue EOFError
|
274
|
+
return
|
275
|
+
end
|
276
|
+
end
|
277
|
+
input = Input.new(reader)
|
278
|
+
parser.on(:stream) {|chunk| input.recieve_chunk(chunk)}
|
279
|
+
parser.on(:finish) {input.finished = true}
|
280
|
+
|
281
|
+
remote_family, remote_port, remote_host, remote_addr = socket.peeraddr
|
282
|
+
while true
|
283
|
+
reader.call until parser.header?
|
284
|
+
env = {SERVER_NAME => @host, SERVER_PORT => @port}.merge!(parser.env
|
285
|
+
).merge!(HTTP_VERSION => parser.version, REMOTE_ADDR => remote_addr,
|
286
|
+
RACK_INPUT => Rack::RewindableInput.new(input)).merge!(ENV_CONSTANTS)
|
287
|
+
input.first_read {socket << response(100)} if env[HTTP_EXPECT] == CONTINUE
|
288
|
+
|
289
|
+
status, header, body = @app.call(env)
|
290
|
+
|
291
|
+
header = Rack::Utils::HeaderHash.new(header)
|
292
|
+
connection_header = header[CONNECTION] || env[HTTP_CONNECTION]
|
293
|
+
keep_alive = ((parser.version.casecmp(HTTP_1_1) == 0 &&
|
294
|
+
(!connection_header || connection_header.casecmp(CLOSE) != 0)) ||
|
295
|
+
(connection_header && connection_header.casecmp(KEEP_ALIVE) == 0)) &&
|
296
|
+
!@shutdown && (header.key?(CONTENT_LENGTH) ||
|
297
|
+
header.key?(TRANSFER_ENCODING) || HTTPTools::NO_BODY[status.to_i])
|
298
|
+
header[CONNECTION] = keep_alive ? KEEP_ALIVE : CLOSE
|
299
|
+
|
300
|
+
socket << response(status, header)
|
301
|
+
body.each {|chunk| socket << chunk}
|
302
|
+
body.close if body.respond_to?(:close)
|
303
|
+
|
304
|
+
if keep_alive
|
305
|
+
reader.call until parser.finished?
|
306
|
+
input.reset
|
307
|
+
remainder = parser.rest.lstrip
|
308
|
+
parser.reset << remainder
|
309
|
+
else
|
310
|
+
break
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
rescue StandardError, LoadError, SyntaxError => e
|
315
|
+
STDERR.puts("#{e.class}: #{e.message} #{e.backtrace.join("\n")}")
|
316
|
+
ensure
|
317
|
+
socket.close
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tracks
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matthew Sadler
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-21 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: &70357509168700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70357509168700
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: http_tools
|
27
|
+
requirement: &70357509168220 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.4.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70357509168220
|
36
|
+
description: A bare-bones Ruby HTTP server that talks Rack and uses a thread per connection
|
37
|
+
model of concurrency.
|
38
|
+
email: mat@sourcetagsandcodes.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.rdoc
|
43
|
+
files:
|
44
|
+
- lib/tracks.rb
|
45
|
+
- README.rdoc
|
46
|
+
homepage: http://github.com/matsadler/tracks
|
47
|
+
licenses: []
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options:
|
50
|
+
- --main
|
51
|
+
- README.rdoc
|
52
|
+
- --charset
|
53
|
+
- utf-8
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 1.8.10
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: Threaded Ruby Rack HTTP server
|
74
|
+
test_files: []
|