senotrusov-ruby-daemonic-threads 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+