diff --git a/.gitignore b/.gitignore index 4040c6c1..81a1c37f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ .bundle Gemfile.lock pkg/* +.ruby-version +.ruby-gemset +.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..f812bcbb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +language: ruby +dist: trusty +matrix: + fast_finish: true + include: + - os: linux + rvm: 3.2 + - os: osx + rvm: 3.2 + - os: linux + rvm: 3.1 + - os: osx + rvm: 3.1 + - os: linux + rvm: 3.0 + - os: osx + rvm: 3.0 + - os: linux + rvm: 2.7 + - os: osx + rvm: 2.7 + - os: linux + rvm: 2.6 + - os: osx + rvm: 2.6 + - os: linux + rvm: 2.5 + - os: osx + rvm: 2.5 + - os: linux + rvm: 2.4 + - os: osx + rvm: 2.4 + - os: linux + rvm: 2.3 diff --git a/ChangeLog b/ChangeLog.md similarity index 80% rename from ChangeLog rename to ChangeLog.md index 97b8fc54..8593f56e 100644 --- a/ChangeLog +++ b/ChangeLog.md @@ -1,3 +1,99 @@ +## [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 +* "exclude file|dir" command added + +## [0.5.0](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.33...v0.5.0) + +* catchpointDeleted event added (under --catchpoint-deleted-event flag) +* --value-as-nested-element to enable just this the extension + +## [0.4.33](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.32...v0.4.33) + +* Fixed problem with inspecting Jbuilder + [RUBY-16838](https://youtrack.jetbrains.com/issue/RUBY-16838) + +## [0.4.32](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.31...v0.4.32) + +* Fixed problem with preloading psych + [RUBY-16721](https://youtrack.jetbrains.com/issue/RUBY-16721) + +## [0.4.31](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.30...v0.4.31) + +* need to handle mock objects somehow + [RUBY-16665](https://youtrack.jetbrains.com/issue/RUBY-16665) + +## [0.4.30](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.29...v0.4.30) + +* reverting fix for + [RUBY-16192](https://youtrack.jetbrains.com/issue/RUBY-16192) to resolve + [RUBY-16435](https://youtrack.jetbrains.com/issue/RUBY-16435) + +* unescaping of empty line fixed + [RUBY-16600](https://youtrack.jetbrains.com/issue/RUBY-16600) + +## [0.4.29](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.28...v0.4.29) + +* Fixed problem with evaluating "%" + [RUBY-16244](https://youtrack.jetbrains.com/issue/RUBY-16244) + +## [0.4.28](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.27...v0.4.28) + +* [better handling for escaped chars and slashes](https://github.com/ruby-debug/ruby-debug-ide/pull/68) + +## [0.4.27](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.26...v0.4.27) + +* Redundant quotes dropped from string variable representation + [RUBY-16275](https://youtrack.jetbrains.com/issue/RUBY-16275) + +## [0.4.26](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.25...v0.4.26) + +* Compact value for inline debugger should be really compact + [RUBY-15932](https://youtrack.jetbrains.com/issue/RUBY-15932) + +## [0.4.25](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.24...v0.4.25) + +* Let's use String#inspect in print variable for String variables + [RUBY-16192](https://youtrack.jetbrains.com/issue/RUBY-16192) + +## [0.4.24](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.24.beta5...v0.4.24) + +* time to release + +## [0.4.24.beta5](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.24.beta4...v0.4.24.beta5) + +* do not print empty value attr in case RubyMine-specific protocol extensions are enabled + +## [0.4.24.beta4](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.23...v0.4.24.beta4) + +* Performance optimisation for variable representation [RUBY-16055](https://youtrack.jetbrains.com/issue/RUBY-16055) +* Added command line argument to enable RubyMine-specific protocol extensions +* Several fixes to make debugger more robust [RUBY-16070](https://youtrack.jetbrains.com/issue/RUBY-16070) + +## [0.4.23](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.23.beta11...v0.4.23) + +* fixed problem with compact name for binary params (strings with invalid encoding) + [RUBY-15960](https://youtrack.jetbrains.com/issue/RUBY-15960) + +## [0.4.23.beta11](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.23.beta10...v0.4.23.beta11) + +* adding breakpoint in non-existing file should not break debugger + [RUBY-15873](https://youtrack.jetbrains.com/issue/RUBY-15873) + +## [0.4.23.beta10](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.23.beta9...v0.4.23.beta10) + +* fixed problem with printing hashes [RUBY-15804](https://youtrack.jetbrains.com/issue/RUBY-15804) + +## [0.4.23.beta9](https://github.com/ruby-debug/ruby-debug-ide/compare/v0.4.23.beta8...v0.4.23.beta9) + +* problem with calculating local variables for 1.8 fixed + +## "per-historical" changes Dennis Ushakov * run context commands on stopped thread to prevent segfaults (3c1b52d5091fccec447d5695d5b43e73f335cc54) * enable building without deps (9b597f8ce2b97ed40bb57e55ad178cf8ce270fa9) diff --git a/Gemfile b/Gemfile index 2ee4fb3c..af81c0e6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,26 @@ 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.map do |v| + %w(ruby mingw x64_mingw).map do |platform| + "#{platform}_#{v}".to_sym unless platform == "x64_mingw" && v < "2.0" + end.delete_if &:nil? + end.flatten +end + +if RUBY_VERSION < '1.9' || defined?(JRUBY_VERSION) + gem "ruby-debug-base", :platforms => [:jruby, *mries('18')] +end + +if RUBY_VERSION && RUBY_VERSION >= "1.9" && RUBY_VERSION < "2.0" + gem "ruby-debug-base19x", ">= 0.11.32" +end + +if RUBY_VERSION && RUBY_VERSION >= "2.0" + gem "debase", "~> 0.2", ">= 0.2.9" +end gemspec @@ -10,6 +29,10 @@ group :development do end group :test do - gem "test-unit" + if RUBY_VERSION < "1.9" + gem "test-unit", "~> 2.1.2" + else + gem "test-unit" + end end diff --git a/README.md b/README.md new file mode 100644 index 00000000..df075152 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +[![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) + +[gem]: https://rubygems.org/gems/ruby-debug-ide +# ruby-debug-ide + +The 'ruby-debug-ide' gem provides the protocol to establish communication between the debugger engine (such as [debase](https://rubygems.org/gems/debase) or [ruby-debug-base](https://rubygems.org/gems/ruby-debug-base)) and IDEs (for example, RubyMine, Visual Studio Code, or Eclipse). 'ruby-debug-ide' redirect commands from the IDE to the debugger engine. Then, it returns answers/events received from the debugger engine to the IDE. To learn more about a communication protocol, see the following document: [ruby-debug-ide protocol](protocol-spec.md). + +## Install debugging gems +Depending on the used Ruby version, you need to add/install the following debugging gems to the Gemfile: +- Ruby 2.x - [ruby-debug-ide](https://rubygems.org/gems/ruby-debug-ide) and [debase](https://rubygems.org/gems/debase) +- Ruby 1.9.x - [ruby-debug-ide](https://rubygems.org/gems/ruby-debug-ide) and [ruby-debug-base19x](https://rubygems.org/gems/ruby-debug-base19x) +- jRuby or Ruby 1.8.x - [ruby-debug-ide](https://rubygems.org/gems/ruby-debug-ide) and [ruby-debug-base](https://rubygems.org/gems/ruby-debug-base) +> For Windows, make sure that the Ruby [DevKit](https://github.com/oneclick/rubyinstaller/wiki/Development-Kit) is installed. + +## Start debugging session +To start the debugging session for a Rails application, run the following command: +```shell +rdebug-ide --host 0.0.0.0 --port 1234 --dispatcher-port 1234 -- bin/rails s +``` +If you want to debug a Rails application run using Docker Compose, you need to start the Rails server from the Docker in the following way: +```yaml +command: bundle exec rdebug-ide --host 0.0.0.0 --port 1234 -- bin/rails s -p 3000 -b 0.0.0.0 +volumes: + - .:/sample_rails_app +ports: + - "1234:1234" + - "3000:3000" + - "26162:26162" +``` +Note that all ports above should be exposed in the Dockerfile. \ No newline at end of file diff --git a/Rakefile b/Rakefile index a169aeec..7a74d18c 100644 --- a/Rakefile +++ b/Rakefile @@ -39,4 +39,55 @@ task :changelog, :since_c, :until_c do |t,args| end puts changelog_content +end + +desc "Generates travis.yaml" +task :gen_travis do + versions = [] + + def versions.add(major:, minor:, include_macos: true) + self << { major: major, minor: [minor], include_macos: include_macos } + end + + versions.add major: '3.0', minor: 1 + versions.add major: '2.7', minor: 3 + versions.add major: '2.6', minor: 7 + versions.add major: '2.5', minor: 9 + versions.add major: '2.4', minor: 10 + versions.add major: '2.3', minor: 8, include_macos: false + versions.add major: '2.2', minor: 10, include_macos: false + versions.add major: '2.1', minor: 10, include_macos: false + versions.add major: '2.0', minor: 0, include_macos: false + versions.add major: '1.9', minor: 3, include_macos: false + versions.add major: '1.8', minor: 7, include_macos: false + + puts < 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 + 'frame_bind' => false, + 'host' => nil, + 'load_mode' => false, + 'port' => 1234, + 'stop' => false, + 'tracing' => false, + 'skip_wait_for_start' => 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, + 'key_value_mode' => false, + 'socket_path' => nil ) opts = OptionParser.new do |opts| opts.banner = < 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("--skip_wait_for_start", "skip wait for 'start' command") {options.skip_wait_for_start = 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 @@ -46,9 +81,29 @@ 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("--key-value", "Key/Value presentation of hash items") do + options.key_value_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 + options.rm_protocol_extensions = true + end + opts.on("--catchpoint-deleted-event", "Enable chatchpointDeleted event") do + options.catchpoint_deleted_event = true + end + opts.on("--value-as-nested-element", "Allow to pass variable's value as nested element instead of attribute") do + options.value_as_nested_element = true + end + opts.on("--socket-path PATH", "Listen for debugger on the given UNIX domain socket path") do |path| + options.socket_path = path + end opts.separator "" opts.separator "Common options:" opts.on_tail("-v", "--version", "Show version") do @@ -72,39 +127,68 @@ 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) +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 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.key_value_mode = options.key_value_mode + +if options.attach_mode + if Debugger::FRONT_END == "debase" + Debugger.init_variables + end -Debugger.debug_program(options) + 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/ext/mkrf_conf.rb b/ext/mkrf_conf.rb index cbb626e3..290936aa 100644 --- a/ext/mkrf_conf.rb +++ b/ext/mkrf_conf.rb @@ -1,11 +1,6 @@ -jruby = defined?(JRUBY_VERSION) || (defined?(RUBY_ENGINE) && 'jruby' == RUBY_ENGINE) +install_dir = File.expand_path("../../../..", __FILE__) -def already_installed(dep) - Gem::DependencyInstaller.new(:domain => :local).find_gems_with_sources(dep) || - Gem::DependencyInstaller.new(:domain => :local,:prerelease => true).find_gems_with_sources(dep) -end - -unless jruby +if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby' require 'rubygems' require 'rubygems/command.rb' require 'rubygems/dependency.rb' @@ -13,31 +8,31 @@ def already_installed(dep) begin Gem::Command.build_args = ARGV - rescue NoMethodError + rescue NoMethodError end if RUBY_VERSION < "1.9" dep = Gem::Dependency.new("ruby-debug-base", '>=0.10.4') elsif RUBY_VERSION < '2.0' - dep = Gem::Dependency.new("ruby-debug-base19x", '>=0.11.24') - else - # dep = Gem::Dependency.new("debase", '> 0') + dep = Gem::Dependency.new("ruby-debug-base19x", '>=0.11.30.pre15') + else + dep = Gem::Dependency.new("debase", '> 0') end begin puts "Installing base gem" - inst = Gem::DependencyInstaller.new + inst = Gem::DependencyInstaller.new(:prerelease => dep.prerelease?, :install_dir => install_dir) inst.install dep rescue - inst = Gem::DependencyInstaller.new(:prerelease => true) begin + inst = Gem::DependencyInstaller.new(:prerelease => true, :install_dir => install_dir) inst.install dep rescue Exception => e puts e puts e.backtrace.join "\n " exit(1) end - end unless dep.nil? || already_installed(dep) + end unless dep.nil? || dep.matching_specs.any? end # create dummy rakefile to indicate success diff --git a/lib/ruby-debug-ide.rb b/lib/ruby-debug-ide.rb index cb6ad768..bc61c90e 100644 --- a/lib/ruby-debug-ide.rb +++ b/lib/ruby-debug-ide.rb @@ -2,20 +2,29 @@ require 'stringio' require "socket" require 'thread' -if (RUBY_VERSION < '2.0') +if RUBY_VERSION < '2.0' || defined?(JRUBY_VERSION) require 'ruby-debug-base' + Debugger::FRONT_END = "ruby-debug-base" else require 'debase' -end + 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' module Debugger - + class << self + def find_free_port(host) + server = TCPServer.open(host, 0) + port = server.addr[1] + server.close + port + end + # Prints to the stderr using printf(*args) if debug logging flag (-d) is on. def print_debug(*args) if Debugger.cli_debug @@ -25,27 +34,32 @@ def print_debug(*args) $stderr.flush end 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 :cli_debug, :xml_debug + + attr_accessor :attached + attr_accessor :key_value_mode + 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 + attr_accessor :catchpoint_deleted_event, :value_as_nested_element + - # # Interrupts the last debugged thread # @@ -59,24 +73,30 @@ def interrupt_last end end - def start_server(host = nil, port = 1234) - return if started? - start - start_control(host, port) + def start_server(host = nil, port = 1234, notify_dispatcher = false) + _start_server_common(host, port, nil, notify_dispatcher) end - def prepare_debugger(options) - start_server(options.host, options.port) + def start_server_unix(socket_path, notify_dispatcher = false) + _start_server_common(nil, 0, socket_path, notify_dispatcher) + end - raise "Control thread did not start (#{@control_thread}}" unless @control_thread && @control_thread.alive? - + def prepare_debugger(options) @mutex = Mutex.new @proceed = ConditionVariable.new - + + if options.socket_path.nil? + start_server(options.host, options.port, options.notify_dispatcher) + else + start_server_unix(options.socket_path, options.notify_dispatcher) + end + + raise "Control thread did not start (#{@control_thread}}" unless @control_thread && @control_thread.alive? + # wait for 'start' command @mutex.synchronize do @proceed.wait(@mutex) - end + end unless options.skip_wait_for_start end def debug_program(options) @@ -89,39 +109,74 @@ def debug_program(options) $stderr.print Debugger.cleanup_backtrace(bt.backtrace).map{|l| "\t#{l}"}.join("\n"), "\n" end end - + def run_prog_script return unless @mutex @mutex.synchronize do @proceed.signal end end - - def start_control(host, port) + + def start_control(host, port, notify_dispatcher) + _start_control_common(host, port, nil, notify_dispatcher) + end + + def start_control_unix(socket_path, notify_dispatcher) + _start_control_common(nil, 0, socket_path, notify_dispatcher) + end + + private + + def _start_server_common(host, port, socket_path, notify_dispatcher) + return if started? + start + _start_control_common(host, port, socket_path, notify_dispatcher) + end + + def _start_control_common(host, port, socket_path, notify_dispatcher) raise "Debugger is not started" unless started? return if @control_thread @control_thread = DebugThread.new do begin - # 127.0.0.1 seemingly works with all systems and with IPv6 as well. - # "localhost" and nil have problems on some systems. - host ||= '127.0.0.1' - gem_name = (defined?(JRUBY_VERSION) || RUBY_VERSION < '1.9.0') ? 'ruby-debug-base' : - RUBY_VERSION < '2.0.0' ? 'ruby-debug-base19x' : 'debase' - $stderr.printf "Fast Debugger (ruby-debug-ide #{IDE_VERSION}, #{gem_name} #{VERSION}) listens on #{host}:#{port}\n" - server = TCPServer.new(host, port) + if socket_path.nil? + # 127.0.0.1 seemingly works with all systems and with IPv6 as well. + # "localhost" and nil have problems on some systems. + host ||= '127.0.0.1' + + server = notify_dispatcher_if_needed(host, port, notify_dispatcher) do |real_port, port_changed| + s = TCPServer.new(host, real_port) + print_greeting_msg $stderr, host, real_port, port_changed ? "Subprocess" : "Fast" if defined? IDE_VERSION + s + end + else + raise "Cannot specify host and socket_file at the same time" if host + File.delete(socket_path) if File.exist?(socket_path) + server = UNIXServer.new(socket_path) + print_greeting_msg $stderr, nil, nil, "Fast", socket_path if defined? IDE_VERSION + end + + return unless server + while (session = server.accept) - $stderr.puts "Connected from #{session.addr[2]}" if Debugger.cli_debug + if Debugger.cli_debug + if session.peeraddr == 'AF_INET' + $stderr.puts "Connected from #{session.peeraddr[2]}" + else + $stderr.puts "Connected from local client" + end + end dispatcher = ENV['IDE_PROCESS_DISPATCHER'] - if (dispatcher) - ENV['IDE_PROCESS_DISPATCHER'] = "#{session.addr[2]}:#{dispatcher}" unless dispatcher.include?(":") - end + if dispatcher + ENV['IDE_PROCESS_DISPATCHER'] = "#{session.peeraddr[2]}:#{dispatcher}" unless dispatcher.include?(":") + ENV['DEBUGGER_HOST'] = host + end begin @interface = RemoteInterface.new(session) self.handler = EventProcessor.new(interface) IdeControlCommandProcessor.new(interface).process_commands rescue StandardError, ScriptError => ex bt = ex.backtrace - $stderr.printf "#{Process.pid}: Exception in DebugThread loop: #{ex.message}\nBacktrace:\n#{bt ? bt.join("\n from: ") : ""}\n" + $stderr.printf "#{Process.pid}: Exception in DebugThread loop: #{ex.message}(#{ex.class})\nBacktrace:\n#{bt ? bt.join("\n from: ") : ""}\n" exit 1 end end @@ -132,9 +187,41 @@ def start_control(host, port) end end end - + + def notify_dispatcher_if_needed(host, port, need_notify) + return yield port unless need_notify + + 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) + dispatcher_answer = s.gets.chomp + + if dispatcher_answer == "true" + port = Debugger.find_free_port(host) + end + + server = yield port, dispatcher_answer == "true" + + s.print(port) + s.close + connected = true + print_debug "Ide process dispatcher notified about sub-debugger which listens on #{port}\n" + return server + rescue => bt + $stderr.puts "#{Process.pid}: connection failed(#{i+1})" + $stderr.puts "Exception: #{bt}" + $stderr.puts bt.backtrace.map { |l| "\t#{l}" }.join("\n") + sleep 0.3 + end unless connected + end + end end - + class Exception # :nodoc: attr_reader :__debug_file, :__debug_line, :__debug_binding, :__debug_context end 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 00000000..d9150a85 --- /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 00000000..bf9085c0 --- /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 00000000..b9bdc312 --- /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 00000000..05ea9d90 --- /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 00000000..cee7ea7b --- /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 00000000..9ab535ed --- /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 24a970ec..ecbd2ba5 100644 --- a/lib/ruby-debug-ide/command.rb +++ b/lib/ruby-debug-ide/command.rb @@ -1,8 +1,16 @@ +if RUBY_VERSION < '2.0' || defined?(JRUBY_VERSION) + require 'ruby-debug-base' +else + require 'debase' +end + +require 'ruby-debug-ide/thread_alias' require 'ruby-debug-ide/helper' +require 'delegate' module Debugger - class Command # :nodoc: + class Command < SimpleDelegator # :nodoc: SubcmdStruct=Struct.new(:name, :min, :short_help, :long_help) unless defined?(SubcmdStruct) @@ -63,10 +71,21 @@ def method_missing(meth, *args, &block) def options @options ||= {} end + + def unescape_incoming(str) + str.gsub(/((?:^|[^\\])(?:\\\\)*)((?:\\n)+)/) do |_| + $1 + "\n" * ($2.size / 2) + end.gsub(/\\\\/, '\\') + end + + def file_filter_supported? + defined?(Debugger.file_filter) + end end def initialize(state, printer) @state, @printer = state, printer + super @printer end def match(input) @@ -75,15 +94,6 @@ def match(input) protected - def method_missing(meth, *args, &block) - if @printer.respond_to? meth - @printer.send meth, *args, &block - else - super - end - end - - # FIXME: use delegate? def errmsg(*args) @printer.print_error(*args) end @@ -111,16 +121,26 @@ def timeout(sec) y.kill if y and y.alive? end end - + def debug_eval(str, b = get_binding) begin str = str.to_s - max_time = 10 - to_inspect = str.gsub(/\\n/, "\n") - @printer.print_debug("Evaluating #{str} with timeout after %i sec", max_time) + 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) + + Debugger::TimeoutHandler.do_thread_alias + + eval_result = nil + timeout(max_time) do - eval(to_inspect, b) + eval_result = eval(to_inspect, b) end + + Debugger::TimeoutHandler.undo_thread_alias + + return eval_result rescue StandardError, ScriptError => e @printer.print_exception(e, @state.binding) throw :debug_error @@ -136,19 +156,8 @@ def debug_silent_eval(str) end end - def hbinding(hash) - code = hash.keys.map{|k| "#{k} = hash['#{k}']"}.join(';') + ';binding' - if obj = @state.context.frame_self(@state.frame_pos) - obj.instance_eval code - else - eval code - end - end - private :hbinding - def get_binding - binding = @state.context.frame_binding(@state.frame_pos) - binding || hbinding(@state.context.frame_locals(@state.frame_pos)) + @state.context.frame_binding(@state.frame_pos) end def line_at(file, line) @@ -157,7 +166,21 @@ def line_at(file, line) def get_context(thnum) Debugger.contexts.find{|c| c.thnum == thnum} - end + end + + def realpath(filename) + is_dir = filename.end_with?(File::SEPARATOR) + if filename.index(File::SEPARATOR) || File::ALT_SEPARATOR && filename.index(File::ALT_SEPARATOR) + filename = File.expand_path(filename) + end + if (RUBY_VERSION < '1.9') || (RbConfig::CONFIG['host_os'] =~ /mswin/) + filename + else + filename = File.realpath(filename) rescue filename + filename = filename + File::SEPARATOR if is_dir && !filename.end_with?(File::SEPARATOR) + filename + end + end end Command.load_commands diff --git a/lib/ruby-debug-ide/commands/breakpoints.rb b/lib/ruby-debug-ide/commands/breakpoints.rb index 6d65d78f..7a4f7901 100644 --- a/lib/ruby-debug-ide/commands/breakpoints.rb +++ b/lib/ruby-debug-ide/commands/breakpoints.rb @@ -35,8 +35,7 @@ def execute end file = klass.name if klass else - file = File.expand_path(file) if file.index(File::SEPARATOR) || \ - File::ALT_SEPARATOR && file.index(File::ALT_SEPARATOR) + file = realpath(file) end end diff --git a/lib/ruby-debug-ide/commands/catchpoint.rb b/lib/ruby-debug-ide/commands/catchpoint.rb index 5aa4e291..eac29d39 100644 --- a/lib/ruby-debug-ide/commands/catchpoint.rb +++ b/lib/ruby-debug-ide/commands/catchpoint.rb @@ -16,22 +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) - print_catchpoint_set(excn) - #print "Catch for exception %s removed.\n", 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' @@ -40,9 +38,27 @@ def help_command def help(cmd) %{ cat[ch]\t\t\tshow catchpoint + cat[ch] off \tremove all catch points cat[ch] \tset catchpoint to an exception + cat[ch] off \tremove catchpoint for an exception } 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/condition.rb b/lib/ruby-debug-ide/commands/condition.rb index 0774ac72..d1890bde 100644 --- a/lib/ruby-debug-ide/commands/condition.rb +++ b/lib/ruby-debug-ide/commands/condition.rb @@ -12,8 +12,8 @@ def execute errmsg "\"condition\" must be followed a breakpoint number and expression\n" else breakpoints = Debugger.breakpoints.sort_by{|b| b.id } - largest = breakpoints.inject(0) do |largest, b| - largest = b.id if b.id > largest + largest = breakpoints.inject(0) do |largest_so_far, b| + b.id if b.id > largest_so_far end if 0 == largest print "No breakpoints have been set.\n" @@ -23,7 +23,7 @@ def execute return unless pos breakpoints.each do |b| if b.id == pos - b.expr = @match[2].empty? ? nil : @match[2] + b.expr = @match[2].empty? ? nil : Command.unescape_incoming(@match[2]) print_contdition_set(b.id) break end diff --git a/lib/ruby-debug-ide/commands/control.rb b/lib/ruby-debug-ide/commands/control.rb index c8ff56f6..6fd030f2 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/enable.rb b/lib/ruby-debug-ide/commands/enable.rb index 51b72215..99c6ecb8 100644 --- a/lib/ruby-debug-ide/commands/enable.rb +++ b/lib/ruby-debug-ide/commands/enable.rb @@ -3,8 +3,8 @@ module Debugger module EnableDisableFunctions # :nodoc: def enable_disable_breakpoints(is_enable, args) breakpoints = Debugger.breakpoints.sort_by{|b| b.id } - largest = breakpoints.inject(0) do |largest, b| - largest = b.id if b.id > largest + largest = breakpoints.inject(0) do |largest_so_far, b| + b.id if b.id > largest_so_far end if 0 == largest errmsg "No breakpoints have been set.\n" diff --git a/lib/ruby-debug-ide/commands/expression_info.rb b/lib/ruby-debug-ide/commands/expression_info.rb new file mode 100644 index 00000000..3eccd52c --- /dev/null +++ b/lib/ruby-debug-ide/commands/expression_info.rb @@ -0,0 +1,71 @@ +require 'stringio' +require 'irb/ruby-lex' + +module Debugger + + class ExpressionInfoCommand < Command + def regexp + /^\s*ex(?:pression_info)?\s+/ + end + + def execute + string_to_parse = Command.unescape_incoming(@match.post_match) + " \n \n\n" + total_lines = string_to_parse.count("\n") + 1 + + lexer = RubyLex.new + lexer.set_input(create_io_reader(string_to_parse)) + + last_statement = '' + last_prompt = '' + last_indent = 0 + lexer.set_prompt do |ltype, indent, continue, lineno| + next if (lineno >= total_lines) + + last_prompt = ltype || '' + last_indent = indent + end + + lexer.each_top_level_statement do |line, line_no| + last_statement = line + end + + incomplete = true + if /\A\s*\Z/m =~ last_statement + incomplete = false + end + + @printer.print_expression_info(incomplete, last_prompt, last_indent) + end + + def create_io_reader(string_to_parse) + io = StringIO.new(string_to_parse) + + if string_to_parse.respond_to?(:encoding) + io.instance_exec(string_to_parse.encoding) do |string_encoding| + @my_encoding = string_encoding + def self.encoding + @my_encoding + end + end + end + + io + end + + class << self + def help_command + "expression_info" + end + + def help(cmd) + %{ + ex[pression_info] \t + returns parser-related information for the expression given\t\t + 'incomplete'=true|false\tindicates whether expression is a complete ruby + expression and can be evaluated without getting syntax errors + } + end + end + end + +end diff --git a/lib/ruby-debug-ide/commands/file_filtering.rb b/lib/ruby-debug-ide/commands/file_filtering.rb new file mode 100644 index 00000000..626d39de --- /dev/null +++ b/lib/ruby-debug-ide/commands/file_filtering.rb @@ -0,0 +1,107 @@ +module Debugger + class IncludeFile < Command # :nodoc: + self.control = true + + def regexp + / ^\s*include\s+(.+?)\s*$/x + end + + def execute + file = @match[1] + + return if file.nil? + file = realpath(file) + + if Command.file_filter_supported? + Debugger.file_filter.include(file) + print_file_included(file) + else + print_debug("file filter is not supported") + end + end + + class << self + def help_command + 'include' + end + + def help(cmd) + %{ + include file - adds file/dir to file filter (either remove already excluded or add as included) + } + end + end + end + + class ExcludeFile < Command # :nodoc: + self.control = true + + def regexp + / ^\s*exclude\s+(.+?)\s*$/x + end + + def execute + file = @match[1] + + return if file.nil? + file = realpath(file) + + if Command.file_filter_supported? + Debugger.file_filter.exclude(file) + print_file_excluded(file) + else + print_debug("file filter is not supported") + end + end + + class << self + def help_command + 'include' + end + + def help(cmd) + %{ + exclude file - exclude file/dir from file filter (either remove already included or add as exclude) + } + end + end + end + + class FileFilterCommand < Command # :nodoc: + self.control = true + + def regexp + / ^\s*file-filter\s+(on|off)\s*$/x + end + + def execute + action = @match[1] + + if Command.file_filter_supported? + if 'on' == action + Debugger.file_filter.enable + print_file_filter_status(true) + elsif 'off' == action + Debugger.file_filter.disable + print_file_filter_status(false) + else + print_error "Unknown option '#{action}'" + end + else + print_debug("file filter is not supported") + end + end + + class << self + def help_command + 'file-filter' + end + + def help(cmd) + %{ + file-filter (on|off) - enable/disable file filtering + } + end + end + end +end \ No newline at end of file diff --git a/lib/ruby-debug-ide/commands/inspect.rb b/lib/ruby-debug-ide/commands/inspect.rb index dfe04ce4..3d4513c1 100644 --- a/lib/ruby-debug-ide/commands/inspect.rb +++ b/lib/ruby-debug-ide/commands/inspect.rb @@ -22,4 +22,4 @@ def execute end end -end \ No newline at end of file +end diff --git a/lib/ruby-debug-ide/commands/jump.rb b/lib/ruby-debug-ide/commands/jump.rb index 7c7c1c85..82eb7dc4 100755 --- a/lib/ruby-debug-ide/commands/jump.rb +++ b/lib/ruby-debug-ide/commands/jump.rb @@ -67,7 +67,7 @@ def help(cmd) Change the next line of code to be executed. } - end + end end end end diff --git a/lib/ruby-debug-ide/commands/pause.rb b/lib/ruby-debug-ide/commands/pause.rb index b75e8b07..b44aa484 100755 --- a/lib/ruby-debug-ide/commands/pause.rb +++ b/lib/ruby-debug-ide/commands/pause.rb @@ -9,12 +9,13 @@ def regexp end def execute - c = get_context(@match[1].to_i) - unless c.respond_to?(:pause) - print_msg "Not implemented" - return + Debugger.contexts.each do |c| + unless c.respond_to?(:pause) + print_msg "Not implemented" + return + end + c.pause end - c.pause end class << self @@ -26,7 +27,7 @@ def help(cmd) %{ pause \tpause a running thread } - end + end end end end diff --git a/lib/ruby-debug-ide/commands/set_type.rb b/lib/ruby-debug-ide/commands/set_type.rb index 10ff8c8e..aebbee74 100755 --- a/lib/ruby-debug-ide/commands/set_type.rb +++ b/lib/ruby-debug-ide/commands/set_type.rb @@ -41,7 +41,7 @@ def help(cmd) Change the type of to } - end + end end end end diff --git a/lib/ruby-debug-ide/commands/threads.rb b/lib/ruby-debug-ide/commands/threads.rb index 8b9f5e7a..4a63668f 100644 --- a/lib/ruby-debug-ide/commands/threads.rb +++ b/lib/ruby-debug-ide/commands/threads.rb @@ -59,6 +59,31 @@ def help(cmd) end end + class ThreadInspectCommand < Command # :nodoc: + self.control = true + self.need_context = true + + def regexp + /^\s*th(?:read)?\s+in(?:spect)?\s+(\d+)\s*$/ + end + + def execute + @state.context = get_context(@match[1].to_i) + end + + class << self + def help_command + 'thread' + end + + def help(cmd) + %{ + th[read] in[spect] \tswitch thread context to nnn but don't resume any threads + } + end + end + end + class ThreadStopCommand < Command # :nodoc: self.control = true self.need_context = true diff --git a/lib/ruby-debug-ide/commands/variables.rb b/lib/ruby-debug-ide/commands/variables.rb index b0b1cdc8..33c4f8e3 100644 --- a/lib/ruby-debug-ide/commands/variables.rb +++ b/lib/ruby-debug-ide/commands/variables.rb @@ -59,6 +59,10 @@ def help(cmd) end class VarInstanceCommand < Command # :nodoc: + # TODO: try to find out a way to use Kernel.binding for Rubinius + # ::Kernel.binding doesn't for for ruby 1.8 (see RUBY-14679) + BINDING_COMMAND = (defined?(Rubinius) || RUBY_VERSION < '1.9') ? 'binding' : '::Kernel.binding' + def regexp # id will be read as first match, name as post match /^\s*v(?:ar)?\s+i(?:nstance)?\s+((?:[\\+-]0x)[\dabcdef]+)?/ @@ -67,24 +71,27 @@ def regexp def execute if (@match[1]) obj = ObjectSpace._id2ref(@match[1].hex) rescue nil + unless obj - # TODO: ensure that empty variables frame will be printed + print_element("variables") @printer.print_msg("Unknown object id : %s", @match[1]) end else obj = debug_eval(@match.post_match) end return unless obj - if (obj.is_a?(Array)) then + if obj.is_a?(Array) print_array(obj) - elsif (obj.is_a?(Hash)) then + elsif obj.is_a?(Hash) print_hash(obj) + elsif obj.is_a?(String) + print_string(obj) else print_element("variables") do # instance variables kind = 'instance' inst_vars = obj.instance_variables - instance_binding = obj.instance_eval{binding()} + instance_binding = obj.instance_eval(BINDING_COMMAND) # print self at top position print_variable('self', debug_eval('self', instance_binding), kind) if inst_vars.include?('self') inst_vars.sort.each do |var| @@ -92,7 +99,7 @@ def execute end # class variables - class_binding = obj.class.class_eval('binding()') + class_binding = obj.class.class_eval(BINDING_COMMAND) obj.class.class_variables.sort.each do |var| print_variable(var, debug_eval(var, class_binding), 'class') end @@ -122,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 _self.to_s == "main" + 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 00000000..f47bde3b --- /dev/null +++ b/lib/ruby-debug-ide/greeter.rb @@ -0,0 +1,42 @@ +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, debugger_name = "Fast", socket_path = nil) + 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" + elsif socket_path + listens_on = " listens on #{socket_path}\n" + else + listens_on = "\n" + end + + msg = "#{debugger_name} Debugger (ruby-debug-ide #{IDE_VERSION}, #{base_gem_name} #{VERSION}, file filtering is #{file_filtering_support})" + listens_on + + stream.printf msg + end + end + +end diff --git a/lib/ruby-debug-ide/ide_processor.rb b/lib/ruby-debug-ide/ide_processor.rb index b9199f02..e04f651f 100644 --- a/lib/ruby-debug-ide/ide_processor.rb +++ b/lib/ruby-debug-ide/ide_processor.rb @@ -28,13 +28,13 @@ def process_commands s.interface = @interface end event_cmds = Command.commands.map{|cmd| cmd.new(state, @printer) } - while !state.proceed? do + until state.proceed? do input = @interface.command_queue.pop catch(:debug_error) do splitter[input].each do |input| # escape % since print_debug might use printf @printer.print_debug "Processing in context: #{input.gsub('%', '%%')}" - if cmd = event_cmds.find { |c| c.match(input) } + if (cmd = event_cmds.find { |c| c.match(input) }) if context.dead? && cmd.class.need_context @printer.print_msg "Command is unavailable\n" else @@ -45,11 +45,9 @@ def process_commands end end end + state.restore_context end - rescue IOError, Errno::EPIPE - @printer.print_error "INTERNAL ERROR!!! #{$!}\n" rescue nil - @printer.print_error $!.backtrace.map{|l| "\t#{l}"}.join("\n") rescue nil - rescue Exception + rescue ::Exception @printer.print_error "INTERNAL ERROR!!! #{$!}\n" rescue nil @printer.print_error $!.backtrace.map{|l| "\t#{l}"}.join("\n") rescue nil end @@ -79,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 @@ -92,10 +89,8 @@ def process_commands end end end - rescue IOError, Errno::EPIPE - @printer.print_error "INTERNAL ERROR!!! #{$!}\n" rescue nil - @printer.print_error $!.backtrace.map{|l| "\t#{l}"}.join("\n") rescue nil - rescue Exception + rescue ::Exception + @printer.print_debug "INTERNAL ERROR!!! #{$!}\n" rescue nil @printer.print_error "INTERNAL ERROR!!! #{$!}\n" rescue nil @printer.print_error $!.backtrace.map{|l| "\t#{l}"}.join("\n") rescue nil ensure @@ -105,7 +100,8 @@ def process_commands class State # :nodoc: - attr_accessor :context, :file, :line, :binding + attr_accessor :context, :original_context + attr_accessor :file, :line, :binding attr_accessor :frame_pos, :previous_line attr_accessor :interface @@ -114,6 +110,7 @@ def initialize @previous_line = nil @proceed = false yield self + @original_context = context end def print(*args) @@ -127,6 +124,10 @@ def proceed? def proceed @proceed = true end + + def restore_context + @context = @original_context + end end class ControlState # :nodoc: diff --git a/lib/ruby-debug-ide/interface.rb b/lib/ruby-debug-ide/interface.rb index 90cf78a6..3d68ba0b 100644 --- a/lib/ruby-debug-ide/interface.rb +++ b/lib/ruby-debug-ide/interface.rb @@ -1,19 +1,6 @@ require 'thread' -class TCPSocket - - # Workaround for JRuby issue http://jira.codehaus.org/browse/JRUBY-2063 - def non_blocking_gets - loop do - result, _, _ = IO.select( [self], nil, nil, 0.2 ) - next unless result - return result[0].gets - end - end - -end - -module Debugger +module Debugger class Interface end @@ -23,7 +10,6 @@ class LocalInterface < Interface class RemoteInterface < Interface # :nodoc: attr_accessor :command_queue - attr_accessor :socket def initialize(socket) @socket = socket @@ -31,7 +17,7 @@ def initialize(socket) end def read_command - result = @socket.non_blocking_gets + result = non_blocking_gets raise IOError unless result result.chomp end @@ -42,9 +28,18 @@ def print(*args) def close @socket.close - rescue Exception + rescue IOError, SystemCallError end - + + # Workaround for JRuby issue http://jira.codehaus.org/browse/JRUBY-2063 + def non_blocking_gets + loop do + result, _, _ = IO.select( [@socket], nil, nil, 0.2 ) + next unless result + return result[0].gets + end + end + end end diff --git a/lib/ruby-debug-ide/multiprocess.rb b/lib/ruby-debug-ide/multiprocess.rb index 13663270..4342ef70 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/monkey.rb b/lib/ruby-debug-ide/multiprocess/monkey.rb index 4c3a5ee1..370f78a5 100644 --- a/lib/ruby-debug-ide/multiprocess/monkey.rb +++ b/lib/ruby-debug-ide/multiprocess/monkey.rb @@ -1,9 +1,10 @@ module Debugger module MultiProcess - def self.create_mp_fork + def self.create_mp_fork(private=false) %Q{ alias pre_debugger_fork fork + #{private ? "private" : ""} def fork(*args) if block_given? return pre_debugger_fork{Debugger::MultiProcess::pre_child; yield} @@ -15,10 +16,11 @@ def fork(*args) } end - def self.create_mp_exec + def self.create_mp_exec(private=false) %Q{ alias pre_debugger_exec exec - + + #{private ? "private" : ""} def exec(*args) Debugger.interface.close pre_debugger_exec(*args) @@ -33,8 +35,8 @@ class << self module_eval Debugger::MultiProcess.create_mp_fork module_eval Debugger::MultiProcess.create_mp_exec end - module_eval Debugger::MultiProcess.create_mp_fork - module_eval Debugger::MultiProcess.create_mp_exec + module_eval Debugger::MultiProcess.create_mp_fork(true) + module_eval Debugger::MultiProcess.create_mp_exec(true) end module Process diff --git a/lib/ruby-debug-ide/multiprocess/pre_child.rb b/lib/ruby-debug-ide/multiprocess/pre_child.rb index 18bb5f9a..b707fc4c 100644 --- a/lib/ruby-debug-ide/multiprocess/pre_child.rb +++ b/lib/ruby-debug-ide/multiprocess/pre_child.rb @@ -1,72 +1,59 @@ module Debugger module MultiProcess class << self - def pre_child + def pre_child(options = nil) + require 'socket' + require 'ostruct' - require "socket" - require "ostruct" - - host = '127.0.0.1' - port = find_free_port(host) - - options = OpenStruct.new( + host = ENV['DEBUGGER_HOST'] + + options ||= OpenStruct.new( 'frame_bind' => false, 'host' => host, 'load_mode' => false, - 'port' => port, + 'port' => Debugger.find_free_port(host), 'stop' => false, 'tracing' => false, - 'int_handler' => true + 'int_handler' => true, + 'cli_debug' => (ENV['DEBUGGER_CLI_DEBUG'] == 'true'), + 'notify_dispatcher' => true, + 'evaluation_timeout' => 10, + 'trace_to_s' => false, + 'debugger_memory_limit' => 10, + 'inspect_time_limit' => 100 ) - - 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) - s.print(port) - s.close - connected = true - start_debugger(options) - return - rescue => bt - $stderr.puts "#{Process.pid}: connection failed(#{i+1})" - $stderr.puts "Exception: #{bt}" - $stderr.puts bt.backtrace.map { |l| "\t#{l}" }.join("\n") - sleep 0.3 - end unless connected + + if(options.ignore_port) + options.port = Debugger.find_free_port(options.host) + options.notify_dispatcher = true end - end + start_debugger(options) + end + def start_debugger(options) if Debugger.started? - #we're in forked child, only need to restart control thread - Debugger.breakpoints.clear + # we're in forked child, only need to restart control thread + Debugger.breakpoints.clear Debugger.control_thread = nil - Debugger.start_control(options.host, options.port) + Debugger.start_control(options.host, options.port, options.notify_dispatcher) end - + 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.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 - - - def find_free_port(host) - server = TCPServer.open(host, 0) - port = server.addr[1] - server.close - port - end end end end \ No newline at end of file diff --git a/lib/ruby-debug-ide/multiprocess/starter.rb b/lib/ruby-debug-ide/multiprocess/starter.rb index 8b557e1e..5e312f83 100644 --- a/lib/ruby-debug-ide/multiprocess/starter.rb +++ b/lib/ruby-debug-ide/multiprocess/starter.rb @@ -1,10 +1,11 @@ if ENV['IDE_PROCESS_DISPATCHER'] require 'rubygems' ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path| - next unless path =~ /ruby-debug-ide|ruby-debug-base|linecache/ + next unless path =~ /ruby-debug-ide|ruby-debug-base|linecache|debase/ $LOAD_PATH << path 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 00000000..82e6c6eb --- /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/thread_alias.rb b/lib/ruby-debug-ide/thread_alias.rb new file mode 100644 index 00000000..4b2054d5 --- /dev/null +++ b/lib/ruby-debug-ide/thread_alias.rb @@ -0,0 +1,27 @@ +module Debugger + module TimeoutHandler + class << self + def do_thread_alias + if defined? ::OldThread + Debugger.print_debug 'Tried to re-alias thread for eval' + return + end + + Object.const_set :OldThread, ::Thread + Object.__send__ :remove_const, :Thread + Object.const_set :Thread, ::Debugger::DebugThread + end + + def undo_thread_alias + unless defined? ::OldThread + Debugger.print_debug 'Tried to de-alias thread twice' + return + end + + Object.__send__ :remove_const, :Thread + Object.const_set :Thread, ::OldThread + Object.__send__ :remove_const, :OldThread + end + end + 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 8373aa7d..22185b92 100755 --- a/lib/ruby-debug-ide/version.rb +++ b/lib/ruby-debug-ide/version.rb @@ -1,3 +1,3 @@ module Debugger - IDE_VERSION='0.4.17.beta14' -end \ No newline at end of file + IDE_VERSION='0.7.5' +end diff --git a/lib/ruby-debug-ide/xml_printer.rb b/lib/ruby-debug-ide/xml_printer.rb index 56c2b091..9dcf0c9d 100644 --- a/lib/ruby-debug-ide/xml_printer.rb +++ b/lib/ruby-debug-ide/xml_printer.rb @@ -1,12 +1,42 @@ +require 'stringio' require 'cgi' -require 'yaml' require 'monitor' 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, File.expand_path(file), context.frame_line(frame_id) + print "", + 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,12 +119,11 @@ def print_contexts(contexts) end end end - + def print_context(context) - current = 'current="yes"' if context.thread == Thread.current - print "", context.thnum, context.thread.status, Process.pid + print "", context.thnum, context.thread.status, Process.pid end - + def print_variables(vars, kind) print_element("variables") do # print self at top position @@ -104,80 +133,221 @@ 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) + + def do_print_hash_key_value(hash) + print_element("variables", {:type => 'hashItem'}) do + hash.each {|(k, v)| + print_variable('key', k, 'instance') + print_variable('value', v, 'instance') + } + end + end + + def do_print_hash(hash) print_element("variables") do - hash.keys.each { | k | + hash.each {|(k, v)| if k.class.name == "String" name = '\'' + k + '\'' else - name = k.to_s + name = exec_with_allocation_control(k, :to_s, OverflowMessageType::EXCEPTION_MESSAGE) + end + + if k.nil? + name = 'nil' end - print_variable(name, hash[k], 'instance') + + print_variable(name, v, 'instance') + } + end + end + + def print_hash(hash) + if Debugger.key_value_mode + do_print_hash_key_value(hash) + else + do_print_hash(hash) + end + end + + def print_string(string) + print_element("variables") do + if string.respond_to?('bytes') + bytes = string.bytes.to_a + InspectCommand.reference_result(bytes) + print_variable('bytes', bytes, 'instance') + end + 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? print("", CGI.escapeHTML(name), kind) return end if value.is_a?(Array) || value.is_a?(Hash) has_children = !value.empty? - unless has_children - value_str = "Empty #{value.class}" + if has_children + size = value.size + value_str = "#{value.class} (#{value.size} element#{size > 1 ? "s" : "" })" else - value_str = "#{value.class} (#{value.size} element(s))" + value_str = "Empty #{value.class}" end + elsif value.is_a?(String) + has_children = value.respond_to?('bytes') || value.respond_to?('encoding') + value_str = value 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 - value_str = "[Binary Data]" if (value_str.respond_to?('is_binary_data?') && value_str.is_binary_data?) - print("", - CGI.escapeHTML(name), kind, CGI.escapeHTML(value_str), value.class, - has_children, value.respond_to?(:object_id) ? value.object_id : value.id) + + if value_str.respond_to?('encode') + # noinspection RubyEmptyRescueBlockInspection + begin + 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.object_id) + + print("", escaped_value_str) if Debugger.value_as_nested_element + print('') + rescue StandardError => e + print_debug "Unexpected exception \"%s\"\n%s", e.to_s, e.backtrace.join("\n") + print("", + CGI.escapeHTML(name), kind, CGI.escapeHTML(safe_to_string(value))) + end + + def print_file_included(file) + print("", file) end - + + def print_file_excluded(file) + print("", file) + end + + def print_file_filter_status(status) + print("", status) + end + 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 @@ -186,43 +356,56 @@ def print_catchpoint_set(exception_class_name) print "", exception_class_name end + def print_catchpoint_deleted(exception_class_name) + if Debugger.catchpoint_deleted_event + print "", exception_class_name + else + print_catchpoint_set(exception_class_name) + end + end + 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 + 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) + 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 else - print "No sourcefile available for %s\n", file + print "No source-file available for %s\n", file end end - + def print_methods(methods) print_element "methods" do methods.each do |method| @@ -230,78 +413,159 @@ def print_methods(methods) end end end - + # Events - - def print_breakpoint(n, breakpoint) - print("", - breakpoint.source, breakpoint.pos, Debugger.current_context.thnum) + + def print_breakpoint(_, breakpoint) + 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 "", - File.expand_path(file), line, context.thnum, context.stack_size + print "", + CGI.escapeHTML(File.expand_path(file)), line, context.thnum, context.stack_size end - - def print_exception(exception, binding) - print_variables(%w(error), 'exception') do |var| + + def print_exception(exception, _) + print_element("variables") do proxy = ExceptionProxy.new(exception) InspectCommand.reference_result(proxy) - proxy + print_variable('error', proxy, 'exception') end - rescue - print "", - exception.class, CGI.escapeHTML(exception.to_s) + rescue Exception + 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) - if exception then - print("", file, exception.class, CGI.escapeHTML(exception.to_s)) + + def print_load_result(file, exception = nil) + if exception + print("", file, exception.class, CGI.escapeHTML(exception.to_s)) else - print("", file) - end + print("", file) + end end - def print_element(name) - print("<#{name}>") + def print_element(name, additional_tags = nil) + additional_tags_presentation = additional_tags.nil? ? '' : additional_tags.map {|tag, value| " #{tag}=\"#{value}\""}.reduce(:+) + + print("<#{name}#{additional_tags_presentation}>") begin - yield + yield if block_given? ensure print("") end end private - + def print(*params) Debugger::print_debug(*params) @interface.print(*params) end + def handle_binary_data(value) + return '[Binary Data]' if (value.respond_to?('is_binary_data?') && value.is_binary_data?) + return '[Invalid encoding]' if (value.respond_to?('valid_encoding?') && !value.valid_encoding?) + value + end + + def current_thread_attr(context) + if context.thread == Thread.current + 'current="yes"' + else + '' + end + end + + def build_compact_name(value, value_str) + return compact_array_str(value) if value.is_a?(Array) + return compact_hash_str(value) if value.is_a?(Hash) + return value_str[0..max_compact_name_size - 3] + '...' if value_str.size > max_compact_name_size + nil + rescue ::Exception => e + print_debug(e) + nil + end + + def max_compact_name_size + # todo: do we want to configure it? + 50 + end + + def compact_array_str(value) + 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) + 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.nil? ? '' : "compactValue=\"#{CGI.escapeHTML(compact_value_str)}\"" + end + + def safe_to_string(value) + begin + str = value.to_s + rescue NoMethodError + str = "(Object doesn't support #to_s)" + end + return str unless str.nil? + + string_io = StringIO.new + string_io.write(value) + string_io.string + end + + def build_value_attr(escaped_value_str) + Debugger.value_as_nested_element ? '' : "value=\"#{escaped_value_str}\"" + end + instance_methods.each do |m| if m.to_s.index('print_') == 0 protect m end end - + end -end +end \ No newline at end of file diff --git a/protocol-spec.md b/protocol-spec.md new file mode 100644 index 00000000..b6f42c0a --- /dev/null +++ b/protocol-spec.md @@ -0,0 +1,406 @@ +ruby-debug-ide protocol +======================= + +* * * + +Next: [Summary](#Summary), Previous: [(dir)](#dir), Up: [(dir)](#dir) + +_ruby-debug-ide_ protocol +------------------------- + +This file contains specification of the protocol used by _ruby-debug-ide_. + +* [Summary](#Summary) +* [Specification](#Specification) +* [Changes](#Changes) + +* * * + +Next: [Specification](#Specification), Previous: [Top](#Top), Up: [Top](#Top) + +1 Summary +--------- + +This document describes protocol used by _ruby-debug-ide_ for communication between debugger engine and a frontend. It is a work in progress and might, and very likely will, change in the future. If you have any comments or questions please [send me](mailto:martin.krauskopf@gmail.com) an email. + +The communication has two parts/sides. First ones are _commands_ sent from a frontend to the debugger engine and the second is the opposite way, _answers_ and _events_ sent from the debugger engine to the frontend. + +_commands_ are almost the same as the ones used by CLI ruby-debug. So you might want to contact [the _ruby-debug-ide_ document](http://bashdb.sourceforge.net/ruby-debug.html). + +_answers_ and _events_ are sent in XML format described in the specification [below](#Specification). + +**Specification is far from complete.** Will be completed as time permits. In the meantime, source code is always the best spec. + +* * * + +Next: [Changes](#Changes), Previous: [Summary](#Summary), Up: [Top](#Top) + +2 Specification +--------------- + +* [Commands](#Commands) +* [Events](#Events) + +Terms: + +* _Command_ is what frontend sends to the debugger engine +* _Answer_ is what debugger engine sends back to the frontend +* _Example_ shows simple example + +* * * + +Next: [Events](#Events), Up: [Specification](#Specification) + +### 2.1 Commands + +* [Adding Breakpoint](#Adding-Breakpoint) +* [Deleting Breakpoint](#Deleting-Breakpoint) +* [Enabling Breakpoint](#Enabling-Breakpoint) +* [Disabling Breakpoint](#Disabling-Breakpoint) +* [Condition](#Condition) +* [Catchpoint](#Catchpoint) +* [Threads](#Threads) +* [Frames](#Frames) +* [Variables](#Variables) + +* * * + +Next: [Deleting Breakpoint](#Deleting-Breakpoint), Up: [Commands](#Commands) + +#### 2.1.1 Adding Breakpoint + +Command: + + break