Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 3 additions & 337 deletions bin/gdb_wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -74,343 +74,9 @@ end
require 'ruby-debug-ide/greeter'
Debugger::print_greeting_msg(nil, nil)

class NativeDebugger

attr_reader :pid, :main_thread, :process_threads, :pipe

# @param executable -- path to ruby interpreter
# @param pid -- pid of process you want to debug
# @param flags -- flags you want to specify to your debugger as a string (e.g. "-nx -nh" for gdb to disable .gdbinit)
def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
@pid = pid
@delimiter = '__OUTPUT_FINISHED__' # for getting response
@tbreak = '__func_to_set_breakpoint_at'
@main_thread = nil
@process_threads = nil
debase_path = gems_to_include.select {|gem_path| gem_path =~ /debase/}
if debase_path.size == 0
raise 'No debase gem found.'
end
@path_to_attach = find_attach_lib(debase_path[0])

@gems_to_include = '["' + gems_to_include * '", "' + '"]'
@debugger_loader_path = debugger_loader_path
@argv = argv

@eval_string = "rb_eval_string_protect(\"require '#{@debugger_loader_path}'; load_debugger(#{@gems_to_include.gsub("\"", "'")}, #{@argv.gsub("\"", "'")})\", (int *)0)"

launch_string = "#{self} #{executable} #{flags}"
@pipe = IO.popen(launch_string, 'r+')
$stdout.puts "executed '#{launch_string}'"
end

def find_attach_lib(debase_path)
attach_lib = debase_path + '/attach'
known_extensions = %w(.so .bundle .dll)
known_extensions.each do |ext|
if File.file?(attach_lib + ext)
return attach_lib + ext
end
end

raise 'Could not find attach library'
end

def attach_to_process
execute "attach #{@pid}"
end

def execute(command)
@pipe.puts command
$stdout.puts "executed `#{command}` command inside #{self}."
if command == 'q'
return ''
end
get_response
end

def get_response
# we need this hack to understand that debugger gave us all output from last executed command
print_delimiter

content = ''
loop do
line = @pipe.readline
break if check_delimiter(line)
DebugPrinter.print_debug('respond line: ' + line)
next if line =~ /\(lldb\)/ # lldb repeats your input to its output
content += line
end

content
end

def update_threads

end

def check_already_under_debug

end

def print_delimiter

end

def check_delimiter(line)

end

def switch_to_thread

end

def set_tbreak(str)
execute "tbreak #{str}"
end

def continue
$stdout.puts 'continuing'
@pipe.puts 'c'
loop do
line = @pipe.readline
break if line =~ /#{Regexp.escape(@tbreak)}/
end
get_response
end

def call_start_attach
raise 'No main thread found. Did you forget to call `update_threads`?' if @main_thread == nil
@main_thread.switch
end

def wait_line_event
call_start_attach
continue
end

def load_debugger

end

def exit
execute 'q'
@pipe.close
end

def to_s
'native_debugger'
end

end

class LLDB < NativeDebugger

