omg-actionpack 8.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +129 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +57 -0
- data/lib/abstract_controller/asset_paths.rb +14 -0
- data/lib/abstract_controller/base.rb +299 -0
- data/lib/abstract_controller/caching/fragments.rb +149 -0
- data/lib/abstract_controller/caching.rb +68 -0
- data/lib/abstract_controller/callbacks.rb +265 -0
- data/lib/abstract_controller/collector.rb +44 -0
- data/lib/abstract_controller/deprecator.rb +9 -0
- data/lib/abstract_controller/error.rb +8 -0
- data/lib/abstract_controller/helpers.rb +243 -0
- data/lib/abstract_controller/logger.rb +16 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +25 -0
- data/lib/abstract_controller/rendering.rb +126 -0
- data/lib/abstract_controller/translation.rb +42 -0
- data/lib/abstract_controller/url_for.rb +37 -0
- data/lib/abstract_controller.rb +36 -0
- data/lib/action_controller/api/api_rendering.rb +18 -0
- data/lib/action_controller/api.rb +155 -0
- data/lib/action_controller/base.rb +332 -0
- data/lib/action_controller/caching.rb +49 -0
- data/lib/action_controller/deprecator.rb +9 -0
- data/lib/action_controller/form_builder.rb +55 -0
- data/lib/action_controller/log_subscriber.rb +96 -0
- data/lib/action_controller/metal/allow_browser.rb +123 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
- data/lib/action_controller/metal/conditional_get.rb +341 -0
- data/lib/action_controller/metal/content_security_policy.rb +86 -0
- data/lib/action_controller/metal/cookies.rb +20 -0
- data/lib/action_controller/metal/data_streaming.rb +154 -0
- data/lib/action_controller/metal/default_headers.rb +21 -0
- data/lib/action_controller/metal/etag_with_flash.rb +22 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +59 -0
- data/lib/action_controller/metal/exceptions.rb +106 -0
- data/lib/action_controller/metal/flash.rb +67 -0
- data/lib/action_controller/metal/head.rb +67 -0
- data/lib/action_controller/metal/helpers.rb +129 -0
- data/lib/action_controller/metal/http_authentication.rb +565 -0
- data/lib/action_controller/metal/implicit_render.rb +67 -0
- data/lib/action_controller/metal/instrumentation.rb +120 -0
- data/lib/action_controller/metal/live.rb +398 -0
- data/lib/action_controller/metal/logging.rb +22 -0
- data/lib/action_controller/metal/mime_responds.rb +337 -0
- data/lib/action_controller/metal/parameter_encoding.rb +84 -0
- data/lib/action_controller/metal/params_wrapper.rb +312 -0
- data/lib/action_controller/metal/permissions_policy.rb +38 -0
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +251 -0
- data/lib/action_controller/metal/renderers.rb +181 -0
- data/lib/action_controller/metal/rendering.rb +260 -0
- data/lib/action_controller/metal/request_forgery_protection.rb +667 -0
- data/lib/action_controller/metal/rescue.rb +33 -0
- data/lib/action_controller/metal/streaming.rb +183 -0
- data/lib/action_controller/metal/strong_parameters.rb +1546 -0
- data/lib/action_controller/metal/testing.rb +25 -0
- data/lib/action_controller/metal/url_for.rb +65 -0
- data/lib/action_controller/metal.rb +339 -0
- data/lib/action_controller/railtie.rb +149 -0
- data/lib/action_controller/railties/helpers.rb +26 -0
- data/lib/action_controller/renderer.rb +161 -0
- data/lib/action_controller/template_assertions.rb +13 -0
- data/lib/action_controller/test_case.rb +691 -0
- data/lib/action_controller.rb +80 -0
- data/lib/action_dispatch/constants.rb +34 -0
- data/lib/action_dispatch/deprecator.rb +9 -0
- data/lib/action_dispatch/http/cache.rb +249 -0
- data/lib/action_dispatch/http/content_disposition.rb +47 -0
- data/lib/action_dispatch/http/content_security_policy.rb +365 -0
- data/lib/action_dispatch/http/filter_parameters.rb +80 -0
- data/lib/action_dispatch/http/filter_redirect.rb +50 -0
- data/lib/action_dispatch/http/headers.rb +134 -0
- data/lib/action_dispatch/http/mime_negotiation.rb +187 -0
- data/lib/action_dispatch/http/mime_type.rb +389 -0
- data/lib/action_dispatch/http/mime_types.rb +54 -0
- data/lib/action_dispatch/http/parameters.rb +119 -0
- data/lib/action_dispatch/http/permissions_policy.rb +189 -0
- data/lib/action_dispatch/http/rack_cache.rb +67 -0
- data/lib/action_dispatch/http/request.rb +498 -0
- data/lib/action_dispatch/http/response.rb +556 -0
- data/lib/action_dispatch/http/upload.rb +107 -0
- data/lib/action_dispatch/http/url.rb +344 -0
- data/lib/action_dispatch/journey/formatter.rb +226 -0
- data/lib/action_dispatch/journey/gtg/builder.rb +149 -0
- data/lib/action_dispatch/journey/gtg/simulator.rb +50 -0
- data/lib/action_dispatch/journey/gtg/transition_table.rb +217 -0
- data/lib/action_dispatch/journey/nfa/dot.rb +27 -0
- data/lib/action_dispatch/journey/nodes/node.rb +208 -0
- data/lib/action_dispatch/journey/parser.rb +103 -0
- data/lib/action_dispatch/journey/path/pattern.rb +209 -0
- data/lib/action_dispatch/journey/route.rb +189 -0
- data/lib/action_dispatch/journey/router/utils.rb +105 -0
- data/lib/action_dispatch/journey/router.rb +151 -0
- data/lib/action_dispatch/journey/routes.rb +82 -0
- data/lib/action_dispatch/journey/scanner.rb +70 -0
- data/lib/action_dispatch/journey/visitors.rb +267 -0
- data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
- data/lib/action_dispatch/journey/visualizer/fsm.js +159 -0
- data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
- data/lib/action_dispatch/journey.rb +7 -0
- data/lib/action_dispatch/log_subscriber.rb +25 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
- data/lib/action_dispatch/middleware/callbacks.rb +38 -0
- data/lib/action_dispatch/middleware/cookies.rb +719 -0
- data/lib/action_dispatch/middleware/debug_exceptions.rb +206 -0
- data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
- data/lib/action_dispatch/middleware/debug_view.rb +73 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +350 -0
- data/lib/action_dispatch/middleware/executor.rb +32 -0
- data/lib/action_dispatch/middleware/flash.rb +318 -0
- data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +64 -0
- data/lib/action_dispatch/middleware/reloader.rb +16 -0
- data/lib/action_dispatch/middleware/remote_ip.rb +199 -0
- data/lib/action_dispatch/middleware/request_id.rb +50 -0
- data/lib/action_dispatch/middleware/server_timing.rb +78 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +112 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +66 -0
- data/lib/action_dispatch/middleware/session/cookie_store.rb +129 -0
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +34 -0
- data/lib/action_dispatch/middleware/show_exceptions.rb +88 -0
- data/lib/action_dispatch/middleware/ssl.rb +180 -0
- data/lib/action_dispatch/middleware/stack.rb +194 -0
- data/lib/action_dispatch/middleware/static.rb +192 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +17 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +35 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +284 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +232 -0
- data/lib/action_dispatch/railtie.rb +77 -0
- data/lib/action_dispatch/request/session.rb +283 -0
- data/lib/action_dispatch/request/utils.rb +109 -0
- data/lib/action_dispatch/routing/endpoint.rb +19 -0
- data/lib/action_dispatch/routing/inspector.rb +323 -0
- data/lib/action_dispatch/routing/mapper.rb +2372 -0
- data/lib/action_dispatch/routing/polymorphic_routes.rb +363 -0
- data/lib/action_dispatch/routing/redirection.rb +218 -0
- data/lib/action_dispatch/routing/route_set.rb +958 -0
- data/lib/action_dispatch/routing/routes_proxy.rb +66 -0
- data/lib/action_dispatch/routing/url_for.rb +244 -0
- data/lib/action_dispatch/routing.rb +262 -0
- data/lib/action_dispatch/system_test_case.rb +206 -0
- data/lib/action_dispatch/system_testing/browser.rb +75 -0
- data/lib/action_dispatch/system_testing/driver.rb +85 -0
- data/lib/action_dispatch/system_testing/server.rb +33 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
- data/lib/action_dispatch/testing/assertion_response.rb +48 -0
- data/lib/action_dispatch/testing/assertions/response.rb +114 -0
- data/lib/action_dispatch/testing/assertions/routing.rb +343 -0
- data/lib/action_dispatch/testing/assertions.rb +25 -0
- data/lib/action_dispatch/testing/integration.rb +694 -0
- data/lib/action_dispatch/testing/request_encoder.rb +60 -0
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +57 -0
- data/lib/action_dispatch/testing/test_request.rb +73 -0
- data/lib/action_dispatch/testing/test_response.rb +58 -0
- data/lib/action_dispatch.rb +147 -0
- data/lib/action_pack/gem_version.rb +19 -0
- data/lib/action_pack/version.rb +12 -0
- data/lib/action_pack.rb +27 -0
- metadata +375 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "abstract_controller/logger"
|
6
|
+
|
7
|
+
module ActionController
|
8
|
+
# # Action Controller Instrumentation
|
9
|
+
#
|
10
|
+
# Adds instrumentation to several ends in ActionController::Base. It also
|
11
|
+
# provides some hooks related with process_action. This allows an ORM like
|
12
|
+
# Active Record and/or DataMapper to plug in ActionController and show related
|
13
|
+
# information.
|
14
|
+
#
|
15
|
+
# Check ActiveRecord::Railties::ControllerRuntime for an example.
|
16
|
+
module Instrumentation
|
17
|
+
extend ActiveSupport::Concern
|
18
|
+
|
19
|
+
include AbstractController::Logger
|
20
|
+
|
21
|
+
attr_internal :view_runtime
|
22
|
+
|
23
|
+
def initialize(...) # :nodoc:
|
24
|
+
super
|
25
|
+
self.view_runtime = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def render(*)
|
29
|
+
render_output = nil
|
30
|
+
self.view_runtime = cleanup_view_runtime do
|
31
|
+
ActiveSupport::Benchmark.realtime(:float_millisecond) { render_output = super }
|
32
|
+
end
|
33
|
+
render_output
|
34
|
+
end
|
35
|
+
|
36
|
+
def send_file(path, options = {})
|
37
|
+
ActiveSupport::Notifications.instrument("send_file.action_controller",
|
38
|
+
options.merge(path: path)) do
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def send_data(data, options = {})
|
44
|
+
ActiveSupport::Notifications.instrument("send_data.action_controller", options) do
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def redirect_to(*)
|
50
|
+
ActiveSupport::Notifications.instrument("redirect_to.action_controller", request: request) do |payload|
|
51
|
+
result = super
|
52
|
+
payload[:status] = response.status
|
53
|
+
payload[:location] = response.filtered_location
|
54
|
+
result
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def process_action(*)
|
60
|
+
ActiveSupport::ExecutionContext[:controller] = self
|
61
|
+
|
62
|
+
raw_payload = {
|
63
|
+
controller: self.class.name,
|
64
|
+
action: action_name,
|
65
|
+
request: request,
|
66
|
+
params: request.filtered_parameters,
|
67
|
+
headers: request.headers,
|
68
|
+
format: request.format.ref,
|
69
|
+
method: request.request_method,
|
70
|
+
path: request.filtered_path
|
71
|
+
}
|
72
|
+
|
73
|
+
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
|
74
|
+
|
75
|
+
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
|
76
|
+
result = super
|
77
|
+
payload[:response] = response
|
78
|
+
payload[:status] = response.status
|
79
|
+
result
|
80
|
+
rescue => error
|
81
|
+
payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
|
82
|
+
raise
|
83
|
+
ensure
|
84
|
+
append_info_to_payload(payload)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# A hook invoked every time a before callback is halted.
|
89
|
+
def halted_callback_hook(filter, _)
|
90
|
+
ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
|
91
|
+
end
|
92
|
+
|
93
|
+
# A hook which allows you to clean up any time, wrongly taken into account in
|
94
|
+
# views, like database querying time.
|
95
|
+
#
|
96
|
+
# def cleanup_view_runtime
|
97
|
+
# super - time_taken_in_something_expensive
|
98
|
+
# end
|
99
|
+
def cleanup_view_runtime # :doc:
|
100
|
+
yield
|
101
|
+
end
|
102
|
+
|
103
|
+
# Every time after an action is processed, this method is invoked with the
|
104
|
+
# payload, so you can add more information.
|
105
|
+
def append_info_to_payload(payload) # :doc:
|
106
|
+
payload[:view_runtime] = view_runtime
|
107
|
+
end
|
108
|
+
|
109
|
+
module ClassMethods
|
110
|
+
# A hook which allows other frameworks to log what happened during controller
|
111
|
+
# process action. This method should return an array with the messages to be
|
112
|
+
# added.
|
113
|
+
def log_process_action(payload) # :nodoc:
|
114
|
+
messages, view_runtime = [], payload[:view_runtime]
|
115
|
+
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
|
116
|
+
messages
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,398 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "action_dispatch/http/response"
|
6
|
+
require "delegate"
|
7
|
+
require "active_support/json"
|
8
|
+
|
9
|
+
module ActionController
|
10
|
+
# # Action Controller Live
|
11
|
+
#
|
12
|
+
# Mix this module into your controller, and all actions in that controller will
|
13
|
+
# be able to stream data to the client as it's written.
|
14
|
+
#
|
15
|
+
# class MyController < ActionController::Base
|
16
|
+
# include ActionController::Live
|
17
|
+
#
|
18
|
+
# def stream
|
19
|
+
# response.headers['Content-Type'] = 'text/event-stream'
|
20
|
+
# 100.times {
|
21
|
+
# response.stream.write "hello world\n"
|
22
|
+
# sleep 1
|
23
|
+
# }
|
24
|
+
# ensure
|
25
|
+
# response.stream.close
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# There are a few caveats with this module. You **cannot** write headers after
|
30
|
+
# the response has been committed (Response#committed? will return truthy).
|
31
|
+
# Calling `write` or `close` on the response stream will cause the response
|
32
|
+
# object to be committed. Make sure all headers are set before calling write or
|
33
|
+
# close on your stream.
|
34
|
+
#
|
35
|
+
# You **must** call close on your stream when you're finished, otherwise the
|
36
|
+
# socket may be left open forever.
|
37
|
+
#
|
38
|
+
# The final caveat is that your actions are executed in a separate thread than
|
39
|
+
# the main thread. Make sure your actions are thread safe, and this shouldn't be
|
40
|
+
# a problem (don't share state across threads, etc).
|
41
|
+
#
|
42
|
+
# Note that Rails includes `Rack::ETag` by default, which will buffer your
|
43
|
+
# response. As a result, streaming responses may not work properly with Rack
|
44
|
+
# 2.2.x, and you may need to implement workarounds in your application. You can
|
45
|
+
# either set the `ETag` or `Last-Modified` response headers or remove
|
46
|
+
# `Rack::ETag` from the middleware stack to address this issue.
|
47
|
+
#
|
48
|
+
# Here's an example of how you can set the `Last-Modified` header if your Rack
|
49
|
+
# version is 2.2.x:
|
50
|
+
#
|
51
|
+
# def stream
|
52
|
+
# response.headers["Content-Type"] = "text/event-stream"
|
53
|
+
# response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
|
54
|
+
# ...
|
55
|
+
# end
|
56
|
+
module Live
|
57
|
+
extend ActiveSupport::Concern
|
58
|
+
|
59
|
+
module ClassMethods
|
60
|
+
def make_response!(request)
|
61
|
+
if request.get_header("HTTP_VERSION") == "HTTP/1.0"
|
62
|
+
super
|
63
|
+
else
|
64
|
+
Live::Response.new.tap do |res|
|
65
|
+
res.request = request
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# # Action Controller Live Server Sent Events
|
72
|
+
#
|
73
|
+
# This class provides the ability to write an SSE (Server Sent Event) to an IO
|
74
|
+
# stream. The class is initialized with a stream and can be used to either write
|
75
|
+
# a JSON string or an object which can be converted to JSON.
|
76
|
+
#
|
77
|
+
# Writing an object will convert it into standard SSE format with whatever
|
78
|
+
# options you have configured. You may choose to set the following options:
|
79
|
+
#
|
80
|
+
# 1) Event. If specified, an event with this name will be dispatched on
|
81
|
+
# the browser.
|
82
|
+
# 2) Retry. The reconnection time in milliseconds used when attempting
|
83
|
+
# to send the event.
|
84
|
+
# 3) Id. If the connection dies while sending an SSE to the browser, then
|
85
|
+
# the server will receive a +Last-Event-ID+ header with value equal to +id+.
|
86
|
+
#
|
87
|
+
# After setting an option in the constructor of the SSE object, all future SSEs
|
88
|
+
# sent across the stream will use those options unless overridden.
|
89
|
+
#
|
90
|
+
# Example Usage:
|
91
|
+
#
|
92
|
+
# class MyController < ActionController::Base
|
93
|
+
# include ActionController::Live
|
94
|
+
#
|
95
|
+
# def index
|
96
|
+
# response.headers['Content-Type'] = 'text/event-stream'
|
97
|
+
# sse = SSE.new(response.stream, retry: 300, event: "event-name")
|
98
|
+
# sse.write({ name: 'John'})
|
99
|
+
# sse.write({ name: 'John'}, id: 10)
|
100
|
+
# sse.write({ name: 'John'}, id: 10, event: "other-event")
|
101
|
+
# sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
|
102
|
+
# ensure
|
103
|
+
# sse.close
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# Note: SSEs are not currently supported by IE. However, they are supported by
|
108
|
+
# Chrome, Firefox, Opera, and Safari.
|
109
|
+
class SSE
|
110
|
+
PERMITTED_OPTIONS = %w( retry event id )
|
111
|
+
|
112
|
+
def initialize(stream, options = {})
|
113
|
+
@stream = stream
|
114
|
+
@options = options
|
115
|
+
end
|
116
|
+
|
117
|
+
def close
|
118
|
+
@stream.close
|
119
|
+
end
|
120
|
+
|
121
|
+
def write(object, options = {})
|
122
|
+
case object
|
123
|
+
when String
|
124
|
+
perform_write(object, options)
|
125
|
+
else
|
126
|
+
perform_write(ActiveSupport::JSON.encode(object), options)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
def perform_write(json, options)
|
132
|
+
current_options = @options.merge(options).stringify_keys
|
133
|
+
|
134
|
+
PERMITTED_OPTIONS.each do |option_name|
|
135
|
+
if (option_value = current_options[option_name])
|
136
|
+
@stream.write "#{option_name}: #{option_value}\n"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
message = json.gsub("\n", "\ndata: ")
|
141
|
+
@stream.write "data: #{message}\n\n"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class ClientDisconnected < RuntimeError
|
146
|
+
end
|
147
|
+
|
148
|
+
class Buffer < ActionDispatch::Response::Buffer # :nodoc:
|
149
|
+
include MonitorMixin
|
150
|
+
|
151
|
+
class << self
|
152
|
+
attr_accessor :queue_size
|
153
|
+
end
|
154
|
+
@queue_size = 10
|
155
|
+
|
156
|
+
# Ignore that the client has disconnected.
|
157
|
+
#
|
158
|
+
# If this value is `true`, calling `write` after the client disconnects will
|
159
|
+
# result in the written content being silently discarded. If this value is
|
160
|
+
# `false` (the default), a ClientDisconnected exception will be raised.
|
161
|
+
attr_accessor :ignore_disconnect
|
162
|
+
|
163
|
+
def initialize(response)
|
164
|
+
super(response, build_queue(self.class.queue_size))
|
165
|
+
@error_callback = lambda { true }
|
166
|
+
@cv = new_cond
|
167
|
+
@aborted = false
|
168
|
+
@ignore_disconnect = false
|
169
|
+
end
|
170
|
+
|
171
|
+
# ActionDispatch::Response delegates #to_ary to the internal
|
172
|
+
# ActionDispatch::Response::Buffer, defining #to_ary is an indicator that the
|
173
|
+
# response body can be buffered and/or cached by Rack middlewares, this is not
|
174
|
+
# the case for Live responses so we undefine it for this Buffer subclass.
|
175
|
+
undef_method :to_ary
|
176
|
+
|
177
|
+
def write(string)
|
178
|
+
unless @response.committed?
|
179
|
+
@response.headers["Cache-Control"] ||= "no-cache"
|
180
|
+
@response.delete_header "Content-Length"
|
181
|
+
end
|
182
|
+
|
183
|
+
super
|
184
|
+
|
185
|
+
unless connected?
|
186
|
+
@buf.clear
|
187
|
+
|
188
|
+
unless @ignore_disconnect
|
189
|
+
# Raise ClientDisconnected, which is a RuntimeError (not an IOError), because
|
190
|
+
# that's more appropriate for something beyond the developer's control.
|
191
|
+
raise ClientDisconnected, "client disconnected"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Same as `write` but automatically include a newline at the end of the string.
|
197
|
+
def writeln(string)
|
198
|
+
write string.end_with?("\n") ? string : "#{string}\n"
|
199
|
+
end
|
200
|
+
|
201
|
+
# Write a 'close' event to the buffer; the producer/writing thread uses this to
|
202
|
+
# notify us that it's finished supplying content.
|
203
|
+
#
|
204
|
+
# See also #abort.
|
205
|
+
def close
|
206
|
+
synchronize do
|
207
|
+
super
|
208
|
+
@buf.push nil
|
209
|
+
@cv.broadcast
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Inform the producer/writing thread that the client has disconnected; the
|
214
|
+
# reading thread is no longer interested in anything that's being written.
|
215
|
+
#
|
216
|
+
# See also #close.
|
217
|
+
def abort
|
218
|
+
synchronize do
|
219
|
+
@aborted = true
|
220
|
+
@buf.clear
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Is the client still connected and waiting for content?
|
225
|
+
#
|
226
|
+
# The result of calling `write` when this is `false` is determined by
|
227
|
+
# `ignore_disconnect`.
|
228
|
+
def connected?
|
229
|
+
!@aborted
|
230
|
+
end
|
231
|
+
|
232
|
+
def on_error(&block)
|
233
|
+
@error_callback = block
|
234
|
+
end
|
235
|
+
|
236
|
+
def call_on_error
|
237
|
+
@error_callback.call
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
def each_chunk(&block)
|
242
|
+
loop do
|
243
|
+
str = nil
|
244
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
245
|
+
str = @buf.pop
|
246
|
+
end
|
247
|
+
break unless str
|
248
|
+
yield str
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def build_queue(queue_size)
|
253
|
+
queue_size ? SizedQueue.new(queue_size) : Queue.new
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class Response < ActionDispatch::Response # :nodoc: all
|
258
|
+
private
|
259
|
+
def before_committed
|
260
|
+
super
|
261
|
+
jar = request.cookie_jar
|
262
|
+
# The response can be committed multiple times
|
263
|
+
jar.write self unless committed?
|
264
|
+
end
|
265
|
+
|
266
|
+
def build_buffer(response, body)
|
267
|
+
buf = Live::Buffer.new response
|
268
|
+
body.each { |part| buf.write part }
|
269
|
+
buf
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def process(name)
|
274
|
+
t1 = Thread.current
|
275
|
+
locals = t1.keys.map { |key| [key, t1[key]] }
|
276
|
+
|
277
|
+
error = nil
|
278
|
+
# This processes the action in a child thread. It lets us return the response
|
279
|
+
# code and headers back up the Rack stack, and still process the body in
|
280
|
+
# parallel with sending data to the client.
|
281
|
+
new_controller_thread {
|
282
|
+
ActiveSupport::Dependencies.interlock.running do
|
283
|
+
t2 = Thread.current
|
284
|
+
|
285
|
+
# Since we're processing the view in a different thread, copy the thread locals
|
286
|
+
# from the main thread to the child thread. :'(
|
287
|
+
locals.each { |k, v| t2[k] = v }
|
288
|
+
ActiveSupport::IsolatedExecutionState.share_with(t1)
|
289
|
+
|
290
|
+
begin
|
291
|
+
super(name)
|
292
|
+
rescue => e
|
293
|
+
if @_response.committed?
|
294
|
+
begin
|
295
|
+
@_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
|
296
|
+
@_response.stream.call_on_error
|
297
|
+
rescue => exception
|
298
|
+
log_error(exception)
|
299
|
+
ensure
|
300
|
+
log_error(e)
|
301
|
+
@_response.stream.close
|
302
|
+
end
|
303
|
+
else
|
304
|
+
error = e
|
305
|
+
end
|
306
|
+
ensure
|
307
|
+
# Ensure we clean up any thread locals we copied so that the thread can reused.
|
308
|
+
ActiveSupport::IsolatedExecutionState.clear
|
309
|
+
locals.each { |k, _| t2[k] = nil }
|
310
|
+
|
311
|
+
@_response.commit!
|
312
|
+
end
|
313
|
+
end
|
314
|
+
}
|
315
|
+
|
316
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
317
|
+
@_response.await_commit
|
318
|
+
end
|
319
|
+
|
320
|
+
raise error if error
|
321
|
+
end
|
322
|
+
|
323
|
+
def response_body=(body)
|
324
|
+
super
|
325
|
+
response.close if response
|
326
|
+
end
|
327
|
+
|
328
|
+
# Sends a stream to the browser, which is helpful when you're generating exports
|
329
|
+
# or other running data where you don't want the entire file buffered in memory
|
330
|
+
# first. Similar to send_data, but where the data is generated live.
|
331
|
+
#
|
332
|
+
# Options:
|
333
|
+
# * `:filename` - suggests a filename for the browser to use.
|
334
|
+
# * `:type` - specifies an HTTP content type. You can specify either a string
|
335
|
+
# or a symbol for a registered type with `Mime::Type.register`, for example
|
336
|
+
# :json. If omitted, type will be inferred from the file extension specified
|
337
|
+
# in `:filename`. If no content type is registered for the extension, the
|
338
|
+
# default type 'application/octet-stream' will be used.
|
339
|
+
# * `:disposition` - specifies whether the file will be shown inline or
|
340
|
+
# downloaded. Valid values are 'inline' and 'attachment' (default).
|
341
|
+
#
|
342
|
+
#
|
343
|
+
# Example of generating a csv export:
|
344
|
+
#
|
345
|
+
# send_stream(filename: "subscribers.csv") do |stream|
|
346
|
+
# stream.write "email_address,updated_at\n"
|
347
|
+
#
|
348
|
+
# @subscribers.find_each do |subscriber|
|
349
|
+
# stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
|
350
|
+
# end
|
351
|
+
# end
|
352
|
+
def send_stream(filename:, disposition: "attachment", type: nil)
|
353
|
+
payload = { filename: filename, disposition: disposition, type: type }
|
354
|
+
ActiveSupport::Notifications.instrument("send_stream.action_controller", payload) do
|
355
|
+
response.headers["Content-Type"] =
|
356
|
+
(type.is_a?(Symbol) ? Mime[type].to_s : type) ||
|
357
|
+
Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete("."))&.to_s ||
|
358
|
+
"application/octet-stream"
|
359
|
+
|
360
|
+
response.headers["Content-Disposition"] =
|
361
|
+
ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
|
362
|
+
|
363
|
+
yield response.stream
|
364
|
+
end
|
365
|
+
ensure
|
366
|
+
response.stream.close
|
367
|
+
end
|
368
|
+
|
369
|
+
private
|
370
|
+
# Spawn a new thread to serve up the controller in. This is to get around the
|
371
|
+
# fact that Rack isn't based around IOs and we need to use a thread to stream
|
372
|
+
# data from the response bodies. Nobody should call this method except in Rails
|
373
|
+
# internals. Seriously!
|
374
|
+
def new_controller_thread # :nodoc:
|
375
|
+
ActionController::Live.live_thread_pool_executor.post do
|
376
|
+
t2 = Thread.current
|
377
|
+
t2.abort_on_exception = true
|
378
|
+
yield
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def self.live_thread_pool_executor
|
383
|
+
@live_thread_pool_executor ||= Concurrent::CachedThreadPool.new(name: "action_controller.live")
|
384
|
+
end
|
385
|
+
|
386
|
+
def log_error(exception)
|
387
|
+
logger = ActionController::Base.logger
|
388
|
+
return unless logger
|
389
|
+
|
390
|
+
logger.fatal do
|
391
|
+
message = +"\n#{exception.class} (#{exception.message}):\n"
|
392
|
+
message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
|
393
|
+
message << " " << exception.backtrace.join("\n ")
|
394
|
+
"#{message}\n\n"
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionController
|
6
|
+
module Logging
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Set a different log level per request.
|
11
|
+
#
|
12
|
+
# # Use the debug log level if a particular cookie is set.
|
13
|
+
# class ApplicationController < ActionController::Base
|
14
|
+
# log_at :debug, if: -> { cookies[:debug] }
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
def log_at(level, **options)
|
18
|
+
around_action ->(_, action) { logger.log_at(level, &action) }, **options
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|