tracks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.rdoc +69 -0
  2. data/lib/tracks.rb +320 -0
  3. 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: []