def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
super(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
end

def set_flags

end

def update_threads
@process_threads = []
info_threads = (execute 'thread list').split("\n")
info_threads.each do |thread_info|
next unless thread_info =~ /[\s*]*thread\s#\d+.*/
is_main = thread_info[0] == '*'
thread_num = thread_info.sub(/[\s*]*thread\s#/, '').sub(/:\s.*$/, '').to_i
thread = ProcessThread.new(thread_num, is_main, thread_info, self)
if thread.is_main
@main_thread = thread
end
@process_threads << thread
end
@process_threads
end

def check_already_under_debug
threads = execute 'thread list'
threads =~ /ruby-debug-ide/
end

def switch_to_thread(thread_num)
execute "thread select #{thread_num}"
end

def call_start_attach
super()
execute "expr (void *) dlopen(\"#{@path_to_attach}\", 2)"
execute 'expr (int) start_attach()'
set_tbreak(@tbreak)
end

def print_delimiter
@pipe.puts "script print \"#{@delimiter}\""
end

def check_delimiter(line)
line =~ /#{@delimiter}$/
end

def load_debugger
execute "expr (VALUE) #{@eval_string}"
end

def to_s
'lldb'
end

end

class GDB < NativeDebugger

def initialize(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
super(executable, pid, flags, gems_to_include, debugger_loader_path, argv)
end

def set_flags
execute 'set scheduler-locking off' # we will deadlock with it
execute 'set unwindonsignal on' # in case of some signal we will exit gdb
end

def update_threads
@process_threads = []
info_threads = (execute 'info threads').split("\n")
info_threads.each do |thread_info|
next unless thread_info =~ /[\s*]*\d+\s+Thread.*/
$stdout.puts "thread_info: #{thread_info}"
is_main = thread_info[0] == '*'
thread_num = thread_info.sub(/[\s*]*/, '').sub(/\s.*$/, '').to_i
thread = ProcessThread.new(thread_num, is_main, thread_info, self)
if thread.is_main
@main_thread = thread
end
@process_threads << thread
end
@process_threads
end

def check_already_under_debug
threads = execute 'info threads'
threads =~ /ruby-debug-ide/
end

def switch_to_thread(thread_num)
execute "thread #{thread_num}"
end

def call_start_attach
super()
execute "call dlopen(\"#{@path_to_attach}\", 2)"
execute 'call start_attach()'
set_tbreak(@tbreak)
end

def print_delimiter
@pipe.puts "print \"#{@delimiter}\""
end

def check_delimiter(line)
line =~ /\$\d+\s=\s"#{@delimiter}"/
end

def load_debugger
execute "call #{@eval_string}"
end

def to_s
'gdb'
end

end

class ProcessThread

attr_reader :thread_num, :is_main, :thread_info, :last_bt

def initialize(thread_num, is_main, thread_info, native_debugger)
@thread_num = thread_num
@is_main = is_main
@native_debugger = native_debugger
@thread_info = thread_info
@last_bt = nil
end

def switch
@native_debugger.switch_to_thread(thread_num)
end

def finish
@native_debugger.execute 'finish'
end

def get_bt
@last_bt = @native_debugger.execute 'bt'
end

def any_caller_match(bt, pattern)
bt =~ /#{pattern}/
end

def is_inside_malloc(bt = get_bt)
if any_caller_match(bt, '(malloc\.c)')
$stderr.puts "process #{@native_debugger.pid} is currently inside malloc."
true
else
false
end
end

def is_inside_gc(bt = get_bt)
if any_caller_match(bt, '(gc\.c)')
$stderr.puts "process #{@native_debugger.pid} is currently in garbage collection phase."
true
else
false
end
end

def need_finish_frame
bt = get_bt
is_inside_malloc(bt) || is_inside_gc(bt)
end

end

def command_exists(command)
checking_command = "checking command #{command} for existence\n"
`command -v #{command} >/dev/null 2>&1 || { exit 1; }`
if $?.exitstatus != 0
DebugPrinter.print_debug("#{checking_command}command does not exist.")
else
DebugPrinter.print_debug("#{checking_command}command does exist.")
end
$?.exitstatus == 0
end

def choose_debugger(ruby_path, pid, gems_to_include, debugger_loader_path, argv)
if command_exists('lldb')
debugger = LLDB.new(ruby_path, pid, '--no-lldbinit', gems_to_include, debugger_loader_path, argv)
elsif command_exists('gdb')
debugger = GDB.new(ruby_path, pid, '-nh -nx', gems_to_include, debugger_loader_path, argv)
else
raise 'Neither gdb nor lldb was found. Aborting.'
end

trap('INT') do
unless debugger.pipe.closed?
$stderr.puts "backtraces for threads:\n\n"
debugger.process_threads.each do |thread|
$stderr.puts "#{thread.thread_info}\n#{thread.last_bt}\n\n"
end
end
exit!
end

debugger
end
require 'ruby-debug-ide/attach/util'
require 'ruby-debug-ide/attach/native_debugger'
require 'ruby-debug-ide/attach/process_thread'

debugger = choose_debugger(options.ruby_path, options.pid, options.gems_to_include, debugger_loader_path, argv)
debugger.attach_to_process
Expand Down
Loading