Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
253 changes: 209 additions & 44 deletions bin/gdb_wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
require 'optparse'
require 'ostruct'

$stdout.sync = true
$stderr.sync = true

options = OpenStruct.new(
'pid' => nil,
'sdk_path' => nil,
'uid' => nil,
'gems_to_include' => []
'pid' => nil,
'sdk_path' => nil,
'uid' => nil,
'gems_to_include' => []
)

opts = OptionParser.new do |opts|
Expand All @@ -16,82 +19,244 @@ opts = OptionParser.new do |opts|
Some useful banner.
EOB

opts.on("--pid PID", "pid of process you want to attach to for debugging") do |pid|
opts.on('--pid PID', 'pid of process you want to attach to for debugging') do |pid|
options.pid = pid
end

opts.on("--ruby-path SDK_PATH", "path to ruby interpreter") do |ruby_path|
opts.on('--ruby-path RUBY_PATH', 'path to ruby interpreter') do |ruby_path|
options.ruby_path = ruby_path
end

opts.on("--uid UID", "uid which this process should set after executing gdb attach") do |uid|
opts.on('--uid UID', 'uid which this process should set after executing gdb attach') do |uid|
options.uid = uid
end

opts.on("--include-gem GEM_LIB_PATH", "lib of gem to include") do |gem_lib_path|
opts.on('--include-gem GEM_LIB_PATH', 'lib of gem to include') do |gem_lib_path|
options.gems_to_include << gem_lib_path
end
end

opts.parse! ARGV

unless options.pid
$stderr.puts "You must specify PID of process you want to attach to"
$stderr.puts 'You should specify PID of process you want to attach to'
exit 1
end

unless options.ruby_path
$stderr.puts "You must specify RUBY_PATH of ruby interpreter"
$stderr.puts 'You should specify path to the ruby interpreter'
exit 1
end

# TODO Denis told not to implement this hack
# So this is only for me while debugging as
# I don't want to get any warnings.
sigints_caught = 0
trap('INT') do
sigints_caught += 1
if sigints_caught == 2
exit 0
end
end

argv = '["' + ARGV * '", "' + '"]'
gems_to_include = '["' + options.gems_to_include * '", "' + '"]'

commands_list = []
path_to_debugger_loader = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader'

def commands_list.<<(command)
self.push "-ex \"#{command}\""
options.gems_to_include.each do |gem_path|
$LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path)
end

path_to_debugger_loader = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader'
require 'ruby-debug-ide/greeter'
Debugger::print_greeting_msg(nil, nil)

# rb_finish: wait while execution comes to the next line.
# This is essential because we could interrupt process in a middle
# of some evaluations (e.g., system call)
commands_list << "call rb_eval_string_protect(\\\"set_trace_func lambda{|event, file, line, id, binding, classname| if /line/ =~ event; sleep 0; set_trace_func(nil); end}\\\", (int *)0)"
commands_list << "tbreak rb_f_sleep"
commands_list << "cont"
$pid = options.pid
$last_bt = ''
$gdb_tmp_file = '/tmp/gdb_out.txt'
Copy link
Copy Markdown
Member

@denofevil denofevil Sep 1, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use tempfile.rb instead. It's cross-platform and generates unique names, so you won't need line 69


# evalr: loading debugger into the process
evalr = "call rb_eval_string_protect(%s, (int *)0)"
commands_list << ("#{evalr}" % ["(\\\"require '#{path_to_debugger_loader}'; load_debugger(#{gems_to_include.gsub("\"", "'")}, #{argv.gsub("\"", "'")})\\\")"])
begin
file = File.open($gdb_tmp_file, 'w')
file.truncate(0)
file.close
rescue Exception => e
$stderr.puts e
$stderr.puts "Could not create file #{$gdb_tmp_file} for gdb logging. Aborting."
exit!
end

# q: exit gdb and continue process execution with debugger
commands_list << "q"
gdb_executed_all_commands = false

