web-console-compat 3.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|