debug 0.2.1 → 1.0.0.alpha0
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 +4 -4
- data/.gitignore +0 -1
- data/LICENSE.txt +19 -20
- data/README.md +142 -28
- data/Rakefile +9 -0
- data/debug.gemspec +20 -10
- data/exe/rdbg +3 -0
- data/lib/debug/bp.vim +68 -0
- data/lib/debug/breakpoint.rb +97 -0
- data/lib/debug/client.rb +135 -0
- data/lib/debug/config.rb +26 -0
- data/lib/debug/repl.rb +67 -0
- data/lib/debug/server.rb +119 -0
- data/lib/debug/session.rb +598 -0
- data/lib/debug/source_repository.rb +20 -0
- data/lib/debug/tcpserver.rb +23 -0
- data/lib/debug/thread_client.rb +402 -0
- data/lib/debug/unixserver.rb +19 -0
- data/lib/debug/version.rb +3 -0
- data/lib/debug.rb +11 -1116
- metadata +61 -21
- data/Gemfile +0 -8
@@ -0,0 +1,402 @@
|
|
1
|
+
module DEBUGGER__
|
2
|
+
class ThreadClient
|
3
|
+
def self.current
|
4
|
+
Thread.current[:DEBUGGER__ThreadClient] || begin
|
5
|
+
tc = SESSION.thread_client
|
6
|
+
Thread.current[:DEBUGGER__ThreadClient] = tc
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :location, :thread
|
11
|
+
|
12
|
+
def initialize q_evt, q_cmd, thr = Thread.current
|
13
|
+
@thread = thr
|
14
|
+
@q_evt = q_evt
|
15
|
+
@q_cmd = q_cmd
|
16
|
+
@step_tp = nil
|
17
|
+
@output = []
|
18
|
+
@mode = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"#<DEBUGGER__ThreadClient #{@thread}>"
|
23
|
+
end
|
24
|
+
|
25
|
+
def puts str = ''
|
26
|
+
case str
|
27
|
+
when nil
|
28
|
+
@output << "\n"
|
29
|
+
when Array
|
30
|
+
str.each{|s| puts s}
|
31
|
+
else
|
32
|
+
@output << str.chomp + "\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def << req
|
37
|
+
@q_cmd << req
|
38
|
+
end
|
39
|
+
|
40
|
+
def event! ev, *args
|
41
|
+
@q_evt << [self, @output, ev, *args]
|
42
|
+
@output = []
|
43
|
+
end
|
44
|
+
|
45
|
+
## events
|
46
|
+
|
47
|
+
def on_trap
|
48
|
+
if @mode == :wait_next_action
|
49
|
+
raise Interrupt
|
50
|
+
else
|
51
|
+
on_suspend :trap
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_pause
|
56
|
+
on_suspend :pause
|
57
|
+
end
|
58
|
+
|
59
|
+
def on_load iseq, eval_src
|
60
|
+
event! :load, iseq, eval_src
|
61
|
+
wait_next_action
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_breakpoint tp
|
65
|
+
on_suspend tp.event, tp
|
66
|
+
end
|
67
|
+
|
68
|
+
def on_suspend event, tp = nil
|
69
|
+
@current_frame_index = 0
|
70
|
+
@target_frames = target_frames
|
71
|
+
cf = @target_frames.first
|
72
|
+
if cf
|
73
|
+
@location = cf.location
|
74
|
+
case event
|
75
|
+
when :return, :b_return, :c_return
|
76
|
+
cf.has_return_value = true
|
77
|
+
cf.return_value = tp.return_value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if event != :pause
|
82
|
+
show_src
|
83
|
+
print_frames 3
|
84
|
+
event! :suspend, :breakpoint
|
85
|
+
end
|
86
|
+
|
87
|
+
wait_next_action
|
88
|
+
end
|
89
|
+
|
90
|
+
## control all
|
91
|
+
|
92
|
+
def step_tp
|
93
|
+
@step_tp.disable if @step_tp
|
94
|
+
@step_tp = TracePoint.new(:line, :b_return, :return){|tp|
|
95
|
+
next if SESSION.break? tp.path, tp.lineno
|
96
|
+
next if !yield
|
97
|
+
tp.disable
|
98
|
+
on_suspend tp.event, tp
|
99
|
+
}
|
100
|
+
|
101
|
+
@step_tp.enable(target_thread: Thread.current)
|
102
|
+
end
|
103
|
+
|
104
|
+
FrameInfo = Struct.new(:location, :self, :binding, :iseq, :class, :has_return_value, :return_value)
|
105
|
+
|
106
|
+
def target_frames
|
107
|
+
RubyVM::DebugInspector.open{|dc|
|
108
|
+
locs = dc.backtrace_locations
|
109
|
+
locs.map.with_index{|e, i|
|
110
|
+
unless File.dirname(e.path) == File.dirname(__FILE__)
|
111
|
+
FrameInfo.new(
|
112
|
+
e,
|
113
|
+
dc.frame_self(i),
|
114
|
+
dc.frame_binding(i),
|
115
|
+
dc.frame_iseq(i),
|
116
|
+
dc.frame_class(i))
|
117
|
+
end
|
118
|
+
}.compact
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def target_frames_count
|
123
|
+
RubyVM::DebugInspector.open{|dc|
|
124
|
+
locs = dc.backtrace_locations
|
125
|
+
locs.count{|e|
|
126
|
+
e.path != __FILE__
|
127
|
+
}
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
def current_frame
|
132
|
+
@target_frames[@current_frame_index]
|
133
|
+
end
|
134
|
+
|
135
|
+
def file_lines path
|
136
|
+
if (src = SESSION.source(path)) && src[0]
|
137
|
+
src[0].lines
|
138
|
+
elsif File.exist?(path)
|
139
|
+
File.readlines(path)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def show_src frame_index = @current_frame_index, max_lines: 10
|
144
|
+
if current_line = @target_frames[frame_index]&.location
|
145
|
+
puts
|
146
|
+
path, line = current_line.path, current_line.lineno - 1
|
147
|
+
if file_lines = file_lines(path)
|
148
|
+
lines = file_lines.map.with_index{|e, i|
|
149
|
+
if i == line
|
150
|
+
"=> #{'%4d' % (i+1)}| #{e}"
|
151
|
+
else
|
152
|
+
" #{'%4d' % (i+1)}| #{e}"
|
153
|
+
end
|
154
|
+
}
|
155
|
+
min = [0, line - max_lines/2].max
|
156
|
+
max = [min+max_lines, lines.size].min
|
157
|
+
puts "[#{min+1}, #{max}] in #{path}"
|
158
|
+
puts lines[min ... max]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def show_locals
|
164
|
+
if s = current_frame&.self
|
165
|
+
puts " %self => #{s}"
|
166
|
+
end
|
167
|
+
if current_frame&.has_return_value
|
168
|
+
puts " %return => #{current_frame.return_value}"
|
169
|
+
end
|
170
|
+
if b = current_frame&.binding
|
171
|
+
b.local_variables.each{|loc|
|
172
|
+
puts " #{loc} => #{b.local_variable_get(loc).inspect}"
|
173
|
+
}
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def show_ivars
|
178
|
+
if s = current_frame&.self
|
179
|
+
puts " self => #{s}"
|
180
|
+
s.instance_variables.eaach{|iv|
|
181
|
+
puts " #{iv} => #{s.instance_variable_get(iv)}"
|
182
|
+
}
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def frame_eval src, failed_value: nil
|
187
|
+
begin
|
188
|
+
b = current_frame.binding
|
189
|
+
if b
|
190
|
+
b.eval(src)
|
191
|
+
else
|
192
|
+
frame_self = current_frame.self
|
193
|
+
frame_self.instance_eval(src)
|
194
|
+
# puts "eval is not supported on this frame."
|
195
|
+
end
|
196
|
+
rescue Exception => e
|
197
|
+
return failed_value if failed_value
|
198
|
+
|
199
|
+
puts "Error: #{e}"
|
200
|
+
e.backtrace_locations.each do |loc|
|
201
|
+
break if loc.path == __FILE__
|
202
|
+
puts " #{loc}"
|
203
|
+
end
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def parameters_info b, vars
|
209
|
+
vars.map{|var|
|
210
|
+
"#{var}=#{short_inspect(b.eval(var.to_s))}"
|
211
|
+
}.join(', ')
|
212
|
+
end
|
213
|
+
|
214
|
+
def klass_sig frame
|
215
|
+
klass = frame.class
|
216
|
+
if klass == frame.self.singleton_class
|
217
|
+
"#{frame.self}."
|
218
|
+
else
|
219
|
+
"#{frame.class}#"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
SHORT_INSPECT_LENGTH = 40
|
224
|
+
|
225
|
+
def short_inspect obj
|
226
|
+
str = obj.inspect
|
227
|
+
if str.length > SHORT_INSPECT_LENGTH
|
228
|
+
str[0...SHORT_INSPECT_LENGTH] + '...'
|
229
|
+
else
|
230
|
+
str
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def frame_str i
|
235
|
+
buff = ''.dup
|
236
|
+
frame = @target_frames[i]
|
237
|
+
b = frame.binding
|
238
|
+
|
239
|
+
buff << (@current_frame_index == i ? '--> ' : ' ')
|
240
|
+
if b
|
241
|
+
buff << "##{i}\t#{frame.location}"
|
242
|
+
else
|
243
|
+
buff << "##{i}\t[C] #{frame.location}"
|
244
|
+
end
|
245
|
+
|
246
|
+
if b && (iseq = frame.iseq)
|
247
|
+
if iseq.type == :block
|
248
|
+
if (argc = iseq.argc) > 0
|
249
|
+
args = parameters_info b, iseq.locals[0...iseq.argc]
|
250
|
+
buff << " {|#{args}|}"
|
251
|
+
end
|
252
|
+
else
|
253
|
+
callee = b.eval('__callee__')
|
254
|
+
if callee && (m = frame.self.method(callee))
|
255
|
+
args = parameters_info b, m.parameters.map{|type, v| v}
|
256
|
+
ksig = klass_sig frame
|
257
|
+
buff << " #{ksig}#{callee}(#{args})"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
if frame.has_return_value
|
262
|
+
buff << " #=> #{short_inspect(frame.return_value)}"
|
263
|
+
end
|
264
|
+
else
|
265
|
+
# p frame.self
|
266
|
+
end
|
267
|
+
|
268
|
+
buff
|
269
|
+
end
|
270
|
+
|
271
|
+
def show_frame_all
|
272
|
+
@target_frames.size.times{|i|
|
273
|
+
puts frame_str(i)
|
274
|
+
}
|
275
|
+
end
|
276
|
+
|
277
|
+
def print_frame i
|
278
|
+
puts frame_str(i)
|
279
|
+
end
|
280
|
+
|
281
|
+
def print_frames n
|
282
|
+
size = @target_frames.size
|
283
|
+
([size, n].min).times{|i|
|
284
|
+
print_frame i
|
285
|
+
}
|
286
|
+
if n < size
|
287
|
+
puts " # and #{size - n} frames (use `bt' command for all frames)"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def wait_next_action
|
292
|
+
@mode = :wait_next_action
|
293
|
+
|
294
|
+
while cmds = @q_cmd.pop
|
295
|
+
cmd, *args = *cmds
|
296
|
+
|
297
|
+
case cmd
|
298
|
+
when :continue
|
299
|
+
break
|
300
|
+
when :step
|
301
|
+
step_type = args[0]
|
302
|
+
case step_type
|
303
|
+
when :in
|
304
|
+
step_tp{true}
|
305
|
+
when :next
|
306
|
+
size = @target_frames.size
|
307
|
+
step_tp{
|
308
|
+
target_frames_count() <= size
|
309
|
+
}
|
310
|
+
when :finish
|
311
|
+
size = @target_frames.size
|
312
|
+
step_tp{target_frames_count() < size}
|
313
|
+
else
|
314
|
+
raise
|
315
|
+
end
|
316
|
+
break
|
317
|
+
when :eval
|
318
|
+
eval_type, eval_src = *args
|
319
|
+
result = frame_eval(eval_src)
|
320
|
+
|
321
|
+
case eval_type
|
322
|
+
when :p
|
323
|
+
puts "=> " + result.inspect
|
324
|
+
when :pp
|
325
|
+
puts "=> "
|
326
|
+
PP.pp(result, out = ''.dup)
|
327
|
+
puts out
|
328
|
+
when :call
|
329
|
+
result = frame_eval(eval_src)
|
330
|
+
when :display
|
331
|
+
eval_src.each_with_index{|src, i|
|
332
|
+
puts "#{i}: #{src} = #{frame_eval(src, failed_value: :error).inspect}"
|
333
|
+
}
|
334
|
+
result = :ok
|
335
|
+
else
|
336
|
+
raise "unknown error option: #{args.inspec}"
|
337
|
+
end
|
338
|
+
event! :result, result
|
339
|
+
when :frame
|
340
|
+
type, arg = *args
|
341
|
+
case type
|
342
|
+
when :up
|
343
|
+
if @current_frame_index + 1 < @target_frames.size
|
344
|
+
@current_frame_index += 1
|
345
|
+
show_src max_lines: 1
|
346
|
+
print_frame(@current_frame_index)
|
347
|
+
end
|
348
|
+
when :down
|
349
|
+
if @current_frame_index > 0
|
350
|
+
@current_frame_index -= 1
|
351
|
+
show_src max_lines: 1
|
352
|
+
print_frame(@current_frame_index)
|
353
|
+
end
|
354
|
+
when :set
|
355
|
+
if arg
|
356
|
+
index = arg.to_i
|
357
|
+
if index >= 0 && index < @target_frames.size
|
358
|
+
@current_frame_index = index
|
359
|
+
else
|
360
|
+
puts "out of frame index: #{index}"
|
361
|
+
end
|
362
|
+
end
|
363
|
+
show_src max_lines: 1
|
364
|
+
print_frame(@current_frame_index)
|
365
|
+
else
|
366
|
+
raise "unsupported frame operation: #{arg.inspect}"
|
367
|
+
end
|
368
|
+
event! :result, nil
|
369
|
+
when :show
|
370
|
+
type, = *args
|
371
|
+
case type
|
372
|
+
when :backtrace
|
373
|
+
show_frame_all
|
374
|
+
when :list
|
375
|
+
show_src
|
376
|
+
when :locals
|
377
|
+
show_locals
|
378
|
+
when :ivars
|
379
|
+
show_ivars
|
380
|
+
else
|
381
|
+
raise "unknown show param: " + args.inspect
|
382
|
+
end
|
383
|
+
event! :result, nil
|
384
|
+
else
|
385
|
+
raise [ev, *args].inspect
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
rescue SystemExit
|
390
|
+
raise
|
391
|
+
rescue Exception => e
|
392
|
+
pp [__FILE__, __LINE__, e, e.backtrace]
|
393
|
+
raise
|
394
|
+
ensure
|
395
|
+
@mode = nil
|
396
|
+
end
|
397
|
+
|
398
|
+
def to_s
|
399
|
+
"(#{@thread.name || @thread.status})@#{current_frame&.location}"
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative 'server'
|
2
|
+
require_relative 'config'
|
3
|
+
|
4
|
+
module DEBUGGER__
|
5
|
+
class UI_UnixDomainServer < UI_Server
|
6
|
+
def accept
|
7
|
+
@file = DEBUGGER__.create_unix_domain_socket_name
|
8
|
+
|
9
|
+
$stderr.puts "Debugger can attach via UNIX domain socket (#{@file})"
|
10
|
+
Socket.unix_server_loop @file do |sock, addr|
|
11
|
+
@client_addr = addr
|
12
|
+
yield sock
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
SESSION = Session.new(s = UI_UnixDomainServer.new)
|
18
|
+
SESSION.management_threads << s.reader_thread
|
19
|
+
end
|