require 'pp' require 'stringio' require "socket" 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/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 $stderr.printf("#{Process.pid}: ") $stderr.printf(*args) $stderr.printf("\n") $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 end 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 # def interrupt_last skip do if context = last_context return nil unless context.thread.alive? context.interrupt end context end end def start_server(host = nil, port = 1234, notify_dispatcher = false) _start_server_common(host, port, nil, notify_dispatcher) end def start_server_unix(socket_path, notify_dispatcher = false) _start_server_common(nil, 0, socket_path, notify_dispatcher) end 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 unless options.skip_wait_for_start end def debug_program(options) prepare_debugger(options) abs_prog_script = File.expand_path(Debugger::PROG_SCRIPT) bt = debug_load(abs_prog_script, options.stop, options.load_mode) if bt && !bt.is_a?(SystemExit) $stderr.print "Uncaught exception: #{bt}\n" $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, 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 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) 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.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}(#{ex.class})\nBacktrace:\n#{bt ? bt.join("\n from: ") : ""}\n" exit 1 end end rescue bt = $!.backtrace $stderr.printf "Fatal exception in DebugThread loop: #{$!.message}\nBacktrace:\n#{bt ? bt.join("\n from: ") : ""}\n" exit 2 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 end