debug 0.2.0 → 1.0.0.beta3
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 +1 -0
- data/Gemfile +4 -5
- data/LICENSE.txt +0 -0
- data/README.md +442 -27
- data/Rakefile +29 -0
- data/debug.gemspec +10 -5
- data/exe/rdbg +34 -0
- data/ext/debug/debug.c +118 -0
- data/ext/debug/extconf.rb +2 -0
- data/ext/debug/iseq_collector.c +91 -0
- data/lib/debug.rb +1 -1111
- data/lib/debug/bp.vim +68 -0
- data/lib/debug/breakpoint.rb +374 -0
- data/lib/debug/client.rb +128 -0
- data/lib/debug/config.rb +125 -0
- data/lib/debug/console.rb +89 -0
- data/lib/debug/open.rb +10 -0
- data/lib/debug/run.rb +2 -0
- data/lib/debug/server.rb +226 -0
- data/lib/debug/session.rb +1071 -0
- data/lib/debug/source_repository.rb +27 -0
- data/lib/debug/thread_client.rb +618 -0
- data/lib/debug/version.rb +3 -0
- data/misc/README.md.erb +324 -0
- metadata +34 -14
data/lib/debug/bp.vim
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
let g:rdb_bps = {}
|
2
|
+
|
3
|
+
function SET_BP()
|
4
|
+
let signed = sign_getplaced(bufname(), {'lnum': line('.')})
|
5
|
+
if empty(signed[0]['signs'])
|
6
|
+
call sign_place(0, '', 'signBP', bufname(), {'lnum': line('.')})
|
7
|
+
else
|
8
|
+
"echo signed[0]['signs']
|
9
|
+
call sign_unplace('', {'buffer': bufname(), 'id': signed[0]['signs'][0]['id']})
|
10
|
+
endif
|
11
|
+
endfunction
|
12
|
+
|
13
|
+
function UPDATE_BPS()
|
14
|
+
let signs = sign_getplaced(bufname())
|
15
|
+
let key = expand('%:p')
|
16
|
+
|
17
|
+
if empty(signs[0]['signs'])
|
18
|
+
let removed = remove(g:rdb_bps, key)
|
19
|
+
else
|
20
|
+
let g:rdb_bps[key] = signs[0]['signs']
|
21
|
+
endif
|
22
|
+
endfunction
|
23
|
+
|
24
|
+
function APPLY_BPS()
|
25
|
+
let key = expand('%:p')
|
26
|
+
if has_key(g:rdb_bps, key)
|
27
|
+
for b in g:rdb_bps[key]
|
28
|
+
call sign_place(0, '', 'signBP', bufname(), {'lnum': b['lnum']})
|
29
|
+
endfor
|
30
|
+
endif
|
31
|
+
endfunction
|
32
|
+
|
33
|
+
function WRITE_BPS()
|
34
|
+
call writefile([json_encode(g:rdb_bps)], '.rdb_breakpoints.json')
|
35
|
+
endfunction
|
36
|
+
|
37
|
+
" load
|
38
|
+
try
|
39
|
+
let json = readfile('.rdb_breakpoints.json')
|
40
|
+
let g:rdb_bps = json_decode(json[0])
|
41
|
+
" {"/full/path/to/file1": [{"lnum": 10}, ...], ...}
|
42
|
+
catch /Can't open/
|
43
|
+
let g:rdb_bps = {}
|
44
|
+
catch /Invalid arguments for function json_decode/
|
45
|
+
let g:rdb_bps = {}
|
46
|
+
endtry
|
47
|
+
|
48
|
+
sign define signBP text=BR
|
49
|
+
|
50
|
+
call APPLY_BPS()
|
51
|
+
|
52
|
+
autocmd BufReadPost * call APPLY_BPS()
|
53
|
+
autocmd BufUnload * call UPDATE_BPS()
|
54
|
+
autocmd VimLeave * call WRITE_BPS()
|
55
|
+
|
56
|
+
function! s:ruby_bp_settings() abort
|
57
|
+
echomsg "Type <Space> to toggle break points and <q> to quit"
|
58
|
+
|
59
|
+
if &readonly
|
60
|
+
nnoremap <silent> <buffer> <Space> :call SET_BP()<CR>
|
61
|
+
nnoremap <silent> <buffer> q :<C-u>quit<CR>
|
62
|
+
endif
|
63
|
+
endfunction
|
64
|
+
|
65
|
+
" autocmd FileType ruby call s:ruby_bp_settings()
|
66
|
+
autocmd BufEnter *.rb call s:ruby_bp_settings()
|
67
|
+
|
68
|
+
call s:ruby_bp_settings()
|
@@ -0,0 +1,374 @@
|
|
1
|
+
module DEBUGGER__
|
2
|
+
class Breakpoint
|
3
|
+
attr_reader :key
|
4
|
+
|
5
|
+
def initialize do_enable = true
|
6
|
+
@deleted = false
|
7
|
+
|
8
|
+
setup
|
9
|
+
enable if do_enable
|
10
|
+
end
|
11
|
+
|
12
|
+
def safe_eval b, expr
|
13
|
+
b.eval(expr)
|
14
|
+
rescue Exception => e
|
15
|
+
puts "[EVAL ERROR]"
|
16
|
+
puts " expr: #{expr}"
|
17
|
+
puts " err: #{e} (#{e.class})"
|
18
|
+
puts "Error caused by #{self}."
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup
|
23
|
+
raise "not implemented..."
|
24
|
+
end
|
25
|
+
|
26
|
+
def enable
|
27
|
+
@tp.enable
|
28
|
+
end
|
29
|
+
|
30
|
+
def disable
|
31
|
+
@tp&.disable
|
32
|
+
end
|
33
|
+
|
34
|
+
def enabled?
|
35
|
+
@tp.enabled?
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete
|
39
|
+
disable
|
40
|
+
@deleted = true
|
41
|
+
end
|
42
|
+
|
43
|
+
def deleted?
|
44
|
+
@deleted
|
45
|
+
end
|
46
|
+
|
47
|
+
def suspend
|
48
|
+
ThreadClient.current.on_breakpoint @tp, self
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
if @cond
|
53
|
+
" if #{@cond}"
|
54
|
+
else
|
55
|
+
""
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class LineBreakpoint < Breakpoint
|
61
|
+
attr_reader :path, :line, :iseq
|
62
|
+
|
63
|
+
def initialize path, line, cond: nil, oneshot: false, hook_call: true
|
64
|
+
@path = path
|
65
|
+
@line = line
|
66
|
+
@cond = cond
|
67
|
+
@oneshot = oneshot
|
68
|
+
@hook_call = hook_call
|
69
|
+
|
70
|
+
@iseq = nil
|
71
|
+
@type = nil
|
72
|
+
|
73
|
+
@key = [@path, @line].freeze
|
74
|
+
|
75
|
+
super()
|
76
|
+
try_activate
|
77
|
+
end
|
78
|
+
|
79
|
+
def setup
|
80
|
+
return unless @type
|
81
|
+
|
82
|
+
if !@cond
|
83
|
+
@tp = TracePoint.new(@type) do |tp|
|
84
|
+
delete if @oneshot
|
85
|
+
suspend
|
86
|
+
end
|
87
|
+
else
|
88
|
+
@tp = TracePoint.new(@type) do |tp|
|
89
|
+
next unless safe_eval tp.binding, @cond
|
90
|
+
delete if @oneshot
|
91
|
+
suspend
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def enable
|
97
|
+
return unless @iseq
|
98
|
+
|
99
|
+
if @type == :line
|
100
|
+
@tp.enable(target: @iseq, target_line: @line)
|
101
|
+
else
|
102
|
+
@tp.enable(target: @iseq)
|
103
|
+
end
|
104
|
+
|
105
|
+
rescue ArgumentError
|
106
|
+
puts @iseq.disasm # for debug
|
107
|
+
raise
|
108
|
+
end
|
109
|
+
|
110
|
+
def activate iseq, event, line
|
111
|
+
@iseq = iseq
|
112
|
+
@type = event
|
113
|
+
@line = line
|
114
|
+
@path = iseq.absolute_path
|
115
|
+
|
116
|
+
@key = [@path, @line].freeze
|
117
|
+
SESSION.rehash_bps
|
118
|
+
|
119
|
+
setup
|
120
|
+
enable
|
121
|
+
end
|
122
|
+
|
123
|
+
def activate_exact iseq, events, line
|
124
|
+
case
|
125
|
+
when events.include?(:RUBY_EVENT_CALL)
|
126
|
+
# "def foo" line set bp on the beginning of method foo
|
127
|
+
activate(iseq, :call, line)
|
128
|
+
when events.include?(:RUBY_EVENT_LINE)
|
129
|
+
activate(iseq, :line, line)
|
130
|
+
when events.include?(:RUBY_EVENT_RETURN)
|
131
|
+
activate(iseq, :return, line)
|
132
|
+
when events.include?(:RUBY_EVENT_B_RETURN)
|
133
|
+
activate(iseq, :b_return, line)
|
134
|
+
when events.include?(:RUBY_EVENT_END)
|
135
|
+
activate(iseq, :end, line)
|
136
|
+
else
|
137
|
+
# not actiavated
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
NearestISeq = Struct.new(:iseq, :line, :events)
|
142
|
+
|
143
|
+
def try_activate
|
144
|
+
nearest = nil # NearestISeq
|
145
|
+
|
146
|
+
ObjectSpace.each_iseq{|iseq|
|
147
|
+
if (iseq.absolute_path || iseq.path) == self.path &&
|
148
|
+
iseq.first_lineno <= self.line &&
|
149
|
+
iseq.type != :ensure # ensure iseq is copied (duplicated)
|
150
|
+
|
151
|
+
iseq.traceable_lines_norec(line_events = {})
|
152
|
+
lines = line_events.keys.sort
|
153
|
+
|
154
|
+
if !lines.empty? && lines.last >= line
|
155
|
+
nline = lines.bsearch{|l| line <= l}
|
156
|
+
events = line_events[nline]
|
157
|
+
|
158
|
+
next if events == [:RUBY_EVENT_B_CALL]
|
159
|
+
|
160
|
+
if @hook_call &&
|
161
|
+
events.include?(:RUBY_EVENT_CALL) &&
|
162
|
+
self.line == iseq.first_lineno
|
163
|
+
nline = iseq.first_lineno
|
164
|
+
end
|
165
|
+
|
166
|
+
if !nearest || ((line - nline).abs < (line - nearest.line).abs)
|
167
|
+
nearest = NearestISeq.new(iseq, nline, events)
|
168
|
+
else
|
169
|
+
if @hook_call && nearest.iseq.first_lineno <= iseq.first_lineno
|
170
|
+
if (nearest.line > line && !nearest.events.include?(:RUBY_EVENT_CALL)) ||
|
171
|
+
(events.include?(:RUBY_EVENT_CALL))
|
172
|
+
nearest = NearestISeq.new(iseq, nline, events)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
}
|
179
|
+
|
180
|
+
if nearest
|
181
|
+
activate_exact nearest.iseq, nearest.events, nearest.line
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def to_s
|
186
|
+
oneshot = @oneshot ? " (oneshot)" : ""
|
187
|
+
|
188
|
+
if @iseq
|
189
|
+
"line bp #{@path}:#{@line} (#{@type})#{oneshot}" + super
|
190
|
+
else
|
191
|
+
"line bp (pending) #{@path}:#{@line}#{oneshot}" + super
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def inspect
|
196
|
+
"<#{self.class.name} #{self.to_s}>"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class CatchBreakpoint < Breakpoint
|
201
|
+
def initialize pat
|
202
|
+
@pat = pat.freeze
|
203
|
+
@key = [:catch, @pat].freeze
|
204
|
+
|
205
|
+
super()
|
206
|
+
end
|
207
|
+
|
208
|
+
def setup
|
209
|
+
@tp = TracePoint.new(:raise){|tp|
|
210
|
+
exc = tp.raised_exception
|
211
|
+
exc.class.ancestors.each{|cls|
|
212
|
+
suspend if pat === cls.name
|
213
|
+
}
|
214
|
+
}
|
215
|
+
end
|
216
|
+
|
217
|
+
def to_s
|
218
|
+
"catch bp #{@pat.inspect}"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
class CheckBreakpoint < Breakpoint
|
223
|
+
def initialize expr
|
224
|
+
@expr = expr.freeze
|
225
|
+
@key = [:check, @expr].freeze
|
226
|
+
|
227
|
+
super()
|
228
|
+
end
|
229
|
+
|
230
|
+
def setup
|
231
|
+
@tp = TracePoint.new(:line){|tp|
|
232
|
+
next if tp.path.start_with? __dir__
|
233
|
+
next if tp.path.start_with? '<internal:'
|
234
|
+
|
235
|
+
if safe_eval tp.binding, @expr
|
236
|
+
suspend
|
237
|
+
end
|
238
|
+
}
|
239
|
+
end
|
240
|
+
|
241
|
+
def to_s
|
242
|
+
"check bp: #{@expr}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class WatchExprBreakpoint < Breakpoint
|
247
|
+
def initialize expr, current
|
248
|
+
@expr = expr.freeze
|
249
|
+
@key = [:watch, @expr].freeze
|
250
|
+
|
251
|
+
@current = current
|
252
|
+
super()
|
253
|
+
end
|
254
|
+
|
255
|
+
def watch_eval b
|
256
|
+
result = b.eval(@expr)
|
257
|
+
if result != @current
|
258
|
+
begin
|
259
|
+
@prev = @current
|
260
|
+
@current = result
|
261
|
+
suspend
|
262
|
+
ensure
|
263
|
+
remove_instance_variable(:@prev)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
rescue Exception => e
|
267
|
+
false
|
268
|
+
end
|
269
|
+
|
270
|
+
def setup
|
271
|
+
@tp = TracePoint.new(:line, :return, :b_return){|tp|
|
272
|
+
next if tp.path.start_with? __dir__
|
273
|
+
next if tp.path.start_with? '<internal:'
|
274
|
+
|
275
|
+
watch_eval(tp.binding)
|
276
|
+
}
|
277
|
+
end
|
278
|
+
|
279
|
+
def to_s
|
280
|
+
if defined? @prev
|
281
|
+
"watch bp: #{@expr} = #{@prev} -> #{@current}"
|
282
|
+
else
|
283
|
+
"watch bp: #{@expr} = #{@current}"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
class MethodBreakpoint < Breakpoint
|
289
|
+
attr_reader :sig_method_name, :method
|
290
|
+
|
291
|
+
def initialize b, klass_name, op, method_name, cond
|
292
|
+
@sig_klass_name = klass_name
|
293
|
+
@sig_op = op
|
294
|
+
@sig_method_name = method_name
|
295
|
+
@klass_eval_binding = b
|
296
|
+
|
297
|
+
@klass = nil
|
298
|
+
@method = nil
|
299
|
+
@cond = cond
|
300
|
+
@key = "#{klass_name}#{op}#{method_name}".freeze
|
301
|
+
|
302
|
+
super(false)
|
303
|
+
end
|
304
|
+
|
305
|
+
def setup
|
306
|
+
if @cond
|
307
|
+
@tp = TracePoint.new(:call){|tp|
|
308
|
+
next unless safe_eval tp.binding, @cond
|
309
|
+
suspend
|
310
|
+
}
|
311
|
+
else
|
312
|
+
@tp = TracePoint.new(:call){|tp|
|
313
|
+
suspend
|
314
|
+
}
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def eval_class_name
|
319
|
+
return @klass if @klass
|
320
|
+
@klass = @klass_eval_binding.eval(@sig_klass_name)
|
321
|
+
@klass_eval_binding = nil
|
322
|
+
@klass
|
323
|
+
end
|
324
|
+
|
325
|
+
def search_method
|
326
|
+
case @sig_op
|
327
|
+
when '.'
|
328
|
+
@method = @klass.method(@sig_method_name)
|
329
|
+
when '#'
|
330
|
+
@method = @klass.instance_method(@sig_method_name)
|
331
|
+
else
|
332
|
+
raise "Unknown op: #{@sig_op}"
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def enable quiet: false
|
337
|
+
eval_class_name
|
338
|
+
search_method
|
339
|
+
|
340
|
+
begin
|
341
|
+
retried = false
|
342
|
+
@tp.enable(target: @method)
|
343
|
+
|
344
|
+
rescue ArgumentError => e
|
345
|
+
raise if retried
|
346
|
+
retried = true
|
347
|
+
|
348
|
+
# maybe C method
|
349
|
+
@klass.module_eval do
|
350
|
+
orig_name = @sig_method_name + '__orig__'
|
351
|
+
alias_method orig_name, @sig_method_name
|
352
|
+
define_method(@sig_method_name) do |*args|
|
353
|
+
send(orig_name, *args)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
retry
|
357
|
+
end
|
358
|
+
rescue Exception
|
359
|
+
raise unless quiet
|
360
|
+
end
|
361
|
+
|
362
|
+
def sig
|
363
|
+
@key
|
364
|
+
end
|
365
|
+
|
366
|
+
def to_s
|
367
|
+
if @method
|
368
|
+
"method bp: #{sig}"
|
369
|
+
else
|
370
|
+
"method bp (pending): #{sig}"
|
371
|
+
end + super
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
data/lib/debug/client.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative 'config'
|
3
|
+
|
4
|
+
module DEBUGGER__
|
5
|
+
class CommandLineOptionError < Exception; end
|
6
|
+
|
7
|
+
class Client
|
8
|
+
begin
|
9
|
+
require 'readline'
|
10
|
+
def readline
|
11
|
+
Readline.readline("\n(rdb) ", true)
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
def readline
|
15
|
+
print "\n(rdb) "
|
16
|
+
gets
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize argv
|
21
|
+
case argv.size
|
22
|
+
when 0
|
23
|
+
connect_unix
|
24
|
+
when 1
|
25
|
+
case arg = argv.shift
|
26
|
+
when /-h/, /--help/
|
27
|
+
help
|
28
|
+
exit
|
29
|
+
when /\A\d+\z/
|
30
|
+
connect_tcp nil, arg.to_i
|
31
|
+
else
|
32
|
+
connect_unix arg
|
33
|
+
end
|
34
|
+
when 2
|
35
|
+
connect_tcp argv[0], argv[1]
|
36
|
+
else
|
37
|
+
raise CommandLineOptionError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def cleanup_unix_domain_sockets
|
42
|
+
Dir.glob(DEBUGGER__.create_unix_domain_socket_name_prefix + '*') do |file|
|
43
|
+
if /(\d+)$/ =~ file
|
44
|
+
begin
|
45
|
+
Process.kill(0, $1.to_i)
|
46
|
+
rescue Errno::ESRCH
|
47
|
+
File.unlink(file)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def connect_unix name = nil
|
54
|
+
if name
|
55
|
+
if File.exist? name
|
56
|
+
@s = Socket.unix(name)
|
57
|
+
else
|
58
|
+
@s = Socket.unix(File.join(DEBUGGER__.unix_domain_socket_basedir, name))
|
59
|
+
end
|
60
|
+
else
|
61
|
+
cleanup_unix_domain_sockets
|
62
|
+
files = Dir.glob(DEBUGGER__.create_unix_domain_socket_name_prefix + '*')
|
63
|
+
case files.size
|
64
|
+
when 0
|
65
|
+
$stderr.puts "No debug session is available."
|
66
|
+
exit
|
67
|
+
when 1
|
68
|
+
@s = Socket.unix(files.first)
|
69
|
+
else
|
70
|
+
$stderr.puts "Please select a debug session:"
|
71
|
+
files.each{|f|
|
72
|
+
$stderr.puts " #{File.basename(f)}"
|
73
|
+
}
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def connect_tcp host, port
|
80
|
+
@s = Socket.tcp(host, port)
|
81
|
+
end
|
82
|
+
|
83
|
+
def connect
|
84
|
+
trap(:SIGINT){
|
85
|
+
@s.puts "pause"
|
86
|
+
}
|
87
|
+
|
88
|
+
while line = @s.gets
|
89
|
+
# p line: line
|
90
|
+
case line
|
91
|
+
when /^out (.*)/
|
92
|
+
puts "#{$1}"
|
93
|
+
when /^input/
|
94
|
+
prev_trap = trap(:SIGINT, 'DEFAULT')
|
95
|
+
|
96
|
+
begin
|
97
|
+
line = readline
|
98
|
+
rescue Interrupt
|
99
|
+
retry
|
100
|
+
ensure
|
101
|
+
trap(:SIGINT, prev_trap)
|
102
|
+
end
|
103
|
+
|
104
|
+
line = (line || 'quit').strip
|
105
|
+
@s.puts "command #{line}"
|
106
|
+
when /^ask (.*)/
|
107
|
+
print $1
|
108
|
+
@s.puts "answer #{gets || ''}"
|
109
|
+
when /^quit/
|
110
|
+
raise 'quit'
|
111
|
+
else
|
112
|
+
puts "(unknown) #{line.inspect}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
rescue
|
116
|
+
STDERR.puts "disconnected (#{$!})"
|
117
|
+
exit
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def connect argv = ARGV
|
123
|
+
DEBUGGER__::Client.new(argv).connect
|
124
|
+
end
|
125
|
+
|
126
|
+
if __FILE__ == $0
|
127
|
+
connect
|
128
|
+
end
|