debug 0.2.1 → 1.0.0.alpha0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|