web-console 2.3.0 → 4.2.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 +5 -5
- data/CHANGELOG.markdown +133 -1
- data/MIT-LICENSE +1 -1
- data/README.markdown +48 -86
- data/Rakefile +14 -12
- data/lib/web-console.rb +3 -1
- data/lib/web_console/context.rb +45 -0
- data/lib/web_console/errors.rb +2 -0
- data/lib/web_console/evaluator.rb +14 -5
- data/lib/web_console/exception_mapper.rb +56 -0
- data/lib/web_console/extensions.rb +28 -17
- data/lib/web_console/injector.rb +32 -0
- data/lib/web_console/interceptor.rb +18 -0
- data/lib/web_console/locales/en.yml +1 -1
- data/lib/web_console/middleware.rb +31 -31
- data/lib/web_console/permissions.rb +42 -0
- data/lib/web_console/railtie.rb +38 -24
- data/lib/web_console/request.rb +8 -20
- data/lib/web_console/session.rb +32 -18
- data/lib/web_console/source_location.rb +15 -0
- data/lib/web_console/tasks/extensions.rake +15 -13
- data/lib/web_console/tasks/templates.rake +56 -0
- data/lib/web_console/template.rb +4 -3
- data/lib/web_console/templates/console.js.erb +497 -37
- data/lib/web_console/templates/error_page.js.erb +7 -8
- data/lib/web_console/templates/index.html.erb +4 -0
- data/lib/web_console/templates/layouts/inlined_string.erb +1 -1
- data/lib/web_console/templates/layouts/javascript.erb +1 -1
- data/lib/web_console/templates/regular_page.js.erb +24 -0
- data/lib/web_console/templates/style.css.erb +182 -27
- data/lib/web_console/testing/erb_precompiler.rb +5 -3
- data/lib/web_console/testing/fake_middleware.rb +7 -10
- data/lib/web_console/testing/helper.rb +3 -1
- data/lib/web_console/version.rb +3 -1
- data/lib/web_console/view.rb +24 -3
- data/lib/web_console/whiny_request.rb +8 -6
- data/lib/web_console.rb +28 -20
- metadata +28 -63
- data/lib/web_console/helper.rb +0 -22
- data/lib/web_console/integration/cruby.rb +0 -40
- data/lib/web_console/integration/jruby.rb +0 -111
- data/lib/web_console/integration/rubinius.rb +0 -67
- data/lib/web_console/integration.rb +0 -8
- data/lib/web_console/response.rb +0 -23
- data/lib/web_console/tasks/test_templates.rake +0 -50
- data/lib/web_console/whitelist.rb +0 -42
|
@@ -1,23 +1,34 @@
|
|
|
1
|
-
|
|
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
|
|
1
|
+
# frozen_string_literal: true
|
|
6
2
|
|
|
7
|
-
|
|
3
|
+
module Kernel
|
|
4
|
+
module_function
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
# Instructs Web Console to render a console in the specified binding.
|
|
7
|
+
#
|
|
8
|
+
# If +binding+ isn't explicitly given it will default to the binding of the
|
|
9
|
+
# previous frame. E.g. the one that invoked +console+.
|
|
10
|
+
#
|
|
11
|
+
# Raises +DoubleRenderError+ if a more than one +console+ invocation per
|
|
12
|
+
# request is detected.
|
|
13
|
+
def console(binding = Bindex.current_bindings.second)
|
|
14
|
+
raise WebConsole::DoubleRenderError if Thread.current[:__web_console_binding]
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
16
|
+
Thread.current[:__web_console_binding] = binding
|
|
17
|
+
|
|
18
|
+
# Make sure nothing is rendered from the view helper. Otherwise
|
|
19
|
+
# you're gonna see unexpected #<Binding:0x007fee4302b078> in the
|
|
20
|
+
# templates.
|
|
21
|
+
nil
|
|
19
22
|
end
|
|
23
|
+
end
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
class Binding
|
|
26
|
+
# Instructs Web Console to render a console in the current binding, without
|
|
27
|
+
# the need to unroll the stack.
|
|
28
|
+
#
|
|
29
|
+
# Raises +DoubleRenderError+ if a more than one +console+ invocation per
|
|
30
|
+
# request is detected.
|
|
31
|
+
def console
|
|
32
|
+
Kernel.console(self)
|
|
33
|
+
end
|
|
23
34
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebConsole
|
|
4
|
+
# Injects content into a Rack body.
|
|
5
|
+
class Injector
|
|
6
|
+
def initialize(body, headers)
|
|
7
|
+
@body = "".dup
|
|
8
|
+
|
|
9
|
+
body.each { |part| @body << part }
|
|
10
|
+
body.close if body.respond_to?(:close)
|
|
11
|
+
|
|
12
|
+
@headers = headers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inject(content)
|
|
16
|
+
# Set Content-Length header to the size of the current body
|
|
17
|
+
# + the extra content. Otherwise the response will be truncated.
|
|
18
|
+
if @headers["Content-Length"]
|
|
19
|
+
@headers["Content-Length"] = (@body.bytesize + content.bytesize).to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
[
|
|
23
|
+
if position = @body.rindex("</body>")
|
|
24
|
+
[ @body.insert(position, content) ]
|
|
25
|
+
else
|
|
26
|
+
[ @body << content ]
|
|
27
|
+
end,
|
|
28
|
+
@headers
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module WebConsole
|
|
2
|
+
module Interceptor
|
|
3
|
+
def self.call(request, exception)
|
|
4
|
+
backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
|
|
5
|
+
error = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).exception
|
|
6
|
+
|
|
7
|
+
# Get the original exception if ExceptionWrapper decides to follow it.
|
|
8
|
+
Thread.current[:__web_console_exception] = error
|
|
9
|
+
|
|
10
|
+
# ActionView::Template::Error bypass ExceptionWrapper original
|
|
11
|
+
# exception following. The backtrace in the view is generated from
|
|
12
|
+
# reaching out to original_exception in the view.
|
|
13
|
+
if error.is_a?(ActionView::Template::Error)
|
|
14
|
+
Thread.current[:__web_console_exception] = error.cause
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
en:
|
|
2
2
|
errors:
|
|
3
3
|
unavailable_session: |
|
|
4
|
-
Session %{id} is
|
|
4
|
+
Session %{id} is no longer available in memory.
|
|
5
5
|
|
|
6
6
|
If you happen to run on a multi-process server (like Unicorn or Puma) the process
|
|
7
7
|
this request hit doesn't store %{id} in memory. Consider turning the number of
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/strip"
|
|
2
4
|
|
|
3
5
|
module WebConsole
|
|
4
6
|
class Middleware
|
|
5
|
-
TEMPLATES_PATH = File.expand_path(
|
|
6
|
-
|
|
7
|
-
cattr_accessor :mount_point
|
|
8
|
-
@@mount_point = '/__web_console'
|
|
7
|
+
TEMPLATES_PATH = File.expand_path("../templates", __FILE__)
|
|
9
8
|
|
|
10
|
-
cattr_accessor :
|
|
11
|
-
|
|
9
|
+
cattr_accessor :mount_point, default: "/__web_console"
|
|
10
|
+
cattr_accessor :whiny_requests, default: true
|
|
12
11
|
|
|
13
12
|
def initialize(app)
|
|
14
13
|
@app = app
|
|
@@ -17,7 +16,7 @@ module WebConsole
|
|
|
17
16
|
def call(env)
|
|
18
17
|
app_exception = catch :app_exception do
|
|
19
18
|
request = create_regular_or_whiny_request(env)
|
|
20
|
-
return
|
|
19
|
+
return call_app(env) unless request.permitted?
|
|
21
20
|
|
|
22
21
|
if id = id_for_repl_session_update(request)
|
|
23
22
|
return update_repl_session(id, request)
|
|
@@ -25,49 +24,46 @@ module WebConsole
|
|
|
25
24
|
return change_stack_trace(id, request)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
status, headers, body = @app.call(env)
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
session = Session.from_exception(exception)
|
|
32
|
-
elsif binding = env['web_console.binding']
|
|
33
|
-
session = Session.from_binding(binding)
|
|
34
|
-
end
|
|
28
|
+
status, headers, body = call_app(env)
|
|
35
29
|
|
|
36
|
-
if session && acceptable_content_type?(headers)
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
if (session = Session.from(Thread.current)) && acceptable_content_type?(headers)
|
|
31
|
+
headers["X-Web-Console-Session-Id"] = session.id
|
|
32
|
+
headers["X-Web-Console-Mount-Point"] = mount_point
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
response.write(template.render('index'))
|
|
43
|
-
response.finish
|
|
44
|
-
else
|
|
45
|
-
[ status, headers, body ]
|
|
34
|
+
template = Template.new(env, session)
|
|
35
|
+
body, headers = Injector.new(body, headers).inject(template.render("index"))
|
|
46
36
|
end
|
|
37
|
+
|
|
38
|
+
[ status, headers, body ]
|
|
47
39
|
end
|
|
48
40
|
rescue => e
|
|
49
41
|
WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}")
|
|
50
42
|
raise e
|
|
51
43
|
ensure
|
|
44
|
+
# Clean up the fiber locals after the session creation. Object#console
|
|
45
|
+
# uses those to communicate the current binding or exception to the middleware.
|
|
46
|
+
Thread.current[:__web_console_exception] = nil
|
|
47
|
+
Thread.current[:__web_console_binding] = nil
|
|
48
|
+
|
|
52
49
|
raise app_exception if Exception === app_exception
|
|
53
50
|
end
|
|
54
51
|
|
|
55
52
|
private
|
|
56
53
|
|
|
57
54
|
def acceptable_content_type?(headers)
|
|
58
|
-
|
|
55
|
+
headers["Content-Type"].to_s.include?("html")
|
|
59
56
|
end
|
|
60
57
|
|
|
61
58
|
def json_response(opts = {})
|
|
62
59
|
status = opts.fetch(:status, 200)
|
|
63
|
-
headers = {
|
|
60
|
+
headers = { "Content-Type" => "application/json; charset = utf-8" }
|
|
64
61
|
body = yield.to_json
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
[ status, headers, [ body ] ]
|
|
67
64
|
end
|
|
68
65
|
|
|
69
66
|
def json_response_with_session(id, request, opts = {})
|
|
70
|
-
return respond_with_unacceptable_request unless request.acceptable?
|
|
71
67
|
return respond_with_unavailable_session(id) unless session = Session.find(id)
|
|
72
68
|
|
|
73
69
|
json_response(opts) { yield session }
|
|
@@ -104,13 +100,17 @@ module WebConsole
|
|
|
104
100
|
|
|
105
101
|
def update_repl_session(id, request)
|
|
106
102
|
json_response_with_session(id, request) do |session|
|
|
107
|
-
|
|
103
|
+
if input = request.params[:input]
|
|
104
|
+
{ output: session.eval(input) }
|
|
105
|
+
elsif input = request.params[:context]
|
|
106
|
+
{ context: session.context(input) }
|
|
107
|
+
end
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
def change_stack_trace(id, request)
|
|
112
112
|
json_response_with_session(id, request) do |session|
|
|
113
|
-
session.switch_binding_to(request.params[:frame_id])
|
|
113
|
+
session.switch_binding_to(request.params[:frame_id], request.params[:exception_object_id])
|
|
114
114
|
|
|
115
115
|
{ ok: true }
|
|
116
116
|
end
|
|
@@ -118,13 +118,13 @@ module WebConsole
|
|
|
118
118
|
|
|
119
119
|
def respond_with_unavailable_session(id)
|
|
120
120
|
json_response(status: 404) do
|
|
121
|
-
{ output: format(I18n.t(
|
|
121
|
+
{ output: format(I18n.t("errors.unavailable_session"), id: id) }
|
|
122
122
|
end
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def respond_with_unacceptable_request
|
|
126
126
|
json_response(status: 406) do
|
|
127
|
-
{ output: I18n.t(
|
|
127
|
+
{ output: I18n.t("errors.unacceptable_request") }
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
130
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module WebConsole
|
|
6
|
+
class Permissions
|
|
7
|
+
# IPv4 and IPv6 localhost should be always allowed.
|
|
8
|
+
ALWAYS_PERMITTED_NETWORKS = %w( 127.0.0.0/8 ::1 )
|
|
9
|
+
|
|
10
|
+
def initialize(networks = nil)
|
|
11
|
+
@networks = normalize_networks(networks).map(&method(:coerce_network_to_ipaddr)).uniq
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def include?(network)
|
|
15
|
+
@networks.any? { |permission| permission.include?(network.to_s) }
|
|
16
|
+
rescue IPAddr::InvalidAddressError
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
@networks.map(&method(:human_readable_ipaddr)).join(", ")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def normalize_networks(networks)
|
|
27
|
+
Array(networks).concat(ALWAYS_PERMITTED_NETWORKS)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def coerce_network_to_ipaddr(network)
|
|
31
|
+
if network.is_a?(IPAddr)
|
|
32
|
+
network
|
|
33
|
+
else
|
|
34
|
+
IPAddr.new(network)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def human_readable_ipaddr(ipaddr)
|
|
39
|
+
ipaddr.to_range.to_s.split("..").uniq.join("/")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/web_console/railtie.rb
CHANGED
|
@@ -1,32 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
2
4
|
|
|
3
5
|
module WebConsole
|
|
4
6
|
class Railtie < ::Rails::Railtie
|
|
5
7
|
config.web_console = ActiveSupport::OrderedOptions.new
|
|
6
|
-
config.web_console.whitelisted_ips = %w( 127.0.0.1 ::1 )
|
|
7
|
-
|
|
8
|
-
initializer 'web_console.initialize' do
|
|
9
|
-
require 'web_console/extensions'
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
initializer "web_console.initialize" do
|
|
10
|
+
require "bindex"
|
|
11
|
+
require "web_console/extensions"
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
ActionController::Base.send(:include, Helper)
|
|
17
|
-
end
|
|
13
|
+
ActionDispatch::DebugExceptions.register_interceptor(Interceptor)
|
|
18
14
|
end
|
|
19
15
|
|
|
20
|
-
initializer
|
|
16
|
+
initializer "web_console.development_only" do
|
|
21
17
|
unless (config.web_console.development_only == false) || Rails.env.development?
|
|
22
18
|
abort <<-END.strip_heredoc
|
|
23
|
-
Web Console is activated in the #{Rails.env} environment
|
|
19
|
+
Web Console is activated in the #{Rails.env} environment. This is
|
|
24
20
|
usually a mistake. To ensure it's only activated in development
|
|
25
21
|
mode, move it to the development group of your Gemfile:
|
|
26
22
|
|
|
27
23
|
gem 'web-console', group: :development
|
|
28
24
|
|
|
29
|
-
If you still want to run it the #{Rails.env} environment (and know
|
|
25
|
+
If you still want to run it in the #{Rails.env} environment (and know
|
|
30
26
|
what you are doing), put this in your Rails application
|
|
31
27
|
configuration:
|
|
32
28
|
|
|
@@ -35,36 +31,54 @@ module WebConsole
|
|
|
35
31
|
end
|
|
36
32
|
end
|
|
37
33
|
|
|
38
|
-
initializer
|
|
34
|
+
initializer "web_console.insert_middleware" do |app|
|
|
39
35
|
app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware
|
|
40
36
|
end
|
|
41
37
|
|
|
42
|
-
initializer
|
|
38
|
+
initializer "web_console.mount_point" do
|
|
43
39
|
if mount_point = config.web_console.mount_point
|
|
44
|
-
Middleware.mount_point = mount_point.chomp(
|
|
40
|
+
Middleware.mount_point = mount_point.chomp("/")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if root = Rails.application.config.relative_url_root
|
|
44
|
+
Middleware.mount_point = File.join(root, Middleware.mount_point)
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
initializer
|
|
48
|
+
initializer "web_console.template_paths" do
|
|
49
49
|
if template_paths = config.web_console.template_paths
|
|
50
50
|
Template.template_paths.unshift(*Array(template_paths))
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
initializer
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
initializer "web_console.permissions" do
|
|
55
|
+
permissions = web_console_permissions
|
|
56
|
+
Request.permissions = Permissions.new(permissions)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def web_console_permissions
|
|
60
|
+
case
|
|
61
|
+
when config.web_console.permissions
|
|
62
|
+
config.web_console.permissions
|
|
63
|
+
when config.web_console.allowed_ips
|
|
64
|
+
config.web_console.allowed_ips
|
|
65
|
+
when config.web_console.whitelisted_ips
|
|
66
|
+
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
|
67
|
+
The config.web_console.whitelisted_ips is deprecated and will be ignored in future release of web_console.
|
|
68
|
+
Please use config.web_console.allowed_ips instead.
|
|
69
|
+
MSG
|
|
70
|
+
config.web_console.whitelisted_ips
|
|
57
71
|
end
|
|
58
72
|
end
|
|
59
73
|
|
|
60
|
-
initializer
|
|
74
|
+
initializer "web_console.whiny_requests" do
|
|
61
75
|
if config.web_console.key?(:whiny_requests)
|
|
62
76
|
Middleware.whiny_requests = config.web_console.whiny_requests
|
|
63
77
|
end
|
|
64
78
|
end
|
|
65
79
|
|
|
66
|
-
initializer
|
|
67
|
-
config.i18n.load_path.concat(Dir[File.expand_path(
|
|
80
|
+
initializer "i18n.load_path" do
|
|
81
|
+
config.i18n.load_path.concat(Dir[File.expand_path("../locales/*.yml", __FILE__)])
|
|
68
82
|
end
|
|
69
83
|
end
|
|
70
84
|
end
|
data/lib/web_console/request.rb
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module WebConsole
|
|
2
|
-
# Web Console tailored request object.
|
|
3
4
|
class Request < ActionDispatch::Request
|
|
4
|
-
|
|
5
|
-
cattr_accessor :whitelisted_ips
|
|
6
|
-
@@whitelisted_ips = Whitelist.new
|
|
7
|
-
|
|
8
|
-
# Define a vendor MIME type. We can call it using Mime::WEB_CONSOLE_V2 constant.
|
|
9
|
-
Mime::Type.register 'application/vnd.web-console.v2', :web_console_v2
|
|
5
|
+
cattr_accessor :permissions, default: Permissions.new
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# For a request to hit Web Console features, it needs to come from a white
|
|
14
|
-
# listed IP.
|
|
15
|
-
def from_whitelited_ip?
|
|
16
|
-
whitelisted_ips.include?(strict_remote_ip)
|
|
7
|
+
def permitted?
|
|
8
|
+
permissions.include?(strict_remote_ip)
|
|
17
9
|
end
|
|
18
10
|
|
|
19
|
-
# Determines the remote IP using our much stricter whitelist.
|
|
20
11
|
def strict_remote_ip
|
|
21
|
-
GetSecureIp.new(self,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Returns whether the request is acceptable.
|
|
25
|
-
def acceptable?
|
|
26
|
-
xhr? && accepts.any? { |mime| Mime::WEB_CONSOLE_V2 == mime }
|
|
12
|
+
GetSecureIp.new(self, permissions).to_s
|
|
13
|
+
rescue ActionDispatch::RemoteIp::IpSpoofAttackError
|
|
14
|
+
"[Spoofed]"
|
|
27
15
|
end
|
|
28
16
|
|
|
29
17
|
private
|
data/lib/web_console/session.rb
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module WebConsole
|
|
2
|
-
# A session lets you persist
|
|
3
|
-
#
|
|
4
|
+
# A session lets you persist an +Evaluator+ instance in memory associated
|
|
5
|
+
# with multiple bindings.
|
|
4
6
|
#
|
|
5
7
|
# Each newly created session is persisted into memory and you can find it
|
|
6
|
-
# later its +id+.
|
|
8
|
+
# later by its +id+.
|
|
7
9
|
#
|
|
8
10
|
# A session may be associated with multiple bindings. This is used by the
|
|
9
11
|
# error pages only, as currently, this is the only client that needs to do
|
|
10
12
|
# that.
|
|
11
13
|
class Session
|
|
12
|
-
cattr_reader :inmemory_storage
|
|
13
|
-
@@inmemory_storage = {}
|
|
14
|
+
cattr_reader :inmemory_storage, default: {}
|
|
14
15
|
|
|
15
16
|
class << self
|
|
16
17
|
# Finds a persisted session in memory by its id.
|
|
@@ -21,24 +22,30 @@ module WebConsole
|
|
|
21
22
|
inmemory_storage[id]
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
# Create a Session from an exception.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
# Create a Session from an binding or exception in a storage.
|
|
26
|
+
#
|
|
27
|
+
# The storage is expected to respond to #[]. The binding is expected in
|
|
28
|
+
# :__web_console_binding and the exception in :__web_console_exception.
|
|
29
|
+
#
|
|
30
|
+
# Can return nil, if no binding or exception have been preserved in the
|
|
31
|
+
# storage.
|
|
32
|
+
def from(storage)
|
|
33
|
+
if exc = storage[:__web_console_exception]
|
|
34
|
+
new(ExceptionMapper.follow(exc))
|
|
35
|
+
elsif binding = storage[:__web_console_binding]
|
|
36
|
+
new([[binding]])
|
|
37
|
+
end
|
|
32
38
|
end
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
# An unique identifier for every REPL.
|
|
36
42
|
attr_reader :id
|
|
37
43
|
|
|
38
|
-
def initialize(
|
|
44
|
+
def initialize(exception_mappers)
|
|
39
45
|
@id = SecureRandom.hex(16)
|
|
40
|
-
|
|
41
|
-
@
|
|
46
|
+
|
|
47
|
+
@exception_mappers = exception_mappers
|
|
48
|
+
@evaluator = Evaluator.new(@current_binding = exception_mappers.first.first)
|
|
42
49
|
|
|
43
50
|
store_into_memory
|
|
44
51
|
end
|
|
@@ -53,8 +60,15 @@ module WebConsole
|
|
|
53
60
|
# Switches the current binding to the one at specified +index+.
|
|
54
61
|
#
|
|
55
62
|
# Returns nothing.
|
|
56
|
-
def switch_binding_to(index)
|
|
57
|
-
|
|
63
|
+
def switch_binding_to(index, exception_object_id)
|
|
64
|
+
bindings = ExceptionMapper.find_binding(@exception_mappers, exception_object_id)
|
|
65
|
+
|
|
66
|
+
@evaluator = Evaluator.new(@current_binding = bindings[index.to_i])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns context of the current binding
|
|
70
|
+
def context(objpath)
|
|
71
|
+
Context.new(@current_binding).extract(objpath)
|
|
58
72
|
end
|
|
59
73
|
|
|
60
74
|
private
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SourceLocation
|
|
4
|
+
def initialize(binding)
|
|
5
|
+
@binding = binding
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
if RUBY_VERSION >= "2.6"
|
|
9
|
+
def path() @binding.source_location.first end
|
|
10
|
+
def lineno() @binding.source_location.last end
|
|
11
|
+
else
|
|
12
|
+
def path() @binding.eval("__FILE__") end
|
|
13
|
+
def lineno() @binding.eval("__LINE__") end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,23 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
namespace :ext do
|
|
2
|
-
rootdir = Pathname(
|
|
4
|
+
rootdir = Pathname("extensions")
|
|
3
5
|
|
|
4
|
-
desc
|
|
5
|
-
task chrome:
|
|
6
|
+
desc "Build Chrome Extension"
|
|
7
|
+
task chrome: "chrome:build"
|
|
6
8
|
|
|
7
9
|
namespace :chrome do
|
|
8
|
-
dist = Pathname(
|
|
10
|
+
dist = Pathname("dist/crx")
|
|
9
11
|
extdir = rootdir.join(dist)
|
|
10
|
-
manifest_json = rootdir.join(
|
|
12
|
+
manifest_json = rootdir.join("chrome/manifest.json")
|
|
11
13
|
|
|
12
14
|
directory extdir
|
|
13
15
|
|
|
14
|
-
task build: [ extdir,
|
|
16
|
+
task build: [ extdir, "lib:templates" ] do
|
|
15
17
|
cd rootdir do
|
|
16
|
-
cp_r [
|
|
18
|
+
cp_r [ "img/", "tmp/lib/" ], dist
|
|
17
19
|
`cd chrome && git ls-files`.split("\n").each do |src|
|
|
18
20
|
dest = dist.join(src)
|
|
19
21
|
mkdir_p dest.dirname
|
|
20
|
-
cp Pathname(
|
|
22
|
+
cp Pathname("chrome").join(src), dest
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
end
|
|
@@ -34,7 +36,7 @@ namespace :ext do
|
|
|
34
36
|
cd(extdir) { sh "zip -r ../crx-web-console-#{version}.zip ./" }
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
desc
|
|
39
|
+
desc "Launch a browser with the chrome extension."
|
|
38
40
|
task run: [ :build ] do
|
|
39
41
|
cd(rootdir) { sh "sh ./script/run_chrome.sh --load-extension=#{dist}" }
|
|
40
42
|
end
|
|
@@ -45,15 +47,15 @@ namespace :ext do
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
namespace :lib do
|
|
48
|
-
templates = Pathname(
|
|
49
|
-
tmplib = rootdir.join(
|
|
50
|
-
js_erb = FileList.new(templates.join(
|
|
50
|
+
templates = Pathname("lib/web_console/templates")
|
|
51
|
+
tmplib = rootdir.join("tmp/lib/")
|
|
52
|
+
js_erb = FileList.new(templates.join("**/*.js.erb"))
|
|
51
53
|
dirs = js_erb.pathmap("%{^#{templates},#{tmplib}}d")
|
|
52
54
|
|
|
53
55
|
task templates: dirs + js_erb.pathmap("%{^#{templates},#{tmplib}}X")
|
|
54
56
|
|
|
55
57
|
dirs.each { |d| directory d }
|
|
56
|
-
rule
|
|
58
|
+
rule ".js" => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t|
|
|
57
59
|
File.write(t.name, WebConsole::Testing::ERBPrecompiler.new(t.source).build)
|
|
58
60
|
end
|
|
59
61
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :templates do
|
|
4
|
+
desc "Run tests for templates"
|
|
5
|
+
task test: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
|
|
6
|
+
task serve: [ :npm, :rackup ]
|
|
7
|
+
|
|
8
|
+
workdir = Pathname(EXPANDED_CWD).join("test/templates")
|
|
9
|
+
pid = Pathname(Dir.tmpdir).join("web_console_test.pid")
|
|
10
|
+
runner = URI.parse("http://#{ENV['IP'] || '127.0.0.1'}:#{ENV['PORT'] || 29292}/html/test_runner.html")
|
|
11
|
+
rackup = "rackup --host #{runner.host} --port #{runner.port}"
|
|
12
|
+
result = nil
|
|
13
|
+
browser = "phantomjs"
|
|
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 += " -D --pid #{pid}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
task npm: [ :phantomjs ] do
|
|
26
|
+
Dir.chdir(workdir) { system "npm install --silent" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
task :phantomjs do
|
|
30
|
+
unless system("which #{browser} >/dev/null")
|
|
31
|
+
browser = "./node_modules/.bin/phantomjs"
|
|
32
|
+
Dir.chdir(workdir) { system("test -f #{browser} || npm install --silent phantomjs-prebuilt") }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
task :rackup do
|
|
37
|
+
Dir.chdir(workdir) { system rackup }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
task :wait do
|
|
41
|
+
cnt = 0
|
|
42
|
+
need_to_wait?(runner) { sleep 1; cnt += 1; cnt < 5 }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
task :mocha do
|
|
46
|
+
Dir.chdir(workdir) { result = system("#{browser} ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js #{runner} dot") }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
task :kill do
|
|
50
|
+
system "kill #{File.read pid}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
task :exit do
|
|
54
|
+
exit result
|
|
55
|
+
end
|
|
56
|
+
end
|