debug 0.2.0 → 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 +19 -9
- data/exe/rdbg +3 -0
- data/lib/debug.rb +11 -1107
- 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
- metadata +61 -21
- data/Gemfile +0 -8
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,97 @@
|
|
1
|
+
module DEBUGGER__
|
2
|
+
class LineBreakpoint
|
3
|
+
attr_reader :path, :line, :key
|
4
|
+
|
5
|
+
def initialize type, iseq, line, cond = nil, oneshot: false
|
6
|
+
@iseq = iseq
|
7
|
+
@path = iseq.path
|
8
|
+
@line = line
|
9
|
+
@type = type
|
10
|
+
@cond = cond
|
11
|
+
@oneshot = oneshot
|
12
|
+
@key = [@path, @line].freeze
|
13
|
+
setup
|
14
|
+
enable
|
15
|
+
end
|
16
|
+
|
17
|
+
def safe_eval b, expr
|
18
|
+
b.eval(expr)
|
19
|
+
rescue Exception => e
|
20
|
+
puts "[EVAL ERROR]"
|
21
|
+
puts " expr: #{expr}"
|
22
|
+
puts " err: #{e} (#{e.class})"
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup
|
27
|
+
if !@cond
|
28
|
+
@tp = TracePoint.new(@type) do |tp|
|
29
|
+
tp.disable if @oneshot
|
30
|
+
ThreadClient.current.on_breakpoint tp
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@tp = TracePoint.new(@type) do |tp|
|
34
|
+
next unless safe_eval tp.binding, @cond
|
35
|
+
tp.disable if @oneshot
|
36
|
+
ThreadClient.current.on_breakpoint tp
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def enable
|
42
|
+
if @type == :line
|
43
|
+
@tp.enable(target: @iseq, target_line: @line)
|
44
|
+
else
|
45
|
+
@tp.enable(target: @iseq)
|
46
|
+
end
|
47
|
+
rescue ArgumentError
|
48
|
+
puts @iseq.disasm # for debug
|
49
|
+
raise
|
50
|
+
end
|
51
|
+
|
52
|
+
def disable
|
53
|
+
@tp.disable
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
"line bp #{@iseq.absolute_path}:#{@line} (#{@type})" +
|
58
|
+
if @cond
|
59
|
+
"if #{@cond}"
|
60
|
+
else
|
61
|
+
""
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def inspect
|
66
|
+
"<#{self.class.name} #{self.to_s}>"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class CatchBreakpoint
|
71
|
+
attr_reader :key
|
72
|
+
|
73
|
+
def initialize pat
|
74
|
+
@pat = pat
|
75
|
+
@tp = TracePoint.new(:raise){|tp|
|
76
|
+
exc = tp.raised_exception
|
77
|
+
exc.class.ancestors.each{|cls|
|
78
|
+
if pat === cls.name
|
79
|
+
puts "catch #{exc.class.inspect} by #{@pat.inspect}"
|
80
|
+
ThreadClient.current.on_suspend :catch
|
81
|
+
end
|
82
|
+
}
|
83
|
+
}
|
84
|
+
@tp.enable
|
85
|
+
|
86
|
+
@key = pat.freeze
|
87
|
+
end
|
88
|
+
|
89
|
+
def disable
|
90
|
+
@tp.disable
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s
|
94
|
+
"catch bp #{@pat.inspect}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/debug/client.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative 'config'
|
3
|
+
|
4
|
+
module DEBUGGER__
|
5
|
+
class Client
|
6
|
+
begin
|
7
|
+
require 'readline'
|
8
|
+
def readline
|
9
|
+
Readline.readline("\n(rdb) ", true)
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
def readline
|
13
|
+
print "\n(rdb) "
|
14
|
+
gets
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def help
|
19
|
+
puts "-run -e attach # connect via UNIX Domain socket"
|
20
|
+
puts "-run -e attach name # connect via UNIX Domain socket with specified name"
|
21
|
+
puts "-run -e attach port_num # connect via TCP/IP socket with specified port"
|
22
|
+
puts "-run -e attach host port_num"
|
23
|
+
puts " # connect via TCP/IP socket with specified host and port"
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize argv
|
27
|
+
case argv.size
|
28
|
+
when 0
|
29
|
+
connect_unix
|
30
|
+
when 1
|
31
|
+
case arg = argv.shift
|
32
|
+
when /-h/, /--help/
|
33
|
+
help
|
34
|
+
exit
|
35
|
+
when /\A\d+\z/
|
36
|
+
connect_tcp nil, arg.to_i
|
37
|
+
else
|
38
|
+
connect_unix arg
|
39
|
+
end
|
40
|
+
when 2
|
41
|
+
connect_tcp argv[0], argv[1]
|
42
|
+
else
|
43
|
+
help
|
44
|
+
exit!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def cleanup_unix_domain_sockets
|
49
|
+
Dir.glob(DEBUGGER__.create_unix_domain_socket_name_prefix + '*') do |file|
|
50
|
+
if /(\d+)$/ =~ file
|
51
|
+
begin
|
52
|
+
Process.kill(0, $1.to_i)
|
53
|
+
rescue Errno::ESRCH
|
54
|
+
File.unlink(file)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def connect_unix name = nil
|
61
|
+
if name
|
62
|
+
if File.exist? name
|
63
|
+
@s = Socket.unix(name)
|
64
|
+
else
|
65
|
+
@s = Socket.unix(File.join(DEBUGGER__.unix_domain_socket_basedir, name))
|
66
|
+
end
|
67
|
+
else
|
68
|
+
cleanup_unix_domain_sockets
|
69
|
+
files = Dir.glob(DEBUGGER__.create_unix_domain_socket_name_prefix + '*')
|
70
|
+
case files.size
|
71
|
+
when 0
|
72
|
+
$stderr.puts "There is no debug sessions."
|
73
|
+
exit
|
74
|
+
when 1
|
75
|
+
@s = Socket.unix(files.first)
|
76
|
+
else
|
77
|
+
$stderr.puts "Please select a debug session:"
|
78
|
+
files.each{|f|
|
79
|
+
$stderr.puts " #{File.basename(f)}"
|
80
|
+
}
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def connect_tcp host, port
|
87
|
+
@s = Socket.tcp(host, port)
|
88
|
+
end
|
89
|
+
|
90
|
+
def connect
|
91
|
+
trap(:SIGINT){
|
92
|
+
@s.puts "pause"
|
93
|
+
}
|
94
|
+
|
95
|
+
while line = @s.gets
|
96
|
+
# p line: line
|
97
|
+
case line
|
98
|
+
when /^out (.*)/
|
99
|
+
puts "#{$1}"
|
100
|
+
when /^input/
|
101
|
+
prev_trap = trap(:SIGINT, 'DEFAULT')
|
102
|
+
|
103
|
+
begin
|
104
|
+
line = readline
|
105
|
+
rescue Interrupt
|
106
|
+
retry
|
107
|
+
ensure
|
108
|
+
trap(:SIGINT, prev_trap)
|
109
|
+
end
|
110
|
+
|
111
|
+
line = (line || 'quit').strip
|
112
|
+
@s.puts "command #{line}"
|
113
|
+
when /^ask (.*)/
|
114
|
+
print $1
|
115
|
+
@s.puts "answer #{gets || ''}"
|
116
|
+
when /^quit/
|
117
|
+
raise 'quit'
|
118
|
+
else
|
119
|
+
puts "(unknown) #{line.inspect}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
rescue
|
123
|
+
STDERR.puts "disconnected (#{$!})"
|
124
|
+
exit
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def connect argv = ARGV
|
130
|
+
DEBUGGER__::Client.new(argv).connect
|
131
|
+
end
|
132
|
+
|
133
|
+
if __FILE__ == $0
|
134
|
+
connect
|
135
|
+
end
|
data/lib/debug/config.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module DEBUGGER__
|
2
|
+
def self.unix_domain_socket_basedir
|
3
|
+
case
|
4
|
+
when path = ENV['RUBY_DEBUG_SOCK_DIR']
|
5
|
+
when path = ENV['XDG_RUNTIME_DIR']
|
6
|
+
when home = ENV['HOME']
|
7
|
+
path = File.join(home, '.ruby-debug-sock')
|
8
|
+
unless File.exist?(path)
|
9
|
+
Dir.mkdir(path, 0700)
|
10
|
+
end
|
11
|
+
else
|
12
|
+
raise 'specify RUBY_DEBUG_SOCK_DIR environment variable for UNIX domain socket directory.'
|
13
|
+
end
|
14
|
+
|
15
|
+
path
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.create_unix_domain_socket_name_prefix
|
19
|
+
user = ENV['USER'] || 'ruby-debug'
|
20
|
+
File.join(unix_domain_socket_basedir, "ruby-debug-#{user}")
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.create_unix_domain_socket_name
|
24
|
+
create_unix_domain_socket_name_prefix + "-#{Process.pid}"
|
25
|
+
end
|
26
|
+
end
|
data/lib/debug/repl.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative 'session'
|
2
|
+
|
3
|
+
module DEBUGGER__
|
4
|
+
class UI_Repl
|
5
|
+
def initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
def quit n
|
9
|
+
exit n
|
10
|
+
end
|
11
|
+
|
12
|
+
def ask prompt
|
13
|
+
print prompt
|
14
|
+
(gets || '').strip
|
15
|
+
end
|
16
|
+
|
17
|
+
def puts str
|
18
|
+
case str
|
19
|
+
when Array
|
20
|
+
str.each{|line|
|
21
|
+
$stdout.puts line.chomp
|
22
|
+
}
|
23
|
+
when String
|
24
|
+
str.each_line{|line|
|
25
|
+
$stdout.puts line.chomp
|
26
|
+
}
|
27
|
+
when nil
|
28
|
+
$stdout.puts
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
require 'readline'
|
34
|
+
def readline
|
35
|
+
setup_interrupt do
|
36
|
+
str = Readline.readline("\n(rdb) ", true)
|
37
|
+
(str || 'quit').strip
|
38
|
+
end
|
39
|
+
end
|
40
|
+
rescue LoadError
|
41
|
+
def readline
|
42
|
+
setup_interrupt do
|
43
|
+
print "\n(rdb) "
|
44
|
+
(gets || 'quit').strip
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def setup_interrupt
|
50
|
+
current_thread = Thread.current # should be session_server thread
|
51
|
+
|
52
|
+
prev_handler = trap(:INT){
|
53
|
+
current_thread.raise Interrupt
|
54
|
+
}
|
55
|
+
|
56
|
+
yield
|
57
|
+
ensure
|
58
|
+
trap(:INT, prev_handler)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
SESSION = Session.new(UI_Repl.new)
|
63
|
+
|
64
|
+
PREV_HANDLER = trap(:SIGINT){
|
65
|
+
ThreadClient.current.on_trap
|
66
|
+
}
|
67
|
+
end
|
data/lib/debug/server.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative 'session'
|
3
|
+
|
4
|
+
module DEBUGGER__
|
5
|
+
class UI_Server
|
6
|
+
def initialize
|
7
|
+
@sock = nil
|
8
|
+
@client_addr = nil
|
9
|
+
@q_msg = Queue.new
|
10
|
+
@q_ans = Queue.new
|
11
|
+
|
12
|
+
@reader_thread = Thread.new do
|
13
|
+
accept do |server|
|
14
|
+
@sock = server
|
15
|
+
@q_msg = Queue.new
|
16
|
+
@q_ans = Queue.new
|
17
|
+
|
18
|
+
setup_interrupt do
|
19
|
+
pause
|
20
|
+
|
21
|
+
begin
|
22
|
+
while line = @sock.gets
|
23
|
+
case line
|
24
|
+
when /\Apause/
|
25
|
+
pause
|
26
|
+
when /\Acommand ?(.+)/
|
27
|
+
@q_msg << $1
|
28
|
+
when /\Aanswer (.*)/
|
29
|
+
@q_ans << $1
|
30
|
+
else
|
31
|
+
STDERR.puts "unsupported: #{line}"
|
32
|
+
exit!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
ensure
|
38
|
+
@sock = nil
|
39
|
+
@q_msg.close
|
40
|
+
@q_ans.close
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def setup_interrupt
|
47
|
+
prev_handler = trap(:SIGINT) do
|
48
|
+
# $stderr.puts "trapped SIGINT"
|
49
|
+
ThreadClient.current.on_trap
|
50
|
+
|
51
|
+
case prev_handler
|
52
|
+
when Proc
|
53
|
+
prev_handler.call
|
54
|
+
else
|
55
|
+
# ignore
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
yield
|
60
|
+
ensure
|
61
|
+
trap(:SIGINT, prev_handler)
|
62
|
+
end
|
63
|
+
|
64
|
+
def accept
|
65
|
+
raise "NOT IMPLEMENTED ERROR"
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :reader_thread
|
69
|
+
|
70
|
+
class NoRemoteError < Exception; end
|
71
|
+
|
72
|
+
def sock
|
73
|
+
yield @sock if @sock
|
74
|
+
rescue Errno::EPIPE
|
75
|
+
# ignore
|
76
|
+
end
|
77
|
+
|
78
|
+
def ask prompt
|
79
|
+
sock do |s|
|
80
|
+
s.puts "ask #{prompt}"
|
81
|
+
@q_ans.pop
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def puts str
|
86
|
+
case str
|
87
|
+
when Array
|
88
|
+
enum = str.each
|
89
|
+
when String
|
90
|
+
enum = str.each_line
|
91
|
+
end
|
92
|
+
|
93
|
+
sock do |s|
|
94
|
+
enum.each do |line|
|
95
|
+
s.puts "out #{line.chomp}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def readline
|
101
|
+
(sock do |s|
|
102
|
+
s.puts "input"
|
103
|
+
@q_msg.pop
|
104
|
+
end || 'continue').strip
|
105
|
+
end
|
106
|
+
|
107
|
+
def pause
|
108
|
+
$stderr.puts "DEBUG: pause request"
|
109
|
+
Process.kill(:SIGINT, Process.pid)
|
110
|
+
end
|
111
|
+
|
112
|
+
def quit n
|
113
|
+
# ignore n
|
114
|
+
sock do |s|
|
115
|
+
s.puts "quit"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|