ruby-debug-ide-docker 0.6.1
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/CHANGES +75 -0
- data/ChangeLog.archive +1073 -0
- data/ChangeLog.md +594 -0
- data/Gemfile +28 -0
- data/MIT-LICENSE +24 -0
- data/Rakefile +42 -0
- data/bin/gdb_wrapper +96 -0
- data/bin/rdebug-ide +183 -0
- data/ext/mkrf_conf.rb +48 -0
- data/lib/ruby-debug-ide.rb +173 -0
- data/lib/ruby-debug-ide/attach/debugger_loader.rb +20 -0
- data/lib/ruby-debug-ide/attach/gdb.rb +73 -0
- data/lib/ruby-debug-ide/attach/lldb.rb +71 -0
- data/lib/ruby-debug-ide/attach/native_debugger.rb +133 -0
- data/lib/ruby-debug-ide/attach/process_thread.rb +54 -0
- data/lib/ruby-debug-ide/attach/util.rb +115 -0
- data/lib/ruby-debug-ide/command.rb +177 -0
- data/lib/ruby-debug-ide/commands/breakpoints.rb +128 -0
- data/lib/ruby-debug-ide/commands/catchpoint.rb +64 -0
- data/lib/ruby-debug-ide/commands/condition.rb +51 -0
- data/lib/ruby-debug-ide/commands/control.rb +158 -0
- data/lib/ruby-debug-ide/commands/enable.rb +203 -0
- data/lib/ruby-debug-ide/commands/eval.rb +64 -0
- data/lib/ruby-debug-ide/commands/expression_info.rb +71 -0
- data/lib/ruby-debug-ide/commands/file_filtering.rb +107 -0
- data/lib/ruby-debug-ide/commands/frame.rb +155 -0
- data/lib/ruby-debug-ide/commands/inspect.rb +25 -0
- data/lib/ruby-debug-ide/commands/jump.rb +73 -0
- data/lib/ruby-debug-ide/commands/load.rb +18 -0
- data/lib/ruby-debug-ide/commands/pause.rb +33 -0
- data/lib/ruby-debug-ide/commands/set_type.rb +47 -0
- data/lib/ruby-debug-ide/commands/stepping.rb +108 -0
- data/lib/ruby-debug-ide/commands/threads.rb +178 -0
- data/lib/ruby-debug-ide/commands/variables.rb +154 -0
- data/lib/ruby-debug-ide/event_processor.rb +71 -0
- data/lib/ruby-debug-ide/greeter.rb +40 -0
- data/lib/ruby-debug-ide/helper.rb +33 -0
- data/lib/ruby-debug-ide/ide_processor.rb +155 -0
- data/lib/ruby-debug-ide/interface.rb +45 -0
- data/lib/ruby-debug-ide/multiprocess.rb +23 -0
- data/lib/ruby-debug-ide/multiprocess/monkey.rb +47 -0
- data/lib/ruby-debug-ide/multiprocess/pre_child.rb +67 -0
- data/lib/ruby-debug-ide/multiprocess/starter.rb +11 -0
- data/lib/ruby-debug-ide/multiprocess/unmonkey.rb +31 -0
- data/lib/ruby-debug-ide/version.rb +3 -0
- data/lib/ruby-debug-ide/xml_printer.rb +545 -0
- data/ruby-debug-ide-docker.gemspec +51 -0
- metadata +110 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
def load_debugger(gems_to_include, new_argv)
|
2
|
+
path_to_rdebug = File.expand_path(File.dirname(__FILE__)) + '/../../../bin/rdebug-ide'
|
3
|
+
|
4
|
+
old_argv = ARGV.clone
|
5
|
+
ARGV.clear
|
6
|
+
new_argv.each do |x|
|
7
|
+
ARGV << x
|
8
|
+
end
|
9
|
+
|
10
|
+
gems_to_include.each do |gem_path|
|
11
|
+
$LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
load path_to_rdebug
|
15
|
+
|
16
|
+
ARGV.clear
|
17
|
+
old_argv.each do |x|
|
18
|
+
ARGV << x
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'ruby-debug-ide/attach/native_debugger'
|
2
|
+
|
3
|
+
class GDB < NativeDebugger
|
4
|
+
|
5
|
+
def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
|
6
|
+
super(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_flags
|
10
|
+
execute 'set scheduler-locking off' # we will deadlock with it
|
11
|
+
execute 'set unwindonsignal on' # in case of some signal we will exit gdb
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_threads
|
15
|
+
@process_threads = []
|
16
|
+
info_threads = (execute 'info threads').split("\n")
|
17
|
+
info_threads.each do |thread_info|
|
18
|
+
next unless thread_info =~ /[\s*]*\d+\s+Thread.*/
|
19
|
+
$stdout.puts "thread_info: #{thread_info}"
|
20
|
+
is_main = thread_info[0] == '*'
|
21
|
+
thread_num = thread_info.sub(/[\s*]*/, '').sub(/\s.*$/, '').to_i
|
22
|
+
thread = ProcessThread.new(thread_num, is_main, thread_info, self)
|
23
|
+
if thread.is_main
|
24
|
+
@main_thread = thread
|
25
|
+
end
|
26
|
+
@process_threads << thread
|
27
|
+
end
|
28
|
+
@process_threads
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_already_under_debug
|
32
|
+
threads = execute 'info threads'
|
33
|
+
threads =~ /ruby-debug-ide/
|
34
|
+
end
|
35
|
+
|
36
|
+
def switch_to_thread(thread_num)
|
37
|
+
execute "thread #{thread_num}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_break(str)
|
41
|
+
execute "tbreak #{str}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def call_start_attach
|
45
|
+
super()
|
46
|
+
execute "call dlopen(\"#{@path_to_attach}\", 2)"
|
47
|
+
execute 'call debase_start_attach()'
|
48
|
+
set_break(@tbreak)
|
49
|
+
end
|
50
|
+
|
51
|
+
def print_delimiter
|
52
|
+
@pipe.puts "print \"#{@delimiter}\""
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_delimiter(line)
|
56
|
+
line =~ /\$\d+\s=\s"#{@delimiter}"/
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_debugger
|
60
|
+
execute "call #{@eval_string}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
GDB.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
class << self
|
68
|
+
def to_s
|
69
|
+
'gdb'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'ruby-debug-ide/attach/native_debugger'
|
2
|
+
|
3
|
+
class LLDB < NativeDebugger
|
4
|
+
|
5
|
+
def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
|
6
|
+
super(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_flags
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
def update_threads
|
14
|
+
@process_threads = []
|
15
|
+
info_threads = (execute 'thread list').split("\n")
|
16
|
+
info_threads.each do |thread_info|
|
17
|
+
next unless thread_info =~ /[\s*]*thread\s#\d+.*/
|
18
|
+
is_main = thread_info[0] == '*'
|
19
|
+
thread_num = thread_info.sub(/[\s*]*thread\s#/, '').sub(/:\s.*$/, '').to_i
|
20
|
+
thread = ProcessThread.new(thread_num, is_main, thread_info, self)
|
21
|
+
if thread.is_main
|
22
|
+
@main_thread = thread
|
23
|
+
end
|
24
|
+
@process_threads << thread
|
25
|
+
end
|
26
|
+
@process_threads
|
27
|
+
end
|
28
|
+
|
29
|
+
def check_already_under_debug
|
30
|
+
threads = execute 'thread list'
|
31
|
+
threads =~ /ruby-debug-ide/
|
32
|
+
end
|
33
|
+
|
34
|
+
def switch_to_thread(thread_num)
|
35
|
+
execute "thread select #{thread_num}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def set_break(str)
|
39
|
+
execute "breakpoint set --shlib #{@path_to_attach} --name #{str}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def call_start_attach
|
43
|
+
super()
|
44
|
+
execute "expr (void *) dlopen(\"#{@path_to_attach}\", 2)"
|
45
|
+
execute 'expr (int) debase_start_attach()'
|
46
|
+
set_break(@tbreak)
|
47
|
+
end
|
48
|
+
|
49
|
+
def print_delimiter
|
50
|
+
@pipe.puts "script print \"#{@delimiter}\""
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_delimiter(line)
|
54
|
+
line =~ /#{@delimiter}$/
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_debugger
|
58
|
+
execute "expr (void) #{@eval_string}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
LLDB.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
class << self
|
66
|
+
def to_s
|
67
|
+
'lldb'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
class NativeDebugger
|
2
|
+
|
3
|
+
attr_reader :pid, :main_thread, :process_threads, :pipe
|
4
|
+
|
5
|
+
# @param executable -- path to ruby interpreter
|
6
|
+
# @param pid -- pid of process you want to debug
|
7
|
+
# @param flags -- flags you want to specify to your debugger as a string (e.g. "-nx -nh" for gdb to disable .gdbinit)
|
8
|
+
def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
|
9
|
+
@pid = pid
|
10
|
+
@delimiter = '__OUTPUT_FINISHED__' # for getting response
|
11
|
+
@tbreak = '__func_to_set_breakpoint_at'
|
12
|
+
@main_thread = nil
|
13
|
+
@process_threads = nil
|
14
|
+
debase_path = gems_to_include.select {|gem_path| gem_path =~ /debase/}
|
15
|
+
if debase_path.size == 0
|
16
|
+
raise 'No debase gem found.'
|
17
|
+
end
|
18
|
+
@path_to_attach = find_attach_lib(debase_path[0])
|
19
|
+
|
20
|
+
@gems_to_include = '["' + gems_to_include * '", "' + '"]'
|
21
|
+
@debugger_loader_path = debugger_loader_path
|
22
|
+
@argv = argv
|
23
|
+
|
24
|
+
@eval_string = "debase_rb_eval(\"require '#{@debugger_loader_path}'; load_debugger(#{@gems_to_include.gsub("\"", "'")}, #{@argv.gsub("\"", "'")})\")"
|
25
|
+
|
26
|
+
launch_string = "#{self} #{executable} #{flags}"
|
27
|
+
@pipe = IO.popen(launch_string, 'r+')
|
28
|
+
$stdout.puts "executed '#{launch_string}'"
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_attach_lib(debase_path)
|
32
|
+
attach_lib = debase_path + '/attach'
|
33
|
+
known_extensions = %w(.so .bundle .dll .dylib)
|
34
|
+
known_extensions.each do |ext|
|
35
|
+
if File.file?(attach_lib + ext)
|
36
|
+
return attach_lib + ext
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
raise 'Could not find attach library'
|
41
|
+
end
|
42
|
+
|
43
|
+
def attach_to_process
|
44
|
+
execute "attach #{@pid}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute(command)
|
48
|
+
@pipe.puts command
|
49
|
+
$stdout.puts "executed `#{command}` command inside #{self}."
|
50
|
+
if command == 'q'
|
51
|
+
return ''
|
52
|
+
end
|
53
|
+
get_response
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_response
|
57
|
+
# we need this hack to understand that debugger gave us all output from last executed command
|
58
|
+
print_delimiter
|
59
|
+
|
60
|
+
content = ''
|
61
|
+
loop do
|
62
|
+
line = @pipe.readline
|
63
|
+
DebugPrinter.print_debug('respond line: ' + line)
|
64
|
+
break if check_delimiter(line)
|
65
|
+
next if line =~ /\(lldb\)/ # lldb repeats your input to its output
|
66
|
+
content += line
|
67
|
+
end
|
68
|
+
|
69
|
+
content
|
70
|
+
end
|
71
|
+
|
72
|
+
def update_threads
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_already_under_debug
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
def print_delimiter
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
def check_delimiter(line)
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
def switch_to_thread
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
def set_break(str)
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
def continue
|
97
|
+
$stdout.puts 'continuing'
|
98
|
+
@pipe.puts 'c'
|
99
|
+
loop do
|
100
|
+
line = @pipe.readline
|
101
|
+
DebugPrinter.print_debug('respond line: ' + line)
|
102
|
+
break if line =~ /#{Regexp.escape(@tbreak)}/
|
103
|
+
end
|
104
|
+
get_response
|
105
|
+
end
|
106
|
+
|
107
|
+
def call_start_attach
|
108
|
+
raise 'No main thread found. Did you forget to call `update_threads`?' if @main_thread == nil
|
109
|
+
@main_thread.switch
|
110
|
+
end
|
111
|
+
|
112
|
+
def wait_line_event
|
113
|
+
call_start_attach
|
114
|
+
continue
|
115
|
+
end
|
116
|
+
|
117
|
+
def load_debugger
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
def exited?
|
122
|
+
@pipe.closed?
|
123
|
+
end
|
124
|
+
|
125
|
+
def exit
|
126
|
+
@pipe.close
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_s
|
130
|
+
'native_debugger'
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'ruby-debug-ide/attach/native_debugger'
|
2
|
+
|
3
|
+
class ProcessThread
|
4
|
+
|
5
|
+
attr_reader :thread_num, :is_main, :thread_info, :last_bt
|
6
|
+
|
7
|
+
def initialize(thread_num, is_main, thread_info, native_debugger)
|
8
|
+
@thread_num = thread_num
|
9
|
+
@is_main = is_main
|
10
|
+
@native_debugger = native_debugger
|
11
|
+
@thread_info = thread_info
|
12
|
+
@last_bt = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def switch
|
16
|
+
@native_debugger.switch_to_thread(thread_num)
|
17
|
+
end
|
18
|
+
|
19
|
+
def finish
|
20
|
+
@native_debugger.execute 'finish'
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_bt
|
24
|
+
@last_bt = @native_debugger.execute 'bt'
|
25
|
+
end
|
26
|
+
|
27
|
+
def any_caller_match(bt, pattern)
|
28
|
+
bt =~ /#{pattern}/
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_inside_malloc(bt = get_bt)
|
32
|
+
if any_caller_match(bt, '(malloc)')
|
33
|
+
$stderr.puts "process #{@native_debugger.pid} is currently inside malloc."
|
34
|
+
true
|
35
|
+
else
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def is_inside_gc(bt = get_bt)
|
41
|
+
if any_caller_match(bt, '(gc\.c)')
|
42
|
+
$stderr.puts "process #{@native_debugger.pid} is currently in garbage collection phase."
|
43
|
+
true
|
44
|
+
else
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def need_finish_frame
|
50
|
+
bt = get_bt
|
51
|
+
is_inside_malloc(bt) || is_inside_gc(bt)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'ruby-debug-ide/attach/lldb'
|
2
|
+
require 'ruby-debug-ide/attach/gdb'
|
3
|
+
require 'socket'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
def attach_and_return_thread(options, pid, debugger_loader_path, argv)
|
7
|
+
Thread.new(argv) do |argv|
|
8
|
+
|
9
|
+
debugger = choose_debugger(options.ruby_path, pid, options.gems_to_include, debugger_loader_path, argv)
|
10
|
+
|
11
|
+
trap('INT') do
|
12
|
+
unless debugger.exited?
|
13
|
+
$stderr.puts "backtraces for threads:\n\n"
|
14
|
+
process_threads = debugger.process_threads
|
15
|
+
if process_threads
|
16
|
+
process_threads.each do |thread|
|
17
|
+
$stderr.puts "#{thread.thread_info}\n#{thread.last_bt}\n\n"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
debugger.exit
|
21
|
+
end
|
22
|
+
exit!
|
23
|
+
end
|
24
|
+
|
25
|
+
debugger.attach_to_process
|
26
|
+
debugger.set_flags
|
27
|
+
|
28
|
+
if debugger.check_already_under_debug
|
29
|
+
$stderr.puts "Process #{debugger.pid} is already under debug"
|
30
|
+
debugger.exit
|
31
|
+
exit!
|
32
|
+
end
|
33
|
+
|
34
|
+
should_check_threads_state = true
|
35
|
+
|
36
|
+
while should_check_threads_state
|
37
|
+
should_check_threads_state = false
|
38
|
+
debugger.update_threads.each do |thread|
|
39
|
+
thread.switch
|
40
|
+
while thread.need_finish_frame
|
41
|
+
should_check_threads_state = true
|
42
|
+
thread.finish
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
debugger.wait_line_event
|
48
|
+
debugger.load_debugger
|
49
|
+
debugger.exit
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_child_pids(pid)
|
54
|
+
return [] unless command_exists 'pgrep'
|
55
|
+
|
56
|
+
pids = Array.new
|
57
|
+
|
58
|
+
q = Queue.new
|
59
|
+
q.push(pid)
|
60
|
+
|
61
|
+
until q.empty? do
|
62
|
+
pid = q.pop
|
63
|
+
|
64
|
+
pipe = IO.popen("pgrep -P #{pid}")
|
65
|
+
|
66
|
+
pipe.readlines.each do |child|
|
67
|
+
child_pid = child.strip.to_i
|
68
|
+
q.push(child_pid)
|
69
|
+
pids << child_pid
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
filter_ruby_processes(pids)
|
74
|
+
end
|
75
|
+
|
76
|
+
def filter_ruby_processes(pids)
|
77
|
+
pipe = IO.popen(%Q(lsof -c ruby | awk '{print $2 ":" $9}' | grep -E 'bin/ruby([[:digit:]]+\.?)*$'))
|
78
|
+
|
79
|
+
ruby_processes = Set.new
|
80
|
+
|
81
|
+
pipe.readlines.each do |process|
|
82
|
+
pid = process.split(/:/).first
|
83
|
+
ruby_processes.add(pid.to_i)
|
84
|
+
end
|
85
|
+
|
86
|
+
ruby_processes_pids, non_ruby_processes_pids = pids.partition {|pid| ruby_processes.include? pid}
|
87
|
+
|
88
|
+
DebugPrinter.print_debug("The following child processes was added to attach: #{ruby_processes_pids.join(', ')}") unless ruby_processes_pids.empty?
|
89
|
+
DebugPrinter.print_debug("The following child are not ruby processes: #{non_ruby_processes_pids.join(', ')}") unless non_ruby_processes_pids.empty?
|
90
|
+
|
91
|
+
ruby_processes_pids
|
92
|
+
end
|
93
|
+
|
94
|
+
def command_exists(command)
|
95
|
+
checking_command = "checking command #{command} for existence\n"
|
96
|
+
`command -v #{command} >/dev/null 2>&1 || { exit 1; }`
|
97
|
+
if $?.exitstatus != 0
|
98
|
+
DebugPrinter.print_debug("#{checking_command}command does not exist.")
|
99
|
+
else
|
100
|
+
DebugPrinter.print_debug("#{checking_command}command does exist.")
|
101
|
+
end
|
102
|
+
$?.exitstatus == 0
|
103
|
+
end
|
104
|
+
|
105
|
+
def choose_debugger(ruby_path, pid, gems_to_include, debugger_loader_path, argv)
|
106
|
+
if command_exists(LLDB.to_s)
|
107
|
+
debugger = LLDB.new(ruby_path, pid, '--no-lldbinit', gems_to_include, debugger_loader_path, argv)
|
108
|
+
elsif command_exists(GDB.to_s)
|
109
|
+
debugger = GDB.new(ruby_path, pid, '-nh -nx', gems_to_include, debugger_loader_path, argv)
|
110
|
+
else
|
111
|
+
raise 'Neither gdb nor lldb was found. Aborting.'
|
112
|
+
end
|
113
|
+
|
114
|
+
debugger
|
115
|
+
end
|