senotrusov-ruby-daemonic-threads 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,378 @@
1
+
2
+ # Copyright 2009 Stanislav Senotrusov <senotrusov@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ # Copyright (c) 2004-2009 David Heinemeier Hansson
18
+ #
19
+ # Permission is hereby granted, free of charge, to any person obtaining
20
+ # a copy of this software and associated documentation files (the
21
+ # "Software"), to deal in the Software without restriction, including
22
+ # without limitation the rights to use, copy, modify, merge, publish,
23
+ # distribute, sublicense, and/or sell copies of the Software, and to
24
+ # permit persons to whom the Software is furnished to do so, subject to
25
+ # the following conditions:
26
+ #
27
+ # The above copyright notice and this permission notice shall be
28
+ # included in all copies or substantial portions of the Software.
29
+ #
30
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
31
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
32
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
33
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
34
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
35
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
36
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37
+
38
+
39
+ class DaemonicThreads::HTTP::HttpRequest
40
+
41
+ # initialize() happens outside daemon exception handling, so it must be lightweight.
42
+ # More robust processing goes to parse()
43
+ # Mongrel handles exceptions too, but it does not logs request details.
44
+ #
45
+ def initialize request, response
46
+ @request = request
47
+ @response = response
48
+
49
+ @mutex = Mutex.new
50
+ end
51
+
52
+ attr_reader :params, :mutex
53
+
54
+ def [] param
55
+ @params[param]
56
+ end
57
+
58
+ def []= param, value
59
+ @params[param] = value
60
+ end
61
+
62
+ def env
63
+ @request.params
64
+ end
65
+
66
+ def body
67
+ @mutex.synchronize do
68
+ result = @request.body.read(@request.params["CONTENT_LENGTH"].to_i)
69
+ @request.body.rewind if @request.body.respond_to?(:rewind)
70
+ result
71
+ end
72
+ end
73
+
74
+ def parse
75
+ @params = parse_rails_style_params
76
+ parse_path_info
77
+ end
78
+
79
+ private
80
+
81
+ def parse_rails_style_params
82
+ result = {}
83
+
84
+ result.update(Rack::Utils.parse_nested_query @request.params["QUERY_STRING"]) if @request.params["QUERY_STRING"]
85
+ result.update(parse_body_params) if @request.params["CONTENT_LENGTH"].to_i > 0
86
+
87
+ normalize_parameters(result)
88
+ end
89
+
90
+
91
+ # based on ActionController::ParamsParser
92
+ #
93
+ # TODO: Handle form data from POST (look at rack/request.rb, def POST, rescue EOFError)
94
+ def parse_body_params
95
+ case Mime::Type.lookup @request.params["CONTENT_TYPE"]
96
+ when Mime::XML
97
+ data = body
98
+ data.blank? ? {} : Hash.from_xml(data)
99
+
100
+ when Mime::JSON
101
+ data = body
102
+ if data.blank?
103
+ {}
104
+ else
105
+ data = ActiveSupport::JSON.decode(data)
106
+ data = {:_json => data} unless data.is_a?(Hash)
107
+ data
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+ # based on ActionController::Request
114
+ #
115
+ # Convert nested Hashs to HashWithIndifferentAccess and replace
116
+ # file upload hashs with UploadedFile objects
117
+ def normalize_parameters(value)
118
+ case value
119
+ when Hash
120
+ if value.has_key?(:tempfile)
121
+ upload = value[:tempfile]
122
+ upload.extend(UploadedFile)
123
+ upload.original_path = value[:filename]
124
+ upload.content_type = value[:type]
125
+ upload
126
+ else
127
+ h = {}
128
+ value.each { |k, v| h[k] = normalize_parameters(v) }
129
+ h.with_indifferent_access
130
+ end
131
+ when Array
132
+ value.map { |e| normalize_parameters(e) }
133
+ else
134
+ value
135
+ end
136
+ end
137
+
138
+
139
+ # based on ActionController::Base
140
+ def normalize_status_code status_code
141
+ status_code.kind_of?(Fixnum) ? status_code : ActionController::StatusCodes::SYMBOL_TO_STATUS_CODE[status_code]
142
+ end
143
+
144
+
145
+ REQUESTED_FORMAT_RE = /\.([\w\d]+)\z/
146
+
147
+ # "" ("/foobars")
148
+ # ".xml" ("/foobars.xml")
149
+ # "/0001" ("/foobars/0001")
150
+ # "/0001.xml" ("/foobars/0001.xml")
151
+ # "/0001/action" ("/foobars/0001/action")
152
+ # "/0001/action.xml" ("/foobars/0001/action.xml")
153
+ #
154
+ # This is the edge case for URI parsing.
155
+ # For now action really goes to @requested_id, and we checks controller for respond_to?(@requested_id)
156
+ # If you are using short alphanumeric ids they can be clashed with some method name.
157
+ #
158
+ # "/action" ("/foobars/0001/action")
159
+ # "/action.xml" ("/foobars/0001/action.xml")
160
+ #
161
+ def parse_path_info
162
+ # "".split(/\//)
163
+ # ".xml".split(/\//)
164
+ # "/0001".split(/\//)
165
+ # "/0001.xml".split(/\//)
166
+ # "/0001/action".split(/\//)
167
+ # "/0001/action.xml".split(/\//)
168
+ splitted = @request.params["PATH_INFO"].split(/\//)
169
+
170
+ if matched_format = splitted.last.match(REQUESTED_FORMAT_RE)
171
+ @requested_format = Mime::Type.lookup_by_extension(matched_format[1]) # nil or string
172
+ splitted.last.gsub!(REQUESTED_FORMAT_RE, '')
173
+ end
174
+
175
+ @requested_id = splitted[1] # nil or string
176
+ @requested_action = splitted[2] # nil or string
177
+ end
178
+
179
+
180
+ public
181
+
182
+ attr_reader :requested_format, :requested_id, :requested_action # nil or string
183
+
184
+ def correct?
185
+ [Mime::XML, Mime::JSON, nil].include?(requested_format)
186
+ end
187
+
188
+ def request_method
189
+ @request.params["REQUEST_METHOD"]
190
+ end
191
+
192
+ def head?
193
+ @request.params["REQUEST_METHOD"] == "HEAD"
194
+ end
195
+
196
+ def get?
197
+ @request.params["REQUEST_METHOD"] == "GET"
198
+ end
199
+
200
+ def post?
201
+ @request.params["REQUEST_METHOD"] == "POST"
202
+ end
203
+
204
+ def put?
205
+ @request.params["REQUEST_METHOD"] == "PUT"
206
+ end
207
+
208
+ def delete?
209
+ @request.params["REQUEST_METHOD"] == "DELETE"
210
+ end
211
+
212
+
213
+ def log! logger, severity = :fatal, title = nil
214
+ logger.__send__(severity, "#{title} -- #{self.inspect rescue "EXCEPTION CALLING inspect()"}\n -- Request body: #{body.inspect rescue "EXCEPTION CALLING body()"}")
215
+ logger.flush if logger.respond_to?(:flush)
216
+ end
217
+
218
+
219
+ def response_sent?
220
+ @mutex.synchronize do
221
+ @response_sent
222
+ end
223
+ end
224
+
225
+
226
+ def error(status, body = nil)
227
+ @mutex.synchronize do
228
+ return if @response_sent
229
+ @response_sent = true
230
+
231
+ @response.start(normalize_status_code status) do |head, out|
232
+ head["Content-Type"] = "text/plain"
233
+ out.write("ERROR: #{body || status}")
234
+ end
235
+ end
236
+ end
237
+
238
+
239
+ # Based on ActionController::Base
240
+ #
241
+ # Return a response that has no content (merely headers). The options
242
+ # argument is interpreted to be a hash of header names and values.
243
+ # This allows you to easily return a response that consists only of
244
+ # significant headers:
245
+ #
246
+ # request.head :created, :location => url_for(person)
247
+ #
248
+ # It can also be used to return exceptional conditions:
249
+ #
250
+ # return request.head(:method_not_allowed) unless request.post?
251
+ # return request.head(:bad_request) unless valid_request?
252
+ #
253
+ def head(*args)
254
+ if args.length > 2
255
+ raise ArgumentError, "too many arguments to head"
256
+ elsif args.empty?
257
+ raise ArgumentError, "too few arguments to head"
258
+ end
259
+
260
+ options = args.extract_options!
261
+
262
+ status_code = normalize_status_code(args.shift || options.delete(:status) || :ok)
263
+
264
+ @mutex.synchronize do
265
+ raise(DaemonicThreads::HTTP::DoubleResponseError, "Can only response once per request") if @response_sent
266
+ @response_sent = true
267
+
268
+ @response.start(status_code) do |head, out|
269
+ options.each do |key, value|
270
+ head[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s
271
+ end
272
+ end
273
+ end
274
+
275
+ end
276
+
277
+
278
+ # Based on ActionController::Base
279
+ #
280
+ # response object||string [, options]
281
+ # response :xml => object||string [, options]
282
+ # response :json => object||string [, options]
283
+ # response :text => string [, options]
284
+ #
285
+ # Options
286
+ # :status
287
+ # :location
288
+ # :callback
289
+ # :content_type
290
+ #
291
+ # TODO: response :update do
292
+ # TODO: response :js =>
293
+ # TODO: response :file => filename [, options]
294
+ #
295
+ def response options, extra_options = {}
296
+
297
+ if options.kind_of?(Hash)
298
+ options.update(extra_options)
299
+
300
+ if data = options[:xml]
301
+ format = Mime::XML
302
+ elsif data = options[:json]
303
+ format = Mime::JSON
304
+ elsif data = options[:text]
305
+ data = data.to_s
306
+ format = Mime::HTML
307
+ else
308
+ raise "You must response with something!"
309
+ end
310
+ else
311
+ data = options
312
+ options = extra_options
313
+
314
+ if requested_format
315
+ format = requested_format
316
+ elsif data.kind_of?(String)
317
+ format = Mime::HTML
318
+ else
319
+ format = Mime::XML
320
+ end
321
+ end
322
+
323
+ raise "You force response format `#{format}' but user requests `#{requested_format}'" if format && requested_format && format != requested_format
324
+
325
+ case format
326
+ when Mime::XML
327
+ data = data.to_xml unless data.kind_of?(String)
328
+ when Mime::JSON
329
+ data = data.to_json unless data.kind_of?(String)
330
+ data = "#{options[:callback]}(#{data})" unless options[:callback].blank?
331
+ end
332
+
333
+
334
+ status_code = normalize_status_code(options[:status] || :ok)
335
+
336
+ if location = options[:location]
337
+ location = url_for(location) unless location.kind_of?(String)
338
+ end
339
+
340
+ @mutex.synchronize do
341
+ raise(DaemonicThreads::HTTP::DoubleResponseError, "Can only response once per request") if @response_sent
342
+ @response_sent = true
343
+
344
+ @response.start(status_code) do |head, out|
345
+ head["Location"] = location if location
346
+ head["Content-Type"] = (options[:content_type] || format).to_s
347
+ out.write(data) unless head?
348
+ end
349
+ end
350
+ end
351
+
352
+
353
+ # Based on ActionController::Base
354
+ #
355
+ # url_for [Object respond_to?(:id)], options
356
+ #
357
+ # :controller -- full absolute path, can be found in .uri()
358
+ # :id
359
+ # :action
360
+ # :format
361
+ #
362
+ # Without any options it returns full URL of current controller
363
+ #
364
+ def url_for options = {}, extra_options = {}
365
+
366
+ options = {:id => options.id} unless options.kind_of?(Hash)
367
+ options.update(extra_options)
368
+
369
+ url = "http://#{env["HTTP_HOST"]}"
370
+ url += options[:controller] ? options[:controller] : env["SCRIPT_NAME"]
371
+ url += "/#{options[:id]}" if options[:id]
372
+ url += "/#{options[:action]}" if options[:action]
373
+ url += ".#{options[:format]}" if options[:format]
374
+ url
375
+ end
376
+
377
+ end
378
+
@@ -0,0 +1,78 @@
1
+
2
+ # Copyright 2009 Stanislav Senotrusov <senotrusov@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ # Почему я не использую Rack
18
+ #
19
+ # 1. У rack нет unregister handle
20
+ #
21
+ # 2. Не понятно - нужно ли хоть что-то из всего разнообразия веб-серверов доступных через Rack
22
+ #
23
+ # 3. Не понятно как работают тругие сервера с тредами.
24
+ # Мы забираем тред из mongrel's ThreadGroup. Как на это отреагируют другие сервера.
25
+ #
26
+ # 4. У mongrel есть run/join/stop
27
+
28
+ class DaemonicThreads::HTTP::Server
29
+ DEFAULT_HTTP_PORT = 4000
30
+ DEFAULT_HTTP_BINDING = "127.0.0.1"
31
+
32
+ def initialize(process)
33
+ argv = process.controller.argv
34
+
35
+ argv.option "-b, --binding IPADDR", "IP address to bind to, #{DEFAULT_HTTP_BINDING} as default"
36
+ argv.option "-p, --port PORT", "HTTP port to listen to, #{DEFAULT_HTTP_PORT} as default"
37
+
38
+ argv.parse!
39
+
40
+ @binding = argv["binding"] || DEFAULT_HTTP_BINDING
41
+ @port = argv["port"] || DEFAULT_HTTP_PORT
42
+ @prefix = process.name
43
+
44
+ @server = Mongrel::HttpServer.new(@binding, @port)
45
+
46
+ @mutex = Mutex.new
47
+ end
48
+
49
+ attr_reader :prefix
50
+
51
+ def start
52
+ @server.run
53
+
54
+ register("/#{@prefix}/status", Mongrel::StatusHandler.new)
55
+ end
56
+
57
+ def join
58
+ @server.acceptor.join if @server.acceptor
59
+ end
60
+
61
+ def stop
62
+ @server.stop
63
+ end
64
+
65
+ def register uri, handler
66
+ @mutex.synchronize do
67
+ @server.register uri, handler
68
+ end
69
+ end
70
+
71
+ def unregister uri
72
+ @mutex.synchronize do
73
+ @server.unregister uri
74
+ end
75
+ end
76
+
77
+ end
78
+