rubinius-debugger 2.0.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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +25 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/rubinius/debugger.rb +486 -0
- data/lib/rubinius/debugger/breakpoint.rb +147 -0
- data/lib/rubinius/debugger/commands.rb +745 -0
- data/lib/rubinius/debugger/display.rb +27 -0
- data/lib/rubinius/debugger/frame.rb +78 -0
- data/lib/rubinius/debugger/version.rb +5 -0
- data/rubinius-debugger.gemspec +23 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6f30dc2838cb2fb12f0164685407943e685081f6
|
4
|
+
data.tar.gz: 253e3ffb5d6daf87b0aeb354dee56f9216b97829
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e7940eb52a17f41399613694c1e3d002ec104b7f8cbc50d37301633ca3dcdb2d3d90139de24eb36ded0247b09df65c76a93cf9b790566fa5ac8639452ebd2bc5
|
7
|
+
data.tar.gz: de1fb048ae548256c7d4db617f3db787d114b0236856aee5ec06b8b708ef0df3476792314806bf543306af756684d34413c6e69c94e9599b92822a50e606bc62
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Copyright (c) 2013, Brian Shirai
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
3. Neither the name of the library nor the names of its contributors may be
|
13
|
+
used to endorse or promote products derived from this software without
|
14
|
+
specific prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
|
20
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
21
|
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
22
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
23
|
+
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
24
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
25
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Rubinius::Debugger
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'rubinius-debugger'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install rubinius-debugger
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,486 @@
|
|
1
|
+
require 'rubysl/readline'
|
2
|
+
require 'rubinius/debugger/frame'
|
3
|
+
require 'rubinius/debugger/commands'
|
4
|
+
require 'rubinius/debugger/breakpoint'
|
5
|
+
require 'rubinius/debugger/display'
|
6
|
+
require 'rubinius/compiler/iseq'
|
7
|
+
|
8
|
+
#
|
9
|
+
# The Rubinius reference debugger.
|
10
|
+
#
|
11
|
+
# This debugger is wired into the debugging APIs provided by Rubinius.
|
12
|
+
# It serves as a simple, builtin debugger that others can use as
|
13
|
+
# an example for how to build a better debugger.
|
14
|
+
#
|
15
|
+
|
16
|
+
class Rubinius::Debugger
|
17
|
+
include Rubinius::Debugger::Display
|
18
|
+
|
19
|
+
# Find the source for the kernel.
|
20
|
+
ROOT_DIR = File.expand_path("../", Rubinius::KERNEL_PATH)
|
21
|
+
|
22
|
+
# Create a new debugger object. The debugger starts up a thread
|
23
|
+
# which is where the command line interface executes from. Other
|
24
|
+
# threads that you wish to debug are told that their debugging
|
25
|
+
# thread is the debugger thread. This is how the debugger is handed
|
26
|
+
# control of execution.
|
27
|
+
#
|
28
|
+
def initialize
|
29
|
+
@file_lines = Hash.new do |hash, path|
|
30
|
+
if File.exists? path
|
31
|
+
hash[path] = File.readlines(path)
|
32
|
+
else
|
33
|
+
ab_path = File.join(@root_dir, path)
|
34
|
+
if File.exists? ab_path
|
35
|
+
hash[path] = File.readlines(ab_path)
|
36
|
+
else
|
37
|
+
hash[path] = []
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
@thread = nil
|
43
|
+
@frames = []
|
44
|
+
|
45
|
+
@variables = {
|
46
|
+
:show_ip => false,
|
47
|
+
:show_bytecode => false,
|
48
|
+
:highlight => false,
|
49
|
+
:list_command_history => {
|
50
|
+
:path => nil,
|
51
|
+
:center_line => nil
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
@loaded_hook = proc { |file|
|
56
|
+
check_deferred_breakpoints
|
57
|
+
}
|
58
|
+
|
59
|
+
@added_hook = proc { |mod, name, exec|
|
60
|
+
check_deferred_breakpoints
|
61
|
+
}
|
62
|
+
|
63
|
+
# Use a few Rubinius specific hooks to trigger checking
|
64
|
+
# for deferred breakpoints.
|
65
|
+
|
66
|
+
Rubinius::CodeLoader.loaded_hook.add @loaded_hook
|
67
|
+
Rubinius.add_method_hook.add @added_hook
|
68
|
+
|
69
|
+
@deferred_breakpoints = []
|
70
|
+
|
71
|
+
@user_variables = 0
|
72
|
+
|
73
|
+
@breakpoints = []
|
74
|
+
|
75
|
+
@history_path = File.expand_path("~/.rbx_debug")
|
76
|
+
|
77
|
+
if File.exists?(@history_path)
|
78
|
+
File.readlines(@history_path).each do |line|
|
79
|
+
Readline::HISTORY << line.strip
|
80
|
+
end
|
81
|
+
@history_io = File.new(@history_path, "a")
|
82
|
+
else
|
83
|
+
@history_io = File.new(@history_path, "w")
|
84
|
+
end
|
85
|
+
|
86
|
+
@history_io.sync = true
|
87
|
+
|
88
|
+
@root_dir = ROOT_DIR
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :variables, :current_frame, :breakpoints, :user_variables
|
92
|
+
attr_reader :locations
|
93
|
+
|
94
|
+
def self.global
|
95
|
+
@global ||= new
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.start
|
99
|
+
global.start(1)
|
100
|
+
end
|
101
|
+
|
102
|
+
# This is simplest API point. This starts up the debugger in the caller
|
103
|
+
# of this method to begin debugging.
|
104
|
+
#
|
105
|
+
def self.here
|
106
|
+
global.start(1)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Startup the debugger, skipping back +offset+ frames. This lets you start
|
110
|
+
# the debugger straight into callers method.
|
111
|
+
#
|
112
|
+
def start(offset=0)
|
113
|
+
spinup_thread
|
114
|
+
|
115
|
+
# Feed info to the debugger thread!
|
116
|
+
locs = Rubinius::VM.backtrace(offset + 1, true)
|
117
|
+
|
118
|
+
method = Rubinius::CompiledCode.of_sender
|
119
|
+
|
120
|
+
bp = BreakPoint.new "<start>", method, 0, 0
|
121
|
+
channel = Rubinius::Channel.new
|
122
|
+
|
123
|
+
@local_channel.send Rubinius::Tuple[bp, Thread.current, channel, locs]
|
124
|
+
|
125
|
+
# wait for the debugger to release us
|
126
|
+
channel.receive
|
127
|
+
|
128
|
+
Thread.current.set_debugger_thread @thread
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
# Stop and wait for a debuggee thread to send us info about
|
133
|
+
# stoping at a breakpoint.
|
134
|
+
#
|
135
|
+
def listen(step_into=false)
|
136
|
+
while true
|
137
|
+
if @channel
|
138
|
+
if step_into
|
139
|
+
@channel << :step
|
140
|
+
else
|
141
|
+
@channel << true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Wait for someone to stop
|
146
|
+
bp, thr, chan, locs = @local_channel.receive
|
147
|
+
|
148
|
+
# Uncache all frames since we stopped at a new place
|
149
|
+
@frames = []
|
150
|
+
|
151
|
+
@locations = locs
|
152
|
+
@breakpoint = bp
|
153
|
+
@debuggee_thread = thr
|
154
|
+
@channel = chan
|
155
|
+
|
156
|
+
@current_frame = frame(0)
|
157
|
+
|
158
|
+
if bp
|
159
|
+
# Only break out if the hit was valid
|
160
|
+
if bp.hit!(locs.first)
|
161
|
+
if bp.has_condition?
|
162
|
+
break if @current_frame.run(bp.condition)
|
163
|
+
else
|
164
|
+
break
|
165
|
+
end
|
166
|
+
end
|
167
|
+
else
|
168
|
+
break
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
puts
|
173
|
+
info "Breakpoint: #{@current_frame.describe}"
|
174
|
+
show_code
|
175
|
+
eval_code(@breakpoint.commands) if @breakpoint && @breakpoint.has_commands?
|
176
|
+
|
177
|
+
if @variables[:show_bytecode]
|
178
|
+
decode_one
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get a command from the user to run using readline
|
184
|
+
#
|
185
|
+
def accept_commands
|
186
|
+
command_list_code = []
|
187
|
+
cmd = Readline.readline "debug> "
|
188
|
+
|
189
|
+
if cmd.nil?
|
190
|
+
# ^D was entered
|
191
|
+
cmd = "quit"
|
192
|
+
elsif cmd.empty?
|
193
|
+
cmd = @last_command
|
194
|
+
else
|
195
|
+
@last_command = cmd
|
196
|
+
end
|
197
|
+
|
198
|
+
command, args = cmd.to_s.strip.split(/\s+/, 2)
|
199
|
+
|
200
|
+
runner = Command.commands.find { |k| k.match?(command) }
|
201
|
+
|
202
|
+
if runner
|
203
|
+
if runner == Command::CommandsList
|
204
|
+
bp_id = (args || @breakpoints.size).to_i
|
205
|
+
|
206
|
+
if @breakpoints.empty?
|
207
|
+
puts "No breakpoint set"
|
208
|
+
return
|
209
|
+
elsif bp_id > @breakpoints.size || bp_id < 1
|
210
|
+
puts "Invalid breakpoint number."
|
211
|
+
return
|
212
|
+
end
|
213
|
+
|
214
|
+
puts "Type commands for breakpoint ##{bp_id}, one per line."
|
215
|
+
puts "End with a line saying just 'END'."
|
216
|
+
code = Readline.readline "> "
|
217
|
+
while code != 'END'
|
218
|
+
command_list_code << code
|
219
|
+
code = Readline.readline "> "
|
220
|
+
end
|
221
|
+
args = {
|
222
|
+
:bp_id => bp_id,
|
223
|
+
:code => command_list_code.empty? ? nil : command_list_code.join(";")
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
runner.new(self).run args
|
228
|
+
else
|
229
|
+
puts "Unrecognized command: #{command}"
|
230
|
+
return
|
231
|
+
end
|
232
|
+
|
233
|
+
# Save it to the history.
|
234
|
+
@history_io.puts cmd
|
235
|
+
unless command_list_code.empty?
|
236
|
+
command_list_code << "END"
|
237
|
+
@history_io.puts command_list_code.join("\n")
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def eval_code(args)
|
242
|
+
obj = @current_frame.run(args)
|
243
|
+
|
244
|
+
idx = @user_variables
|
245
|
+
@user_variables += 1
|
246
|
+
|
247
|
+
str = "$d#{idx}"
|
248
|
+
Rubinius::Globals[str.to_sym] = obj
|
249
|
+
puts "#{str} = #{obj.inspect}\n"
|
250
|
+
end
|
251
|
+
|
252
|
+
def frame(num)
|
253
|
+
@frames[num] ||= Frame.new(self, num, @locations[num])
|
254
|
+
end
|
255
|
+
|
256
|
+
def set_frame(num)
|
257
|
+
@current_frame = frame(num)
|
258
|
+
end
|
259
|
+
|
260
|
+
def each_frame(start=0)
|
261
|
+
start = start.number if start.kind_of?(Frame)
|
262
|
+
|
263
|
+
start.upto(@locations.size-1) do |idx|
|
264
|
+
yield frame(idx)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def set_breakpoint_method(descriptor, method, line=nil, condition=nil)
|
269
|
+
exec = method.executable
|
270
|
+
|
271
|
+
unless exec.kind_of?(Rubinius::CompiledCode)
|
272
|
+
error "Unsupported method type: #{exec.class}"
|
273
|
+
return
|
274
|
+
end
|
275
|
+
|
276
|
+
if line
|
277
|
+
ip = exec.first_ip_on_line(line)
|
278
|
+
|
279
|
+
if !ip
|
280
|
+
error "Unknown line '#{line}' in method '#{method.name}'"
|
281
|
+
return
|
282
|
+
end
|
283
|
+
else
|
284
|
+
line = exec.first_line
|
285
|
+
ip = 0
|
286
|
+
end
|
287
|
+
|
288
|
+
bp = BreakPoint.new(descriptor, exec, ip, line, condition)
|
289
|
+
bp.activate
|
290
|
+
|
291
|
+
@breakpoints << bp
|
292
|
+
|
293
|
+
info "Set breakpoint #{@breakpoints.size}: #{bp.location}"
|
294
|
+
|
295
|
+
return bp
|
296
|
+
end
|
297
|
+
|
298
|
+
def delete_breakpoint(i)
|
299
|
+
bp = @breakpoints[i-1]
|
300
|
+
|
301
|
+
unless bp
|
302
|
+
error "Unknown breakpoint '#{i}'"
|
303
|
+
return
|
304
|
+
end
|
305
|
+
|
306
|
+
bp.delete!
|
307
|
+
|
308
|
+
@breakpoints[i-1] = nil
|
309
|
+
end
|
310
|
+
|
311
|
+
def add_deferred_breakpoint(klass_name, which, name, line)
|
312
|
+
dbp = DeferredBreakPoint.new(self, @current_frame, klass_name, which, name,
|
313
|
+
line, @deferred_breakpoints)
|
314
|
+
@deferred_breakpoints << dbp
|
315
|
+
@breakpoints << dbp
|
316
|
+
end
|
317
|
+
|
318
|
+
def check_deferred_breakpoints
|
319
|
+
@deferred_breakpoints.delete_if do |bp|
|
320
|
+
bp.resolve!
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def send_between(exec, start, fin)
|
325
|
+
ss = Rubinius::InstructionSet.opcodes_map[:send_stack]
|
326
|
+
sm = Rubinius::InstructionSet.opcodes_map[:send_method]
|
327
|
+
sb = Rubinius::InstructionSet.opcodes_map[:send_stack_with_block]
|
328
|
+
|
329
|
+
iseq = exec.iseq
|
330
|
+
|
331
|
+
fin = iseq.size if fin < 0
|
332
|
+
|
333
|
+
i = start
|
334
|
+
while i < fin
|
335
|
+
op = iseq[i]
|
336
|
+
case op
|
337
|
+
when ss, sm, sb
|
338
|
+
return exec.literals[iseq[i + 1]]
|
339
|
+
else
|
340
|
+
op = Rubinius::InstructionSet[op]
|
341
|
+
i += (op.arg_count + 1)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
return nil
|
346
|
+
end
|
347
|
+
|
348
|
+
def list_code_around_line(path, center_line, lines_to_show)
|
349
|
+
lines_around = lines_to_show / 2
|
350
|
+
start_line = center_line - lines_around
|
351
|
+
end_line = center_line + lines_around
|
352
|
+
|
353
|
+
list_code_range(path, start_line, end_line, center_line)
|
354
|
+
end
|
355
|
+
|
356
|
+
def list_code_range(path, start_line, end_line, center_line)
|
357
|
+
if !File.exists?(path) && !File.exists?(File.join(@root_dir, path))
|
358
|
+
error "Cannot find file #{path}"
|
359
|
+
return
|
360
|
+
end
|
361
|
+
|
362
|
+
if start_line > @file_lines[path].size
|
363
|
+
error "Line number #{@file_lines[path].size + 1} out of range: #{path} has #{@file_lines[path].size} lines."
|
364
|
+
return
|
365
|
+
end
|
366
|
+
|
367
|
+
start_line = 1 if start_line < 1
|
368
|
+
end_line = @file_lines[path].size if end_line > @file_lines[path].size
|
369
|
+
|
370
|
+
@variables[:list_command_history][:path] = path
|
371
|
+
@variables[:list_command_history][:center_line] = center_line
|
372
|
+
|
373
|
+
(start_line).upto(end_line) do |i|
|
374
|
+
show_code(i, path)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def show_code(line = @current_frame.line,
|
379
|
+
path = @current_frame.method.active_path)
|
380
|
+
if str = @file_lines[path][line - 1]
|
381
|
+
if @variables[:highlight]
|
382
|
+
if fin = @current_frame.method.first_ip_on_line(line + 1)
|
383
|
+
if name = send_between(@current_frame.method, @current_frame.ip, fin)
|
384
|
+
str = str.gsub name.to_s, "\033[0;4m#{name}\033[0m"
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
info "#{line}: #{str}"
|
389
|
+
else
|
390
|
+
show_bytecode(line)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def decode_one
|
395
|
+
ip = @current_frame.ip
|
396
|
+
|
397
|
+
meth = @current_frame.method
|
398
|
+
decoder = Rubinius::InstructionDecoder.new(meth.iseq)
|
399
|
+
partial = decoder.decode_between(ip, ip+1)
|
400
|
+
|
401
|
+
partial.each do |ins|
|
402
|
+
op = ins.shift
|
403
|
+
|
404
|
+
ins.each_index do |i|
|
405
|
+
case op.args[i]
|
406
|
+
when :literal
|
407
|
+
ins[i] = meth.literals[ins[i]].inspect
|
408
|
+
when :local
|
409
|
+
if meth.local_names
|
410
|
+
ins[i] = meth.local_names[ins[i]]
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
display "ip #{ip} = #{op.opcode} #{ins.join(', ')}"
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
def show_bytecode(line=@current_frame.line)
|
420
|
+
meth = @current_frame.method
|
421
|
+
start = meth.first_ip_on_line(line)
|
422
|
+
fin = meth.first_ip_on_line(line+1)
|
423
|
+
|
424
|
+
if !fin
|
425
|
+
fin = meth.iseq.size
|
426
|
+
end
|
427
|
+
|
428
|
+
section "Bytecode between #{start} and #{fin-1} for line #{line}"
|
429
|
+
|
430
|
+
decoder = Rubinius::InstructionDecoder.new(meth.iseq)
|
431
|
+
partial = decoder.decode_between(start, fin)
|
432
|
+
|
433
|
+
ip = start
|
434
|
+
|
435
|
+
partial.each do |ins|
|
436
|
+
op = ins.shift
|
437
|
+
|
438
|
+
ins.each_index do |i|
|
439
|
+
case op.args[i]
|
440
|
+
when :literal
|
441
|
+
ins[i] = meth.literals[ins[i]].inspect
|
442
|
+
when :local
|
443
|
+
if meth.local_names
|
444
|
+
ins[i] = meth.local_names[ins[i]]
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
info " %4d: #{op.opcode} #{ins.join(', ')}" % ip
|
450
|
+
|
451
|
+
ip += (ins.size + 1)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def spinup_thread
|
456
|
+
return if @thread
|
457
|
+
|
458
|
+
@local_channel = Rubinius::Channel.new
|
459
|
+
|
460
|
+
@thread = Thread.new do
|
461
|
+
begin
|
462
|
+
listen
|
463
|
+
rescue Exception => e
|
464
|
+
e.render("Listening")
|
465
|
+
break
|
466
|
+
end
|
467
|
+
|
468
|
+
while true
|
469
|
+
begin
|
470
|
+
accept_commands
|
471
|
+
rescue Exception => e
|
472
|
+
begin
|
473
|
+
e.render "Error in debugger"
|
474
|
+
rescue Exception => e2
|
475
|
+
puts "Error rendering backtrace in debugger!"
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
@thread.setup_control!(@local_channel)
|
482
|
+
end
|
483
|
+
|
484
|
+
private :spinup_thread
|
485
|
+
|
486
|
+
end
|