lib/debug/session.rb in debug-1.0.0.beta5 vs lib/debug/session.rb in debug-1.0.0.beta6

- old
+ new

@@ -1,6 +1,7 @@ - +# frozen_string_literal: true + # skip to load debugger for bundle exec return if $0.end_with?('bin/bundle') && ARGV.first == 'exec' require_relative 'config' require_relative 'thread_client' @@ -50,10 +51,12 @@ self.to_a[4][:code_location][0] end end module DEBUGGER__ + PresetCommand = Struct.new(:commands, :source, :auto_continue) + class Session def initialize ui @ui = ui @sr = SourceRepository.new @bps = {} # bp.key => bp @@ -65,11 +68,11 @@ @th_clients = {} # {Thread => ThreadClient} @q_evt = Queue.new @displays = [] @tc = nil @tc_id = 0 - @initial_commands = [] + @preset_command = nil @frame_map = {} # {id => [threadId, frame_depth]} for DAP @var_map = {1 => [:globals], } # {id => ...} for DAP @src_map = {} # {id => src} @@ -96,17 +99,25 @@ end } @tp_thread_begin.enable end + def reset_ui ui + @ui.close + @ui = ui + @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread + end + def session_server_main while evt = @q_evt.pop # varible `@internal_info` is only used for test tc, output, ev, @internal_info, *ev_args = evt output.each{|str| @ui.puts str} case ev + when :init + wait_command_loop tc when :load iseq, src = ev_args on_load iseq, src @ui.event :load tc << :continue @@ -131,26 +142,22 @@ else tc << [:eval, :display, @displays] end when :result case ev_args.first - when :watch - bp = ev_args[1] - @bps[bp.key] = bp - show_bps bp when :try_display failed_results = ev_args[1] if failed_results.size > 0 i, _msg = failed_results.last if i+1 == @displays.size @ui.puts "canceled: #{@displays.pop}" end end - when :method_breakpoint + when :method_breakpoint, :watch_breakpoint bp = ev_args[1] if bp - @bps[bp.key] = bp + add_breakpoint(bp) show_bps bp else # can't make a bp end else @@ -167,19 +174,28 @@ ensure @bps.each{|k, bp| bp.disable} @th_clients.each{|th, thc| thc.close} end - def add_initial_commands cmds - cmds.each{|c| - c.gsub('#.*', '').strip! - @initial_commands << c unless c.empty? - } + def add_preset_commands name, cmds, kick: true, continue: true + cs = cmds.map{|c| + c = c.strip.gsub(/\A\s*\#.*/, '').strip + c unless c.empty? + }.compact + + unless cs.empty? + if @preset_command + @preset_command.commands += cs + else + @preset_command = PresetCommand.new(cs, name, continue) + end + ThreadClient.current.on_init name if kick + end end def source iseq - if CONFIG[:use_colorize] + if !CONFIG[:no_color] @sr.get_colored(iseq) else @sr.get(iseq) end end @@ -205,16 +221,27 @@ ensure @tc = nil end def wait_command - if @initial_commands.empty? + if @preset_command + if @preset_command.commands.empty? + if @preset_command.auto_continue + @preset_command = nil + @tc << :continue + return + else + @preset_command = nil + return :retry + end + else + line = @preset_command.commands.shift + @ui.puts "(rdbg:#{@preset_command.source}) #{line}" + end + else @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE'] line = @ui.readline - else - line = @initial_commands.shift.strip - @ui.puts "(rdbg:init) #{line}" end case line when String process_command line @@ -245,25 +272,29 @@ ### Control flow # * `s[tep]` # * Step in. Resume the program until next breakable point. when 's', 'step' + cancel_auto_continue @tc << [:step, :in] # * `n[ext]` # * Step over. Resume the program until next line. when 'n', 'next' + cancel_auto_continue @tc << [:step, :next] # * `fin[ish]` # * Finish this frame. Resume the program until the current frame is finished. when 'fin', 'finish' + cancel_auto_continue @tc << [:step, :finish] # * `c[ontinue]` # * Resume the program. when 'c', 'continue' + cancel_auto_continue @tc << :continue # * `q[uit]` or `Ctrl-D` # * Finish debugger (with the debuggee process on non-remote debugging). when 'q', 'quit' @@ -364,12 +395,12 @@ # * `watch @ivar` # * Stop the execution when the result of current scope's `@ivar` is changed. # * Note that this feature is super slow. when 'wat', 'watch' - if arg - @tc << [:eval, :watch, arg] + if arg && arg.match?(/\A@\w+/) + @tc << [:breakpoint, :watch, arg] else show_bps return :retry end @@ -479,13 +510,11 @@ # * Remove a specified display setting. when 'undisplay' case arg when /(\d+)/ if @displays[n = $1.to_i] - if ask "clear \##{n} #{@displays[n]}?" - @displays.delete_at n - end + @displays.delete_at n end @tc << [:eval, :display, @displays] when nil if ask "clear all?", 'N' @displays.clear @@ -558,10 +587,13 @@ @ui.puts "not supported on the remote console." return :retry end @tc << [:eval, :call, 'binding.irb'] + # don't repeat irb command + @repl_prev_line = nil + ### Thread control # * `th[read]` # * Show all threads. # * `th[read] <thnum>` @@ -606,10 +638,16 @@ @ui.puts "[REPL ERROR] #{e.inspect}" @ui.puts e.backtrace.map{|e| ' ' + e} return :retry end + def cancel_auto_continue + if @preset_command&.auto_continue + @preset_command.auto_continue = false + end + end + def show_help arg DEBUGGER__.helps.each{|cat, cs| cs.each{|ws, desc| if ws.include? arg @ui.puts desc @@ -881,14 +919,20 @@ # breakpoint management def add_breakpoint bp if @bps.has_key? bp.key - @ui.puts "duplicated breakpoint: #{bp}" + unless bp.duplicable? + @ui.puts "duplicated breakpoint: #{bp}" + bp.disable + end else @bps[bp.key] = bp end + + # don't repeat commands that add breakpoints + @repl_prev_line = nil end def rehash_bps bps = @bps.values @bps.clear @@ -930,10 +974,11 @@ end def add_line_breakpoint file, line, **kw file = resolve_path(file) bp = LineBreakpoint.new(file, line, **kw) + add_breakpoint bp rescue Errno::ENOENT => e @ui.puts e.message end @@ -1020,104 +1065,126 @@ nil end # start methods - def self.console **kw + def self.start nonstop: false, **kw set_config(kw) - require_relative 'console' + unless defined? SESSION + require_relative 'console' + initialize_session UI_Console.new + end - initialize_session UI_Console.new - - @prev_handler = trap(:SIGINT){ - ThreadClient.current.on_trap :SIGINT - } + setup_initial_suspend unless nonstop end - def self.open host: nil, port: ::DEBUGGER__::CONFIG[:port], sock_path: nil, sock_dir: nil, **kw + def self.open host: nil, port: ::DEBUGGER__::CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw set_config(kw) if port - open_tcp host: host, port: port + open_tcp host: host, port: port, nonstop: nonstop else - open_unix sock_path: sock_path, sock_dir: sock_dir + open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop end end - def self.open_tcp host: nil, port:, **kw + def self.open_tcp host: nil, port:, nonstop: false, **kw set_config(kw) require_relative 'server' - initialize_session UI_TcpServer.new(host: host, port: port) + + if defined? SESSION + SESSION.reset_ui UI_TcpServer.new(host: host, port: port) + else + initialize_session UI_TcpServer.new(host: host, port: port) + end + + setup_initial_suspend unless nonstop end - def self.open_unix sock_path: nil, sock_dir: nil, **kw + def self.open_unix sock_path: nil, sock_dir: nil, nonstop: false, **kw set_config(kw) require_relative 'server' - initialize_session UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path) + + if defined? SESSION + SESSION.reset_ui UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path) + else + initialize_session UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path) + end + + setup_initial_suspend unless nonstop end # boot utilities + def self.setup_initial_suspend + if !::DEBUGGER__::CONFIG[:nonstop] + if loc = ::DEBUGGER__.require_location + # require 'debug/console' or 'debug' + add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false + else + # -r + add_line_breakpoint $0, 1, oneshot: true, hook_call: false + end + end + end + class << self define_method :initialize_session do |ui| + DEBUGGER__.warn "Session start (pid: #{Process.pid})" + ::DEBUGGER__.const_set(:SESSION, Session.new(ui)) # default breakpoints # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError' Binding.module_eval do - def bp command: nil + def bp command: nil, nonstop: nil if command cmds = command.split(";;") - SESSION.add_initial_commands cmds + # nonstop + # nil, true -> auto_continue + # false -> stop + SESSION.add_preset_commands 'binding.bp(command:)', cmds, + kick: false, + continue: nonstop == false ? false : true end ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true true end end - if !::DEBUGGER__::CONFIG[:nonstop] - if loc = ::DEBUGGER__.require_location - # require 'debug/console' or 'debug' - add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false - else - # -r - add_line_breakpoint $0, 1, oneshot: true, hook_call: false - end - end - load_rc end end def self.load_rc - ['./rdbgrc.rb', File.expand_path('~/.rdbgrc.rb')].each{|path| - if File.file? path - load path - end - } + [[File.expand_path('~/.rdbgrc'), true], + [File.expand_path('~/.rdbgrc.rb'), true], + # ['./.rdbgrc', true], # disable because of security concern + [::DEBUGGER__::CONFIG[:init_script], false], + ].each{|(path, rc)| + next unless path + next if rc && ::DEBUGGER__::CONFIG[:no_rc] # ignore rc - # debug commands file - [init_script = ::DEBUGGER__::CONFIG[:init_script], - './.rdbgrc', - File.expand_path('~/.rdbgrc')].each{|path| - next unless path - if File.file? path - ::DEBUGGER__::SESSION.add_initial_commands File.readlines(path) - elsif path == init_script + if path.end_with?('.rb') + load path + else + ::DEBUGGER__::SESSION.add_preset_commands path, File.readlines(path) + end + elsif !rc warn "Not found: #{path}" end } # given debug commands if ::DEBUGGER__::CONFIG[:commands] cmds = ::DEBUGGER__::CONFIG[:commands].split(';;') - ::DEBUGGER__::SESSION.add_initial_commands cmds + ::DEBUGGER__::SESSION.add_preset_commands "commands", cmds, kick: false, continue: false end end def self.parse_help helps = Hash.new{|h, k| h[k] = []} @@ -1190,8 +1257,17 @@ str = obj.inspect if use_short && str.length > SHORT_INSPECT_LENGTH str[0...SHORT_INSPECT_LENGTH] + '...' else str + end + end + + def self.warn msg, level = :warn + case level + when :warn + STDERR.puts "DEBUGGER: #{msg}" unless CONFIG[:quiet] + when :error + STDERR.puts "DEBUGGER: #{msg}" end end end