cmd = "gdb #{options.ruby_path} #{options.pid} -nh -nx -batch #{commands_list.join(" ")}"
IO.popen("gdb #{options.ruby_path} #{options.pid} -nh -nx", 'r+') do |gdb|

options.gems_to_include.each do |gem_path|
$LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path)
end
$gdb = gdb
$main_thread = nil

require 'ruby-debug-ide/greeter'
Debugger::print_greeting_msg(nil, nil)
$stderr.puts "Running command #{cmd}"
class ProcessThread

attr_reader :thread_num, :is_main

def initialize(thread_num, is_main)
@thread_num = thread_num
@is_main = is_main
end

def switch
$gdb.execute "thread #{thread_num}"
end

def finish
$gdb.finish
end

def get_bt
return $gdb.execute 'bt'
end

def top_caller_match(bt, pattern)
return bt.split('#')[1] =~ /#{pattern}/
end

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

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

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

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

end

def gdb.update_threads
process_threads = []
info_threads = (self.execute 'info threads').split("\n")
# first line of gdb's response is ` Id Target Id Frame` info line
# last line of gdb's response is `(gdb) `
info_threads.shift
info_threads.pop
# each thread info looks like this:
# 3 Thread 0x7ff535405700 (LWP 8291) "ruby-timer-thr" 0x00007ff534a15fdd in poll () at ../sysdeps/unix/syscall-template.S:81
info_threads.each do |thread_info|
next unless thread_info =~ /[\s*]*\d+\s+Thread.*/
$stderr.puts "thread_info: #{thread_info}"
is_main = thread_info[0] == '*'
thread_info.sub!(/[\s*]*/, '')
thread_info.sub!(/\s.*$/, '')
thread = ProcessThread.new(thread_info.to_i, is_main)
if thread.is_main
$main_thread = thread
end
process_threads << thread
end
process_threads
end

def gdb.get_response
content = ''
loop do
sleep 0.01 # give time to gdb to finish command execution and print it to file
file = File.open($gdb_tmp_file, 'r')
content = file.read
file.close
break if content =~ /\(gdb\)\s\z/
end
content
end

def gdb.enable_logging
self.puts 'set logging on'
end

def gdb.disable_logging
self.puts 'set logging off'
end

def gdb.overwrite_file
disable_logging
enable_logging
end

def gdb.execute(command)
self.overwrite_file
self.puts command
$stdout.puts "executed command '#{command}' inside gdb."
if command == 'q'
return ''
end
response = self.get_response
if command == 'bt'
$last_bt = response
end
return response
end

def gdb.finish
$stdout.puts 'trying to finish current frame.'
self.execute 'finish'
end

def gdb.set_logging
self.puts "set logging file #{$gdb_tmp_file}"
self.puts 'set logging overwrite on'
self.puts 'set logging redirect on'
self.enable_logging

$stdout.puts "all gdb output redirected to #{$gdb_tmp_file}."
end

def gdb.check_already_under_debug
threads = self.execute 'info threads'
return threads =~ /ruby-debug-ide/
end

`#{cmd}` or raise "GDB failed. Aborting."
gdb.set_logging

if gdb.check_already_under_debug
$stderr.puts "Process #{$pid} is already under debug"
gdb.execute 'q'
end

gdb.execute 'set scheduler-locking off'
gdb.execute 'set unwindonsignal on'

should_check_threads_state = true

while should_check_threads_state
should_check_threads_state = false
gdb.update_threads.each do |thread|
thread.switch
while thread.need_finish_frame
should_check_threads_state = true
thread.finish
end
end
end

$main_thread.switch

gdb.execute "call dlopen(\"/home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\", 2)"
gdb.execute "call start_attach(\"require '#{path_to_debugger_loader}'; load_debugger(#{gems_to_include.gsub("\"", "'")}, #{argv.gsub("\"", "'")})\")"

