web-console 2.2.1 → 2.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +7 -1
- data/README.markdown +12 -0
- data/Rakefile +3 -34
- data/lib/web_console.rb +2 -0
- data/lib/web_console/extensions.rb +5 -2
- data/lib/web_console/locales/en.yml +15 -0
- data/lib/web_console/middleware.rb +57 -54
- data/lib/web_console/railtie.rb +8 -10
- data/lib/web_console/request.rb +18 -25
- data/lib/web_console/response.rb +23 -0
- data/lib/web_console/tasks/extensions.rake +60 -0
- data/lib/web_console/tasks/test_templates.rake +50 -0
- data/lib/web_console/template.rb +3 -29
- data/lib/web_console/templates/_markup.html.erb +3 -2
- data/lib/web_console/templates/console.js.erb +107 -40
- data/lib/web_console/templates/style.css.erb +5 -0
- data/lib/web_console/testing/erb_precompiler.rb +25 -0
- data/lib/web_console/testing/fake_middleware.rb +43 -0
- data/lib/web_console/testing/helper.rb +9 -0
- data/lib/web_console/version.rb +1 -1
- data/lib/web_console/view.rb +37 -0
- data/lib/web_console/whiny_request.rb +0 -7
- metadata +11 -4
- data/lib/web_console/tracer.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02d40821ae813ac4e5893b4c853725833017cb46
|
4
|
+
data.tar.gz: 56b2826919677b8a93e6ecedce69a176949d15ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c6a07f241b5bfad39c7a588ac92bccc8f0786db1b807d3ab94b4ec13464d7d8514493634c88247a868e78e43169094d216da6f01c3559be45af39854a76a0a0
|
7
|
+
data.tar.gz: 4b656f08efb441715996f541f94806d9f6249171502fded529041c5fc5cead0a73ce4f155fe246f0c4a36c4a0b54f9bdd65fb3091ed5e9b9bfa86b9d95f3a552
|
data/CHANGELOG.markdown
CHANGED
@@ -2,9 +2,14 @@
|
|
2
2
|
|
3
3
|
## master (unreleased)
|
4
4
|
|
5
|
+
## 2.3.0
|
6
|
+
|
7
|
+
* [#181](https://github.com/rails/web-console/pull/181) Log internal Web Console errors ([@schneems])
|
8
|
+
* [#150](https://github.com/rails/web-console/pull/150) Revert #150. ([@gsamokovarov])
|
9
|
+
|
5
10
|
## 2.2.1
|
6
11
|
|
7
|
-
* [#150](https://github.com/rails/web-console/pull/150) Change config.development_only default until 4.2.4 is released.
|
12
|
+
* [#150](https://github.com/rails/web-console/pull/150) Change config.development_only default until 4.2.4 is released. ([@gsamokovarov])
|
8
13
|
|
9
14
|
## 2.2.0
|
10
15
|
|
@@ -48,3 +53,4 @@
|
|
48
53
|
[@parterburn]: https://github.com/parterburn
|
49
54
|
[@sh19910711]: https://github.com/sh19910711
|
50
55
|
[@frenesim]: https://github.com/frenesim
|
56
|
+
[@schneems]: https://github.com/schneems
|
data/README.markdown
CHANGED
@@ -161,6 +161,18 @@ end
|
|
161
161
|
You may wanna check the [templates] folder at the source tree for the files you
|
162
162
|
may override.
|
163
163
|
|
164
|
+
### config.web_console.mount_point
|
165
|
+
|
166
|
+
Usually the middleware of _Web Console_ is mounted at `/__web_console`.
|
167
|
+
If you wanna change the path for some reasons, you can specify it
|
168
|
+
by `config.web_console.mount_point`:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
Rails.application.configure do
|
172
|
+
config.web_console.mount_point = '/path/to/web_console'
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
164
176
|
## FAQ
|
165
177
|
|
166
178
|
### Where did /console go?
|
data/Rakefile
CHANGED
@@ -8,6 +8,8 @@ require 'socket'
|
|
8
8
|
require 'rake/testtask'
|
9
9
|
require 'tmpdir'
|
10
10
|
require 'securerandom'
|
11
|
+
require 'json'
|
12
|
+
require 'web_console/testing/erb_precompiler'
|
11
13
|
|
12
14
|
EXPANDED_CWD = File.expand_path(File.dirname(__FILE__))
|
13
15
|
|
@@ -18,40 +20,7 @@ Rake::TestTask.new(:test) do |t|
|
|
18
20
|
t.verbose = false
|
19
21
|
end
|
20
22
|
|
21
|
-
|
22
|
-
desc "Run tests for templates"
|
23
|
-
task :templates => "templates:all"
|
24
|
-
|
25
|
-
namespace :templates do
|
26
|
-
task :all => [:daemonize, :npm, :rackup, :mocha, :kill]
|
27
|
-
task :serve => [:npm, :rackup]
|
28
|
-
|
29
|
-
work_dir = Pathname(__FILE__).dirname.join("test/templates")
|
30
|
-
pid_file = Pathname(Dir.tmpdir).join("web_console.#{SecureRandom.uuid}.pid")
|
31
|
-
server_port = 29292
|
32
|
-
rackup_opts = "-p #{server_port}"
|
33
|
-
|
34
|
-
task :daemonize do
|
35
|
-
rackup_opts += " -D -P #{pid_file}"
|
36
|
-
end
|
37
|
-
|
38
|
-
task :npm do
|
39
|
-
Dir.chdir(work_dir) { system "npm install --silent" }
|
40
|
-
end
|
41
|
-
|
42
|
-
task :rackup do
|
43
|
-
Dir.chdir(work_dir) { system "bundle exec rackup #{rackup_opts}" }
|
44
|
-
end
|
45
|
-
|
46
|
-
task :mocha do
|
47
|
-
Dir.chdir(work_dir) { system "$(npm bin)/mocha-phantomjs http://localhost:#{server_port}/html/spec_runner.html" }
|
48
|
-
end
|
49
|
-
|
50
|
-
task :kill do
|
51
|
-
system "kill #{File.read pid_file}"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
23
|
+
Dir['lib/web_console/tasks/**/*.rake'].each { |task| load task }
|
55
24
|
|
56
25
|
Bundler::GemHelper.install_tasks
|
57
26
|
|
data/lib/web_console.rb
CHANGED
@@ -13,6 +13,8 @@ require 'web_console/template'
|
|
13
13
|
require 'web_console/middleware'
|
14
14
|
require 'web_console/whitelist'
|
15
15
|
require 'web_console/request'
|
16
|
+
require 'web_console/response'
|
17
|
+
require 'web_console/view'
|
16
18
|
require 'web_console/whiny_request'
|
17
19
|
|
18
20
|
module WebConsole
|
@@ -1,6 +1,9 @@
|
|
1
1
|
ActionDispatch::DebugExceptions.class_eval do
|
2
|
-
def render_exception_with_web_console(
|
3
|
-
render_exception_without_web_console(
|
2
|
+
def render_exception_with_web_console(request, exception)
|
3
|
+
render_exception_without_web_console(request, exception).tap do
|
4
|
+
# Retain superficial Rails 5 compatibility.
|
5
|
+
env = Hash === request ? request : request.env
|
6
|
+
|
4
7
|
error = ActionDispatch::ExceptionWrapper.new(env, exception).exception
|
5
8
|
|
6
9
|
# Get the original exception if ExceptionWrapper decides to follow it.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
en:
|
2
|
+
errors:
|
3
|
+
unavailable_session: |
|
4
|
+
Session %{id} is is no longer available in memory.
|
5
|
+
|
6
|
+
If you happen to run on a multi-process server (like Unicorn or Puma) the process
|
7
|
+
this request hit doesn't store %{id} in memory. Consider turning the number of
|
8
|
+
processes/workers to one (1) or using a different server in development.
|
9
|
+
|
10
|
+
unacceptable_request: |
|
11
|
+
A supported version is expected in the Accept header.
|
12
|
+
|
13
|
+
connection_refused: |
|
14
|
+
Oops! Failed to connect to the Web Console middleware.
|
15
|
+
Please make sure a rails development server is running.
|
@@ -4,60 +4,60 @@ module WebConsole
|
|
4
4
|
class Middleware
|
5
5
|
TEMPLATES_PATH = File.expand_path('../templates', __FILE__)
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
binding_change_re: %r{/repl_sessions/(?<id>.+?)/trace\z}
|
10
|
-
}
|
11
|
-
|
12
|
-
UNAVAILABLE_SESSION_MESSAGE = <<-END.strip_heredoc
|
13
|
-
Session %{id} is is no longer available in memory.
|
14
|
-
|
15
|
-
If you happen to run on a multi-process server (like Unicorn) the process
|
16
|
-
this request hit doesn't store %{id} in memory.
|
17
|
-
END
|
18
|
-
|
19
|
-
UNACCEPTABLE_REQUEST_MESSAGE = "A supported version is expected in the Accept header."
|
7
|
+
cattr_accessor :mount_point
|
8
|
+
@@mount_point = '/__web_console'
|
20
9
|
|
21
10
|
cattr_accessor :whiny_requests
|
22
11
|
@@whiny_requests = true
|
23
12
|
|
24
|
-
def initialize(app
|
25
|
-
@app
|
26
|
-
@options = DEFAULT_OPTIONS.merge(options)
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
27
15
|
end
|
28
16
|
|
29
17
|
def call(env)
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
18
|
+
app_exception = catch :app_exception do
|
19
|
+
request = create_regular_or_whiny_request(env)
|
20
|
+
return @app.call(env) unless request.from_whitelited_ip?
|
21
|
+
|
22
|
+
if id = id_for_repl_session_update(request)
|
23
|
+
return update_repl_session(id, request)
|
24
|
+
elsif id = id_for_repl_session_stack_frame_change(request)
|
25
|
+
return change_stack_trace(id, request)
|
26
|
+
end
|
38
27
|
|
39
|
-
|
28
|
+
status, headers, body = @app.call(env)
|
40
29
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
30
|
+
if exception = env['web_console.exception']
|
31
|
+
session = Session.from_exception(exception)
|
32
|
+
elsif binding = env['web_console.binding']
|
33
|
+
session = Session.from_binding(binding)
|
34
|
+
end
|
46
35
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
template = Template.new(env, session)
|
36
|
+
if session && acceptable_content_type?(headers)
|
37
|
+
response = Response.new(body, status, headers)
|
38
|
+
template = Template.new(env, session)
|
51
39
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
40
|
+
response.headers["X-Web-Console-Session-Id"] = session.id
|
41
|
+
response.headers["X-Web-Console-Mount-Point"] = mount_point
|
42
|
+
response.write(template.render('index'))
|
43
|
+
response.finish
|
44
|
+
else
|
45
|
+
[ status, headers, body ]
|
46
|
+
end
|
56
47
|
end
|
48
|
+
rescue => e
|
49
|
+
WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}")
|
50
|
+
raise e
|
51
|
+
ensure
|
52
|
+
raise app_exception if Exception === app_exception
|
57
53
|
end
|
58
54
|
|
59
55
|
private
|
60
56
|
|
57
|
+
def acceptable_content_type?(headers)
|
58
|
+
Mime::Type.parse(headers['Content-Type']).first == Mime::HTML
|
59
|
+
end
|
60
|
+
|
61
61
|
def json_response(opts = {})
|
62
62
|
status = opts.fetch(:status, 200)
|
63
63
|
headers = { 'Content-Type' => 'application/json; charset = utf-8' }
|
@@ -67,17 +67,10 @@ module WebConsole
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def json_response_with_session(id, request, opts = {})
|
70
|
-
|
71
|
-
|
72
|
-
return respond_with_unacceptable_request
|
73
|
-
end
|
70
|
+
return respond_with_unacceptable_request unless request.acceptable?
|
71
|
+
return respond_with_unavailable_session(id) unless session = Session.find(id)
|
74
72
|
|
75
|
-
|
76
|
-
return respond_with_unavailable_session(id)
|
77
|
-
end
|
78
|
-
|
79
|
-
yield session
|
80
|
-
end
|
73
|
+
json_response(opts) { yield session }
|
81
74
|
end
|
82
75
|
|
83
76
|
def create_regular_or_whiny_request(env)
|
@@ -85,23 +78,27 @@ module WebConsole
|
|
85
78
|
whiny_requests ? WhinyRequest.new(request) : request
|
86
79
|
end
|
87
80
|
|
81
|
+
def repl_sessions_re
|
82
|
+
@_repl_sessions_re ||= %r{#{mount_point}/repl_sessions/(?<id>[^/]+)}
|
83
|
+
end
|
84
|
+
|
88
85
|
def update_re
|
89
|
-
@
|
86
|
+
@_update_re ||= %r{#{repl_sessions_re}\z}
|
90
87
|
end
|
91
88
|
|
92
89
|
def binding_change_re
|
93
|
-
@
|
90
|
+
@_binding_change_re ||= %r{#{repl_sessions_re}/trace\z}
|
94
91
|
end
|
95
92
|
|
96
93
|
def id_for_repl_session_update(request)
|
97
94
|
if request.xhr? && request.put?
|
98
|
-
update_re.match(request.
|
95
|
+
update_re.match(request.path) { |m| m[:id] }
|
99
96
|
end
|
100
97
|
end
|
101
98
|
|
102
99
|
def id_for_repl_session_stack_frame_change(request)
|
103
100
|
if request.xhr? && request.post?
|
104
|
-
binding_change_re.match(request.
|
101
|
+
binding_change_re.match(request.path) { |m| m[:id] }
|
105
102
|
end
|
106
103
|
end
|
107
104
|
|
@@ -121,14 +118,20 @@ module WebConsole
|
|
121
118
|
|
122
119
|
def respond_with_unavailable_session(id)
|
123
120
|
json_response(status: 404) do
|
124
|
-
{ output: format(
|
121
|
+
{ output: format(I18n.t('errors.unavailable_session'), id: id)}
|
125
122
|
end
|
126
123
|
end
|
127
124
|
|
128
125
|
def respond_with_unacceptable_request
|
129
126
|
json_response(status: 406) do
|
130
|
-
{
|
127
|
+
{ output: I18n.t('errors.unacceptable_request') }
|
131
128
|
end
|
132
129
|
end
|
130
|
+
|
131
|
+
def call_app(env)
|
132
|
+
@app.call(env)
|
133
|
+
rescue => e
|
134
|
+
throw :app_exception, e
|
135
|
+
end
|
133
136
|
end
|
134
137
|
end
|
data/lib/web_console/railtie.rb
CHANGED
@@ -5,10 +5,6 @@ module WebConsole
|
|
5
5
|
config.web_console = ActiveSupport::OrderedOptions.new
|
6
6
|
config.web_console.whitelisted_ips = %w( 127.0.0.1 ::1 )
|
7
7
|
|
8
|
-
# See rails/web-console#150 and rails/rails#20319. Revert when Ruby on
|
9
|
-
# Rails 4.2.4 is released.
|
10
|
-
config.web_console.development_only = false
|
11
|
-
|
12
8
|
initializer 'web_console.initialize' do
|
13
9
|
require 'web_console/extensions'
|
14
10
|
|
@@ -43,6 +39,12 @@ module WebConsole
|
|
43
39
|
app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware
|
44
40
|
end
|
45
41
|
|
42
|
+
initializer 'web_console.mount_point' do
|
43
|
+
if mount_point = config.web_console.mount_point
|
44
|
+
Middleware.mount_point = mount_point.chomp('/')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
46
48
|
initializer 'web_console.template_paths' do
|
47
49
|
if template_paths = config.web_console.template_paths
|
48
50
|
Template.template_paths.unshift(*Array(template_paths))
|
@@ -61,12 +63,8 @@ module WebConsole
|
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
64
|
-
|
65
|
-
|
66
|
-
initializer 'web_console.acceptable_content_types' do
|
67
|
-
if acceptable_content_types = config.web_console.acceptable_content_types
|
68
|
-
Request.acceptable_content_types.concat(Array(acceptable_content_types))
|
69
|
-
end
|
66
|
+
initializer 'i18n.load_path' do
|
67
|
+
config.i18n.load_path.concat(Dir[File.expand_path('../locales/*.yml', __FILE__)])
|
70
68
|
end
|
71
69
|
end
|
72
70
|
end
|
data/lib/web_console/request.rb
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
module WebConsole
|
2
2
|
# Web Console tailored request object.
|
3
3
|
class Request < ActionDispatch::Request
|
4
|
-
# While most of the servers will return blank content type if none given,
|
5
|
-
# Puma will return text/plain.
|
6
|
-
cattr_accessor :acceptable_content_types
|
7
|
-
@@acceptable_content_types = [Mime::HTML, Mime::TEXT, Mime::URL_ENCODED_FORM]
|
8
|
-
|
9
4
|
# Configurable set of whitelisted networks.
|
10
5
|
cattr_accessor :whitelisted_ips
|
11
6
|
@@whitelisted_ips = Whitelist.new
|
@@ -23,16 +18,7 @@ module WebConsole
|
|
23
18
|
|
24
19
|
# Determines the remote IP using our much stricter whitelist.
|
25
20
|
def strict_remote_ip
|
26
|
-
GetSecureIp.new(
|
27
|
-
end
|
28
|
-
|
29
|
-
# Returns whether the request is from an acceptable content type.
|
30
|
-
#
|
31
|
-
# We can render a console for HTML and TEXT by default. If a client didn't
|
32
|
-
# specified any content type and the server returned it as blank, we'll
|
33
|
-
# render it as well.
|
34
|
-
def acceptable_content_type?
|
35
|
-
content_type.blank? || content_type.in?(acceptable_content_types)
|
21
|
+
GetSecureIp.new(self, whitelisted_ips).to_s
|
36
22
|
end
|
37
23
|
|
38
24
|
# Returns whether the request is acceptable.
|
@@ -40,18 +26,25 @@ module WebConsole
|
|
40
26
|
xhr? && accepts.any? { |mime| Mime::WEB_CONSOLE_V2 == mime }
|
41
27
|
end
|
42
28
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
29
|
+
private
|
30
|
+
|
31
|
+
class GetSecureIp < ActionDispatch::RemoteIp::GetIp
|
32
|
+
def initialize(req, proxies)
|
33
|
+
# After rails/rails@07b2ff0 ActionDispatch::RemoteIp::GetIp initializes
|
34
|
+
# with a ActionDispatch::Request object instead of plain Rack
|
35
|
+
# environment hash. Keep both @req and @env here, so we don't if/else
|
36
|
+
# on Rails versions.
|
37
|
+
@req = req
|
38
|
+
@env = req.env
|
39
|
+
@check_ip = true
|
40
|
+
@proxies = proxies
|
41
|
+
end
|
49
42
|
|
50
|
-
|
51
|
-
|
52
|
-
|
43
|
+
def filter_proxies(ips)
|
44
|
+
ips.reject do |ip|
|
45
|
+
@proxies.include?(ip)
|
46
|
+
end
|
53
47
|
end
|
54
48
|
end
|
55
|
-
end
|
56
49
|
end
|
57
50
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module WebConsole
|
2
|
+
# A response object that writes content before the closing </body> tag, if
|
3
|
+
# possible.
|
4
|
+
#
|
5
|
+
# The object quacks like Rack::Response.
|
6
|
+
class Response < Struct.new(:body, :status, :headers)
|
7
|
+
def write(content)
|
8
|
+
raw_body = Array(body).first.to_s
|
9
|
+
|
10
|
+
if position = raw_body.rindex('</body>')
|
11
|
+
raw_body.insert(position, content)
|
12
|
+
else
|
13
|
+
raw_body << content
|
14
|
+
end
|
15
|
+
|
16
|
+
self.body = raw_body
|
17
|
+
end
|
18
|
+
|
19
|
+
def finish
|
20
|
+
Rack::Response.new(body, status, headers).finish
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
namespace :ext do
|
2
|
+
rootdir = Pathname('extensions')
|
3
|
+
|
4
|
+
desc 'Build Chrome Extension'
|
5
|
+
task chrome: 'chrome:build'
|
6
|
+
|
7
|
+
namespace :chrome do
|
8
|
+
dist = Pathname('dist/crx')
|
9
|
+
extdir = rootdir.join(dist)
|
10
|
+
manifest_json = rootdir.join('chrome/manifest.json')
|
11
|
+
|
12
|
+
directory extdir
|
13
|
+
|
14
|
+
task build: [ extdir, 'lib:templates' ] do
|
15
|
+
cd rootdir do
|
16
|
+
cp_r [ 'img/', 'tmp/lib/' ], dist
|
17
|
+
`cd chrome && git ls-files`.split("\n").each do |src|
|
18
|
+
dest = dist.join(src)
|
19
|
+
mkdir_p dest.dirname
|
20
|
+
cp Pathname('chrome').join(src), dest
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Generate a .crx file.
|
26
|
+
task crx: [ :build, :npm ] do
|
27
|
+
out = "crx-web-console-#{JSON.parse(File.read(manifest_json))["version"]}.crx"
|
28
|
+
cd(extdir) { sh "node \"$(npm bin)/crx\" pack ./ -p ../crx-web-console.pem -o ../#{out}" }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Generate a .zip file for Chrome Web Store.
|
32
|
+
task zip: [ :build ] do
|
33
|
+
version = JSON.parse(File.read(manifest_json))["version"]
|
34
|
+
cd(extdir) { sh "zip -r ../crx-web-console-#{version}.zip ./" }
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'Launch a browser with the chrome extension.'
|
38
|
+
task run: [ :build ] do
|
39
|
+
cd(rootdir) { sh "sh ./script/run_chrome.sh --load-extension=#{dist}" }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
task :npm do
|
44
|
+
cd(rootdir) { sh "npm install --silent" }
|
45
|
+
end
|
46
|
+
|
47
|
+
namespace :lib do
|
48
|
+
templates = Pathname('lib/web_console/templates')
|
49
|
+
tmplib = rootdir.join('tmp/lib/')
|
50
|
+
js_erb = FileList.new(templates.join('**/*.js.erb'))
|
51
|
+
dirs = js_erb.pathmap("%{^#{templates},#{tmplib}}d")
|
52
|
+
|
53
|
+
task templates: dirs + js_erb.pathmap("%{^#{templates},#{tmplib}}X")
|
54
|
+
|
55
|
+
dirs.each { |d| directory d }
|
56
|
+
rule '.js' => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t|
|
57
|
+
File.write(t.name, WebConsole::Testing::ERBPrecompiler.new(t.source).build)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
namespace :test do
|
2
|
+
desc "Run tests for templates"
|
3
|
+
task templates: "templates:all"
|
4
|
+
|
5
|
+
namespace :templates do
|
6
|
+
task all: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
|
7
|
+
task serve: [ :npm, :rackup ]
|
8
|
+
|
9
|
+
work_dir = Pathname(EXPANDED_CWD).join("test/templates")
|
10
|
+
pid_file = Pathname(Dir.tmpdir).join("web_console.#{SecureRandom.uuid}.pid")
|
11
|
+
runner_uri = URI.parse("http://localhost:29292/html/spec_runner.html")
|
12
|
+
rackup_opts = "-p #{runner_uri.port}"
|
13
|
+
test_result = nil
|
14
|
+
|
15
|
+
def need_to_wait?(uri)
|
16
|
+
Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) }
|
17
|
+
rescue Errno::ECONNREFUSED
|
18
|
+
retry if yield
|
19
|
+
end
|
20
|
+
|
21
|
+
task :daemonize do
|
22
|
+
rackup_opts += " -D -P #{pid_file}"
|
23
|
+
end
|
24
|
+
|
25
|
+
task :npm do
|
26
|
+
Dir.chdir(work_dir) { system "npm install --silent" }
|
27
|
+
end
|
28
|
+
|
29
|
+
task :rackup do
|
30
|
+
Dir.chdir(work_dir) { system "bundle exec rackup #{rackup_opts}" }
|
31
|
+
end
|
32
|
+
|
33
|
+
task :wait do
|
34
|
+
cnt = 0
|
35
|
+
need_to_wait?(runner_uri) { sleep 1; cnt += 1; cnt < 5 }
|
36
|
+
end
|
37
|
+
|
38
|
+
task :mocha do
|
39
|
+
Dir.chdir(work_dir) { test_result = system("$(npm bin)/mocha-phantomjs #{runner_uri}") }
|
40
|
+
end
|
41
|
+
|
42
|
+
task :kill do
|
43
|
+
system "kill #{File.read pid_file}"
|
44
|
+
end
|
45
|
+
|
46
|
+
task :exit do
|
47
|
+
exit test_result
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/web_console/template.rb
CHANGED
@@ -4,33 +4,6 @@ module WebConsole
|
|
4
4
|
# It introduces template helpers to ease the inclusion of scripts only on
|
5
5
|
# Rails error pages.
|
6
6
|
class Template
|
7
|
-
class Context < ActionView::Base
|
8
|
-
# Execute a block only on error pages.
|
9
|
-
#
|
10
|
-
# The error pages are special, because they are the only pages that
|
11
|
-
# currently require multiple bindings. We get those from exceptions.
|
12
|
-
def only_on_error_page(*args)
|
13
|
-
yield if @env['web_console.exception'].present?
|
14
|
-
end
|
15
|
-
|
16
|
-
# Render JavaScript inside a script tag and a closure.
|
17
|
-
#
|
18
|
-
# This one lets write JavaScript that will automatically get wrapped in a
|
19
|
-
# script tag and enclosed in a closure, so you don't have to worry for
|
20
|
-
# leaking globals, unless you explicitly want to.
|
21
|
-
def render_javascript(template)
|
22
|
-
render(template: template, layout: 'layouts/javascript')
|
23
|
-
end
|
24
|
-
|
25
|
-
# Render inlined string to be used inside of JavaScript code.
|
26
|
-
#
|
27
|
-
# The inlined string is returned as an actual JavaScript string. You
|
28
|
-
# don't need to wrap the result yourself.
|
29
|
-
def render_inlined_string(template)
|
30
|
-
render(template: template, layout: 'layouts/inlined_string')
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
7
|
# Lets you customize the default templates folder location.
|
35
8
|
cattr_accessor :template_paths
|
36
9
|
@@template_paths = [ File.expand_path('../templates', __FILE__) ]
|
@@ -38,12 +11,13 @@ module WebConsole
|
|
38
11
|
def initialize(env, session)
|
39
12
|
@env = env
|
40
13
|
@session = session
|
14
|
+
@mount_point = Middleware.mount_point
|
41
15
|
end
|
42
16
|
|
43
17
|
# Render a template (inferred from +template_paths+) as a plain string.
|
44
18
|
def render(template)
|
45
|
-
|
46
|
-
|
19
|
+
view = View.new(template_paths, instance_values)
|
20
|
+
view.render(template: template, layout: false)
|
47
21
|
end
|
48
22
|
end
|
49
23
|
end
|
@@ -54,11 +54,69 @@ var styleElementId = 'sr02459pvbvrmhco';
|
|
54
54
|
|
55
55
|
// REPLConsole Constructor
|
56
56
|
function REPLConsole(config) {
|
57
|
+
function getConfig(key, defaultValue) {
|
58
|
+
return config && config[key] || defaultValue;
|
59
|
+
}
|
60
|
+
|
57
61
|
this.commandStorage = new CommandStorage();
|
58
|
-
this.prompt =
|
59
|
-
this.
|
62
|
+
this.prompt = getConfig('promptLabel', ' >>');
|
63
|
+
this.mountPoint = getConfig('mountPoint');
|
64
|
+
this.sessionId = getConfig('sessionId');
|
60
65
|
}
|
61
66
|
|
67
|
+
REPLConsole.prototype.getSessionUrl = function(path) {
|
68
|
+
var parts = [ this.mountPoint, 'repl_sessions', this.sessionId ];
|
69
|
+
if (path) {
|
70
|
+
parts.push(path);
|
71
|
+
}
|
72
|
+
// Join and remove duplicate slashes.
|
73
|
+
return parts.join('/').replace(/([^:]\/)\/+/g, '$1');
|
74
|
+
};
|
75
|
+
|
76
|
+
REPLConsole.prototype.commandHandle = function(line, callback) {
|
77
|
+
var self = this;
|
78
|
+
var params = 'input=' + encodeURIComponent(line);
|
79
|
+
callback = callback || function() {};
|
80
|
+
|
81
|
+
function isSuccess(status) {
|
82
|
+
return status >= 200 && status < 300 || status === 304;
|
83
|
+
}
|
84
|
+
|
85
|
+
function parseJSON(text) {
|
86
|
+
try {
|
87
|
+
return JSON.parse(text);
|
88
|
+
} catch (e) {
|
89
|
+
return null;
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
function getErrorText(xhr) {
|
94
|
+
if (!xhr.status) {
|
95
|
+
return "<%= t 'errors.connection_refused' %>";
|
96
|
+
} else {
|
97
|
+
return xhr.status + ' ' + xhr.statusText;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
putRequest(self.getSessionUrl(), params, function(xhr) {
|
102
|
+
var response = parseJSON(xhr.responseText);
|
103
|
+
var result = isSuccess(xhr.status);
|
104
|
+
if (result) {
|
105
|
+
self.writeOutput(response.output);
|
106
|
+
} else {
|
107
|
+
if (response && response.output) {
|
108
|
+
self.writeError(response.output);
|
109
|
+
} else {
|
110
|
+
self.writeError(getErrorText(xhr));
|
111
|
+
}
|
112
|
+
}
|
113
|
+
callback(result, response);
|
114
|
+
});
|
115
|
+
};
|
116
|
+
|
117
|
+
REPLConsole.prototype.uninstall = function() {
|
118
|
+
this.container.parentNode.removeChild(this.container);
|
119
|
+
};
|
62
120
|
|
63
121
|
REPLConsole.prototype.install = function(container) {
|
64
122
|
var _this = this;
|
@@ -99,8 +157,8 @@ REPLConsole.prototype.install = function(container) {
|
|
99
157
|
|
100
158
|
// Make the console resizable.
|
101
159
|
function resizeContainer(ev) {
|
102
|
-
var startY
|
103
|
-
var startHeight
|
160
|
+
var startY = ev.clientY;
|
161
|
+
var startHeight = parseInt(document.defaultView.getComputedStyle(container).height, 10);
|
104
162
|
var scrollTopStart = consoleOuter.scrollTop;
|
105
163
|
var clientHeightStart = consoleOuter.clientHeight;
|
106
164
|
|
@@ -137,10 +195,10 @@ REPLConsole.prototype.install = function(container) {
|
|
137
195
|
}
|
138
196
|
|
139
197
|
// Initialize
|
198
|
+
this.container = container;
|
140
199
|
this.outer = consoleOuter;
|
141
200
|
this.inner = findChild(this.outer, 'console-inner');
|
142
201
|
this.clipboard = findChild(container, 'clipboard');
|
143
|
-
this.remotePath = container.dataset.remotePath;
|
144
202
|
this.newPromptBox();
|
145
203
|
this.insertCss();
|
146
204
|
|
@@ -263,6 +321,13 @@ REPLConsole.prototype.writeOutput = function(output) {
|
|
263
321
|
consoleMessage.innerHTML = escapeHTML(output);
|
264
322
|
this.inner.appendChild(consoleMessage);
|
265
323
|
this.newPromptBox();
|
324
|
+
return consoleMessage;
|
325
|
+
};
|
326
|
+
|
327
|
+
REPLConsole.prototype.writeError = function(output) {
|
328
|
+
var consoleMessage = this.writeOutput(output);
|
329
|
+
addClass(consoleMessage, "error-message");
|
330
|
+
return consoleMessage;
|
266
331
|
};
|
267
332
|
|
268
333
|
REPLConsole.prototype.onEnterKey = function() {
|
@@ -385,7 +450,7 @@ REPLConsole.prototype.scrollToBottom = function() {
|
|
385
450
|
|
386
451
|
// Change the binding of the console
|
387
452
|
REPLConsole.prototype.switchBindingTo = function(frameId, callback) {
|
388
|
-
var url = this.
|
453
|
+
var url = this.getSessionUrl('trace');
|
389
454
|
var params = "frame_id=" + encodeURIComponent(frameId);
|
390
455
|
postRequest(url, params, callback);
|
391
456
|
};
|
@@ -394,22 +459,16 @@ REPLConsole.prototype.switchBindingTo = function(frameId, callback) {
|
|
394
459
|
* Install the console into the element with a specific ID.
|
395
460
|
* Example: REPLConsole.installInto("target-id")
|
396
461
|
*/
|
397
|
-
REPLConsole.installInto = function(id) {
|
462
|
+
REPLConsole.installInto = function(id, options) {
|
398
463
|
var consoleElement = document.getElementById(id);
|
399
|
-
var remotePath = consoleElement.dataset.remotePath;
|
400
|
-
var replConsole = new REPLConsole({
|
401
|
-
promptLabel: consoleElement.dataset.initialPrompt,
|
402
|
-
commandHandle: function(line) {
|
403
|
-
var _this = this;
|
404
|
-
var url = remotePath;
|
405
|
-
var params = "input=" + encodeURIComponent(line);
|
406
|
-
putRequest(url, params, function(xhr) {
|
407
|
-
var response = JSON.parse(xhr.responseText);
|
408
|
-
_this.writeOutput(response.output);
|
409
|
-
});
|
410
|
-
}
|
411
|
-
});
|
412
464
|
|
465
|
+
options = options || {};
|
466
|
+
|
467
|
+
for (var prop in consoleElement.dataset) {
|
468
|
+
options[prop] = options[prop] || consoleElement.dataset[prop];
|
469
|
+
}
|
470
|
+
|
471
|
+
var replConsole = new REPLConsole(options);
|
413
472
|
replConsole.install(consoleElement);
|
414
473
|
return replConsole;
|
415
474
|
};
|
@@ -419,6 +478,26 @@ REPLConsole.installInto = function(id) {
|
|
419
478
|
// It allows to operate the current session from the other scripts.
|
420
479
|
REPLConsole.currentSession = null;
|
421
480
|
|
481
|
+
// This line is for the Firefox Add-on, because it doesn't have XMLHttpRequest as default.
|
482
|
+
// And so we need to require a module compatible with XMLHttpRequest from SDK.
|
483
|
+
REPLConsole.XMLHttpRequest = typeof XMLHttpRequest === 'undefined' ? null : XMLHttpRequest;
|
484
|
+
|
485
|
+
REPLConsole.request = function request(method, url, params, callback) {
|
486
|
+
var xhr = new REPLConsole.XMLHttpRequest();
|
487
|
+
|
488
|
+
xhr.open(method, url, true);
|
489
|
+
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
490
|
+
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
491
|
+
xhr.setRequestHeader("Accept", "<%= Mime::WEB_CONSOLE_V2 %>");
|
492
|
+
xhr.send(params);
|
493
|
+
|
494
|
+
xhr.onreadystatechange = function() {
|
495
|
+
if (xhr.readyState === 4) {
|
496
|
+
callback(xhr);
|
497
|
+
}
|
498
|
+
};
|
499
|
+
};
|
500
|
+
|
422
501
|
// DOM helpers
|
423
502
|
function hasClass(el, className) {
|
424
503
|
var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
|
@@ -470,28 +549,16 @@ function escapeHTML(html) {
|
|
470
549
|
}
|
471
550
|
|
472
551
|
// XHR helpers
|
473
|
-
function
|
474
|
-
|
475
|
-
|
476
|
-
xhr.open(method, url, true);
|
477
|
-
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
478
|
-
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
479
|
-
xhr.setRequestHeader("Accept", "<%= Mime::WEB_CONSOLE_V2 %>");
|
480
|
-
xhr.send(params);
|
481
|
-
|
482
|
-
xhr.onreadystatechange = function() {
|
483
|
-
if (xhr.readyState === 4) {
|
484
|
-
callback(xhr);
|
485
|
-
}
|
486
|
-
}
|
552
|
+
function postRequest() {
|
553
|
+
REPLConsole.request.apply(this, ["POST"].concat([].slice.call(arguments)));
|
487
554
|
}
|
488
555
|
|
489
|
-
function
|
490
|
-
request("
|
556
|
+
function putRequest() {
|
557
|
+
REPLConsole.request.apply(this, ["PUT"].concat([].slice.call(arguments)));
|
491
558
|
}
|
492
559
|
|
493
|
-
|
494
|
-
|
560
|
+
if (typeof exports === 'object') {
|
561
|
+
exports.REPLConsole = REPLConsole;
|
562
|
+
} else {
|
563
|
+
window.REPLConsole = REPLConsole;
|
495
564
|
}
|
496
|
-
|
497
|
-
window.REPLConsole = REPLConsole;
|
@@ -10,6 +10,7 @@
|
|
10
10
|
.console .console-inner { font-family: monospace; font-size: 11px; width: 100%; height: 100%; overflow: none; background: #333; }
|
11
11
|
.console .console-prompt-box { color: #FFF; }
|
12
12
|
.console .console-message { color: #1AD027; margin: 0; border: 0; white-space: pre-wrap; background-color: #333; padding: 0; }
|
13
|
+
.console .console-message.error-message { color: #fc9; }
|
13
14
|
.console .console-focus .console-cursor { background: #FEFEFE; color: #333; font-weight: bold; }
|
14
15
|
.console .resizer { background: #333; width: 100%; height: 4px; cursor: ns-resize; }
|
15
16
|
.console .console-actions { padding-right: 3px; }
|
@@ -20,3 +21,7 @@
|
|
20
21
|
.console .clipboard { height: 0px; padding: 0px; margin: 0px; width: 0px; margin-left: -1000px; }
|
21
22
|
.console .console-prompt-label { display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0; }
|
22
23
|
.console .console-prompt-display { display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0; }
|
24
|
+
.console.full-screen { height: 100%; }
|
25
|
+
.console.full-screen .console-outer { padding-top: 3px; }
|
26
|
+
.console.full-screen .resizer { display: none; }
|
27
|
+
.console.full-screen .close-button { display: none; }
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'web_console/testing/helper'
|
2
|
+
require 'web_console/testing/fake_middleware'
|
3
|
+
|
4
|
+
module WebConsole
|
5
|
+
module Testing
|
6
|
+
# This class is to pre-compile 'templates/*.erb'.
|
7
|
+
class ERBPrecompiler
|
8
|
+
def initialize(path)
|
9
|
+
@erb = ERB.new(File.read(path))
|
10
|
+
@view = FakeMiddleware.new(
|
11
|
+
view_path: Helper.gem_root.join('lib/web_console/templates'),
|
12
|
+
).view
|
13
|
+
end
|
14
|
+
|
15
|
+
def build
|
16
|
+
@erb.result(binding)
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(name, *args, &block)
|
20
|
+
return super unless @view.respond_to?(name)
|
21
|
+
@view.send(name, *args, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'action_dispatch'
|
3
|
+
require 'active_support/core_ext/string/access'
|
4
|
+
require 'json'
|
5
|
+
require 'web_console/whitelist'
|
6
|
+
require 'web_console/request'
|
7
|
+
require 'web_console/view'
|
8
|
+
require 'web_console/testing/helper'
|
9
|
+
|
10
|
+
module WebConsole
|
11
|
+
module Testing
|
12
|
+
class FakeMiddleware
|
13
|
+
I18n.load_path.concat(Dir[Helper.gem_root.join('lib/web_console/locales/*.yml')])
|
14
|
+
|
15
|
+
DEFAULT_HEADERS = { "Content-Type" => "application/javascript" }
|
16
|
+
|
17
|
+
def initialize(opts)
|
18
|
+
@headers = opts.fetch(:headers, DEFAULT_HEADERS)
|
19
|
+
@req_path_regex = opts[:req_path_regex]
|
20
|
+
@view_path = opts[:view_path]
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
[ 200, @headers, [ render(req_path(env)) ] ]
|
25
|
+
end
|
26
|
+
|
27
|
+
def view
|
28
|
+
@view ||= View.new(@view_path)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# extract target path from REQUEST_PATH
|
34
|
+
def req_path(env)
|
35
|
+
env["REQUEST_PATH"].match(@req_path_regex)[1]
|
36
|
+
end
|
37
|
+
|
38
|
+
def render(template)
|
39
|
+
view.render(template: template, layout: nil)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/web_console/version.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
module WebConsole
|
2
|
+
class View < ActionView::Base
|
3
|
+
# Execute a block only on error pages.
|
4
|
+
#
|
5
|
+
# The error pages are special, because they are the only pages that
|
6
|
+
# currently require multiple bindings. We get those from exceptions.
|
7
|
+
def only_on_error_page(*args)
|
8
|
+
yield if @env['web_console.exception'].present?
|
9
|
+
end
|
10
|
+
|
11
|
+
# Render JavaScript inside a script tag and a closure.
|
12
|
+
#
|
13
|
+
# This one lets write JavaScript that will automatically get wrapped in a
|
14
|
+
# script tag and enclosed in a closure, so you don't have to worry for
|
15
|
+
# leaking globals, unless you explicitly want to.
|
16
|
+
def render_javascript(template)
|
17
|
+
render(template: template, layout: 'layouts/javascript')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Render inlined string to be used inside of JavaScript code.
|
21
|
+
#
|
22
|
+
# The inlined string is returned as an actual JavaScript string. You
|
23
|
+
# don't need to wrap the result yourself.
|
24
|
+
def render_inlined_string(template)
|
25
|
+
render(template: template, layout: 'layouts/inlined_string')
|
26
|
+
end
|
27
|
+
|
28
|
+
# Override method for ActionView::Helpers::TranslationHelper#t.
|
29
|
+
#
|
30
|
+
# This method escapes the original return value for JavaScript, since the
|
31
|
+
# method returns a HTML tag with some attributes when the key is not found,
|
32
|
+
# so it could cause a syntax error if we use the value in the string literals.
|
33
|
+
def t(key, options = {})
|
34
|
+
j super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -11,13 +11,6 @@ module WebConsole
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
def acceptable_content_type?
|
15
|
-
whine_unless request.acceptable_content_type? do
|
16
|
-
"Cannot render console with content type #{request.content_type}" \
|
17
|
-
"Allowed content types: #{request.acceptable_content_types}"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
14
|
private
|
22
15
|
|
23
16
|
def whine_unless(condition)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: web-console
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Charlie Somerville
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2016-01-27 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: railties
|
@@ -127,10 +127,14 @@ files:
|
|
127
127
|
- lib/web_console/integration/cruby.rb
|
128
128
|
- lib/web_console/integration/jruby.rb
|
129
129
|
- lib/web_console/integration/rubinius.rb
|
130
|
+
- lib/web_console/locales/en.yml
|
130
131
|
- lib/web_console/middleware.rb
|
131
132
|
- lib/web_console/railtie.rb
|
132
133
|
- lib/web_console/request.rb
|
134
|
+
- lib/web_console/response.rb
|
133
135
|
- lib/web_console/session.rb
|
136
|
+
- lib/web_console/tasks/extensions.rake
|
137
|
+
- lib/web_console/tasks/test_templates.rake
|
134
138
|
- lib/web_console/template.rb
|
135
139
|
- lib/web_console/templates/_inner_console_markup.html.erb
|
136
140
|
- lib/web_console/templates/_markup.html.erb
|
@@ -142,8 +146,11 @@ files:
|
|
142
146
|
- lib/web_console/templates/layouts/javascript.erb
|
143
147
|
- lib/web_console/templates/main.js.erb
|
144
148
|
- lib/web_console/templates/style.css.erb
|
145
|
-
- lib/web_console/
|
149
|
+
- lib/web_console/testing/erb_precompiler.rb
|
150
|
+
- lib/web_console/testing/fake_middleware.rb
|
151
|
+
- lib/web_console/testing/helper.rb
|
146
152
|
- lib/web_console/version.rb
|
153
|
+
- lib/web_console/view.rb
|
147
154
|
- lib/web_console/whiny_request.rb
|
148
155
|
- lib/web_console/whitelist.rb
|
149
156
|
homepage: https://github.com/rails/web-console
|
@@ -166,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
173
|
version: '0'
|
167
174
|
requirements: []
|
168
175
|
rubyforge_project:
|
169
|
-
rubygems_version: 2.
|
176
|
+
rubygems_version: 2.5.1
|
170
177
|
signing_key:
|
171
178
|
specification_version: 4
|
172
179
|
summary: A debugging tool for your Ruby on Rails applications.
|