From 3681a0592cb8894f8c89b99c7a11bc4ca0729374 Mon Sep 17 00:00:00 2001 From: "dmitrii.kravchenko" Date: Mon, 8 Aug 2016 16:30:48 +0300 Subject: [PATCH 01/14] attach mode added --- bin/rdebug-ide | 28 ++++++++++++++------ lib/ruby-debug-ide.rb | 13 ++++----- lib/ruby-debug-ide/commands/control.rb | 27 +++++++++++++++++++ lib/ruby-debug-ide/ide_processor.rb | 5 ++-- lib/ruby-debug-ide/multiprocess/pre_child.rb | 8 +++--- 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/bin/rdebug-ide b/bin/rdebug-ide index 574dc44..d73b94d 100755 --- a/bin/rdebug-ide +++ b/bin/rdebug-ide @@ -22,7 +22,8 @@ options = OpenStruct.new( 'evaluation_timeout' => 10, 'rm_protocol_extensions' => false, 'catchpoint_deleted_event' => false, - 'value_as_nested_element' => false + 'value_as_nested_element' => false, + 'attach_mode' => false ) opts = OptionParser.new do |opts| @@ -54,7 +55,9 @@ EOB opts.on("-I", "--include PATH", String, "Add PATH to $LOAD_PATH") do |path| $LOAD_PATH.unshift(path) end - + opts.on("--attach-mode", "Tells that rdebug-ide is working in attach mode") do + options.attach_mode = true + end opts.on("--keep-frame-binding", "Keep frame bindings") {options.frame_bind = true} opts.on("--disable-int-handler", "Disables interrupt signal handler") {options.int_handler = false} opts.on("--rubymine-protocol-extensions", "Enable all RubyMine-specific incompatible protocol extensions") do @@ -89,15 +92,17 @@ rescue StandardError => e exit(1) end -if ARGV.empty? +if ARGV.empty? && !options.attach_mode puts opts puts puts "Must specify a script to run" exit(1) -end +end -# save script name -Debugger::PROG_SCRIPT = ARGV.shift +unless options.attach_mode + # save script name + Debugger::PROG_SCRIPT = ARGV.shift +end if options.dispatcher_port != -1 ENV['IDE_PROCESS_DISPATCHER'] = options.dispatcher_port.to_s @@ -119,7 +124,7 @@ if options.int_handler # install interruption handler trap('INT') { Debugger.interrupt_last } end - + # set options Debugger.keep_frame_binding = options.frame_bind Debugger.tracing = options.tracing @@ -127,5 +132,12 @@ Debugger.evaluation_timeout = options.evaluation_timeout Debugger.catchpoint_deleted_event = options.catchpoint_deleted_event || options.rm_protocol_extensions Debugger.value_as_nested_element = options.value_as_nested_element || options.rm_protocol_extensions -Debugger.debug_program(options) +if options.attach_mode + Debugger::MultiProcess::pre_child(options) + if Debugger::FRONT_END == "debase" + Debugger.enable_trace_points + end +else + Debugger.debug_program(options) +end diff --git a/lib/ruby-debug-ide.rb b/lib/ruby-debug-ide.rb index 9704f94..4e0552b 100644 --- a/lib/ruby-debug-ide.rb +++ b/lib/ruby-debug-ide.rb @@ -4,14 +4,16 @@ require 'thread' if RUBY_VERSION < '2.0' || defined?(JRUBY_VERSION) require 'ruby-debug-base' + Debugger::FRONT_END = "ruby-debug-base" else require 'debase' + Debugger::FRONT_END = "debase" end -require 'ruby-debug-ide/version' -require 'ruby-debug-ide/xml_printer' -require 'ruby-debug-ide/ide_processor' -require 'ruby-debug-ide/event_processor' +require_relative 'ruby-debug-ide/version' +require_relative 'ruby-debug-ide/xml_printer' +require_relative 'ruby-debug-ide/ide_processor' +require_relative 'ruby-debug-ide/event_processor' module Debugger @@ -110,7 +112,6 @@ def start_control(host, port, notify_dispatcher) server = TCPServer.new(host, port) print_greeting_msg(host, port) notify_dispatcher(port) if notify_dispatcher - while (session = server.accept) $stderr.puts "Connected from #{session.peeraddr[2]}" if Debugger.cli_debug dispatcher = ENV['IDE_PROCESS_DISPATCHER'] @@ -160,8 +161,8 @@ def notify_dispatcher(port) return unless ENV['IDE_PROCESS_DISPATCHER'] acceptor_host, acceptor_port = ENV['IDE_PROCESS_DISPATCHER'].split(":") acceptor_host, acceptor_port = '127.0.0.1', acceptor_host unless acceptor_port - connected = false + 3.times do |i| begin s = TCPSocket.open(acceptor_host, acceptor_port) diff --git a/lib/ruby-debug-ide/commands/control.rb b/lib/ruby-debug-ide/commands/control.rb index c8ff56f..0fffb4d8 100644 --- a/lib/ruby-debug-ide/commands/control.rb +++ b/lib/ruby-debug-ide/commands/control.rb @@ -126,4 +126,31 @@ def help(cmd) end end end + + + class DetachCommand < Command # :nodoc: + self.control = true + + def regexp + /^\s*detach\s*$/ + end + + def execute + Debugger.stop + Debugger.control_thread = nil + Thread.current.exit #@control_thread is a current thread + end + + class << self + def help_command + 'detach' + end + + def help(cmd) + %{ + detach\ndetach debugger\nnote: this option is only for remote debugging (or local attach) + } + end + end + end end diff --git a/lib/ruby-debug-ide/ide_processor.rb b/lib/ruby-debug-ide/ide_processor.rb index e5b7e98..ac07a0f 100644 --- a/lib/ruby-debug-ide/ide_processor.rb +++ b/lib/ruby-debug-ide/ide_processor.rb @@ -1,5 +1,5 @@ -require 'ruby-debug-ide/interface' -require 'ruby-debug-ide/command' +require_relative 'interface' +require_relative 'command' module Debugger class IdeCommandProcessor @@ -77,7 +77,6 @@ def process_commands ctrl_cmd_classes = Command.commands.select{|cmd| cmd.control} state = ControlState.new(@interface) ctrl_cmds = ctrl_cmd_classes.map{|cmd| cmd.new(state, @printer)} - while input = @interface.read_command # escape % since print_debug might use printf # sleep 0.3 diff --git a/lib/ruby-debug-ide/multiprocess/pre_child.rb b/lib/ruby-debug-ide/multiprocess/pre_child.rb index 1b9c2cc..eb96242 100644 --- a/lib/ruby-debug-ide/multiprocess/pre_child.rb +++ b/lib/ruby-debug-ide/multiprocess/pre_child.rb @@ -1,19 +1,17 @@ module Debugger module MultiProcess class << self - def pre_child - + def pre_child(options = nil) require 'socket' require 'ostruct' host = ENV['DEBUGGER_HOST'] - port = find_free_port(host) - options = OpenStruct.new( + options ||= OpenStruct.new( 'frame_bind' => false, 'host' => host, 'load_mode' => false, - 'port' => port, + 'port' => find_free_port(host), 'stop' => false, 'tracing' => false, 'int_handler' => true, From 72b1c4af766f3926c9373a257efcb58bb65d27da Mon Sep 17 00:00:00 2001 From: "dmitrii.kravchenko" Date: Mon, 8 Aug 2016 16:59:19 +0300 Subject: [PATCH 02/14] gdb_wrapper and debugger_loader added --- bin/gdb_wrapper | 93 ++++++++++++++++++++ lib/ruby-debug-ide/attach/debugger_loader.rb | 20 +++++ 2 files changed, 113 insertions(+) create mode 100755 bin/gdb_wrapper create mode 100644 lib/ruby-debug-ide/attach/debugger_loader.rb diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper new file mode 100755 index 0000000..c56597d --- /dev/null +++ b/bin/gdb_wrapper @@ -0,0 +1,93 @@ +#!/usr/bin/env ruby + +require 'optparse' +require 'ostruct' + +options = OpenStruct.new( + 'pid' => nil, + 'sdk_path' => nil, + 'uid' => nil, + 'gems_to_include' => [] +) + +opts = OptionParser.new do |opts| + # TODO need some banner + opts.banner = < Date: Tue, 9 Aug 2016 14:16:01 +0300 Subject: [PATCH 03/14] closing interface --- lib/ruby-debug-ide/commands/control.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ruby-debug-ide/commands/control.rb b/lib/ruby-debug-ide/commands/control.rb index 0fffb4d8..f518c02 100644 --- a/lib/ruby-debug-ide/commands/control.rb +++ b/lib/ruby-debug-ide/commands/control.rb @@ -137,6 +137,7 @@ def regexp def execute Debugger.stop + Debugger.interface.close Debugger.control_thread = nil Thread.current.exit #@control_thread is a current thread end From fa128e1b496cfdd74f0cff78f773f5a3228dc56e Mon Sep 17 00:00:00 2001 From: "dmitrii.kravchenko" Date: Mon, 15 Aug 2016 16:00:15 +0300 Subject: [PATCH 04/14] relative imports replaced with normal ones. `print_greeting_msg` now in separate file. `enable_trace_points` call (wrong) replaced with empty_file loading (ugly but should always work). Including gems into $LOAD_PATH. --- bin/gdb_wrapper | 39 +++++++++++-------- bin/rdebug-ide | 11 ++++-- lib/ruby-debug-ide.rb | 25 ++---------- lib/ruby-debug-ide/attach/debugger_loader.rb | 6 +-- lib/ruby-debug-ide/attach/empty_file.rb | 0 lib/ruby-debug-ide/greeter.rb | 40 ++++++++++++++++++++ lib/ruby-debug-ide/ide_processor.rb | 4 +- ruby-debug-ide.gemspec | 2 +- 8 files changed, 81 insertions(+), 46 deletions(-) create mode 100644 lib/ruby-debug-ide/attach/empty_file.rb create mode 100644 lib/ruby-debug-ide/greeter.rb diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index c56597d..ba56a55 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -13,15 +13,15 @@ options = OpenStruct.new( opts = OptionParser.new do |opts| # TODO need some banner opts.banner = < Date: Wed, 31 Aug 2016 19:14:11 +0300 Subject: [PATCH 05/14] bug fix: cli_debug should be part of options because pre_child sets Debugger.cli_debug = options.cli_debug; bug fix: check RUBYOPT and $: if they already contain our paths (relevant in case of attaching several time to some process); bug fix: calling `prepare_context` instead of `enable_tracepoints`; calling `init_variables` to reset all debase variables from previous attaching --- bin/rdebug-ide | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/bin/rdebug-ide b/bin/rdebug-ide index 6ba9047..26295f3 100755 --- a/bin/rdebug-ide +++ b/bin/rdebug-ide @@ -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| @@ -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 s for debugging ruby-debug itself") do Debugger.xml_debug = true @@ -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 @@ -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) From 0c2a3295c91306ca57acc7a39dfc2fefab06ffca Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Wed, 31 Aug 2016 21:45:46 +0300 Subject: [PATCH 06/14] added interaction with gdb, added c-level attaching --- bin/gdb_wrapper | 253 +++++++++++++++++++----- ext/Makefile | 10 + ext/do_attach.c | 37 ++++ ext/do_attach.h | 10 + lib/ruby-debug-ide/attach/empty_file.rb | 0 5 files changed, 266 insertions(+), 44 deletions(-) create mode 100644 ext/Makefile create mode 100644 ext/do_attach.c create mode 100644 ext/do_attach.h delete mode 100644 lib/ruby-debug-ide/attach/empty_file.rb diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index ba56a55..4562384 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -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| @@ -16,19 +19,19 @@ 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 @@ -36,62 +39,224 @@ 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' -# 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) diff --git a/ext/Makefile b/ext/Makefile new file mode 100644 index 0000000..9bec56c --- /dev/null +++ b/ext/Makefile @@ -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.* diff --git a/ext/do_attach.c b/ext/do_attach.c new file mode 100644 index 0000000..7291b48 --- /dev/null +++ b/ext/do_attach.c @@ -0,0 +1,37 @@ +#include "do_attach.h" + +static const char *_command_to_eval; + +static int +__check_gc(void) +{ + if (rb_during_gc()) { + fprintf(stderr, "Can not connect during garbage collection phase. Please, try again later.\n"); + return 1; + } + return 0; +} + +static void +__catch_line_event(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE klass) +{ + (void)sizeof(evflag); + (void)sizeof(self); + (void)sizeof(mid); + (void)sizeof(klass); + + rb_remove_event_hook(__catch_line_event); + if (__check_gc()) + return; + rb_eval_string_protect(_command_to_eval, NULL); // TODO pass something more useful than NULL +} + +void +start_attach(const char* command) +{ + _command_to_eval = command; + if (__check_gc()) + return; + rb_global_variable((VALUE *) _command_to_eval); + rb_add_event_hook(__catch_line_event, RUBY_EVENT_LINE, (VALUE) NULL); +} diff --git a/ext/do_attach.h b/ext/do_attach.h new file mode 100644 index 0000000..3697d3f --- /dev/null +++ b/ext/do_attach.h @@ -0,0 +1,10 @@ +#ifndef __DO_ATTACH_H__ +#define __DO_ATTACH_H__ + +#include +#include +#include + +void start_attach(const char *command); + +#endif //__DO_ATTACH_H__ \ No newline at end of file diff --git a/lib/ruby-debug-ide/attach/empty_file.rb b/lib/ruby-debug-ide/attach/empty_file.rb deleted file mode 100644 index e69de29..0000000 From 9c96a7689f8cc5249e3fad5cdd71470a550f914a Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Thu, 1 Sep 2016 14:20:56 +0300 Subject: [PATCH 07/14] added `attached` flag to indicate if we are currently under debug. This is needed for attach mode: if we detached `pre_child` should not be called in `fork` and `exec` --- bin/rdebug-ide | 2 ++ lib/ruby-debug-ide.rb | 1 + lib/ruby-debug-ide/commands/control.rb | 1 + lib/ruby-debug-ide/multiprocess/pre_child.rb | 2 ++ 4 files changed, 6 insertions(+) diff --git a/bin/rdebug-ide b/bin/rdebug-ide index 26295f3..dced3a7 100755 --- a/bin/rdebug-ide +++ b/bin/rdebug-ide @@ -140,6 +140,8 @@ Debugger.evaluation_timeout = options.evaluation_timeout Debugger.catchpoint_deleted_event = options.catchpoint_deleted_event || options.rm_protocol_extensions Debugger.value_as_nested_element = options.value_as_nested_element || options.rm_protocol_extensions +Debugger.attached = true + if options.attach_mode if Debugger::FRONT_END == "debase" Debugger.init_variables diff --git a/lib/ruby-debug-ide.rb b/lib/ruby-debug-ide.rb index db12c79..30fc079 100644 --- a/lib/ruby-debug-ide.rb +++ b/lib/ruby-debug-ide.rb @@ -43,6 +43,7 @@ def cleanup_backtrace(backtrace) cleared end + attr_accessor :attached attr_accessor :cli_debug, :xml_debug, :evaluation_timeout attr_accessor :control_thread attr_reader :interface diff --git a/lib/ruby-debug-ide/commands/control.rb b/lib/ruby-debug-ide/commands/control.rb index f518c02..bc09bda 100644 --- a/lib/ruby-debug-ide/commands/control.rb +++ b/lib/ruby-debug-ide/commands/control.rb @@ -136,6 +136,7 @@ def regexp end def execute + Debugger.attached = false Debugger.stop Debugger.interface.close Debugger.control_thread = nil diff --git a/lib/ruby-debug-ide/multiprocess/pre_child.rb b/lib/ruby-debug-ide/multiprocess/pre_child.rb index eb96242..7a2f4fc 100644 --- a/lib/ruby-debug-ide/multiprocess/pre_child.rb +++ b/lib/ruby-debug-ide/multiprocess/pre_child.rb @@ -2,6 +2,8 @@ module Debugger module MultiProcess class << self def pre_child(options = nil) + return unless Debugger.attached + require 'socket' require 'ostruct' From 32f61feb231bf3953027bf2a0bd79add4b41af73 Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Fri, 2 Sep 2016 21:19:26 +0300 Subject: [PATCH 08/14] lldb added --- bin/gdb_wrapper | 378 +++++++++++++++++++++++++++++------------------- ext/do_attach.c | 14 +- ext/do_attach.h | 2 +- 3 files changed, 244 insertions(+), 150 deletions(-) diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index 4562384..5482744 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -6,7 +6,7 @@ require 'ostruct' $stdout.sync = true $stderr.sync = true -options = OpenStruct.new( +@options = OpenStruct.new( 'pid' => nil, 'sdk_path' => nil, 'uid' => nil, @@ -20,246 +20,332 @@ Some useful banner. EOB opts.on('--pid PID', 'pid of process you want to attach to for debugging') do |pid| - options.pid = pid + @options.pid = pid end opts.on('--ruby-path RUBY_PATH', 'path to ruby interpreter') do |ruby_path| - options.ruby_path = ruby_path + @options.ruby_path = ruby_path end opts.on('--uid UID', 'uid which this process should set after executing gdb attach') do |uid| - options.uid = uid + @options.uid = uid end opts.on('--include-gem GEM_LIB_PATH', 'lib of gem to include') do |gem_lib_path| - options.gems_to_include << gem_lib_path + @options.gems_to_include << gem_lib_path end end opts.parse! ARGV -unless options.pid +unless @options.pid $stderr.puts 'You should specify PID of process you want to attach to' exit 1 end -unless options.ruby_path +unless @options.ruby_path $stderr.puts 'You should specify path to the ruby interpreter' exit 1 end -argv = '["' + ARGV * '", "' + '"]' -gems_to_include = '["' + options.gems_to_include * '", "' + '"]' +$argv = '["' + ARGV * '", "' + '"]' +$gems_to_include = '["' + @options.gems_to_include * '", "' + '"]' +$path_to_debugger_loader = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader' -path_to_debugger_loader = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader' - -options.gems_to_include.each do |gem_path| +@options.gems_to_include.each do |gem_path| $LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path) end require 'ruby-debug-ide/greeter' Debugger::print_greeting_msg(nil, nil) -$pid = options.pid -$last_bt = '' -$gdb_tmp_file = '/tmp/gdb_out.txt' - -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 - -gdb_executed_all_commands = false +# this class is some kind of "interface" +class NativeDebugger -IO.popen("gdb #{options.ruby_path} #{options.pid} -nh -nx", 'r+') do |gdb| + attr_reader :pid, :last_bt - $gdb = gdb - $main_thread = nil + # @param executable -- path to ruby interpreter + # @param pid -- pid of process you want to debug + # @param options -- flags you want to specify to your debugger as a string (e.g. "-nx -nh" for gdb to disable .gdbinit) + def initialize(executable, pid, options) + @pid = pid + @main_thread = nil + @delimiter = '__OUTPUT_FINISHED__' # for getting response + @last_bt = '' # to shaw it to user - class ProcessThread + launch_string = "#{self} #{executable} #{options}" + @pipe = IO.popen(launch_string, 'r+') + $stdout.puts "executed '#{launch_string}'" + end - attr_reader :thread_num, :is_main + def attach_to_process + execute "attach #{@pid}" + end - def initialize(thread_num, is_main) - @thread_num = thread_num - @is_main = is_main + def execute(command) + @pipe.puts command + $stdout.puts "executed `#{command}` command inside #{self}." + if command == 'q' + @pipe.close + return '' end - - def switch - $gdb.execute "thread #{thread_num}" + response = get_response + if command == 'bt' + @last_bt = response end + $stdout.puts "response for #{command}:\n#{response}\n\n\n\n" + response + end - def finish - $gdb.finish - end + def get_response + # we need this hack to understand that debugger gave us all output from last executed command + @pipe.puts "print \"#{@delimiter}\"" - def get_bt - return $gdb.execute 'bt' + content = '' + loop do + line = @pipe.readline + $stderr.puts line + next if line =~ /\(lldb\)/ # lldb repeats your input to its output + break if line =~ /\$\d+\s=\s"__OUTPUT_FINISHED__"/ + content += line end + content + end - def top_caller_match(bt, pattern) - return bt.split('#')[1] =~ /#{pattern}/ - end + def update_threads - def any_caller_match(bt, pattern) - return bt =~ /#{pattern}/ - end + 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 + def check_already_under_debug + + end + + def switch_to_thread + + end + + def set_tbreak(str) + execute "tbreak #{str}" + end + + def continue + @pipe.puts 'c' + loop do + line = @pipe.readline + break if line =~ /__func_to_set_breakpoint_at/ 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 exit + execute 'q' + end + + def to_s + 'native_debugger' + 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 + +class LLDB < NativeDebugger + + def initialize(executable, pid, options) + super(executable, pid, options) + 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+.*/ + $stdout.puts "thread_info: #{thread_info}" + is_main = thread_info[0] == '*' + thread_num = thread_info.sub(/[\s*]*thread\s#/, '').sub(/:\s.*$/, '').to_i + thread = ProcessThread.new(thread_num, is_main, self) + if thread.is_main + @main_thread = thread end + process_threads << thread end + process_threads + end - def need_finish_frame - bt = get_bt - return is_inside_malloc(bt) || is_inside_gc(bt) - 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(\"/home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\", 2)" + execute "call start_attach(\"require '#{$path_to_debugger_loader}'; load_debugger(#{$gems_to_include.gsub("\"", "'")}, #{$argv.gsub("\"", "'")})\")" + end + + def to_s + 'lldb-3.8' + end +end + +class GDB < NativeDebugger + + def initialize(executable, pid, options) + super(executable, pid, options) end - def gdb.update_threads + 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 = (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 + info_threads = (execute 'info threads').split("\n") # 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}" + $stdout.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) + thread_num = thread_info.sub(/[\s*]*/, '').sub(/\s.*$/, '').to_i + thread = ProcessThread.new(thread_num, is_main, self) if thread.is_main - $main_thread = thread + @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 + def check_already_under_debug + threads = execute 'info threads' + threads =~ /ruby-debug-ide/ end - def gdb.enable_logging - self.puts 'set logging on' + def switch_to_thread(thread_num) + execute "thread #{thread_num}" end - def gdb.disable_logging - self.puts 'set logging off' + def call_start_attach + super() + execute "call dlopen(\"/home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\", 2)" + execute "call start_attach(\"require '#{$path_to_debugger_loader}'; load_debugger(#{$gems_to_include.gsub("\"", "'")}, #{$argv.gsub("\"", "'")})\")" end - def gdb.overwrite_file - disable_logging - enable_logging + def to_s + 'gdb' 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 + +class ProcessThread + + attr_reader :thread_num, :is_main + + def initialize(thread_num, is_main, native_debugger) + @thread_num = thread_num + @is_main = is_main + @native_debugger = native_debugger end - def gdb.finish - $stdout.puts 'trying to finish current frame.' - self.execute 'finish' + def switch + @native_debugger.switch_to_thread(thread_num) 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 + def finish + @native_debugger.execute 'finish' + end - $stdout.puts "all gdb output redirected to #{$gdb_tmp_file}." + def get_bt + @native_debugger.execute 'bt' end - def gdb.check_already_under_debug - threads = self.execute 'info threads' - return threads =~ /ruby-debug-ide/ + def any_caller_match(bt, pattern) + bt =~ /#{pattern}/ end - gdb.set_logging + 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 - if gdb.check_already_under_debug - $stderr.puts "Process #{$pid} is already under debug" - gdb.execute 'q' + 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 - gdb.execute 'set scheduler-locking off' - gdb.execute 'set unwindonsignal on' + def need_finish_frame + bt = get_bt + is_inside_malloc(bt) || is_inside_gc(bt) + end - should_check_threads_state = true +end - 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 +def choose_debugger + if true + GDB.new(@options.ruby_path, @options.pid, '-nh -nx') + else + LLDB.new(@options.ruby_path, @options.pid, '--no-lldbinit') end +end - $main_thread.switch +gdb = choose_debugger +gdb.attach_to_process +gdb.set_flags - 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("\"", "'")})\")" +if gdb.check_already_under_debug + $stderr.puts "Process #{gdb.pid} is already under debug" + gdb.exit + exit! +end - gdb_executed_all_commands = true - gdb.execute 'q' +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 +gdb.call_start_attach +gdb.set_tbreak('__func_to_set_breakpoint_at') +gdb.continue +gdb.execute "call rb_eval_string_protect(\"require '#{$path_to_debugger_loader}'; load_debugger(#{$gems_to_include.gsub("\"", "'")}, #{$argv.gsub("\"", "'")})\", (int *)0)" +gdb.exit + 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 + $stderr.puts "Last backtrace:\n#{gdb.last_bt}" + exit! end -if options.uid - Process::Sys.setuid(options.uid.to_i) +if @options.uid + Process::Sys.setuid(@options.uid.to_i) end sleep diff --git a/ext/do_attach.c b/ext/do_attach.c index 7291b48..c29c9f6 100644 --- a/ext/do_attach.c +++ b/ext/do_attach.c @@ -12,6 +12,11 @@ __check_gc(void) return 0; } +static void +__func_to_set_breakpoint_at() +{ +} + static void __catch_line_event(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE klass) { @@ -23,15 +28,18 @@ __catch_line_event(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE rb_remove_event_hook(__catch_line_event); if (__check_gc()) return; - rb_eval_string_protect(_command_to_eval, NULL); // TODO pass something more useful than NULL + __func_to_set_breakpoint_at(); +// rb_eval_string_protect(_command_to_eval, NULL); // TODO pass something more useful than NULL } -void +int start_attach(const char* command) { + rb_eval_string("puts 'bla bla bla'"); _command_to_eval = command; if (__check_gc()) - return; + return 1; rb_global_variable((VALUE *) _command_to_eval); rb_add_event_hook(__catch_line_event, RUBY_EVENT_LINE, (VALUE) NULL); + return 2323; } diff --git a/ext/do_attach.h b/ext/do_attach.h index 3697d3f..5886935 100644 --- a/ext/do_attach.h +++ b/ext/do_attach.h @@ -5,6 +5,6 @@ #include #include -void start_attach(const char *command); +int start_attach(const char *command); #endif //__DO_ATTACH_H__ \ No newline at end of file From 36d3686d31106024082d6855bfdfd3c2cdfbe663 Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Sun, 4 Sep 2016 22:07:20 +0300 Subject: [PATCH 09/14] some cosmetic changes + choosing with debugger to use --- bin/gdb_wrapper | 82 ++++++++++++++++++++++++++++++------------------- ext/do_attach.c | 10 ++---- ext/do_attach.h | 2 +- 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index 5482744..18c1c3e 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -59,19 +59,19 @@ end require 'ruby-debug-ide/greeter' Debugger::print_greeting_msg(nil, nil) -# this class is some kind of "interface" class NativeDebugger - attr_reader :pid, :last_bt + attr_reader :pid, :main_thread, :process_threads # @param executable -- path to ruby interpreter # @param pid -- pid of process you want to debug # @param options -- flags you want to specify to your debugger as a string (e.g. "-nx -nh" for gdb to disable .gdbinit) def initialize(executable, pid, options) @pid = pid - @main_thread = nil @delimiter = '__OUTPUT_FINISHED__' # for getting response - @last_bt = '' # to shaw it to user + @tbreak = '__func_to_set_breakpoint_at' + @main_thread = nil + @process_threads = nil launch_string = "#{self} #{executable} #{options}" @pipe = IO.popen(launch_string, 'r+') @@ -90,9 +90,6 @@ class NativeDebugger return '' end response = get_response - if command == 'bt' - @last_bt = response - end $stdout.puts "response for #{command}:\n#{response}\n\n\n\n" response end @@ -106,7 +103,7 @@ class NativeDebugger line = @pipe.readline $stderr.puts line next if line =~ /\(lldb\)/ # lldb repeats your input to its output - break if line =~ /\$\d+\s=\s"__OUTPUT_FINISHED__"/ + break if line =~ /\$\d+\s=\s"#{@delimiter}"/ content += line end content @@ -129,10 +126,12 @@ class NativeDebugger end def continue + $stdout.puts 'continuing' @pipe.puts 'c' loop do line = @pipe.readline - break if line =~ /__func_to_set_breakpoint_at/ + $stderr.puts line + break if line =~ /#{Regexp.escape(@tbreak)}/ end get_response end @@ -142,6 +141,15 @@ class NativeDebugger @main_thread.switch end + def wait_line_event + call_start_attach + continue + end + + def load_debugger + execute "call rb_eval_string_protect(\"require '#{$path_to_debugger_loader}'; load_debugger(#{$gems_to_include.gsub("\"", "'")}, #{$argv.gsub("\"", "'")})\", (int *)0)" + end + def exit execute 'q' end @@ -163,20 +171,20 @@ class LLDB < NativeDebugger end def update_threads - process_threads = [] + @process_threads = [] info_threads = (execute 'thread list').split("\n") info_threads.each do |thread_info| next unless thread_info =~ /[\s*]*thread\s#\d+.*/ $stdout.puts "thread_info: #{thread_info}" is_main = thread_info[0] == '*' thread_num = thread_info.sub(/[\s*]*thread\s#/, '').sub(/:\s.*$/, '').to_i - thread = ProcessThread.new(thread_num, is_main, self) + thread = ProcessThread.new(thread_num, is_main, thread_info, self) if thread.is_main @main_thread = thread end - process_threads << thread + @process_threads << thread end - process_threads + @process_threads end def check_already_under_debug @@ -191,11 +199,12 @@ class LLDB < NativeDebugger def call_start_attach super() execute "expr (void *) dlopen(\"/home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\", 2)" - execute "call start_attach(\"require '#{$path_to_debugger_loader}'; load_debugger(#{$gems_to_include.gsub("\"", "'")}, #{$argv.gsub("\"", "'")})\")" + execute 'call start_attach()' + set_tbreak(@tbreak) end def to_s - 'lldb-3.8' + 'lldb' end end @@ -212,22 +221,20 @@ class GDB < NativeDebugger end def update_threads - process_threads = [] + @process_threads = [] info_threads = (execute 'info threads').split("\n") - # 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.*/ $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, self) + thread = ProcessThread.new(thread_num, is_main, thread_info, self) if thread.is_main @main_thread = thread end - process_threads << thread + @process_threads << thread end - process_threads + @process_threads end def check_already_under_debug @@ -242,7 +249,8 @@ class GDB < NativeDebugger def call_start_attach super() execute "call dlopen(\"/home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\", 2)" - execute "call start_attach(\"require '#{$path_to_debugger_loader}'; load_debugger(#{$gems_to_include.gsub("\"", "'")}, #{$argv.gsub("\"", "'")})\")" + execute 'call start_attach()' + set_tbreak(@tbreak) end def to_s @@ -253,12 +261,14 @@ end class ProcessThread - attr_reader :thread_num, :is_main + attr_reader :thread_num, :is_main, :thread_info, :last_bt - def initialize(thread_num, is_main, native_debugger) + 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 @@ -270,7 +280,7 @@ class ProcessThread end def get_bt - @native_debugger.execute 'bt' + @last_bt = @native_debugger.execute 'bt' end def any_caller_match(bt, pattern) @@ -302,11 +312,19 @@ class ProcessThread end +def exists_command(command) + `command -v #{command} >/dev/null 2>&1 || { exit 1; }` + $?.exitstatus == 0 +end + def choose_debugger - if true + $stderr.puts "exists: #{exists_command('gdb')}, #{exists_command('lldb')}" + if exists_command('gdb') GDB.new(@options.ruby_path, @options.pid, '-nh -nx') - else + elsif exists_command('lldb') LLDB.new(@options.ruby_path, @options.pid, '--no-lldbinit') + else + raise 'Neither gdb nor lldb was found. Aborting.' end end @@ -333,14 +351,14 @@ while should_check_threads_state end end -gdb.call_start_attach -gdb.set_tbreak('__func_to_set_breakpoint_at') -gdb.continue -gdb.execute "call rb_eval_string_protect(\"require '#{$path_to_debugger_loader}'; load_debugger(#{$gems_to_include.gsub("\"", "'")}, #{$argv.gsub("\"", "'")})\", (int *)0)" +gdb.wait_line_event +gdb.load_debugger gdb.exit trap('INT') do - $stderr.puts "Last backtrace:\n#{gdb.last_bt}" + gdb.process_threads.each do |thread| + $stderr.puts "Last backtrace for thread #{thread.thread_info}:\n#{thread.last_bt}" + end exit! end diff --git a/ext/do_attach.c b/ext/do_attach.c index c29c9f6..6778efc 100644 --- a/ext/do_attach.c +++ b/ext/do_attach.c @@ -1,7 +1,5 @@ #include "do_attach.h" -static const char *_command_to_eval; - static int __check_gc(void) { @@ -29,17 +27,13 @@ __catch_line_event(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE if (__check_gc()) return; __func_to_set_breakpoint_at(); -// rb_eval_string_protect(_command_to_eval, NULL); // TODO pass something more useful than NULL } int -start_attach(const char* command) +start_attach() { - rb_eval_string("puts 'bla bla bla'"); - _command_to_eval = command; if (__check_gc()) return 1; - rb_global_variable((VALUE *) _command_to_eval); rb_add_event_hook(__catch_line_event, RUBY_EVENT_LINE, (VALUE) NULL); - return 2323; + return 0; } diff --git a/ext/do_attach.h b/ext/do_attach.h index 5886935..e9e5603 100644 --- a/ext/do_attach.h +++ b/ext/do_attach.h @@ -5,6 +5,6 @@ #include #include -int start_attach(const char *command); +int start_attach(); #endif //__DO_ATTACH_H__ \ No newline at end of file From 096cd724eface3178d38320dd0c0d7e040bf307d Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Mon, 5 Sep 2016 15:07:48 +0300 Subject: [PATCH 10/14] printing backtrace of all threads in case of fail + some minor chages --- bin/gdb_wrapper | 66 +++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index 18c1c3e..abe68ba 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -61,7 +61,7 @@ Debugger::print_greeting_msg(nil, nil) class NativeDebugger - attr_reader :pid, :main_thread, :process_threads + attr_reader :pid, :main_thread, :process_threads, :pipe # @param executable -- path to ruby interpreter # @param pid -- pid of process you want to debug @@ -89,9 +89,7 @@ class NativeDebugger @pipe.close return '' end - response = get_response - $stdout.puts "response for #{command}:\n#{response}\n\n\n\n" - response + get_response end def get_response @@ -101,7 +99,6 @@ class NativeDebugger content = '' loop do line = @pipe.readline - $stderr.puts line next if line =~ /\(lldb\)/ # lldb repeats your input to its output break if line =~ /\$\d+\s=\s"#{@delimiter}"/ content += line @@ -152,6 +149,7 @@ class NativeDebugger def exit execute 'q' + @pipe.close end def to_s @@ -312,29 +310,44 @@ class ProcessThread end -def exists_command(command) +def command_exists(command) `command -v #{command} >/dev/null 2>&1 || { exit 1; }` $?.exitstatus == 0 end def choose_debugger - $stderr.puts "exists: #{exists_command('gdb')}, #{exists_command('lldb')}" - if exists_command('gdb') - GDB.new(@options.ruby_path, @options.pid, '-nh -nx') - elsif exists_command('lldb') - LLDB.new(@options.ruby_path, @options.pid, '--no-lldbinit') + if command_exists('gdb') + debugger = GDB.new(@options.ruby_path, @options.pid, '-nh -nx') + elsif command_exists('lldb') + debugger = LLDB.new(@options.ruby_path, @options.pid, '--no-lldbinit') 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 -gdb = choose_debugger -gdb.attach_to_process -gdb.set_flags +debugger = choose_debugger +debugger.attach_to_process +debugger.set_flags -if gdb.check_already_under_debug - $stderr.puts "Process #{gdb.pid} is already under debug" - gdb.exit +if @options.uid + Process::Sys.setuid(@options.uid.to_i) +end + +if debugger.check_already_under_debug + $stderr.puts "Process #{debugger.pid} is already under debug" + debugger.exit exit! end @@ -342,7 +355,7 @@ should_check_threads_state = true while should_check_threads_state should_check_threads_state = false - gdb.update_threads.each do |thread| + debugger.update_threads.each do |thread| thread.switch while thread.need_finish_frame should_check_threads_state = true @@ -351,19 +364,8 @@ while should_check_threads_state end end -gdb.wait_line_event -gdb.load_debugger -gdb.exit - -trap('INT') do - gdb.process_threads.each do |thread| - $stderr.puts "Last backtrace for thread #{thread.thread_info}:\n#{thread.last_bt}" - end - exit! -end - -if @options.uid - Process::Sys.setuid(@options.uid.to_i) -end +debugger.wait_line_event +debugger.load_debugger +debugger.exit sleep From c0668953ac07df8addb5158d96728d375145e5a2 Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Tue, 6 Sep 2016 11:11:39 +0300 Subject: [PATCH 11/14] little refactoring. deleted unused functions. --- bin/gdb_wrapper | 1 - ext/Makefile | 4 ++-- ext/{do_attach.c => attach.c} | 16 +++------------- ext/attach.h | 9 +++++++++ ext/do_attach.h | 10 ---------- 5 files changed, 14 insertions(+), 26 deletions(-) rename ext/{do_attach.c => attach.c} (64%) create mode 100644 ext/attach.h delete mode 100644 ext/do_attach.h diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index abe68ba..308dacd 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -127,7 +127,6 @@ class NativeDebugger @pipe.puts 'c' loop do line = @pipe.readline - $stderr.puts line break if line =~ /#{Regexp.escape(@tbreak)}/ end get_response diff --git a/ext/Makefile b/ext/Makefile index 9bec56c..6c52c90 100644 --- a/ext/Makefile +++ b/ext/Makefile @@ -3,8 +3,8 @@ 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 +libAttach.o: 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/ attach.c -o libAttach.o clean: rm libAttach.* diff --git a/ext/do_attach.c b/ext/attach.c similarity index 64% rename from ext/do_attach.c rename to ext/attach.c index 6778efc..d4d4dbe 100644 --- a/ext/do_attach.c +++ b/ext/attach.c @@ -1,14 +1,4 @@ -#include "do_attach.h" - -static int -__check_gc(void) -{ - if (rb_during_gc()) { - fprintf(stderr, "Can not connect during garbage collection phase. Please, try again later.\n"); - return 1; - } - return 0; -} +#include "attach.h" static void __func_to_set_breakpoint_at() @@ -24,7 +14,7 @@ __catch_line_event(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE (void)sizeof(klass); rb_remove_event_hook(__catch_line_event); - if (__check_gc()) + if (rb_during_gc()) return; __func_to_set_breakpoint_at(); } @@ -32,7 +22,7 @@ __catch_line_event(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE int start_attach() { - if (__check_gc()) + if (rb_during_gc()) return 1; rb_add_event_hook(__catch_line_event, RUBY_EVENT_LINE, (VALUE) NULL); return 0; diff --git a/ext/attach.h b/ext/attach.h new file mode 100644 index 0000000..9c98192 --- /dev/null +++ b/ext/attach.h @@ -0,0 +1,9 @@ +#ifndef __ATTACH_H__ +#define __ATTACH_H__ + +#include +#include + +int start_attach(); + +#endif //__ATTACH_H__ \ No newline at end of file diff --git a/ext/do_attach.h b/ext/do_attach.h deleted file mode 100644 index e9e5603..0000000 --- a/ext/do_attach.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef __DO_ATTACH_H__ -#define __DO_ATTACH_H__ - -#include -#include -#include - -int start_attach(); - -#endif //__DO_ATTACH_H__ \ No newline at end of file From cede1279387a54c42002b5a5eb623173f29baaf8 Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Wed, 7 Sep 2016 14:59:38 +0300 Subject: [PATCH 12/14] finding attach.so file in debase added --- bin/gdb_wrapper | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index 308dacd..dbe2f2c 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -6,7 +6,7 @@ require 'ostruct' $stdout.sync = true $stderr.sync = true -@options = OpenStruct.new( +$options = OpenStruct.new( 'pid' => nil, 'sdk_path' => nil, 'uid' => nil, @@ -20,39 +20,39 @@ Some useful banner. EOB opts.on('--pid PID', 'pid of process you want to attach to for debugging') do |pid| - @options.pid = pid + $options.pid = pid end opts.on('--ruby-path RUBY_PATH', 'path to ruby interpreter') do |ruby_path| - @options.ruby_path = ruby_path + $options.ruby_path = ruby_path end opts.on('--uid UID', 'uid which this process should set after executing gdb attach') do |uid| - @options.uid = uid + $options.uid = uid end opts.on('--include-gem GEM_LIB_PATH', 'lib of gem to include') do |gem_lib_path| - @options.gems_to_include << gem_lib_path + $options.gems_to_include << gem_lib_path end end opts.parse! ARGV -unless @options.pid +unless $options.pid $stderr.puts 'You should specify PID of process you want to attach to' exit 1 end -unless @options.ruby_path +unless $options.ruby_path $stderr.puts 'You should specify path to the ruby interpreter' exit 1 end $argv = '["' + ARGV * '", "' + '"]' -$gems_to_include = '["' + @options.gems_to_include * '", "' + '"]' +$gems_to_include = '["' + $options.gems_to_include * '", "' + '"]' $path_to_debugger_loader = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader' -@options.gems_to_include.each do |gem_path| +$options.gems_to_include.each do |gem_path| $LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path) end @@ -72,6 +72,11 @@ class NativeDebugger @tbreak = '__func_to_set_breakpoint_at' @main_thread = nil @process_threads = nil + debase_path = $options.gems_to_include.select {|gem_path| gem_path =~ /debase/} + if debase_path.size == 0 + raise 'No debase gem found.' + end + @path_to_attach = debase_path[0] + '/attach.so' launch_string = "#{self} #{executable} #{options}" @pipe = IO.popen(launch_string, 'r+') @@ -195,7 +200,7 @@ class LLDB < NativeDebugger def call_start_attach super() - execute "expr (void *) dlopen(\"/home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\", 2)" + execute "expr (void *) dlopen(\"#{@path_to_attach}\", 2)" execute 'call start_attach()' set_tbreak(@tbreak) end @@ -245,7 +250,7 @@ class GDB < NativeDebugger def call_start_attach super() - execute "call dlopen(\"/home/equi/Job/JetBrains/Internship_2016/ruby-debug-ide/ext/libAttach.so\", 2)" + execute "call dlopen(\"#{@path_to_attach}\", 2)" execute 'call start_attach()' set_tbreak(@tbreak) end @@ -316,9 +321,9 @@ end def choose_debugger if command_exists('gdb') - debugger = GDB.new(@options.ruby_path, @options.pid, '-nh -nx') + debugger = GDB.new($options.ruby_path, $options.pid, '-nh -nx') elsif command_exists('lldb') - debugger = LLDB.new(@options.ruby_path, @options.pid, '--no-lldbinit') + debugger = LLDB.new($options.ruby_path, $options.pid, '--no-lldbinit') else raise 'Neither gdb nor lldb was found. Aborting.' end @@ -340,8 +345,8 @@ debugger = choose_debugger debugger.attach_to_process debugger.set_flags -if @options.uid - Process::Sys.setuid(@options.uid.to_i) +if $options.uid + Process::Sys.setuid($options.uid.to_i) end if debugger.check_already_under_debug From ea4af282cc07eb909c04f2700456af36a809876f Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Fri, 9 Sep 2016 21:29:51 +0300 Subject: [PATCH 13/14] bug fix: no need to close already closed stream (it works well only starting from ruby 2.3) --- bin/gdb_wrapper | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index dbe2f2c..d20f700 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -91,7 +91,6 @@ class NativeDebugger @pipe.puts command $stdout.puts "executed `#{command}` command inside #{self}." if command == 'q' - @pipe.close return '' end get_response From 7a15facd8663de89b4cbfa961b43ab6c255272a3 Mon Sep 17 00:00:00 2001 From: equivalence1 Date: Mon, 12 Sep 2016 00:54:50 +0300 Subject: [PATCH 14/14] replaced global variables with local ones --- bin/gdb_wrapper | 55 ++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper index d20f700..08c37d2 100755 --- a/bin/gdb_wrapper +++ b/bin/gdb_wrapper @@ -6,7 +6,7 @@ require 'ostruct' $stdout.sync = true $stderr.sync = true -$options = OpenStruct.new( +options = OpenStruct.new( 'pid' => nil, 'sdk_path' => nil, 'uid' => nil, @@ -20,39 +20,38 @@ Some useful banner. EOB opts.on('--pid PID', 'pid of process you want to attach to for debugging') do |pid| - $options.pid = pid + options.pid = pid end opts.on('--ruby-path RUBY_PATH', 'path to ruby interpreter') do |ruby_path| - $options.ruby_path = ruby_path + options.ruby_path = ruby_path end opts.on('--uid UID', 'uid which this process should set after executing gdb attach') do |uid| - $options.uid = uid + options.uid = uid end opts.on('--include-gem GEM_LIB_PATH', 'lib of gem to include') do |gem_lib_path| - $options.gems_to_include << gem_lib_path + options.gems_to_include << gem_lib_path end end opts.parse! ARGV -unless $options.pid +unless options.pid $stderr.puts 'You should specify PID of process you want to attach to' exit 1 end -unless $options.ruby_path +unless options.ruby_path $stderr.puts 'You should specify path to the ruby interpreter' exit 1 end -$argv = '["' + ARGV * '", "' + '"]' -$gems_to_include = '["' + $options.gems_to_include * '", "' + '"]' -$path_to_debugger_loader = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader' +argv = '["' + ARGV * '", "' + '"]' +debugger_loader_path = File.expand_path(File.dirname(__FILE__)) + '/../lib/ruby-debug-ide/attach/debugger_loader' -$options.gems_to_include.each do |gem_path| +options.gems_to_include.each do |gem_path| $LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path) end @@ -65,20 +64,24 @@ class NativeDebugger # @param executable -- path to ruby interpreter # @param pid -- pid of process you want to debug - # @param options -- flags you want to specify to your debugger as a string (e.g. "-nx -nh" for gdb to disable .gdbinit) - def initialize(executable, pid, options) + # @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 = $options.gems_to_include.select {|gem_path| gem_path =~ /debase/} + 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 = debase_path[0] + '/attach.so' - launch_string = "#{self} #{executable} #{options}" + @gems_to_include = '["' + gems_to_include * '", "' + '"]' + @debugger_loader_path = debugger_loader_path + @argv = argv + + launch_string = "#{self} #{executable} #{flags}" @pipe = IO.popen(launch_string, 'r+') $stdout.puts "executed '#{launch_string}'" end @@ -147,7 +150,7 @@ class NativeDebugger end def load_debugger - execute "call rb_eval_string_protect(\"require '#{$path_to_debugger_loader}'; load_debugger(#{$gems_to_include.gsub("\"", "'")}, #{$argv.gsub("\"", "'")})\", (int *)0)" + execute "call rb_eval_string_protect(\"require '#{@debugger_loader_path}'; load_debugger(#{@gems_to_include.gsub("\"", "'")}, #{@argv.gsub("\"", "'")})\", (int *)0)" end def exit @@ -163,8 +166,8 @@ end class LLDB < NativeDebugger - def initialize(executable, pid, options) - super(executable, pid, options) + 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 @@ -212,8 +215,8 @@ end class GDB < NativeDebugger - def initialize(executable, pid, options) - super(executable, pid, options) + 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 @@ -318,11 +321,11 @@ def command_exists(command) $?.exitstatus == 0 end -def choose_debugger +def choose_debugger(ruby_path, pid, gems_to_include, debugger_loader_path, argv) if command_exists('gdb') - debugger = GDB.new($options.ruby_path, $options.pid, '-nh -nx') + debugger = GDB.new(ruby_path, pid, '-nh -nx', gems_to_include, debugger_loader_path, argv) elsif command_exists('lldb') - debugger = LLDB.new($options.ruby_path, $options.pid, '--no-lldbinit') + debugger = LLDB.new(ruby_path, pid, '--no-lldbinit', gems_to_include, debugger_loader_path, argv) else raise 'Neither gdb nor lldb was found. Aborting.' end @@ -340,12 +343,12 @@ def choose_debugger debugger end -debugger = choose_debugger +debugger = choose_debugger(options.ruby_path, options.pid, options.gems_to_include, debugger_loader_path, argv) debugger.attach_to_process debugger.set_flags -if $options.uid - Process::Sys.setuid($options.uid.to_i) +if options.uid + Process::Sys.setuid(options.uid.to_i) end if debugger.check_already_under_debug