rgdb 0.0.1.245
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +676 -0
- data/ChangeLog +5 -0
- data/README +43 -0
- data/Rakefile +24 -0
- data/bin/rgdb +46 -0
- data/lib/gdb.rb +413 -0
- data/lib/rgdb.rb +75 -0
- data/lib/rgdb/version.rb +27 -0
- metadata +70 -0
data/ChangeLog
ADDED
data/README
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
= rgdb - GDB inside IRB
|
2
|
+
|
3
|
+
== VERSION
|
4
|
+
|
5
|
+
This documentation refers to rgdb version 0.0.1
|
6
|
+
|
7
|
+
|
8
|
+
== DESCRIPTION
|
9
|
+
|
10
|
+
Runs Jamis Buck's GDB wrapper inside an IRB session. Inspired by Jonathan
|
11
|
+
Leighton's rgdb. But contrary to his solution, this one runs the IRB session
|
12
|
+
in the context of the GDB::Ruby object.
|
13
|
+
|
14
|
+
See
|
15
|
+
* http://weblog.jamisbuck.org/2006/9/25/gdb-wrapper-for-ruby
|
16
|
+
* http://dev.turnipspatch.com:9013/trac/browser/rgdb/trunk?rev=31
|
17
|
+
|
18
|
+
|
19
|
+
== AUTHORS
|
20
|
+
|
21
|
+
* Jens Wille <mailto:jens.wille@uni-koeln.de>
|
22
|
+
|
23
|
+
|
24
|
+
== LICENSE AND COPYRIGHT
|
25
|
+
|
26
|
+
Copyright (C) 2008 University of Cologne,
|
27
|
+
Albertus-Magnus-Platz, 50932 Cologne, Germany
|
28
|
+
|
29
|
+
rgdb is free software: you can redistribute it and/or modify it under the terms
|
30
|
+
of the GNU General Public License as published by the Free Software Foundation,
|
31
|
+
either version 3 of the License, or (at your option) any later version.
|
32
|
+
|
33
|
+
rgdb is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
34
|
+
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
35
|
+
PURPOSE. See the GNU General Public License for more details.
|
36
|
+
|
37
|
+
You should have received a copy of the GNU General Public License along with
|
38
|
+
rgdb. If not, see <http://www.gnu.org/licenses/>.
|
39
|
+
|
40
|
+
=== GDB
|
41
|
+
|
42
|
+
The GDB library is copyright (C) 2006 Jamis Buck <mailto:jamis@37signals.com>,
|
43
|
+
released in the public domain.
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require %q{lib/rgdb/version}
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'hen'
|
5
|
+
|
6
|
+
Hen.lay! {{
|
7
|
+
:rubyforge => {
|
8
|
+
:project => %q{prometheus},
|
9
|
+
:package => %q{rgdb}
|
10
|
+
},
|
11
|
+
|
12
|
+
:gem => {
|
13
|
+
:version => RGDB::VERSION,
|
14
|
+
:summary => %q{GDB inside IRB},
|
15
|
+
:files => FileList['lib/**/*.rb', 'bin/*'].to_a,
|
16
|
+
:extra_files => FileList['[A-Z]*'].to_a,
|
17
|
+
:dependencies => %w[]
|
18
|
+
}
|
19
|
+
}}
|
20
|
+
rescue LoadError
|
21
|
+
abort "Please install the 'hen' gem first."
|
22
|
+
end
|
23
|
+
|
24
|
+
### Place your custom Rake tasks here.
|
data/bin/rgdb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#! /usr/bin/ruby
|
2
|
+
|
3
|
+
#--
|
4
|
+
###############################################################################
|
5
|
+
# #
|
6
|
+
# rgdb -- GDB inside IRB #
|
7
|
+
# #
|
8
|
+
# Copyright (C) 2008 University of Cologne, #
|
9
|
+
# Albertus-Magnus-Platz, #
|
10
|
+
# 50932 Cologne, Germany #
|
11
|
+
# #
|
12
|
+
# Authors: #
|
13
|
+
# Jens Wille <jens.wille@uni-koeln.de> #
|
14
|
+
# #
|
15
|
+
# rgdb is free software; you can redistribute it and/or modify it under the #
|
16
|
+
# terms of the GNU General Public License as published by the Free Software #
|
17
|
+
# Foundation; either version 3 of the License, or (at your option) any later #
|
18
|
+
# version. #
|
19
|
+
# #
|
20
|
+
# rgdb is distributed in the hope that it will be useful, but WITHOUT ANY #
|
21
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
22
|
+
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
23
|
+
# details. #
|
24
|
+
# #
|
25
|
+
# You should have received a copy of the GNU General Public License along #
|
26
|
+
# with rgdb. If not, see <http://www.gnu.org/licenses/>. #
|
27
|
+
# #
|
28
|
+
###############################################################################
|
29
|
+
#++
|
30
|
+
|
31
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
32
|
+
|
33
|
+
require 'rgdb'
|
34
|
+
|
35
|
+
USAGE = "Usage: #{$0} [-h|--help] <pid>"
|
36
|
+
|
37
|
+
case arg = ARGV.shift
|
38
|
+
when '-h', '--help'
|
39
|
+
puts USAGE
|
40
|
+
when '--version'
|
41
|
+
puts "rgdb v#{RGDB::VERSION}"
|
42
|
+
when /\A\d+\z/
|
43
|
+
RGDB.start(arg, __FILE__)
|
44
|
+
else
|
45
|
+
abort USAGE
|
46
|
+
end
|
data/lib/gdb.rb
ADDED
@@ -0,0 +1,413 @@
|
|
1
|
+
# gdb.rb -- a simple wrapper around the GDB console to make inspecting
|
2
|
+
# ruby processes less painful.
|
3
|
+
#
|
4
|
+
# Author: Jamis Buck <jamis@37signals.com>
|
5
|
+
#
|
6
|
+
# This library is in the public domain. You are free to use it, modify
|
7
|
+
# it, redistribute it, or ignore it, as you wish, with absolutely no
|
8
|
+
# restrictions.
|
9
|
+
#
|
10
|
+
# CAVEATS:
|
11
|
+
# * The current incarnation is not very error tolerant. If you run
|
12
|
+
# into problems, try running it with the $gdb_version variable set
|
13
|
+
# to true.
|
14
|
+
# * Sometimes, the current implementation will kill the process it
|
15
|
+
# attaches to. I suspect it has something to do with garbage
|
16
|
+
# collection kicking in... I leave it as an exercise for the reader.
|
17
|
+
#
|
18
|
+
# See the bottom of this file for a handful of unit tests.
|
19
|
+
|
20
|
+
require 'rbconfig'
|
21
|
+
|
22
|
+
class Integer
|
23
|
+
# Monkey patch the Integer class to make it easier to compute
|
24
|
+
# word offsets. "5.ints" gives you the size (in bytes) of 5
|
25
|
+
# integers (words) on the current platform.
|
26
|
+
def ints
|
27
|
+
self * 1.size
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# The GDB module provides a "raw", low-level, language-independent
|
32
|
+
# interface to GDB (GDB::Interface) as well as a higher-level
|
33
|
+
# interface specifically for Ruby programs (GDB::Ruby). Theoretically,
|
34
|
+
# GDB::Interface could be extended to support other languages
|
35
|
+
# than Ruby.
|
36
|
+
module GDB
|
37
|
+
# This module is used to extend objects (like Strings) that originate
|
38
|
+
# in the remote instance. This lets us reuse them easily in subsequent
|
39
|
+
# calls to the remote instance.
|
40
|
+
module RemoteObject
|
41
|
+
attr_accessor :address
|
42
|
+
end
|
43
|
+
|
44
|
+
# The basic interface for wrapping GDB. You could concievably use this
|
45
|
+
# to interace with arbitrary remote processes, though it is best used
|
46
|
+
# by extending it to add platform-specific functionality.
|
47
|
+
class Interface
|
48
|
+
# Creates a new GDB::Interface that connects to process-id and
|
49
|
+
# executable.
|
50
|
+
def initialize(executable, pid)
|
51
|
+
@gdb = IO.popen("gdb -q #{executable} #{pid} 2>&1", "r+")
|
52
|
+
read_to_prompt
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the (32-bit) integer at the given address
|
56
|
+
def int_at(address)
|
57
|
+
command("x/1dw #{address}").first.split(/:/).last.to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the string located at the given address
|
61
|
+
def string_at(address)
|
62
|
+
command("x/1s *#{address}").first.match(/"(.*)"/)[1]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the (8-bit) double precision floating point value at
|
66
|
+
# the given address.
|
67
|
+
def double_at(address)
|
68
|
+
command("x/1fg #{address}").first.split(/:/).last.to_f
|
69
|
+
end
|
70
|
+
|
71
|
+
# Executes the given command and returns the output as an array
|
72
|
+
# of lines. The command must be in GDB's syntax. If $gdb_verbose
|
73
|
+
# is true, this will print the command to STDOUT as well.
|
74
|
+
def command(cmd)
|
75
|
+
puts "(gdb) #{cmd}" if $gdb_verbose
|
76
|
+
@gdb.puts(cmd.strip)
|
77
|
+
read_to_prompt
|
78
|
+
end
|
79
|
+
|
80
|
+
# Call a C function. The +function+ parameter must include any
|
81
|
+
# parameters, all of which must be in valid C syntax. +opts+
|
82
|
+
# should include a <code>:return</code> key, which indicates
|
83
|
+
# what the expected return type of the function is:
|
84
|
+
#
|
85
|
+
# * <code>:int</code>
|
86
|
+
# * <code>:string</code>
|
87
|
+
# * <code>:void</code>
|
88
|
+
#
|
89
|
+
# Currently, the default is <code>:void</code>. The method
|
90
|
+
# returns a value of the expected type.
|
91
|
+
def call(function, opts={})
|
92
|
+
cast = case opts[:return]
|
93
|
+
when :int then "int"
|
94
|
+
when :string then "char*"
|
95
|
+
when nil, :void then "void"
|
96
|
+
else Kernel.raise "unsupported return type #{opts[:return].inspect}"
|
97
|
+
end
|
98
|
+
|
99
|
+
result = command("call (#{cast})#{function}")
|
100
|
+
|
101
|
+
case opts[:return]
|
102
|
+
when :int
|
103
|
+
if md = result.first.match(/= (-?\d+)/)
|
104
|
+
md[1].to_i
|
105
|
+
else
|
106
|
+
Kernel.raise "couldn't match integer result from #{result.inspect}"
|
107
|
+
end
|
108
|
+
when :string then result.first.match(/"(.*)"/)[1]
|
109
|
+
else nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Reads all lines emitted by GDB until a recognized prompt is encountered.
|
114
|
+
# Currently recognized prompts are:
|
115
|
+
#
|
116
|
+
# * "(gdb) "
|
117
|
+
# * " >"
|
118
|
+
#
|
119
|
+
# If your version of GDB uses a different kind of prompt, you should
|
120
|
+
# modify this method to include it.
|
121
|
+
#
|
122
|
+
# This method returns the lines read as an array. If $gdb_verbose is
|
123
|
+
# true, the lines will be echoed to STDOUT.
|
124
|
+
def read_to_prompt
|
125
|
+
lines = []
|
126
|
+
line = ""
|
127
|
+
while result = IO.select([@gdb])
|
128
|
+
next if result.empty?
|
129
|
+
c = @gdb.read(1)
|
130
|
+
break if c.nil?
|
131
|
+
line << c
|
132
|
+
break if line == "(gdb) " || line == " >"
|
133
|
+
if line[-1] == ?\n
|
134
|
+
lines << line
|
135
|
+
line = ""
|
136
|
+
end
|
137
|
+
end
|
138
|
+
puts lines.map { |l| "> #{l}" } if $gdb_verbose
|
139
|
+
lines
|
140
|
+
end
|
141
|
+
|
142
|
+
# A convenience method for executing GDB commands. Any unrecognized
|
143
|
+
# message is packaged up and invoked by GDB.
|
144
|
+
def method_missing(sym, *args)
|
145
|
+
cmd = sym.to_s
|
146
|
+
cmd << " #{args.join(' ')}" unless args.empty?
|
147
|
+
command(cmd)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# This is an extension of the Interface class that implements Ruby-specific
|
152
|
+
# functionality.
|
153
|
+
class Ruby < Interface
|
154
|
+
# A wrapper for a remote Object instance.
|
155
|
+
class Object
|
156
|
+
attr_reader :gdb
|
157
|
+
attr_reader :address
|
158
|
+
|
159
|
+
# Given a gdb instance and an address, determine what type of Object
|
160
|
+
# is at the given address, and return it.
|
161
|
+
def self.from_ptr(gdb, address)
|
162
|
+
case
|
163
|
+
when (address & 0x01) == 1 then address >> 1
|
164
|
+
when address == 0 then false
|
165
|
+
when address == 2 then true
|
166
|
+
when address == 4 then nil
|
167
|
+
when (address & 0xff) == 0x0e then gdb.id2name(address)
|
168
|
+
else
|
169
|
+
case (type = (gdb.int_at(address) & 0x3f))
|
170
|
+
when 2 then Object.new(gdb, address)
|
171
|
+
when 6 then gdb.double_at(address+2.ints)
|
172
|
+
when 7 then
|
173
|
+
string = gdb.string_at(address+3.ints)
|
174
|
+
string.extend(RemoteObject)
|
175
|
+
string.address = address
|
176
|
+
string
|
177
|
+
when 9 then Array.new(gdb, address)
|
178
|
+
when 11 then Hash.new(gdb, address)
|
179
|
+
else Object.new(gdb, address)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Given a value, return an integer representing the VALUE of the
|
185
|
+
# object on the remote instance.
|
186
|
+
def to_gdb_value(value)
|
187
|
+
return value.address if value.respond_to?(:address)
|
188
|
+
|
189
|
+
case value
|
190
|
+
when Fixnum then ((value << 1)+1).to_s
|
191
|
+
when false then "0"
|
192
|
+
when true then "2"
|
193
|
+
when nil then "4"
|
194
|
+
when String then gdb.call("rb_str_new2(#{value.inspect})", :return => :int).to_s
|
195
|
+
else Kernel.raise "not supported yet: #{value.class.name}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Creates a new remote Object reference for the given gdb instance
|
200
|
+
# and address.
|
201
|
+
def initialize(gdb, address)
|
202
|
+
@gdb = gdb
|
203
|
+
@address = address.to_i
|
204
|
+
end
|
205
|
+
|
206
|
+
# Invokes a method on the remote Object reference, returning the
|
207
|
+
# result as another Object.
|
208
|
+
def send(sym, *args)
|
209
|
+
command = "rb_funcall(#{address}, #{gdb.intern(sym.to_s)}, #{args.length}"
|
210
|
+
args = args.map { |arg| to_gdb_value(arg) }.join(", ")
|
211
|
+
command << ", #{args}" unless args.empty?
|
212
|
+
command << ")"
|
213
|
+
|
214
|
+
gdb.object_from(command)
|
215
|
+
end
|
216
|
+
|
217
|
+
# A convenience method for #send
|
218
|
+
def method_missing(sym, *args)
|
219
|
+
send(sym, *args)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# A representation of a remote Array instance.
|
224
|
+
class Array < Object
|
225
|
+
# Query the length of the remote Array instance
|
226
|
+
def length
|
227
|
+
@length ||= gdb.int_at(address+2.ints)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Return the object at the given index
|
231
|
+
def [](index)
|
232
|
+
gdb.object_from("rb_ary_entry(#{address}, #{index})")
|
233
|
+
end
|
234
|
+
|
235
|
+
# Iterate over each element of the remote array
|
236
|
+
def each
|
237
|
+
length.times { |idx| yield self[idx] }
|
238
|
+
end
|
239
|
+
|
240
|
+
# Convert the remote Array into a local (true) Array instance.
|
241
|
+
def to_ary
|
242
|
+
ary = []
|
243
|
+
each { |item| ary << item }
|
244
|
+
ary
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Wraps a remote Hash instance. All it really provides, though, is a
|
249
|
+
# way to build a local Hash instance that mirrors it.
|
250
|
+
class Hash < Object
|
251
|
+
# Return a ::Hash instance that has the same keys and values as
|
252
|
+
# the remote hash instance. Default values are not preserved.
|
253
|
+
def to_h
|
254
|
+
keys.to_ary.inject({}) do |hash, key|
|
255
|
+
hash[key] = self[key]
|
256
|
+
hash
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Create a new GDB::Ruby instance that attaches to the given
|
262
|
+
# process ID. By default, the same ruby interpreter is used
|
263
|
+
# as is running the GDB::Ruby script, but you can override
|
264
|
+
# that by using the second parameter.
|
265
|
+
def initialize(pid, ruby=nil)
|
266
|
+
ruby ||= File.join(Config::CONFIG["bindir"],
|
267
|
+
Config::CONFIG["RUBY_INSTALL_NAME"] +
|
268
|
+
Config::CONFIG["EXEEXT"])
|
269
|
+
@interns = {}
|
270
|
+
@names = {}
|
271
|
+
super(ruby, pid)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Returns the symbol for the given numberic ID, using the remote
|
275
|
+
# environment.
|
276
|
+
def id2name(id)
|
277
|
+
@names[id] ||= call("rb_id2name(#{id >> 8})", :return => :string).to_sym
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns the numeric ID on the remote host that represents the
|
281
|
+
# internalized string.
|
282
|
+
def intern(string)
|
283
|
+
@interns[string] ||= call("rb_intern(#{string.inspect})", :return => :int)
|
284
|
+
end
|
285
|
+
|
286
|
+
# Executes the given command (C API) and returns the result as a wrapped
|
287
|
+
# GDB::Ruby::Object.
|
288
|
+
def object_from(cmd)
|
289
|
+
Object.from_ptr(self, call("#{cmd}", :return => :int))
|
290
|
+
end
|
291
|
+
|
292
|
+
# Evaluates the given string in the remote environment, returning the
|
293
|
+
# resulting object. Exceptions are silently ignored.
|
294
|
+
def eval(string)
|
295
|
+
object_from("rb_eval_string_protect(#{string.inspect}, (int*)0)")
|
296
|
+
end
|
297
|
+
|
298
|
+
# Returns a hash of the local variables that are active in the remote
|
299
|
+
# process, at it's current scope.
|
300
|
+
def local_variables
|
301
|
+
eval("local_variables").to_ary.inject({}) do |hash, name|
|
302
|
+
hash[name] = eval(name)
|
303
|
+
hash
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Returns an array of the backtrace of the remote process.
|
308
|
+
def backtrace
|
309
|
+
object_from("backtrace(-1)").to_ary
|
310
|
+
end
|
311
|
+
|
312
|
+
# Raises an exception in the remote process. The +exc+ parameter
|
313
|
+
# must be one of the core exception classes, like Exception,
|
314
|
+
# RuntimeError, and so forth.
|
315
|
+
def raise(exc, message)
|
316
|
+
call "rb_raise((int)rb_e#{exc}, #{message.inspect})"
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns a hash of the classes known to the remote instance,
|
320
|
+
# with the number of instances of each of the classes.
|
321
|
+
def object_space
|
322
|
+
eval("h={}; ObjectSpace.each_object { |obj| h[obj.class.name] ||= 0; h[obj.class.name] += 1 }; h").to_h
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
if __FILE__ == $0
|
328
|
+
require 'test/unit'
|
329
|
+
|
330
|
+
SENTINEL = "/tmp/gdb-ruby-test.pid"
|
331
|
+
FLAG_FILE = "/tmp/gdb-ruby-test.flag"
|
332
|
+
|
333
|
+
SCRIPT = <<EOS
|
334
|
+
class ZxyVlqrt; end
|
335
|
+
glooble = ZxyVlqrt.new
|
336
|
+
|
337
|
+
def a(n)
|
338
|
+
b(n, n+1)
|
339
|
+
end
|
340
|
+
|
341
|
+
def b(x, y)
|
342
|
+
z = x + y
|
343
|
+
File.open("#{SENTINEL}", "w") { |f| f.write($$) }
|
344
|
+
loop { sleep 1 }
|
345
|
+
end
|
346
|
+
|
347
|
+
begin
|
348
|
+
a(5)
|
349
|
+
rescue RuntimeError => e
|
350
|
+
File.open("#{FLAG_FILE}", "w") { |f| f.write(e.message) }
|
351
|
+
end
|
352
|
+
EOS
|
353
|
+
|
354
|
+
SCRIPT_NAME = "/tmp/gdb-ruby-test.rb"
|
355
|
+
|
356
|
+
class GDBRubyTest < Test::Unit::TestCase
|
357
|
+
def setup
|
358
|
+
File.open(SCRIPT_NAME, "w") { |f| f.write(SCRIPT) }
|
359
|
+
start_script
|
360
|
+
@gdb = GDB::Ruby.new(@pid)
|
361
|
+
end
|
362
|
+
|
363
|
+
def teardown
|
364
|
+
@gdb.quit
|
365
|
+
kill_script
|
366
|
+
File.delete(FLAG_FILE) rescue nil
|
367
|
+
File.delete(SENTINEL) rescue nil
|
368
|
+
File.delete(SCRIPT_NAME) rescue nil
|
369
|
+
end
|
370
|
+
|
371
|
+
def test_local_variables
|
372
|
+
expect = {"x" => 5, "y" => 6, "z" => 11}
|
373
|
+
assert_equal(expect, @gdb.local_variables)
|
374
|
+
end
|
375
|
+
|
376
|
+
def test_backtrace
|
377
|
+
expect = ["#{SCRIPT_NAME}:11:in `sleep'",
|
378
|
+
"#{SCRIPT_NAME}:11:in `b'",
|
379
|
+
"#{SCRIPT_NAME}:11:in `b'",
|
380
|
+
"#{SCRIPT_NAME}:5:in `a'",
|
381
|
+
"#{SCRIPT_NAME}:15"]
|
382
|
+
|
383
|
+
assert_equal expect, @gdb.backtrace
|
384
|
+
end
|
385
|
+
|
386
|
+
def test_raise
|
387
|
+
assert !File.exist?(FLAG_FILE)
|
388
|
+
@gdb.raise RuntimeError, "going boom"
|
389
|
+
assert File.exist?(FLAG_FILE)
|
390
|
+
assert_equal "going boom", File.read(FLAG_FILE)
|
391
|
+
end
|
392
|
+
|
393
|
+
def test_object_space
|
394
|
+
space = @gdb.object_space
|
395
|
+
assert_equal 1, space["ZxyVlqrt"]
|
396
|
+
end
|
397
|
+
|
398
|
+
private
|
399
|
+
|
400
|
+
def start_script
|
401
|
+
if (@pid = fork)
|
402
|
+
sleep 0.1 while !File.exist?(SENTINEL)
|
403
|
+
return
|
404
|
+
end
|
405
|
+
|
406
|
+
exec "ruby", SCRIPT_NAME
|
407
|
+
end
|
408
|
+
|
409
|
+
def kill_script
|
410
|
+
Process.kill("TERM", @pid)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|