crampy 0.15.3

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009-2011 Pratik Naik
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/cramp ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cramp'
4
+ require 'cramp/generators/application'
5
+
6
+ if ['--version', '-v'].include?(ARGV.first)
7
+ puts "Cramp #{Cramp::VERSION}"
8
+ exit(0)
9
+ end
10
+
11
+ if ARGV.first != "new"
12
+ ARGV[0] = "--help"
13
+ else
14
+ ARGV.shift
15
+ end
16
+
17
+ Cramp::Generators::Application.start
@@ -0,0 +1,104 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ module Cramp
4
+ class Abstract
5
+ include Callbacks
6
+ include FiberPool
7
+
8
+ class_attribute :transport
9
+ self.transport = :regular
10
+
11
+ class << self
12
+ def call(env)
13
+ new(env).process
14
+ end
15
+ end
16
+
17
+ def initialize(env)
18
+ @env = env
19
+ @finished = false
20
+
21
+ @_state = :init
22
+ end
23
+
24
+ def process
25
+ EM.next_tick { before_start }
26
+ throw :async
27
+ end
28
+
29
+ protected
30
+
31
+ def continue
32
+ init_async_body
33
+ send_headers
34
+
35
+ @_state = :started
36
+ EM.next_tick { on_start }
37
+ end
38
+
39
+ def send_headers
40
+ status, headers = build_headers
41
+ send_initial_response(status, headers, @body)
42
+ rescue StandardError, LoadError, SyntaxError => exception
43
+ handle_exception(exception)
44
+ end
45
+
46
+ def build_headers
47
+ status, headers = respond_to?(:respond_with, true) ? respond_with.dup : [200, {'Content-Type' => 'text/html'}]
48
+ headers['Connection'] ||= 'keep-alive'
49
+ [status, headers]
50
+ end
51
+
52
+ def init_async_body
53
+ @body = Body.new
54
+
55
+ if self.class.on_finish_callbacks.any?
56
+ @body.callback { on_finish }
57
+ @body.errback { on_finish }
58
+ end
59
+ end
60
+
61
+ def finished?
62
+ !!@finished
63
+ end
64
+
65
+ def finish
66
+ @_state = :finishing
67
+ @body.succeed if is_finishable?
68
+ ensure
69
+ @_state = :finished
70
+ @finished = true
71
+ end
72
+
73
+ def send_initial_response(response_status, response_headers, response_body)
74
+ send_response(response_status, response_headers, response_body)
75
+ end
76
+
77
+ def halt(status, headers = {}, halt_body = '')
78
+ send_response(status, headers, halt_body)
79
+ end
80
+
81
+ def send_response(response_status, response_headers, response_body)
82
+ @env['async.callback'].call [response_status, response_headers, response_body]
83
+ end
84
+
85
+ def request
86
+ @request ||= Rack::Request.new(@env)
87
+ end
88
+
89
+ def params
90
+ @params ||= request.params.update(route_params).symbolize_keys
91
+ end
92
+
93
+ def route_params
94
+ @env['router.params'] || {}
95
+ end
96
+
97
+ private
98
+
99
+ def is_finishable?
100
+ !finished? && @body && !@body.closed?
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,134 @@
1
+ module Cramp
2
+ class Action < Abstract
3
+ include PeriodicTimer
4
+ include KeepConnectionAlive
5
+
6
+ def initialize(env)
7
+ super
8
+
9
+ case
10
+ when Faye::EventSource.eventsource?(env)
11
+ # request has Accept: text/event-stream
12
+ # faye server adapter intercepts headers - need to send them in send_initial_response or use faye's implementation
13
+ @eventsource_detected = true
14
+ unless transport == :sse
15
+ err = "WARNING: Cramp got request with EventSource header on action with transport #{transport} (not sse)! Response may not contain valid http headers!"
16
+ Cramp.logger ? Cramp.logger.error(err) : $stderr.puts(err)
17
+ end
18
+ when Faye::WebSocket.websocket?(env)
19
+ @web_socket = Faye::WebSocket.new(env)
20
+ @web_socket.onmessage = lambda do |event|
21
+ message = event.data
22
+ _invoke_data_callbacks(message) if message.is_a?(String)
23
+ end
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def render(body, *args)
30
+ send(:"render_#{transport}", body, *args)
31
+ end
32
+
33
+ def send_initial_response(status, headers, body)
34
+ case transport
35
+ when :long_polling
36
+ # Dont send no initial response. Just cache it for later.
37
+ @_lp_status = status
38
+ @_lp_headers = headers
39
+ when :sse
40
+ super
41
+ if @eventsource_detected
42
+ # Reconstruct headers that were killed by faye server adapter:
43
+ @body.call("HTTP/1.1 200 OK\r\n#{headers.map{|(k,v)| "#{k}: #{v.is_a?(Time) ? v.httpdate : v.to_s}"}.join("\r\n")}\r\n\r\n")
44
+ end
45
+ # send retry? @body.call("retry: #{ (@retry * 1000).floor }\r\n\r\n")
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ class_attribute :default_sse_headers
52
+ self.default_sse_headers = {'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache, no-store', 'Connection' => 'keep-alive'}
53
+
54
+ class_attribute :default_chunked_headers
55
+ self.default_chunked_headers = {'Transfer-Encoding' => 'chunked', 'Connection' => 'keep-alive'}
56
+
57
+ def build_headers
58
+ case transport
59
+ when :sse
60
+ status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {'Content-Type' => 'text/html'}]
61
+ [status, headers.merge(self.default_sse_headers)]
62
+ when :chunked
63
+ status, headers = respond_to?(:respond_with, true) ? respond_with : [200, {}]
64
+
65
+ headers = headers.merge(self.default_chunked_headers)
66
+ headers['Content-Type'] ||= 'text/html'
67
+ headers['Cache-Control'] ||= 'no-cache'
68
+
69
+ [status, headers]
70
+ else
71
+ super
72
+ end
73
+ end
74
+
75
+ def render_regular(body, *)
76
+ @body.call(body)
77
+ end
78
+
79
+ def render_long_polling(data, *)
80
+ @_lp_headers['Content-Length'] = data.size.to_s
81
+
82
+ send_response(@_lp_status, @_lp_headers, @body)
83
+ @body.call(data)
84
+
85
+ finish
86
+ end
87
+
88
+ def render_sse(data, options = {})
89
+ #TODO: Faye uses \r\n for newlines, some compatibility?
90
+ result = "id: #{sse_event_id}\n"
91
+ result << "event: #{options[:event]}\n" if options[:event]
92
+ result << "retry: #{options[:retry]}\n" if options[:retry]
93
+
94
+ data.split(/\n/).each {|d| result << "data: #{d}\n" }
95
+ result << "\n"
96
+
97
+ @body.call(result)
98
+ end
99
+
100
+ def render_websocket(body, *)
101
+ @web_socket.send(body)
102
+ end
103
+
104
+ CHUNKED_TERM = "\r\n"
105
+ CHUNKED_TAIL = "0#{CHUNKED_TERM}#{CHUNKED_TERM}"
106
+
107
+ def render_chunked(body, *)
108
+ data = [Rack::Utils.bytesize(body).to_s(16), CHUNKED_TERM, body, CHUNKED_TERM].join
109
+
110
+ @body.call(data)
111
+ end
112
+
113
+ # Used by SSE
114
+ def sse_event_id
115
+ @sse_event_id ||= Time.now.to_i
116
+ end
117
+
118
+ def encode(string, encoding = 'UTF-8')
119
+ string.respond_to?(:force_encoding) ? string.force_encoding(encoding) : string
120
+ end
121
+
122
+ protected
123
+
124
+ def finish
125
+ case transport
126
+ when :chunked
127
+ @body.call(CHUNKED_TAIL) if is_finishable?
128
+ end
129
+
130
+ super
131
+ end
132
+
133
+ end
134
+ end
data/lib/cramp/body.rb ADDED
@@ -0,0 +1,48 @@
1
+ # Copyright 2008 James Tucker <raggi@rubyforge.org>.
2
+
3
+ module Cramp
4
+ class Body
5
+ include EventMachine::Deferrable
6
+
7
+ def initialize
8
+ @queue = []
9
+
10
+ # Make sure to flush out the queue before closing the connection
11
+ callback { flush }
12
+ end
13
+
14
+ def call(body)
15
+ @queue << body
16
+ schedule_dequeue
17
+ end
18
+
19
+ def each &blk
20
+ @body_callback = blk
21
+ schedule_dequeue
22
+ end
23
+
24
+ def closed?
25
+ @deferred_status != :unknown
26
+ end
27
+
28
+ def flush
29
+ return unless @body_callback
30
+
31
+ until @queue.empty?
32
+ Array(@queue.shift).each {|chunk| @body_callback.call(chunk) }
33
+ end
34
+ end
35
+
36
+ def schedule_dequeue
37
+ return unless @body_callback
38
+
39
+ EventMachine.next_tick do
40
+ next unless body = @queue.shift
41
+
42
+ Array(body).each {|chunk| @body_callback.call(chunk) }
43
+ schedule_dequeue unless @queue.empty?
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,92 @@
1
+ module Cramp
2
+ module Callbacks
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ #was class_inheritable_accessor
8
+ class_attribute :before_start_callbacks, :on_finish_callbacks, :on_start_callback, :on_data_callbacks, :instance_reader => false
9
+
10
+ self.before_start_callbacks = []
11
+ self.on_finish_callbacks = []
12
+ self.on_start_callback = []
13
+ self.on_data_callbacks = []
14
+ end
15
+
16
+ module ClassMethods
17
+ def before_start(*methods)
18
+ self.before_start_callbacks += methods
19
+ end
20
+
21
+ def on_finish(*methods)
22
+ self.on_finish_callbacks += methods
23
+ end
24
+
25
+ def on_start(*methods)
26
+ self.on_start_callback += methods
27
+ end
28
+
29
+ def on_data(*methods)
30
+ self.on_data_callbacks += methods
31
+ end
32
+ end
33
+
34
+ def before_start(n = 0)
35
+ if callback = self.class.before_start_callbacks[n]
36
+ callback_wrapper { send(callback) { before_start(n+1) } }
37
+ else
38
+ continue
39
+ end
40
+ end
41
+
42
+ def on_start
43
+ callback_wrapper { start } if respond_to?(:start)
44
+
45
+ self.class.on_start_callback.each do |callback|
46
+ callback_wrapper { send(callback) unless @finished }
47
+ end
48
+ end
49
+
50
+ def on_finish
51
+ self.class.on_finish_callbacks.each do |callback|
52
+ callback_wrapper { send(callback) }
53
+ end
54
+ end
55
+
56
+ def callback_wrapper
57
+ EM.next_tick do
58
+ begin
59
+ yield
60
+ rescue StandardError, LoadError, SyntaxError => exception
61
+ handle_exception(exception)
62
+ end
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ def _invoke_data_callbacks(message)
69
+ self.class.on_data_callbacks.each do |callback|
70
+ callback_wrapper { send(callback, message) }
71
+ end
72
+ end
73
+
74
+ def handle_exception(exception)
75
+ handler = ExceptionHandler.new(@env, exception)
76
+
77
+ # Log the exception
78
+ unless ENV['RACK_ENV'] == 'test'
79
+ exception_body = handler.dump_exception
80
+ Cramp.logger ? Cramp.logger.error(exception_body) : $stderr.puts(exception_body)
81
+ end
82
+
83
+ case @_state
84
+ when :init
85
+ halt 500, {"Content-Type" => 'text/html'}, ENV['RACK_ENV'] == 'development' ? handler.pretty : 'Something went wrong'
86
+ else
87
+ finish
88
+ end
89
+ end
90
+
91
+ end
92
+ end