gdb_executed_all_commands = true
gdb.execute 'q'

end

trap('INT') do
unless gdb_executed_all_commands
$stderr.puts "Seems like could not attach to process. Its backtrace:\n#{$last_bt}"
$stderr.flush
end
exit 1
end

if options.uid
Process::Sys.setuid(options.uid.to_i)
Expand Down
26 changes: 17 additions & 9 deletions bin/rdebug-ide
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ options = OpenStruct.new(
'rm_protocol_extensions' => false,
'catchpoint_deleted_event' => false,
'value_as_nested_element' => false,
'attach_mode' => false
'attach_mode' => false,
'cli_debug' => false
)

opts = OptionParser.new do |opts|
Expand All @@ -48,6 +49,7 @@ EOB
opts.on("-l", "--load-mode", "load mode (experimental)") {options.load_mode = true}
opts.on("-d", "--debug", "Debug self - prints information for debugging ruby-debug itself") do
Debugger.cli_debug = true
options.cli_debug = true
end
opts.on("--xml-debug", "Debug self - sends information <message>s for debugging ruby-debug itself") do
Debugger.xml_debug = true
Expand Down Expand Up @@ -109,16 +111,20 @@ end
if options.dispatcher_port != -1
ENV['IDE_PROCESS_DISPATCHER'] = options.dispatcher_port.to_s
if RUBY_VERSION < "1.9"
$: << File.expand_path(File.dirname(__FILE__) + "/../lib/")
lib_path = File.expand_path(File.dirname(__FILE__) + "/../lib/")
$: << lib_path unless $:.include? lib_path
require 'ruby-debug-ide/multiprocess'
else
require_relative '../lib/ruby-debug-ide/multiprocess'
end

ENV['DEBUGGER_STORED_RUBYLIB'] = ENV['RUBYLIB']
old_opts = ENV['RUBYOPT']
ENV['RUBYOPT'] = "-r#{File.expand_path(File.dirname(__FILE__))}/../lib/ruby-debug-ide/multiprocess/starter"
ENV['RUBYOPT'] += " #{old_opts}" if old_opts
old_opts = ENV['RUBYOPT'] || ''
starter = "-r#{File.expand_path(File.dirname(__FILE__))}/../lib/ruby-debug-ide/multiprocess/starter"
unless old_opts.include? starter
ENV['RUBYOPT'] = starter
ENV['RUBYOPT'] += " #{old_opts}" if old_opts != ''
end
ENV['DEBUGGER_CLI_DEBUG'] = Debugger.cli_debug.to_s
end

Expand All @@ -135,13 +141,15 @@ Debugger.catchpoint_deleted_event = options.catchpoint_deleted_event || options.
Debugger.value_as_nested_element = options.value_as_nested_element || options.rm_protocol_extensions

if options.attach_mode
if Debugger::FRONT_END == "debase"
Debugger.init_variables
end

Debugger::MultiProcess::pre_child(options)

# This will trigger `setup_tracepoints` and `prepare_context` (which is private in debase)
# without any actual excessive code execution.
if Debugger::FRONT_END == "debase"
EMPTY_TEMPLATE = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/empty_file.rb'
Debugger.debug_load(EMPTY_TEMPLATE)
Debugger.setup_tracepoints
Debugger.prepare_context
end
else
Debugger.debug_program(options)
Expand Down
10 changes: 10 additions & 0 deletions ext/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
all: libAttach.so

libAttach.so: libAttach.o
gcc -shared -o libAttach.so libAttach.o

libAttach.o: do_attach.c
gcc -Wall -g -fPIC -c -I/home/equi/.rvm/rubies/ruby-2.3.1/include/ruby-2.3.0 -I/home/equi/.rvm/rubies/ruby-2.3.1/include/ruby-2.3.0/x86_64-linux/ do_attach.c -o libAttach.o

clean:
rm libAttach.*
Loading