diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..31827b8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +language: ruby +dist: trusty +os: + - linux + - osx + +rvm: + - 1.9.3 + - 2.0.0 + - 2.1.10 + - 2.2.8 + - 2.3.5 + - 2.4.2 + - 2.5.0 + - ruby-head + +matrix: + fast_finish: true + allow_failures: + - rvm: ruby-head + + exclude: + - os: osx + rvm: 1.9.3 + - os: osx + rvm: 2.0.0 + +# include: +# - os: osx +# rvm: 1.9.3 +# before_script: rvm install ruby-1.9.3 # not binary +# - os: osx +# rvm: 2.0.0 +# before_script: rvm install ruby-2.0.0 # not binary \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md index a739807..8593f56 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,8 @@ -## [master](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.5.0...master) +## [master](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.6.0...master) + +* let's use Debugger#remove_catchpoint and Debugger#clear_catchpoints if available + +## [0.6.0](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.5.0...0.6.0) * "file-filter on|off" command added * "include file|dir" command added diff --git a/Gemfile b/Gemfile index 2ee4fb3..9d8626c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,20 @@ source "http://rubygems.org/" -gem "ruby-debug-base", :platforms => [:jruby, :ruby_18, :mingw_18] -gem "ruby-debug-base19x", ">= 0.11.30.pre4", :platforms => [:ruby_19, :mingw_19] +# @param [Array] versions compatible ruby versions +# @return [Array] an array with mri platforms of given versions +def mries(*versions) + versions.flat_map do |v| + %w(ruby mingw x64_mingw) + .map { |platform| "#{platform}_#{v}".to_sym unless platform == "x64_mingw" && v < "2.0" } + .delete_if &:nil? + end +end + +gem "ruby-debug-base", :platforms => [:jruby, *mries('18')] +gem "ruby-debug-base19x", ">= 0.11.32", :platforms => mries('19') +if RUBY_VERSION && RUBY_VERSION >= "2.0" + gem "debase", "~> 0.2.2", :platforms => mries('20', '21', '22', '23', '24', '25') +end gemspec diff --git a/README.md b/README.md index 9e4024d..d0522d6 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,6 @@ # ruby-debug-ide An interface which glues ruby-debug to IDEs like Eclipse (RDT), NetBeans and RubyMine. +[![official JetBrains project](http://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![Gem Version](https://badge.fury.io/rb/ruby-debug-ide.svg)][gem] +[![Build Status](https://travis-ci.org/ruby-debug/ruby-debug-ide.svg?branch=master)](https://travis-ci.org/ruby-debug/ruby-debug-ide) diff --git a/bin/gdb_wrapper b/bin/gdb_wrapper new file mode 100755 index 0000000..a439204 --- /dev/null +++ b/bin/gdb_wrapper @@ -0,0 +1,96 @@ +#!/usr/bin/env ruby + +require 'optparse' +require 'thread' +require 'ostruct' + +$stdout.sync = true +$stderr.sync = true + +options = OpenStruct.new( + 'pid' => nil, + 'sdk_path' => nil, + 'uid' => nil, + 'gems_to_include' => [] +) + +module DebugPrinter + + class << self + attr_accessor :cli_debug + + def print_debug(msg) + if DebugPrinter.cli_debug + $stderr.puts msg + end + end + end + +end + +DebugPrinter.cli_debug = ARGV.include? '--debug' + +opts = OptionParser.new do |opts| + # TODO need some banner + opts.banner = < false, - 'host' => nil, - 'load_mode' => false, - 'port' => 1234, - 'stop' => false, - 'tracing' => false, - 'int_handler' => true, - 'dispatcher_port' => -1, - 'evaluation_timeout' => 10, - 'rm_protocol_extensions' => false, - 'catchpoint_deleted_event' => false, - 'value_as_nested_element' => false + 'frame_bind' => false, + 'host' => nil, + 'load_mode' => false, + 'port' => 1234, + 'stop' => false, + 'tracing' => false, + 'int_handler' => true, + 'dispatcher_port' => -1, + 'evaluation_timeout' => 10, + 'trace_to_s' => false, + 'debugger_memory_limit' => 10, + 'inspect_time_limit' => 100, + 'rm_protocol_extensions' => false, + 'catchpoint_deleted_event' => false, + 'value_as_nested_element' => false, + 'attach_mode' => false, + 'cli_debug' => false ) opts = OptionParser.new do |opts| @@ -34,19 +39,37 @@ Usage: rdebug-ide is supposed to be called from RDT, NetBeans, RubyMine, or EOB opts.separator "" opts.separator "Options:" + opts.on("-h", "--host HOST", "Host name used for remote debugging") {|host| options.host = host} - opts.on("-p", "--port PORT", Integer, "Port used for remote debugging") {|port| options.port = port} - opts.on("--dispatcher-port PORT", Integer, "Port used for multi-process debugging dispatcher") do |dp| + opts.on("-p", "--port PORT", Integer, "Port used for remote debugging") {|port| options.port = port} + opts.on("--dispatcher-port PORT", Integer, "Port used for multi-process debugging dispatcher") do |dp| options.dispatcher_port = dp end opts.on('--evaluation-timeout TIMEOUT', Integer,'evaluation timeout in seconds (default: 10)') do |timeout| options.evaluation_timeout = timeout end + opts.on("--evaluation-control", "trace to_s evaluation") {options.trace_to_s = true} + + opts.on("-m", "--memory-limit LIMIT", Integer, "evaluation memory limit in mb (default: 10)") do |limit| + if defined?(JRUBY_VERSION) || RUBY_VERSION < '2.0' + $stderr.puts "Evaluation memory limit is ineffective in JRuby and MRI < 2.0" + limit = 0 + end + options.debugger_memory_limit = limit + options.trace_to_s ||= limit > 0 + end + + opts.on("-t", "--time-limit LIMIT", Integer, "evaluation time limit in milliseconds (default: 100)") do |limit| + options.inspect_time_limit = limit + options.trace_to_s ||= limit > 0 + end + opts.on('--stop', 'stop when the script is loaded') {options.stop = true} opts.on("-x", "--trace", "turn on line tracing") {options.tracing = true} 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 @@ -54,7 +77,12 @@ 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("--ignore-port", "Generate another port") do + options.ignore_port = 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,29 +117,38 @@ 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 +if !options.attach_mode + Debugger::PROG_SCRIPT = ARGV.shift +else + Debugger::PROG_SCRIPT = $0 +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 + Debugger::MultiProcess.do_monkey - 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 + ENV['DEBUGGER_STORED_RUBYLIB'] = ENV['RUBYLIB'] + 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 @@ -119,13 +156,28 @@ 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 Debugger.evaluation_timeout = options.evaluation_timeout +Debugger.trace_to_s = options.trace_to_s && (options.debugger_memory_limit > 0 || options.inspect_time_limit > 0) +Debugger.debugger_memory_limit = options.debugger_memory_limit +Debugger.inspect_time_limit = options.inspect_time_limit 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 + if Debugger::FRONT_END == "debase" + Debugger.init_variables + end + Debugger::MultiProcess::pre_child(options) + + if Debugger::FRONT_END == "debase" + Debugger.setup_tracepoints + Debugger.prepare_context + end +else + Debugger.debug_program(options) +end diff --git a/lib/ruby-debug-ide.rb b/lib/ruby-debug-ide.rb index 9704f94..62c0671 100644 --- a/lib/ruby-debug-ide.rb +++ b/lib/ruby-debug-ide.rb @@ -4,11 +4,13 @@ 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/greeter' require 'ruby-debug-ide/xml_printer' require 'ruby-debug-ide/ide_processor' require 'ruby-debug-ide/event_processor' @@ -27,21 +29,23 @@ def print_debug(*args) end def cleanup_backtrace(backtrace) - cleared = [] - return cleared unless backtrace - backtrace.each do |line| - if line.index(File.expand_path(File.dirname(__FILE__) + "/..")) == 0 - next - end - if line.index("-e:1") == 0 - break - end - cleared << line - end - cleared + cleared = [] + return cleared unless backtrace + backtrace.each do |line| + if line.index(File.expand_path(File.dirname(__FILE__) + "/..")) == 0 + next + end + if line.index("-e:1") == 0 + break + end + cleared << line + end + cleared end + attr_accessor :attached attr_accessor :cli_debug, :xml_debug, :evaluation_timeout + attr_accessor :trace_to_s, :debugger_memory_limit, :inspect_time_limit attr_accessor :control_thread attr_reader :interface # protocol extensions @@ -108,9 +112,8 @@ def start_control(host, port, notify_dispatcher) # "localhost" and nil have problems on some systems. host ||= '127.0.0.1' server = TCPServer.new(host, port) - print_greeting_msg(host, port) + print_greeting_msg($stderr, host, port) if defined? IDE_VERSION 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'] @@ -136,23 +139,6 @@ def start_control(host, port, notify_dispatcher) end end - def print_greeting_msg(host, port) - base_gem_name = if defined?(JRUBY_VERSION) || RUBY_VERSION < '1.9.0' - 'ruby-debug-base' - elsif RUBY_VERSION < '2.0.0' - 'ruby-debug-base19x' - else - 'debase' - end - - file_filtering_support = if Command.file_filter_supported? - 'supported' - else - 'not supported' - end - $stderr.printf "Fast Debugger (ruby-debug-ide #{IDE_VERSION}, #{base_gem_name} #{VERSION}, file filtering is #{file_filtering_support}) listens on #{host}:#{port}\n" - end - private @@ -160,8 +146,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/attach/debugger_loader.rb b/lib/ruby-debug-ide/attach/debugger_loader.rb new file mode 100644 index 0000000..d9150a8 --- /dev/null +++ b/lib/ruby-debug-ide/attach/debugger_loader.rb @@ -0,0 +1,20 @@ +def load_debugger(gems_to_include, new_argv) + path_to_rdebug = File.expand_path(File.dirname(__FILE__)) + '/../../../bin/rdebug-ide' + + old_argv = ARGV.clone + ARGV.clear + new_argv.each do |x| + ARGV << x + end + + gems_to_include.each do |gem_path| + $LOAD_PATH.unshift(gem_path) unless $LOAD_PATH.include?(gem_path) + end + + load path_to_rdebug + + ARGV.clear + old_argv.each do |x| + ARGV << x + end +end diff --git a/lib/ruby-debug-ide/attach/gdb.rb b/lib/ruby-debug-ide/attach/gdb.rb new file mode 100644 index 0000000..bf9085c --- /dev/null +++ b/lib/ruby-debug-ide/attach/gdb.rb @@ -0,0 +1,73 @@ +require 'ruby-debug-ide/attach/native_debugger' + +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 set_break(str) + execute "tbreak #{str}" + end + + def call_start_attach + super() + execute "call dlopen(\"#{@path_to_attach}\", 2)" + execute 'call debase_start_attach()' + set_break(@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.to_s + end + + class << self + def to_s + 'gdb' + end + end + +end diff --git a/lib/ruby-debug-ide/attach/lldb.rb b/lib/ruby-debug-ide/attach/lldb.rb new file mode 100644 index 0000000..b9bdc31 --- /dev/null +++ b/lib/ruby-debug-ide/attach/lldb.rb @@ -0,0 +1,71 @@ +require 'ruby-debug-ide/attach/native_debugger' + +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 set_break(str) + execute "breakpoint set --shlib #{@path_to_attach} --name #{str}" + end + + def call_start_attach + super() + execute "expr (void *) dlopen(\"#{@path_to_attach}\", 2)" + execute 'expr (int) debase_start_attach()' + set_break(@tbreak) + end + + def print_delimiter + @pipe.puts "script print \"#{@delimiter}\"" + end + + def check_delimiter(line) + line =~ /#{@delimiter}$/ + end + + def load_debugger + execute "expr (void) #{@eval_string}" + end + + def to_s + LLDB.to_s + end + + class << self + def to_s + 'lldb' + end + end + +end diff --git a/lib/ruby-debug-ide/attach/native_debugger.rb b/lib/ruby-debug-ide/attach/native_debugger.rb new file mode 100644 index 0000000..05ea9d9 --- /dev/null +++ b/lib/ruby-debug-ide/attach/native_debugger.rb @@ -0,0 +1,133 @@ +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 = "debase_rb_eval(\"require '#{@debugger_loader_path}'; load_debugger(#{@gems_to_include.gsub("\"", "'")}, #{@argv.gsub("\"", "'")})\")" + + 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 .dylib) + 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 + DebugPrinter.print_debug('respond line: ' + line) + break if check_delimiter(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_break(str) + + end + + def continue + $stdout.puts 'continuing' + @pipe.puts 'c' + loop do + line = @pipe.readline + DebugPrinter.print_debug('respond line: ' + line) + 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 exited? + @pipe.closed? + end + + def exit + @pipe.close + end + + def to_s + 'native_debugger' + end + +end diff --git a/lib/ruby-debug-ide/attach/process_thread.rb b/lib/ruby-debug-ide/attach/process_thread.rb new file mode 100644 index 0000000..cee7ea7 --- /dev/null +++ b/lib/ruby-debug-ide/attach/process_thread.rb @@ -0,0 +1,54 @@ +require 'ruby-debug-ide/attach/native_debugger' + +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)') + $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 diff --git a/lib/ruby-debug-ide/attach/util.rb b/lib/ruby-debug-ide/attach/util.rb new file mode 100644 index 0000000..9ab535e --- /dev/null +++ b/lib/ruby-debug-ide/attach/util.rb @@ -0,0 +1,115 @@ +require 'ruby-debug-ide/attach/lldb' +require 'ruby-debug-ide/attach/gdb' +require 'socket' +require 'set' + +def attach_and_return_thread(options, pid, debugger_loader_path, argv) + Thread.new(argv) do |argv| + + debugger = choose_debugger(options.ruby_path, pid, options.gems_to_include, debugger_loader_path, argv) + + trap('INT') do + unless debugger.exited? + $stderr.puts "backtraces for threads:\n\n" + process_threads = debugger.process_threads + if process_threads + process_threads.each do |thread| + $stderr.puts "#{thread.thread_info}\n#{thread.last_bt}\n\n" + end + end + debugger.exit + end + exit! + end + + debugger.attach_to_process + debugger.set_flags + + if debugger.check_already_under_debug + $stderr.puts "Process #{debugger.pid} is already under debug" + debugger.exit + exit! + end + + should_check_threads_state = true + + while should_check_threads_state + should_check_threads_state = false + debugger.update_threads.each do |thread| + thread.switch + while thread.need_finish_frame + should_check_threads_state = true + thread.finish + end + end + end + + debugger.wait_line_event + debugger.load_debugger + debugger.exit + end +end + +def get_child_pids(pid) + return [] unless command_exists 'pgrep' + + pids = Array.new + + q = Queue.new + q.push(pid) + + until q.empty? do + pid = q.pop + + pipe = IO.popen("pgrep -P #{pid}") + + pipe.readlines.each do |child| + child_pid = child.strip.to_i + q.push(child_pid) + pids << child_pid + end + end + + filter_ruby_processes(pids) +end + +def filter_ruby_processes(pids) + pipe = IO.popen(%Q(lsof -c ruby | awk '{print $2 ":" $9}' | grep -E 'bin/ruby([[:digit:]]+\.?)*$')) + + ruby_processes = Set.new + + pipe.readlines.each do |process| + pid = process.split(/:/).first + ruby_processes.add(pid.to_i) + end + + ruby_processes_pids, non_ruby_processes_pids = pids.partition {|pid| ruby_processes.include? pid} + + DebugPrinter.print_debug("The following child processes was added to attach: #{ruby_processes_pids.join(', ')}") unless ruby_processes_pids.empty? + DebugPrinter.print_debug("The following child are not ruby processes: #{non_ruby_processes_pids.join(', ')}") unless non_ruby_processes_pids.empty? + + ruby_processes_pids +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.to_s) + debugger = LLDB.new(ruby_path, pid, '--no-lldbinit', gems_to_include, debugger_loader_path, argv) + elsif command_exists(GDB.to_s) + 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 + + debugger +end \ No newline at end of file diff --git a/lib/ruby-debug-ide/command.rb b/lib/ruby-debug-ide/command.rb index 1839af2..e6ee3e1 100644 --- a/lib/ruby-debug-ide/command.rb +++ b/lib/ruby-debug-ide/command.rb @@ -1,3 +1,9 @@ +if RUBY_VERSION < '2.0' || defined?(JRUBY_VERSION) + require 'ruby-debug-base' +else + require 'debase' +end + require 'ruby-debug-ide/helper' require 'delegate' @@ -118,6 +124,7 @@ def timeout(sec) def debug_eval(str, b = get_binding) begin str = str.to_s + str.force_encoding('UTF-8') if(RUBY_VERSION >= '2.0') to_inspect = Command.unescape_incoming(str) max_time = Debugger.evaluation_timeout @printer.print_debug("Evaluating %s with timeout after %i sec", str, max_time) diff --git a/lib/ruby-debug-ide/commands/catchpoint.rb b/lib/ruby-debug-ide/commands/catchpoint.rb index f6e9592..eac29d3 100644 --- a/lib/ruby-debug-ide/commands/catchpoint.rb +++ b/lib/ruby-debug-ide/commands/catchpoint.rb @@ -16,21 +16,20 @@ def execute elsif not @match[2] # One arg given. if 'off' == excn - Debugger.catchpoints.clear + clear_catchpoints else Debugger.add_catchpoint(excn) print_catchpoint_set(excn) end elsif @match[2] != 'off' errmsg "Off expected. Got %s\n", @match[2] - elsif Debugger.catchpoints.member?(excn) - Debugger.catchpoints.delete(excn) + elsif remove_catchpoint(excn) print_catchpoint_deleted(excn) else errmsg "Catch for exception %s not found.\n", excn end end - + class << self def help_command 'catch' @@ -45,5 +44,21 @@ def help(cmd) } end end + + private + + def clear_catchpoints + if Debugger.respond_to?(:clear_catchpoints) + Debugger.clear_catchpoints + else + Debugger.catchpoints.clear + end + end + + def remove_catchpoint(excn) + return Debugger.remove_catchpoint(excn) if Debugger.respond_to?(:remove_catchpoint) + return Debugger.catchpoints.delete(excn) if Debugger.catchpoints.member?(excn) + false + end end end diff --git a/lib/ruby-debug-ide/commands/control.rb b/lib/ruby-debug-ide/commands/control.rb index c8ff56f..6fd030f 100644 --- a/lib/ruby-debug-ide/commands/control.rb +++ b/lib/ruby-debug-ide/commands/control.rb @@ -126,4 +126,33 @@ def help(cmd) end end end + + + class DetachCommand < Command # :nodoc: + self.control = true + + def regexp + /^\s*detach\s*$/ + end + + def execute + Debugger.stop + Debugger.interface.close + Debugger::MultiProcess.undo_monkey + 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/commands/expression_info.rb b/lib/ruby-debug-ide/commands/expression_info.rb index 795ca2d..3eccd52 100644 --- a/lib/ruby-debug-ide/commands/expression_info.rb +++ b/lib/ruby-debug-ide/commands/expression_info.rb @@ -9,7 +9,7 @@ def regexp end def execute - string_to_parse = Command.unescape_incoming(@match.post_match) + "\n\n\n" + string_to_parse = Command.unescape_incoming(@match.post_match) + " \n \n\n" total_lines = string_to_parse.count("\n") + 1 lexer = RubyLex.new diff --git a/lib/ruby-debug-ide/commands/variables.rb b/lib/ruby-debug-ide/commands/variables.rb index cc8d935..33c4f8e 100644 --- a/lib/ruby-debug-ide/commands/variables.rb +++ b/lib/ruby-debug-ide/commands/variables.rb @@ -129,7 +129,7 @@ def execute locals = @state.context.frame_locals(@state.frame_pos) _self = @state.context.frame_self(@state.frame_pos) begin - locals['self'] = _self unless "main" == _self.to_s + locals['self'] = _self unless TOPLEVEL_BINDING.eval('self') == _self rescue => ex locals['self'] = "" $stderr << "Cannot evaluate self\n#{ex.class.name}: #{ex.message}\n #{ex.backtrace.join("\n ")}" diff --git a/lib/ruby-debug-ide/greeter.rb b/lib/ruby-debug-ide/greeter.rb new file mode 100644 index 0000000..df9b042 --- /dev/null +++ b/lib/ruby-debug-ide/greeter.rb @@ -0,0 +1,40 @@ +if RUBY_VERSION < '2.0' || defined?(JRUBY_VERSION) + require 'ruby-debug-base' +else + require 'debase' +end + +require 'ruby-debug-ide/version' +require 'ruby-debug-ide/ide_processor' + +module Debugger + + class << self + def print_greeting_msg(stream, host, port) + base_gem_name = if defined?(JRUBY_VERSION) || RUBY_VERSION < '1.9.0' + 'ruby-debug-base' + elsif RUBY_VERSION < '2.0.0' + 'ruby-debug-base19x' + else + 'debase' + end + + file_filtering_support = if Command.file_filter_supported? + 'supported' + else + 'not supported' + end + + if host && port + listens_on = " listens on #{host}:#{port}\n" + else + listens_on = "\n" + end + + msg = "Fast Debugger (ruby-debug-ide #{IDE_VERSION}, #{base_gem_name} #{VERSION}, file filtering is #{file_filtering_support})" + listens_on + + stream.printf msg + end + end + +end \ No newline at end of file diff --git a/lib/ruby-debug-ide/ide_processor.rb b/lib/ruby-debug-ide/ide_processor.rb index e5b7e98..e04f651 100644 --- a/lib/ruby-debug-ide/ide_processor.rb +++ b/lib/ruby-debug-ide/ide_processor.rb @@ -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.rb b/lib/ruby-debug-ide/multiprocess.rb index 1366327..4342ef7 100644 --- a/lib/ruby-debug-ide/multiprocess.rb +++ b/lib/ruby-debug-ide/multiprocess.rb @@ -1,7 +1,23 @@ -if RUBY_VERSION < "1.9" +if RUBY_VERSION < '1.9' require 'ruby-debug-ide/multiprocess/pre_child' - require 'ruby-debug-ide/multiprocess/monkey' else require_relative 'multiprocess/pre_child' - require_relative 'multiprocess/monkey' +end + +module Debugger + module MultiProcess + class << self + def do_monkey + load File.expand_path(File.dirname(__FILE__) + '/multiprocess/monkey.rb') + end + + def undo_monkey + if ENV['IDE_PROCESS_DISPATCHER'] + load File.expand_path(File.dirname(__FILE__) + '/multiprocess/unmonkey.rb') + ruby_opts = ENV['RUBYOPT'].split(' ') + ENV['RUBYOPT'] = ruby_opts.keep_if {|opt| !opt.end_with?('ruby-debug-ide/multiprocess/starter')}.join(' ') + end + end + end + end end \ No newline at end of file diff --git a/lib/ruby-debug-ide/multiprocess/pre_child.rb b/lib/ruby-debug-ide/multiprocess/pre_child.rb index 1b9c2cc..4a349fd 100644 --- a/lib/ruby-debug-ide/multiprocess/pre_child.rb +++ b/lib/ruby-debug-ide/multiprocess/pre_child.rb @@ -1,26 +1,33 @@ 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, 'cli_debug' => (ENV['DEBUGGER_CLI_DEBUG'] == 'true'), - 'notify_dispatcher' => true + 'notify_dispatcher' => true, + 'evaluation_timeout' => 10, + 'trace_to_s' => false, + 'debugger_memory_limit' => 10, + 'inspect_time_limit' => 100 ) + if(options.ignore_port) + options.port = find_free_port(options.host) + options.notify_dispatcher = true + end + start_debugger(options) end @@ -40,8 +47,11 @@ def start_debugger(options) # set options Debugger.keep_frame_binding = options.frame_bind Debugger.tracing = options.tracing + Debugger.evaluation_timeout = options.evaluation_timeout + Debugger.trace_to_s = options.trace_to_s + Debugger.debugger_memory_limit = options.debugger_memory_limit + Debugger.inspect_time_limit = options.inspect_time_limit Debugger.cli_debug = options.cli_debug - Debugger.prepare_debugger(options) end diff --git a/lib/ruby-debug-ide/multiprocess/starter.rb b/lib/ruby-debug-ide/multiprocess/starter.rb index f8435e4..5e312f8 100644 --- a/lib/ruby-debug-ide/multiprocess/starter.rb +++ b/lib/ruby-debug-ide/multiprocess/starter.rb @@ -6,5 +6,6 @@ end require 'ruby-debug-ide' require 'ruby-debug-ide/multiprocess' + Debugger::MultiProcess::do_monkey Debugger::MultiProcess::pre_child end \ No newline at end of file diff --git a/lib/ruby-debug-ide/multiprocess/unmonkey.rb b/lib/ruby-debug-ide/multiprocess/unmonkey.rb new file mode 100644 index 0000000..82e6c6e --- /dev/null +++ b/lib/ruby-debug-ide/multiprocess/unmonkey.rb @@ -0,0 +1,31 @@ +module Debugger + module MultiProcess + def self.restore_fork + %Q{ + alias fork pre_debugger_fork + } + end + + def self.restore_exec + %Q{ + alias exec pre_debugger_exec + } + end + end +end + +module Kernel + class << self + module_eval Debugger::MultiProcess.restore_fork + module_eval Debugger::MultiProcess.restore_exec + end + module_eval Debugger::MultiProcess.restore_fork + module_eval Debugger::MultiProcess.restore_exec +end + +module Process + class << self + module_eval Debugger::MultiProcess.restore_fork + module_eval Debugger::MultiProcess.restore_exec + end +end \ No newline at end of file diff --git a/lib/ruby-debug-ide/version.rb b/lib/ruby-debug-ide/version.rb index b8e0c25..41f8b9f 100755 --- a/lib/ruby-debug-ide/version.rb +++ b/lib/ruby-debug-ide/version.rb @@ -1,3 +1,3 @@ module Debugger - IDE_VERSION='0.6.0' + IDE_VERSION='0.6.1' end diff --git a/lib/ruby-debug-ide/xml_printer.rb b/lib/ruby-debug-ide/xml_printer.rb index 3ef5140..49b0b2e 100644 --- a/lib/ruby-debug-ide/xml_printer.rb +++ b/lib/ruby-debug-ide/xml_printer.rb @@ -4,9 +4,39 @@ module Debugger + module OverflowMessageType + NIL_MESSAGE = lambda {|e| nil} + EXCEPTION_MESSAGE = lambda {|e| e.message} + SPECIAL_SYMBOL_MESSAGE = lambda {|e| ''} + end + + class ExecError + attr_reader :message + attr_reader :backtrace + + def initialize(message, backtrace = []) + @message = message + @backtrace = backtrace + end + end + + class SimpleTimeLimitError < StandardError + attr_reader :message + + def initialize(message) + @message = message + end + end + + class MemoryLimitError < ExecError; + end + + class TimeLimitError < ExecError; + end + class XmlPrinter # :nodoc: class ExceptionProxy - instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$|^instance_variables$|^instance_eval$)/ } + instance_methods.each {|m| undef_method m unless m =~ /(^__|^send$|^object_id$|^instance_variables$|^instance_eval$)/} def initialize(exception) @exception = exception @@ -14,9 +44,9 @@ def initialize(exception) @backtrace = Debugger.cleanup_backtrace(exception.backtrace) end - private - def method_missing(called, *args, &block) - @exception.__send__(called, *args, &block) + private + def method_missing(called, *args, &block) + @exception.__send__(called, *args, &block) end end @@ -31,15 +61,15 @@ def #{mname}(*args, &block) end end } - end + end @@monitor = Monitor.new attr_accessor :interface - + def initialize(interface) @interface = interface end - + def print_msg(*args) msg, *args = args xml_message = CGI.escapeHTML(msg % args) @@ -62,7 +92,7 @@ def print_error(*args) print CGI.escapeHTML(msg % args) end end - + def print_frames(context, current_frame_id) print_element("frames") do (0...context.stack_size).each do |id| @@ -70,18 +100,18 @@ def print_frames(context, current_frame_id) end end end - + def print_current_frame(frame_pos) print_debug "Selected frame no #{frame_pos}" end - + def print_frame(context, frame_id, current_frame_id) # idx + 1: one-based numbering as classic-debugger file = context.frame_file(frame_id) print "", - frame_id + 1, CGI.escapeHTML(File.expand_path(file)), context.frame_line(frame_id) + frame_id + 1, CGI.escapeHTML(File.expand_path(file)), context.frame_line(frame_id) end - + def print_contexts(contexts) print_element("threads") do contexts.each do |c| @@ -89,11 +119,11 @@ def print_contexts(contexts) end end end - + def print_context(context) print "", context.thnum, context.thread.status, Process.pid end - + def print_variables(vars, kind) print_element("variables") do # print self at top position @@ -103,26 +133,26 @@ def print_variables(vars, kind) end end end - + def print_array(array) print_element("variables") do - index = 0 - array.each { |e| - print_variable('[' + index.to_s + ']', e, 'instance') - index += 1 + index = 0 + array.each {|e| + print_variable('[' + index.to_s + ']', e, 'instance') + index += 1 } end end - + def print_hash(hash) print_element("variables") do - hash.keys.each { | k | + hash.keys.each {|k| if k.class.name == "String" name = '\'' + k + '\'' else - name = k.to_s + name = exec_with_allocation_control(k, :to_s, OverflowMessageType::EXCEPTION_MESSAGE) end - print_variable(name, hash[k], 'instance') + print_variable(name, hash[k], 'instance') } end end @@ -134,10 +164,83 @@ def print_string(string) InspectCommand.reference_result(bytes) print_variable('bytes', bytes, 'instance') end - print_variable('encoding', string.encoding, 'instance') if string.respond_to?('encoding') + print_variable('encoding', string.encoding, 'instance') if string.respond_to?('encoding') + end + end + + def exec_with_timeout(sec, error_message) + return yield if sec == nil or sec.zero? + if Thread.respond_to?(:critical) and Thread.critical + raise ThreadError, "timeout within critical session" + end + begin + x = Thread.current + y = DebugThread.start { + sleep sec + x.raise SimpleTimeLimitError.new(error_message) if x.alive? + } + yield sec + ensure + y.kill if y and y.alive? + end + end + + def exec_with_allocation_control(value, exec_method, overflow_message_type) + return value.send exec_method unless Debugger.trace_to_s + + memory_limit = Debugger.debugger_memory_limit + time_limit = Debugger.inspect_time_limit + + if defined?(JRUBY_VERSION) || RUBY_VERSION < '2.0' || memory_limit <= 0 + return exec_with_timeout(time_limit * 1e-3, "Timeout: evaluation of #{exec_method} took longer than #{time_limit}ms.") { value.send exec_method } + end + + require 'objspace' + trace_queue = Queue.new + + inspect_thread = DebugThread.start do + start_alloc_size = ObjectSpace.memsize_of_all + start_time = Time.now.to_f + + trace_point = TracePoint.new(:c_call, :call) do |tp| + curr_time = Time.now.to_f + + if (curr_time - start_time) * 1e3 > time_limit + trace_queue << TimeLimitError.new("Timeout: evaluation of #{exec_method} took longer than #{time_limit}ms.", caller.to_a) + trace_point.disable + inspect_thread.kill + end + + next unless rand > 0.75 + + curr_alloc_size = ObjectSpace.memsize_of_all + start_alloc_size = curr_alloc_size if curr_alloc_size < start_alloc_size + + if curr_alloc_size - start_alloc_size > 1e6 * memory_limit + trace_queue << MemoryLimitError.new("Out of memory: evaluation of #{exec_method} took more than #{memory_limit}mb.", caller.to_a) + trace_point.disable + inspect_thread.kill + end + end + trace_point.enable + result = value.send exec_method + trace_queue << result + trace_point.disable + end + + while(mes = trace_queue.pop) + if(mes.is_a? TimeLimitError or mes.is_a? MemoryLimitError) + print_debug(mes.message + "\n" + mes.backtrace.map {|l| "\t#{l}"}.join("\n")) + return overflow_message_type.call(mes) + else + return mes + end end + rescue SimpleTimeLimitError => e + print_debug(e.message) + return overflow_message_type.call(e) end - + def print_variable(name, value, kind) name = name.to_s if value.nil? @@ -147,7 +250,7 @@ def print_variable(name, value, kind) if value.is_a?(Array) || value.is_a?(Hash) has_children = !value.empty? if has_children - size = value.size + size = value.size value_str = "#{value.class} (#{value.size} element#{size > 1 ? "s" : "" })" else value_str = "Empty #{value.class}" @@ -155,27 +258,28 @@ def print_variable(name, value, kind) elsif value.is_a?(String) has_children = value.respond_to?('bytes') || value.respond_to?('encoding') value_str = value - else + else has_children = !value.instance_variables.empty? || !value.class.class_variables.empty? - value_str = value.to_s || 'nil' rescue "<#to_s method raised exception: #{$!}>" + + value_str = exec_with_allocation_control(value, :to_s, OverflowMessageType::EXCEPTION_MESSAGE) || 'nil' rescue "<#to_s method raised exception: #{$!}>" unless value_str.is_a?(String) - value_str = "ERROR: #{value.class}.to_s method returns #{value_str.class}. Should return String." + value_str = "ERROR: #{value.class}.to_s method returns #{value_str.class}. Should return String." end end if value_str.respond_to?('encode') # noinspection RubyEmptyRescueBlockInspection begin - value_str = value_str.encode("UTF-8") + value_str = value_str.encode("UTF-8") rescue end end value_str = handle_binary_data(value_str) escaped_value_str = CGI.escapeHTML(value_str) print("", - CGI.escapeHTML(name), build_compact_value_attr(value, value_str), kind, - build_value_attr(escaped_value_str), value.class, - has_children, value.respond_to?(:object_id) ? value.object_id : value.id) + CGI.escapeHTML(name), build_compact_value_attr(value, value_str), kind, + build_value_attr(escaped_value_str), value.class, + has_children, value.object_id) print("", escaped_value_str) if Debugger.value_as_nested_element print('') rescue StandardError => e @@ -198,28 +302,28 @@ def print_file_filter_status(status) def print_breakpoints(breakpoints) print_element 'breakpoints' do - breakpoints.sort_by{|b| b.id }.each do |b| - print "", b.id, b.source, b.pos.to_s + breakpoints.sort_by {|b| b.id}.each do |b| + print "", b.id, CGI.escapeHTML(b.source), b.pos.to_s end end end - + def print_breakpoint_added(b) - print "", b.id, b.source, b.pos + print "", b.id, CGI.escapeHTML(b.source), b.pos end - + def print_breakpoint_deleted(b) print "", b.id end - + def print_breakpoint_enabled(b) print "", b.id end - + def print_breakpoint_disabled(b) print "", b.id end - + def print_contdition_set(bp_id) print "", bp_id end @@ -239,37 +343,37 @@ def print_catchpoint_deleted(exception_class_name) def print_expressions(exps) print_element "expressions" do exps.each_with_index do |(exp, value), idx| - print_expression(exp, value, idx+1) + print_expression(exp, value, idx + 1) end end unless exps.empty? end - + def print_expression(exp, value, idx) print "", exp, value, idx end def print_expression_info(incomplete, prompt, indent) print "", - incomplete, CGI.escapeHTML(prompt), indent + incomplete, CGI.escapeHTML(prompt), indent end - + def print_eval(exp, value) - print "", CGI.escapeHTML(exp), value + print "", CGI.escapeHTML(exp), value end - + def print_pp(value) print value end - + def print_list(b, e, file, line) print "[%d, %d] in %s\n", b, e, file if (lines = Debugger.source_for(file)) b.upto(e) do |n| - if n > 0 && lines[n-1] + if n > 0 && lines[n - 1] if n == line - print "=> %d %s\n", n, lines[n-1].chomp + print "=> %d %s\n", n, lines[n - 1].chomp else - print " %d %s\n", n, lines[n-1].chomp + print " %d %s\n", n, lines[n - 1].chomp end end end @@ -277,7 +381,7 @@ def print_list(b, e, file, line) print "No source-file available for %s\n", file end end - + def print_methods(methods) print_element "methods" do methods.each do |method| @@ -285,31 +389,31 @@ def print_methods(methods) end end end - + # Events - + def print_breakpoint(_, breakpoint) - print("", - breakpoint.source, breakpoint.pos, Debugger.current_context.thnum) + print("", + CGI.escapeHTML(breakpoint.source), breakpoint.pos, Debugger.current_context.thnum) end - + def print_catchpoint(exception) context = Debugger.current_context - print("", - context.frame_file(0), context.frame_line(0), exception.class, CGI.escapeHTML(exception.to_s), context.thnum) + print("", + CGI.escapeHTML(context.frame_file(0)), context.frame_line(0), exception.class, CGI.escapeHTML(exception.to_s), context.thnum) end - + def print_trace(context, file, line) Debugger::print_debug "trace: location=\"%s:%s\", threadId=%d", file, line, context.thnum # TBD: do we want to clog fronend with the elements? There are tons of them. # print "", file, line, context.thnum end - + def print_at_line(context, file, line) print "", CGI.escapeHTML(File.expand_path(file)), line, context.thnum, context.stack_size end - + def print_exception(exception, _) print_element("variables") do proxy = ExceptionProxy.new(exception) @@ -317,21 +421,21 @@ def print_exception(exception, _) print_variable('error', proxy, 'exception') end rescue Exception - print "", - exception.class, CGI.escapeHTML(exception.to_s) + print "", + exception.class, CGI.escapeHTML(exception.to_s) end - + def print_inspect(eval_result) - print_element("variables") do + print_element("variables") do print_variable("eval_result", eval_result, 'local') end end - - def print_load_result(file, exception=nil) + + def print_load_result(file, exception = nil) if exception - print("", file, exception.class, CGI.escapeHTML(exception.to_s)) + print("", file, exception.class, CGI.escapeHTML(exception.to_s)) else - print("", file) + print("", file) end end @@ -345,7 +449,7 @@ def print_element(name) end private - + def print(*params) Debugger::print_debug(*params) @interface.print(*params) @@ -381,27 +485,44 @@ def max_compact_name_size end def compact_array_str(value) - slice = value[0..10] - compact = slice.inspect - if value.size != slice.size - compact[0..compact.size-2] + ", ...]" + slice = value[0..10] + + compact = exec_with_allocation_control(slice, :inspect, OverflowMessageType::NIL_MESSAGE) + + if compact && value.size != slice.size + compact[0..compact.size - 2] + ", ...]" end compact end def compact_hash_str(value) - slice = value.sort_by { |k, _| k.to_s }[0..5] - compact = slice.map { |kv| "#{kv[0]}: #{handle_binary_data(kv[1])}" }.join(", ") + keys_strings = Hash.new + + slice = value.sort_by do |k, _| + keys_string = exec_with_allocation_control(k, :to_s, OverflowMessageType::SPECIAL_SYMBOL_MESSAGE) + keys_strings[k] = keys_string + keys_string + end[0..5] + + compact = slice.map do |kv| + key_string = keys_strings[kv[0]] + value_string = exec_with_allocation_control(kv[1], :to_s, OverflowMessageType::SPECIAL_SYMBOL_MESSAGE) + "#{key_string}: #{handle_binary_data(value_string)}" + end.join(", ") "{" + compact + (slice.size != value.size ? ", ..." : "") + "}" end def build_compact_value_attr(value, value_str) - compact_value_str = build_compact_name(value, value_str) + compact_value_str = build_compact_name(value, value_str) compact_value_str.nil? ? '' : "compactValue=\"#{CGI.escapeHTML(compact_value_str)}\"" end def safe_to_string(value) - str = value.to_s + begin + str = value.to_s + rescue NoMethodError + str = "(Object doesn't support #to_s)" + end return str unless str.nil? string_io = StringIO.new @@ -418,7 +539,7 @@ def build_value_attr(escaped_value_str) protect m end end - + end -end +end \ No newline at end of file diff --git a/ruby-debug-ide.gemspec b/ruby-debug-ide.gemspec index 00d2e45..7cd29ef 100644 --- a/ruby-debug-ide.gemspec +++ b/ruby-debug-ide.gemspec @@ -32,10 +32,11 @@ EOF spec.author = "Markus Barchfeld, Martin Krauskopf, Mark Moseley, JetBrains RubyMine Team" spec.email = "rubymine-feedback@jetbrains.com" + spec.license = "MIT" spec.platform = Gem::Platform::RUBY spec.require_path = "lib" spec.bindir = "bin" - spec.executables = ["rdebug-ide"] + spec.executables = ["rdebug-ide", "gdb_wrapper"] spec.files = FILES spec.extensions << "ext/mkrf_conf.rb" unless ENV['NO_EXT'] diff --git a/test-base/test_base.rb b/test-base/test_base.rb index 3399c8b..01bd85d 100644 --- a/test-base/test_base.rb +++ b/test-base/test_base.rb @@ -57,7 +57,7 @@ def teardown send_ruby("cont") end debug "Waiting for the server process to finish..." - (config_load('server_start_up_timeout')*4).times do + (config_load('server_start_up_timeout')*5).times do unless @process_finished debug '.' sleep 0.25 @@ -86,7 +86,7 @@ def start_ruby_process(script) @port = TestBase.find_free_port cmd = debug_command(script, @port) debug "Starting: #{cmd}\n" - + Thread.new do if RUBY_VERSION < '1.9' (_, p_out, p_err) = Open3.popen3(cmd) @@ -132,7 +132,8 @@ def TestBase.find_free_port(port = 1098) end def create_file(script_name, lines) - script_path = File.join(TMP_DIR, script_name) + script_path = File.realdirpath(File.join(TMP_DIR, script_name)) + File.open(script_path, "w") do |script| script.printf(lines.join("\n")) end @@ -141,7 +142,7 @@ def create_file(script_name, lines) def create_test2(lines) @test2_name = "test2.rb" - @test2_path = create_file(@test2_name, lines) + @test2_path = create_file(@test2_name, lines).force_encoding(Encoding::UTF_8) end # Creates test.rb with the given lines, set up @test_name and @test_path @@ -222,7 +223,9 @@ def assert_suspension(exp_file, exp_line, exp_frames, exp_thread_id=1) suspension = read_suspension assert_equal(exp_file, suspension.file) assert_equal(exp_line, suspension.line) + exp_frames += 2 if Debugger::FRONT_END == "debase" assert_equal(exp_frames, suspension.frames) + exp_thread_id += 1 if Debugger::FRONT_END == "debase" assert_equal(exp_thread_id, suspension.threadId) end @@ -271,6 +274,7 @@ def assert_exception(exp_file, exp_line, exp_type, exp_thread_id=1) assert_equal(exp_file, exception.file) assert_equal(exp_line, exception.line) assert_equal(exp_type, exception.type) + exp_thread_id += 1 if Debugger::FRONT_END == "debase" assert_equal(exp_thread_id, exception.threadId) assert_not_nil(exception.message) end diff --git a/test-base/threads_and_frames_test.rb b/test-base/threads_and_frames_test.rb index c96b19a..e04cc8f 100644 --- a/test-base/threads_and_frames_test.rb +++ b/test-base/threads_and_frames_test.rb @@ -14,7 +14,11 @@ def test_frames assert_breakpoint_added_no(2) send_ruby("w") frames = read_frames - assert_equal(2, frames.length) + + needed_frame_length = 2 + needed_frame_length += 2 if Debugger::FRONT_END == "debase" + assert_equal(needed_frame_length, frames.length) + frame1 = frames[0] assert_equal(@test2_path, frame1.file) assert_equal(1, frame1.no) @@ -27,7 +31,11 @@ def test_frames assert_test_breakpoint(4) send_ruby("w") frames = read_frames - assert_equal(1, frames.length) + + needed_frame_length = 1 + needed_frame_length += 2 if Debugger::FRONT_END == "debase" + + assert_equal(needed_frame_length, frames.length) send_cont # test:4 -> test2:3 assert_breakpoint("test2.rb", 3) send_cont # test2:3 -> finish @@ -42,7 +50,11 @@ def test_frames_when_thread_spawned "def calc", "5 + 5", "end", "start_thread()", "calc()"] run_to_line(5) send_ruby("w") - assert_equal(2, read_frames.length) + + needed_length = 2 + needed_length += 2 if Debugger::FRONT_END == "debase" + + assert_equal(needed_length, read_frames.length) send_cont end diff --git a/test-base/variables_test.rb b/test-base/variables_test.rb index e0d3140..67f9b48 100644 --- a/test-base/variables_test.rb +++ b/test-base/variables_test.rb @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# encoding: utf-8 $:.unshift File.join(File.dirname(__FILE__), "..", "lib") @@ -25,7 +26,7 @@ def test_variable_with_xml_content {:name => "stringA"}, {:name => "testHashValue"}) # will receive '' - assert_equal("", variables[0].value) + assert_equal(CGI.escapeHTML(""), variables[0].value) assert_local(variables[0]) # the testHashValue contains an example, where the name consists of special # characters @@ -46,7 +47,7 @@ def test_variable_in_object {:name => "self", :value => "test", :type => "Test", :hasChildren => true}) send_ruby("v i self") assert_variables(read_variables, 1, - {:name => "@y", :value => "5", :type => "Fixnum", :hasChildren => false, :kind => "instance"}) + {:name => "@y", :value => "5", :type => int_type_name, :hasChildren => false, :kind => "instance"}) send_cont end @@ -59,7 +60,7 @@ def test_class_variables {:name => "self", :hasChildren => true}) send_ruby("v i self") assert_variables(read_variables, 1, - {:name => "@@class_var", :value => "55", :type => "Fixnum", :kind => "class"}) + {:name => "@@class_var", :value => "55", :type => int_type_name, :kind => "class"}) send_cont end @@ -69,7 +70,7 @@ def test_singleton_class_variables run_to_line(3) send_ruby("v i self") assert_variables(read_variables, 1, - {:name => "@@class_var", :value => "55", :type => "Fixnum", :hasChildren => false, :kind => "class"}) + {:name => "@@class_var", :value => "55", :type => int_type_name, :hasChildren => false, :kind => "class"}) send_cont end @@ -94,17 +95,19 @@ def test_variable_local assert_not_nil variables[1].objectId send_ruby("v i " + variables[1].objectId) # 'user' variable assert_variables(read_variables, 1, - {:name => "@id", :value => "22", :type => "Fixnum", :hasChildren => false}) + {:name => "@id", :value => "22", :type => int_type_name, :hasChildren => false}) send_cont end def test_variable_instance - create_socket ["require 'test2.rb'", "custom_object=Test2.new", "puts custom_object"] + create_socket ["require_relative 'test2.rb'", "custom_object=Test2.new", "puts custom_object"] create_test2 ["class Test2", "def initialize", "@y=5", "end", "def to_s", "'test'", "end", "end"] run_to("test2.rb", 6) - send_ruby("frame 3; v i custom_object") + frame_number = 3 + frame_number -= 1 if Debugger::FRONT_END == "debase" + send_ruby("frame #{frame_number}; v i custom_object") assert_variables(read_variables, 1, - {:name => "@y", :value => "5", :type => "Fixnum", :hasChildren => false}) + {:name => "@y", :value => "5", :type => int_type_name, :hasChildren => false}) send_cont end @@ -116,7 +119,7 @@ def test_variable_array {:name => "array", :type => "Array", :hasChildren => true}) send_ruby("v i array") assert_variables(read_variables, 2, - {:name => "[0]", :value => "1", :type => "Fixnum"}) + {:name => "[0]", :value => "1", :type => int_type_name}) send_cont end @@ -128,7 +131,7 @@ def test_variable_hash_with_string_keys {:name => "hash", :hasChildren => true}) send_ruby("v i hash") assert_variables(read_variables, 2, - {:name => "'a'", :value => "z", :type => "String"}) + {:name => CGI.escape_html("'a'"), :value => "z", :type => "String"}) send_cont end @@ -149,7 +152,7 @@ def test_variable_hash_with_object_keys # get the value send_ruby("frame 1 ; v i " + elements[0].objectId) assert_variables(read_variables, 1, - {:name => "@a", :value => "66", :type => "Fixnum"}) + {:name => "@a", :value => "66", :type => int_type_name}) send_cont end @@ -176,7 +179,7 @@ def test_non_string_from_to_s create_socket ["class BugExample; def to_s; 1; end; end", "b = BugExample.new", "sleep 0.01"] run_to_line(3) send_ruby("v local") - assert_variables(read_variables, 1, {:value => "ERROR: BugExample.to_s method returns Fixnum. Should return String."}) + assert_variables(read_variables, 1, {:value => "ERROR: BugExample.to_s method returns #{int_type_name}. Should return String."}) send_cont end @@ -196,6 +199,34 @@ def test_to_s_raises_exception send_cont end + def test_to_s_timelimit + create_socket ['class A', + 'def to_s', + 'a = 1', + 'loop do', + 'a = a + 1', + 'sleep 1', + 'break if (a > 2)', + 'end', + 'a.to_s', + 'end', + 'end', + 'b = Hash.new', + 'b[A.new] = A.new', + 'b[1] = A.new', + 'puts b #bp here'] + run_to_line(15) + send_ruby('v l') + assert_variables(read_variables, 1, + {:name => "b", :value => "Hash (2 elements)", :type => "Hash"}) + + send_ruby("v i b") + assert_variables(read_variables, 2, + {:name => "Timeout: evaluation of to_s took longer than 100ms.", :value => "Timeout: evaluation of to_s took longer than 100ms.", :type => "A"}, + {:name => "1", :value => "Timeout: evaluation of to_s took longer than 100ms.", :type => "A"}) + send_cont + end + def assert_xml(expected_xml, actual_xml) # XXX is there a better way then html_escape in standard libs? assert_equal(ERB::Util.html_escape(expected_xml), actual_xml) @@ -230,5 +261,11 @@ def assert_variables(vars, count, *expected) end end + private + + def int_type_name + (Fixnum || Integer).name + end + end diff --git a/test/rd_test_base.rb b/test/rd_test_base.rb index d872f4e..7d29edc 100644 --- a/test/rd_test_base.rb +++ b/test/rd_test_base.rb @@ -24,7 +24,7 @@ def debug_command(script, port) cmd << " -J-Xdebug -J-Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y" if jruby? and debug_jruby? cmd << " -I 'lib:#{File.dirname(script)}' #{@rdebug_ide}" + (@verbose_server ? " -d" : "") + - " -p #{port} -- '#{script}'" + " -p #{port} --evaluation-control --time-limit 100 --memory-limit 0 -- '#{script}'" end def start_debugger diff --git a/test/ruby-debug/xml_printer_test.rb b/test/ruby-debug/xml_printer_test.rb index 08eb537..ab58014 100644 --- a/test/ruby-debug/xml_printer_test.rb +++ b/test/ruby-debug/xml_printer_test.rb @@ -48,8 +48,8 @@ def test_print_frames test_path = File.join(Dir.pwd, 'test.rb') expected = [ "", - "", - "", + "", + "", ""] assert_equal(expected, interface.data) end @@ -64,7 +64,10 @@ def test_print_at_line Debugger.stop end test_path = File.join(Dir.pwd, 'test.rb') - expected = [""] + + #TODO investigate + expected = [""] + assert_equal(expected, interface.data) end