web-console-compat 3.5.1
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 +7 -0
- data/CHANGELOG.markdown +110 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +5 -0
- data/Rakefile +27 -0
- data/lib/web-console-compat.rb +1 -0
- data/lib/web-console.rb +1 -0
- data/lib/web_console.rb +28 -0
- data/lib/web_console/context.rb +43 -0
- data/lib/web_console/errors.rb +7 -0
- data/lib/web_console/evaluator.rb +33 -0
- data/lib/web_console/exception_mapper.rb +33 -0
- data/lib/web_console/extensions.rb +44 -0
- data/lib/web_console/integration.rb +31 -0
- data/lib/web_console/integration/cruby.rb +23 -0
- data/lib/web_console/integration/rubinius.rb +39 -0
- data/lib/web_console/locales/en.yml +15 -0
- data/lib/web_console/middleware.rb +140 -0
- data/lib/web_console/railtie.rb +71 -0
- data/lib/web_console/request.rb +50 -0
- data/lib/web_console/response.rb +23 -0
- data/lib/web_console/session.rb +76 -0
- data/lib/web_console/tasks/extensions.rake +60 -0
- data/lib/web_console/tasks/templates.rake +54 -0
- data/lib/web_console/template.rb +23 -0
- data/lib/web_console/templates/_inner_console_markup.html.erb +8 -0
- data/lib/web_console/templates/_markup.html.erb +5 -0
- data/lib/web_console/templates/_prompt_box_markup.html.erb +2 -0
- data/lib/web_console/templates/console.js.erb +922 -0
- data/lib/web_console/templates/error_page.js.erb +70 -0
- data/lib/web_console/templates/index.html.erb +8 -0
- data/lib/web_console/templates/layouts/inlined_string.erb +1 -0
- data/lib/web_console/templates/layouts/javascript.erb +5 -0
- data/lib/web_console/templates/main.js.erb +1 -0
- data/lib/web_console/templates/style.css.erb +33 -0
- data/lib/web_console/testing/erb_precompiler.rb +25 -0
- data/lib/web_console/testing/fake_middleware.rb +39 -0
- data/lib/web_console/testing/helper.rb +9 -0
- data/lib/web_console/version.rb +3 -0
- data/lib/web_console/view.rb +50 -0
- data/lib/web_console/whiny_request.rb +31 -0
- data/lib/web_console/whitelist.rb +44 -0
- metadata +147 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
en:
|
2
|
+
errors:
|
3
|
+
unavailable_session: |
|
4
|
+
Session %{id} 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.
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'active_support/core_ext/string/strip'
|
2
|
+
|
3
|
+
module WebConsole
|
4
|
+
class Middleware
|
5
|
+
TEMPLATES_PATH = File.expand_path('../templates', __FILE__)
|
6
|
+
|
7
|
+
cattr_accessor :mount_point
|
8
|
+
@@mount_point = '/__web_console'
|
9
|
+
|
10
|
+
cattr_accessor :whiny_requests
|
11
|
+
@@whiny_requests = true
|
12
|
+
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
app_exception = catch :app_exception do
|
19
|
+
request = create_regular_or_whiny_request(env)
|
20
|
+
return call_app(env) unless request.from_whitelisted_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
|
27
|
+
|
28
|
+
status, headers, body = call_app(env)
|
29
|
+
|
30
|
+
if session = Session.from(Thread.current) and acceptable_content_type?(headers)
|
31
|
+
response = Response.new(body, status, headers)
|
32
|
+
template = Template.new(env, session)
|
33
|
+
|
34
|
+
response.headers["X-Web-Console-Session-Id"] = session.id
|
35
|
+
response.headers["X-Web-Console-Mount-Point"] = mount_point
|
36
|
+
response.write(template.render('index'))
|
37
|
+
response.finish
|
38
|
+
else
|
39
|
+
[ status, headers, body ]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
rescue => e
|
43
|
+
WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}")
|
44
|
+
raise e
|
45
|
+
ensure
|
46
|
+
# Clean up the fiber locals after the session creation. Object#console
|
47
|
+
# uses those to communicate the current binding or exception to the middleware.
|
48
|
+
Thread.current[:__web_console_exception] = nil
|
49
|
+
Thread.current[:__web_console_binding] = nil
|
50
|
+
|
51
|
+
raise app_exception if Exception === app_exception
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def acceptable_content_type?(headers)
|
57
|
+
Mime::Type.parse(headers['Content-Type'].to_s).first == Mime[:html]
|
58
|
+
end
|
59
|
+
|
60
|
+
def json_response(opts = {})
|
61
|
+
status = opts.fetch(:status, 200)
|
62
|
+
headers = { 'Content-Type' => 'application/json; charset = utf-8' }
|
63
|
+
body = yield.to_json
|
64
|
+
|
65
|
+
Rack::Response.new(body, status, headers).finish
|
66
|
+
end
|
67
|
+
|
68
|
+
def json_response_with_session(id, request, opts = {})
|
69
|
+
return respond_with_unacceptable_request unless request.acceptable?
|
70
|
+
return respond_with_unavailable_session(id) unless session = Session.find(id)
|
71
|
+
|
72
|
+
json_response(opts) { yield session }
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_regular_or_whiny_request(env)
|
76
|
+
request = Request.new(env)
|
77
|
+
whiny_requests ? WhinyRequest.new(request) : request
|
78
|
+
end
|
79
|
+
|
80
|
+
def repl_sessions_re
|
81
|
+
@_repl_sessions_re ||= %r{#{mount_point}/repl_sessions/(?<id>[^/]+)}
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_re
|
85
|
+
@_update_re ||= %r{#{repl_sessions_re}\z}
|
86
|
+
end
|
87
|
+
|
88
|
+
def binding_change_re
|
89
|
+
@_binding_change_re ||= %r{#{repl_sessions_re}/trace\z}
|
90
|
+
end
|
91
|
+
|
92
|
+
def id_for_repl_session_update(request)
|
93
|
+
if request.xhr? && request.put?
|
94
|
+
update_re.match(request.path) { |m| m[:id] }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def id_for_repl_session_stack_frame_change(request)
|
99
|
+
if request.xhr? && request.post?
|
100
|
+
binding_change_re.match(request.path) { |m| m[:id] }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def update_repl_session(id, request)
|
105
|
+
json_response_with_session(id, request) do |session|
|
106
|
+
if input = request.params[:input]
|
107
|
+
{ output: session.eval(input) }
|
108
|
+
elsif input = request.params[:context]
|
109
|
+
{ context: session.context(input) }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def change_stack_trace(id, request)
|
115
|
+
json_response_with_session(id, request) do |session|
|
116
|
+
session.switch_binding_to(request.params[:frame_id])
|
117
|
+
|
118
|
+
{ ok: true }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def respond_with_unavailable_session(id)
|
123
|
+
json_response(status: 404) do
|
124
|
+
{ output: format(I18n.t('errors.unavailable_session'), id: id)}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def respond_with_unacceptable_request
|
129
|
+
json_response(status: 406) do
|
130
|
+
{ output: I18n.t('errors.unacceptable_request') }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def call_app(env)
|
135
|
+
@app.call(env)
|
136
|
+
rescue => e
|
137
|
+
throw :app_exception, e
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
|
3
|
+
module WebConsole
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
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/integration'
|
10
|
+
require 'web_console/extensions'
|
11
|
+
|
12
|
+
if logger = ::Rails.logger
|
13
|
+
WebConsole.logger = logger
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
initializer 'web_console.development_only' do
|
18
|
+
unless (config.web_console.development_only == false) || Rails.env.development?
|
19
|
+
abort <<-END.strip_heredoc
|
20
|
+
Web Console is activated in the #{Rails.env} environment. This is
|
21
|
+
usually a mistake. To ensure it's only activated in development
|
22
|
+
mode, move it to the development group of your Gemfile:
|
23
|
+
|
24
|
+
gem 'web-console', group: :development
|
25
|
+
|
26
|
+
If you still want to run it in the #{Rails.env} environment (and know
|
27
|
+
what you are doing), put this in your Rails application
|
28
|
+
configuration:
|
29
|
+
|
30
|
+
config.web_console.development_only = false
|
31
|
+
END
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
initializer 'web_console.insert_middleware' do |app|
|
36
|
+
app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware
|
37
|
+
end
|
38
|
+
|
39
|
+
initializer 'web_console.mount_point' do
|
40
|
+
if mount_point = config.web_console.mount_point
|
41
|
+
Middleware.mount_point = mount_point.chomp('/')
|
42
|
+
end
|
43
|
+
|
44
|
+
if root = Rails.application.config.relative_url_root
|
45
|
+
Middleware.mount_point = File.join(root, Middleware.mount_point)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
initializer 'web_console.template_paths' do
|
50
|
+
if template_paths = config.web_console.template_paths
|
51
|
+
Template.template_paths.unshift(*Array(template_paths))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
initializer 'web_console.whitelisted_ips' do
|
56
|
+
if whitelisted_ips = config.web_console.whitelisted_ips
|
57
|
+
Request.whitelisted_ips = Whitelist.new(whitelisted_ips)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
initializer 'web_console.whiny_requests' do
|
62
|
+
if config.web_console.key?(:whiny_requests)
|
63
|
+
Middleware.whiny_requests = config.web_console.whiny_requests
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
initializer 'i18n.load_path' do
|
68
|
+
config.i18n.load_path.concat(Dir[File.expand_path('../locales/*.yml', __FILE__)])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module WebConsole
|
2
|
+
# Web Console tailored request object.
|
3
|
+
class Request < ActionDispatch::Request
|
4
|
+
# Configurable set of whitelisted networks.
|
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].
|
9
|
+
Mime::Type.register 'application/vnd.web-console.v2', :web_console_v2
|
10
|
+
|
11
|
+
# Returns whether a request came from a whitelisted IP.
|
12
|
+
#
|
13
|
+
# For a request to hit Web Console features, it needs to come from a white
|
14
|
+
# listed IP.
|
15
|
+
def from_whitelisted_ip?
|
16
|
+
whitelisted_ips.include?(strict_remote_ip)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Determines the remote IP using our much stricter whitelist.
|
20
|
+
def strict_remote_ip
|
21
|
+
GetSecureIp.new(self, whitelisted_ips).to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns whether the request is acceptable.
|
25
|
+
def acceptable?
|
26
|
+
xhr? && accepts.any? { |mime| Mime[:web_console_v2] == mime }
|
27
|
+
end
|
28
|
+
|
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
|
42
|
+
|
43
|
+
def filter_proxies(ips)
|
44
|
+
ips.reject do |ip|
|
45
|
+
@proxies.include?(ip)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
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,76 @@
|
|
1
|
+
module WebConsole
|
2
|
+
# A session lets you persist an +Evaluator+ instance in memory associated
|
3
|
+
# with multiple bindings.
|
4
|
+
#
|
5
|
+
# Each newly created session is persisted into memory and you can find it
|
6
|
+
# later by its +id+.
|
7
|
+
#
|
8
|
+
# A session may be associated with multiple bindings. This is used by the
|
9
|
+
# error pages only, as currently, this is the only client that needs to do
|
10
|
+
# that.
|
11
|
+
class Session
|
12
|
+
cattr_reader :inmemory_storage
|
13
|
+
@@inmemory_storage = {}
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Finds a persisted session in memory by its id.
|
17
|
+
#
|
18
|
+
# Returns a persisted session if found in memory.
|
19
|
+
# Raises NotFound error unless found in memory.
|
20
|
+
def find(id)
|
21
|
+
inmemory_storage[id]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create a Session from an binding or exception in a storage.
|
25
|
+
#
|
26
|
+
# The storage is expected to respond to #[]. The binding is expected in
|
27
|
+
# :__web_console_binding and the exception in :__web_console_exception.
|
28
|
+
#
|
29
|
+
# Can return nil, if no binding or exception have been preserved in the
|
30
|
+
# storage.
|
31
|
+
def from(storage)
|
32
|
+
if exc = storage[:__web_console_exception]
|
33
|
+
new(ExceptionMapper.new(exc))
|
34
|
+
elsif binding = storage[:__web_console_binding]
|
35
|
+
new([binding])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# An unique identifier for every REPL.
|
41
|
+
attr_reader :id
|
42
|
+
|
43
|
+
def initialize(bindings)
|
44
|
+
@id = SecureRandom.hex(16)
|
45
|
+
@bindings = bindings
|
46
|
+
@evaluator = Evaluator.new(@current_binding = bindings.first)
|
47
|
+
|
48
|
+
store_into_memory
|
49
|
+
end
|
50
|
+
|
51
|
+
# Evaluate +input+ on the current Evaluator associated binding.
|
52
|
+
#
|
53
|
+
# Returns a string of the Evaluator output.
|
54
|
+
def eval(input)
|
55
|
+
@evaluator.eval(input)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Switches the current binding to the one at specified +index+.
|
59
|
+
#
|
60
|
+
# Returns nothing.
|
61
|
+
def switch_binding_to(index)
|
62
|
+
@evaluator = Evaluator.new(@current_binding = @bindings[index.to_i])
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns context of the current binding
|
66
|
+
def context(objpath)
|
67
|
+
Context.new(@current_binding).extract(objpath)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def store_into_memory
|
73
|
+
inmemory_storage[id] = self
|
74
|
+
end
|
75
|
+
end
|
76
|
+
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,54 @@
|
|
1
|
+
namespace :templates do
|
2
|
+
desc 'Run tests for templates'
|
3
|
+
task test: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
|
4
|
+
task serve: [ :npm, :rackup ]
|
5
|
+
|
6
|
+
workdir = Pathname(EXPANDED_CWD).join('test/templates')
|
7
|
+
pid = Pathname(Dir.tmpdir).join("web_console_test.pid")
|
8
|
+
runner = URI.parse("http://#{ENV['IP'] || '127.0.0.1'}:#{ENV['PORT'] || 29292}/html/test_runner.html")
|
9
|
+
rackup = "rackup --host #{runner.host} --port #{runner.port}"
|
10
|
+
result = nil
|
11
|
+
browser = 'phantomjs'
|
12
|
+
|
13
|
+
def need_to_wait?(uri)
|
14
|
+
Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) }
|
15
|
+
rescue Errno::ECONNREFUSED
|
16
|
+
retry if yield
|
17
|
+
end
|
18
|
+
|
19
|
+
task :daemonize do
|
20
|
+
rackup += " -D --pid #{pid}"
|
21
|
+
end
|
22
|
+
|
23
|
+
task :npm => [ :phantomjs ] do
|
24
|
+
Dir.chdir(workdir) { system 'npm install --silent' }
|
25
|
+
end
|
26
|
+
|
27
|
+
task :phantomjs do
|
28
|
+
unless system("which #{browser} >/dev/null")
|
29
|
+
browser = './node_modules/.bin/phantomjs'
|
30
|
+
Dir.chdir(workdir) { system("test -f #{browser} || npm install --silent phantomjs-prebuilt") }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
task :rackup do
|
35
|
+
Dir.chdir(workdir) { system rackup }
|
36
|
+
end
|
37
|
+
|
38
|
+
task :wait do
|
39
|
+
cnt = 0
|
40
|
+
need_to_wait?(runner) { sleep 1; cnt += 1; cnt < 5 }
|
41
|
+
end
|
42
|
+
|
43
|
+
task :mocha do
|
44
|
+
Dir.chdir(workdir) { result = system("#{browser} ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js #{runner} dot") }
|
45
|
+
end
|
46
|
+
|
47
|
+
task :kill do
|
48
|
+
system "kill #{File.read pid}"
|
49
|
+
end
|
50
|
+
|
51
|
+
task :exit do
|
52
|
+
exit result
|
53
|
+
end
|
54
|
+
end
|