Giter Club home page Giter Club logo

debug's Introduction

Ruby Protocol

debug.rb

This library provides debugging functionality to Ruby (MRI) 2.7 and later.

This debug.rb is the replacement of traditional lib/debug.rb standard library, which is implemented by set_trace_func. New debug.rb has several advantages:

  • Fast: No performance penalty on non-stepping mode and non-breakpoints.

  • Remote debugging: Support remote debugging natively.

  • Extensible: application can introduce debugging support in several ways:

    • By rdbg command
    • By loading libraries with -r command line option
    • By calling Ruby's method explicitly
  • Misc

    • Support threads (almost done) and ractors (TODO).
    • Support suspending and entering to the console debugging with Ctrl-C at most of timing.
    • Show parameters on backtrace command.
    • Support recording & replay debugging.

Installation

$ gem install debug

or specify -Ipath/to/debug/lib in RUBYOPT or each ruby command-line option, especially for debug this gem development.

If you use Bundler, write the following line to your Gemfile.

gem "debug", ">= 1.0.0"

(The version constraint is important; debug < 1.0.0 is an older, abandoned gem that is completely different from this product.)

HOW TO USE

To use a debugger, roughly you will do the following steps:

  1. Set breakpoints.
  2. Run a program with the debugger.
  3. At the breakpoint, enter the debugger console.
  4. Use debug commands.

Invoke with the debugger

There are several options for (1) and (2). Please choose your favorite way.

Modify source code with binding.break (similar to binding.pry or binding.irb)

If you can modify the source code, you can use the debugger by adding require 'debug' at the top of your program and putting binding.break method into lines where you want to stop as breakpoints like binding.pry and binding.irb.

You can also use its 2 aliases in the same way:

  • binding.b
  • debugger

After that, run the program as usual and you will enter the debug console at breakpoints you inserted.

The following example shows the demonstration of binding.break.

$ cat target.rb                        # Sample program
require 'debug'

a = 1
b = 2
binding.break                          # Program will stop here
c = 3
d = 4
binding.break                          # Program will stop here
p [a, b, c, d]

$ ruby target.rb                       # Run the program normally.
DEBUGGER: Session start (pid: 7604)
[1, 10] in target.rb
      1| require 'debug'
      2|
      3| a = 1
      4| b = 2
=>    5| binding.break                 # Now you can see it stops at this line
      6| c = 3
      7| d = 4
      8| binding.break
      9| p [a, b, c, d]
     10|
=>#0    <main> at target.rb:5

(rdbg) info locals                     # You can show local variables
=>#0    <main> at target.rb:5
%self => main
a => 1
b => 2
c => nil
d => nil

(rdbg) continue                        # Continue the execution
[3, 11] in target.rb
      3| a = 1
      4| b = 2
      5| binding.break
      6| c = 3
      7| d = 4
=>    8| binding.break                 # Again the program stops here
      9| p [a, b, c, d]
     10|
     11| __END__
=>#0    <main> at target.rb:8

(rdbg) info locals                     # And you can see the updated local variables
=>#0    <main> at target.rb:8
%self => main
a => 1
b => 2
c => 3
d => 4

(rdbg) continue
[1, 2, 3, 4]

Invoke the program from the debugger as a traditional debuggers

If you don't want to modify the source code, you can set breakpoints with a debug command break (b for short). Using rdbg command (or bundle exec rdbg) to launch the program without any modifications, you can run the program with the debugger.

$ cat target.rb                        # Sample program
a = 1
b = 2
c = 3
d = 4
p [a, b, c, d]

$ rdbg target.rb                       # run like `ruby target.rb`
DEBUGGER: Session start (pid: 7656)
[1, 7] in target.rb
=>    1| a = 1
      2| b = 2
      3| c = 3
      4| d = 4
      5| p [a, b, c, d]
      6|
      7| __END__
=>#0    <main> at target.rb:1

(rdbg)

rdbg command suspends the program at the beginning of the given script (target.rb in this case) and you can use debug commands. (rdbg) is prompt. Let's set breakpoints on line 3 and line 5 with break command (b for short).

(rdbg) break 3                         # set breakpoint at line 3
#0  BP - Line  /mnt/c/ko1/src/rb/ruby-debug/target.rb:3 (line)

(rdbg) b 5                             # set breakpoint at line 5
#1  BP - Line  /mnt/c/ko1/src/rb/ruby-debug/target.rb:5 (line)

(rdbg) break                           # show all registered breakpoints
#0  BP - Line  /mnt/c/ko1/src/rb/ruby-debug/target.rb:3 (line)
#1  BP - Line  /mnt/c/ko1/src/rb/ruby-debug/target.rb:5 (line)

You can see that two breakpoints are registered. Let's continue the program by using the continue command.

(rdbg) continue
[1, 7] in target.rb
      1| a = 1
      2| b = 2
=>    3| c = 3
      4| d = 4
      5| p [a, b, c, d]
      6|
      7| __END__
=>#0    <main> at target.rb:3

Stop by #0  BP - Line  /mnt/c/ko1/src/rb/ruby-debug/target.rb:3 (line)

(rdbg)

You can see that we can stop at line 3. Let's see the local variables with the info command, and continue. You can also confirm that the program will suspend at line 5 and you can use the info command again.

(rdbg) info
=>#0    <main> at target.rb:3
%self => main
a => 1
b => 2
c => nil
d => nil

(rdbg) continue
[1, 7] in target.rb
      1| a = 1
      2| b = 2
      3| c = 3
      4| d = 4
=>    5| p [a, b, c, d]
      6|
      7| __END__
=>#0    <main> at target.rb:5

Stop by #1  BP - Line  /mnt/c/ko1/src/rb/ruby-debug/target.rb:5 (line)

(rdbg) info
=>#0    <main> at target.rb:5
%self => main
a => 1
b => 2
c => 3
d => 4

(rdbg) continue
[1, 2, 3, 4]

By the way, using rdbg command you can suspend your application with C-c (SIGINT) and enter the debug console. It will help if you want to know what the program is doing.

Use rdbg with commands written in Ruby

If you want to run a command written in Ruby like rake, rails, bundle, rspec, and so on, you can use rdbg -c option.

  • Without -c option, rdbg <name> means that <name> is Ruby script and invoke it like ruby <name> with the debugger.
  • With -c option, rdbg -c <name> means that <name> is a command in PATH and simply invokes it with the debugger.

Examples:

  • rdbg -c -- rails server
  • rdbg -c -- bundle exec ruby foo.rb
  • rdbg -c -- bundle exec rake test
  • rdbg -c -- ruby target.rb is same as rdbg target.rb

NOTE: -- is needed to separate the command line options for rdbg and invoking command. For example, rdbg -c rake -T is recognized like rdbg -c -T -- rake. It should be rdbg -c -- rake -T.

NOTE: If you want to use bundler (bundle command), you need to write gem debug line in your Gemfile.

Using VSCode

Like other languages, you can use this debugger on the VSCode.

  1. Install VSCode rdbg Ruby Debugger - Visual Studio Marketplace
  2. Open .rb file (e.g. target.rb)
  3. Register breakpoints with "Toggle breakpoint" in the Run menu (or type F9 key)
  4. Choose "Start debugging" in "Run" menu (or type F5 key)
  5. You will see a dialog "Debug command line" and you can choose your favorite command line you want to run.
  6. Chosen command line is invoked with rdbg -c, and VSCode shows the details at breakpoints.

Please refer to Debugging in Visual Studio Code for operations on VSCode.

You can configure the extension in .vscode/launch.json. Please see the extension page for more details.

Remote debugging

You can use this debugger as a remote debugger. For example, it will help in the following situations:

  • Your application does not run on TTY, and it is hard to use binding.pry or binding.irb.
    • Your application is running on a Docker container, and there is no TTY.
    • Your application is running as a daemon.
    • Your application uses pipe for STDIN or STDOUT.
  • Your application is running as a daemon and you want to query the running status (checking a backtrace and so on).

You can run your application as a remote debuggee, and the remote debugger console can attach to the debuggee anytime.

Invoke as a remote debuggee

There are multiple ways to run your program as a debuggee:

Stop at program start rdbg option require debugger API
Yes rdbg --open require "debug/open" DEBUGGER__.open
No rdbg --open --nonstop require "debug/open_nonstop" DEBUGGER__.open(nonstop: true)

rdbg --open (or rdbg -O for short)

You can run a script with rdbg --open target.rb command and run a target.rb as a debuggee program. It also opens the network port and suspends at the beginning of target.rb.

$ exe/rdbg --open target.rb
DEBUGGER: Session start (pid: 7773)
DEBUGGER: Debugger can attach via UNIX domain socket (/home/ko1/.ruby-debug-sock/ruby-debug-ko1-7773)
DEBUGGER: wait for debugger connection...

By default, rdbg --open uses UNIX domain socket and generates the path name automatically (/home/ko1/.ruby-debug-sock/ruby-debug-ko1-7773 in this case).

You can connect to the debuggee with rdbg --attach command (rdbg -A for short).

$ rdbg -A
[1, 7] in target.rb
=>    1| a = 1
      2| b = 2
      3| c = 3
      4| d = 4
      5| p [a, b, c, d]
      6|
      7| __END__
=>#0    <main> at target.rb:1

(rdbg:remote)

If there are no other opening ports on the default directory, rdbg --attach command chooses the only one opening UNIX domain socket and connects to it. If there are more files, you need to specify the file.

When rdbg --attach connects to the debuggee, you can use any debug commands (set breakpoints, continue the program, and so on) like the local debug console. When a debuggee program exits, the remote console will also terminate.

NOTE: If you use the quit command, only the remote console exits and the debuggee program continues to run (and you can connect it again). If you want to exit the debuggee program, use kill command.

If you want to use TCP/IP for the remote debugging, you need to specify the port and host with --port like rdbg --open --port 12345 and it binds to localhost:12345.

To connect to the debuggee, you need to specify the port.

$ rdbg --attach 12345

If you want to choose the host to bind, you can use --host option. Note that all messages communicated between the debugger and the debuggee are NOT encrypted so please use remote debugging carefully.

require 'debug/open' in a program

If you can modify the program, you can open the debugging port by adding require 'debug/open' line in the program.

If you don't want to stop the program at the beginning, you can also use require 'debug/open_nonstop'. Using debug/open_nonstop is useful if you want to open a backdoor to the application. However, it is also dangerous because it can become another vulnerability. Please use it carefully.

By default, UNIX domain socket is used for the debugging port. To use TCP/IP, you can set the RUBY_DEBUG_PORT environment variable.

$ RUBY_DEBUG_PORT=12345 ruby target.rb

Integration with external debugger frontend

You can attach with external debugger frontend with VSCode and Chrome.

$ rdbg --open=[frontend] target.rb

will open a debug port and [frontend] can attach to the port.

Also open command allows opening the debug port.

VSCode integration

(vscode-rdbg v0.0.9 or later is required)

If you don't run a debuggee Ruby process on VSCode, you can attach it to VSCode later with the following steps.

rdbg --open=vscode opens the debug port and tries to invoke the VSCode (code command).

$ rdbg --open=vscode target.rb
DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/ruby-debug-sock-1000/ruby-debug-ko1-27706)
DEBUGGER: wait for debugger connection...
Launching: code /tmp/ruby-debug-vscode-20211014-27706-gd7e85/ /tmp/ruby-debug-vscode-20211014-27706-gd7e85/README.rb
DEBUGGER: Connected.

And it tries to invoke the new VSCode window and VSCode starts attaching to the debuggee Ruby program automatically.

You can also use open vscode command in REPL.

$ rdbg target.rb
[1, 8] in target.rb
     1|
=>   2| p a = 1
     3| p b = 2
     4| p c = 3
     5| p d = 4
     6| p e = 5
     7|
     8| __END__
=>#0    <main> at target.rb:2
(rdbg) open vscode    # command
DEBUGGER: wait for debugger connection...
DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/ruby-debug-sock-1000/ruby-debug-ko1-28337)
Launching: code /tmp/ruby-debug-vscode-20211014-28337-kg9dm/ /tmp/ruby-debug-vscode-20211014-28337-kg9dm/README.rb
DEBUGGER: Connected.

If the machine which runs the Ruby process doesn't have a code command, the following message will be shown:

(rdbg) open vscode
DEBUGGER: wait for debugger connection...
DEBUGGER: Debugger can attach via UNIX domain socket (/tmp/ruby-debug-sock-1000/ruby-debug-ko1-455)
Launching: code /tmp/ruby-debug-vscode-20211014-455-gtjpwi/ /tmp/ruby-debug-vscode-20211014-455-gtjpwi/README.rb
DEBUGGER: Can not invoke the command.
Use the command-line on your terminal (with modification if you need).

  code /tmp/ruby-debug-vscode-20211014-455-gtjpwi/ /tmp/ruby-debug-vscode-20211014-455-gtjpwi/README.rb

If your application is running on a SSH remote host, please try:

  code --remote ssh-remote+[SSH hostname] /tmp/ruby-debug-vscode-20211014-455-gtjpwi/ /tmp/ruby-debug-vscode-20211014-455-gtjpwi/README.rb

and try to use the proposed commands.

Note that you can attach with rdbg --attach and continue REPL debugging.

Chrome DevTool integration

With rdbg --open=chrome command will show the following message.

$ rdbg target.rb --open=chrome
DEBUGGER: Debugger can attach via TCP/IP (127.0.0.1:43633)
DEBUGGER: With Chrome browser, type the following URL in the address-bar:

   devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=127.0.0.1:57231/b32a55cd-2eb5-4c5c-87d8-b3dfc59d80ef

DEBUGGER: wait for debugger connection...

Type devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=127.0.0.1:57231/b32a55cd-2eb5-4c5c-87d8-b3dfc59d80ef in the address bar on Chrome browser, and you can continue the debugging with chrome browser.

Also open chrome command works like open vscode.

For more information about how to use Chrome debugging, you might want to read here.

Configuration

You can configure the debugger's behavior with debug commands and environment variables. When the debug session is started, initial scripts are loaded so you can put your favorite configurations in the initial scripts.

Configuration list

You can configure the debugger's behavior with environment variables and config command. Each configuration has an environment variable and a name which can be specified by config command.

# configuration example
config set log_level INFO
config set no_color true
  • UI

    • RUBY_DEBUG_LOG_LEVEL (log_level): Log level same as Logger (default: WARN)
    • RUBY_DEBUG_SHOW_SRC_LINES (show_src_lines): Show n lines source code on breakpoint (default: 10)
    • RUBY_DEBUG_SHOW_EVALEDSRC (show_evaledsrc): Show actually evaluated source (default: false)
    • RUBY_DEBUG_SHOW_FRAMES (show_frames): Show n frames on breakpoint (default: 2)
    • RUBY_DEBUG_USE_SHORT_PATH (use_short_path): Show shorten PATH (like $(Gem)/foo.rb) (default: false)
    • RUBY_DEBUG_NO_COLOR (no_color): Do not use colorize (default: false)
    • RUBY_DEBUG_NO_SIGINT_HOOK (no_sigint_hook): Do not suspend on SIGINT (default: false)
    • RUBY_DEBUG_NO_RELINE (no_reline): Do not use Reline library (default: false)
    • RUBY_DEBUG_NO_HINT (no_hint): Do not show the hint on the REPL (default: false)
    • RUBY_DEBUG_NO_LINENO (no_lineno): Do not show line numbers (default: false)
    • RUBY_DEBUG_IRB_CONSOLE (irb_console): Use IRB as the console (default: false)
  • CONTROL

    • RUBY_DEBUG_SKIP_PATH (skip_path): Skip showing/entering frames for given paths
    • RUBY_DEBUG_SKIP_NOSRC (skip_nosrc): Skip on no source code lines (default: false)
    • RUBY_DEBUG_KEEP_ALLOC_SITE (keep_alloc_site): Keep allocation site and p, pp shows it (default: false)
    • RUBY_DEBUG_POSTMORTEM (postmortem): Enable postmortem debug (default: false)
    • RUBY_DEBUG_FORK_MODE (fork_mode): Control which process activates a debugger after fork (both/parent/child) (default: both)
    • RUBY_DEBUG_SIGDUMP_SIG (sigdump_sig): Sigdump signal (default: false)
  • BOOT

    • RUBY_DEBUG_NONSTOP (nonstop): Nonstop mode (default: false)
    • RUBY_DEBUG_STOP_AT_LOAD (stop_at_load): Stop at just loading location (default: false)
    • RUBY_DEBUG_INIT_SCRIPT (init_script): debug command script path loaded at first stop
    • RUBY_DEBUG_COMMANDS (commands): debug commands invoked at first stop. Commands should be separated by ;;
    • RUBY_DEBUG_NO_RC (no_rc): ignore loading ~/.rdbgrc(.rb) (default: false)
    • RUBY_DEBUG_HISTORY_FILE (history_file): history file (default: ~/.rdbg_history)
    • RUBY_DEBUG_SAVE_HISTORY (save_history): maximum save history lines (default: 10000)
  • REMOTE

    • RUBY_DEBUG_OPEN (open): Open remote port (same as rdbg --open option)
    • RUBY_DEBUG_PORT (port): TCP/IP remote debugging: port
    • RUBY_DEBUG_HOST (host): TCP/IP remote debugging: host (default: 127.0.0.1)
    • RUBY_DEBUG_SOCK_PATH (sock_path): UNIX Domain Socket remote debugging: socket path
    • RUBY_DEBUG_SOCK_DIR (sock_dir): UNIX Domain Socket remote debugging: socket directory
    • RUBY_DEBUG_LOCAL_FS_MAP (local_fs_map): Specify local fs map
    • RUBY_DEBUG_SKIP_BP (skip_bp): Skip breakpoints if no clients are attached (default: false)
    • RUBY_DEBUG_COOKIE (cookie): Cookie for negotiation
    • RUBY_DEBUG_SESSION_NAME (session_name): Session name for differentiating multiple sessions
    • RUBY_DEBUG_CHROME_PATH (chrome_path): Platform dependent path of Chrome (For more information, See here)
  • OBSOLETE

    • RUBY_DEBUG_PARENT_ON_FORK (parent_on_fork): Keep debugging parent process on fork (default: false)

There are other environment variables:

  • NO_COLOR: If the value is set, set RUBY_DEBUG_NO_COLOR (NO_COLOR: disabling ANSI color output in various Unix commands).
  • RUBY_DEBUG_ENABLE: If the value is 0, do not enable debug.gem feature.
  • RUBY_DEBUG_ADDED_RUBYOPT: Remove this value from RUBYOPT at first. This feature helps loading debug.gem with RUBYOPT='-r debug/...', and you don't want to derive it to child processes. In this case, you can set RUBY_DEBUG_ADDED_RUBYOPT='-r debug/...' (same value), and this string will be deleted from RUBYOPT at first.
  • RUBY_DEBUG_EDITOR or EDITOR: An editor used by edit debug command.
  • RUBY_DEBUG_BB: Define Kernel#bb method which is alias of Kernel#debugger.

Initial scripts

If there is ~/.rdbgrc, the file is loaded as an initial script (which contains debug commands) when the debug session is started.

  • RUBY_DEBUG_INIT_SCRIPT environment variable can specify the initial script file.
  • You can specify the initial script with rdbg -x initial_script (like gdb's -x option).

Initial scripts are useful to write your favorite configurations. For example, you can set breakpoints with break file:123 in ~/.rdbgrc.

If there are ~/.rdbgrc.rb is available, it is also loaded as a ruby script at same timing.

Debug command on the debug console

On the debug console, you can use the following debug commands.

There are additional features:

  • <expr> without debug command is almost the same as pp <expr>.
    • If the input line <expr> does NOT start with any debug command, the line <expr> will be evaluated as a Ruby expression, and the result will be printed with pp method. So that the input foo.bar is the same as pp foo.bar.
    • If <expr> is recognized as a debug command, of course, it is not evaluated as a Ruby expression but is executed as debug command. For example, you can not evaluate such single-letter local variables i, b, n, c because they are single-letter debug commands. Use p i instead.
    • So the author (Koichi Sasada) recommends using p, pp or eval command to evaluate the Ruby expression every time.
  • Enter without any input repeats the last command (useful when repeating steps) for some commands.
  • Ctrl-D is equal to quit command.
  • debug command compare sheet - Google Sheets

You can use the following debug commands. Each command should be written in 1 line. The [...] notation means this part can be eliminated. For example, s[tep] means s or step is a valid command. ste is not valid. The <...> notation means the argument.

Control flow

  • s[tep]
    • Step in. Resume the program until next breakable point.
  • s[tep] <n>
    • Step in, resume the program at <n>th breakable point.
  • n[ext]
    • Step over. Resume the program until next line.
  • n[ext] <n>
    • Step over, same as step <n>.
  • fin[ish]
    • Finish this frame. Resume the program until the current frame is finished.
  • fin[ish] <n>
    • Finish <n>th frames.
  • u[ntil]
    • Similar to next command, but only stop later lines or the end of the current frame.
    • Similar to gdb's advance command.
  • u[ntil] <[file:]line>
    • Run til the program reaches given location or the end of the current frame.
  • u[ntil] <name>
    • Run til the program invokes a method <name>. <name> can be a regexp with /name/.
  • c or cont or continue
    • Resume the program.
  • q[uit] or Ctrl-D
    • Finish debugger (with the debuggee process on non-remote debugging).
  • q[uit]!
    • Same as q[uit] but without the confirmation prompt.
  • kill
    • Stop the debuggee process with Kernel#exit!.
  • kill!
    • Same as kill but without the confirmation prompt.
  • sigint
    • Execute SIGINT handler registered by the debuggee.
    • Note that this command should be used just after stop by SIGINT.

Breakpoint

  • b[reak]
    • Show all breakpoints.
  • b[reak] <line>
    • Set breakpoint on <line> at the current frame's file.
  • b[reak] <file>:<line> or <file> <line>
    • Set breakpoint on <file>:<line>.
  • b[reak] <class>#<name>
    • Set breakpoint on the method <class>#<name>.
  • b[reak] <expr>.<name>
    • Set breakpoint on the method <expr>.<name>.
  • b[reak] ... if: <expr>
    • break if <expr> is true at specified location.
  • b[reak] ... pre: <command>
    • break and run <command> before stopping.
  • b[reak] ... do: <command>
    • break and run <command>, and continue.
  • b[reak] ... path: <path>
    • break if the path matches to <path>. <path> can be a regexp with /regexp/.
  • b[reak] if: <expr>
    • break if: <expr> is true at any lines.
    • Note that this feature is super slow.
  • catch <Error>
    • Set breakpoint on raising <Error>.
  • catch ... if: <expr>
    • stops only if <expr> is true as well.
  • catch ... pre: <command>
    • runs <command> before stopping.
  • catch ... do: <command>
    • stops and run <command>, and continue.
  • catch ... path: <path>
    • stops if the exception is raised from a <path>. <path> can be a regexp with /regexp/.
  • watch @ivar
    • Stop the execution when the result of current scope's @ivar is changed.
    • Note that this feature is super slow.
  • watch ... if: <expr>
    • stops only if <expr> is true as well.
  • watch ... pre: <command>
    • runs <command> before stopping.
  • watch ... do: <command>
    • stops and run <command>, and continue.
  • watch ... path: <path>
    • stops if the path matches <path>. <path> can be a regexp with /regexp/.
  • del[ete]
    • delete all breakpoints.
  • del[ete] <bpnum>
    • delete specified breakpoint.

Information

  • bt or backtrace
    • Show backtrace (frame) information.
  • bt <num> or backtrace <num>
    • Only shows first <num> frames.
  • bt /regexp/ or backtrace /regexp/
    • Only shows frames with method name or location info that matches /regexp/.
  • bt <num> /regexp/ or backtrace <num> /regexp/
    • Only shows first <num> frames with method name or location info that matches /regexp/.
  • l[ist]
    • Show current frame's source code.
    • Next list command shows the successor lines.
  • l[ist] -
    • Show predecessor lines as opposed to the list command.
  • l[ist] <start> or l[ist] <start>-<end>
    • Show current frame's source code from the line to if given.
  • whereami
    • Show the current frame with source code.
  • edit
    • Open the current file on the editor (use EDITOR environment variable).
    • Note that edited file will not be reloaded.
  • edit <file>
    • Open on the editor.
  • i[nfo]
    • Show information about current frame (local/instance variables and defined constants).
  • i[nfo]
    • info has the following sub-commands.
    • Sub-commands can be specified with few letters which is unambiguous, like l for 'locals'.
  • i[nfo] l or locals or local_variables
    • Show information about the current frame (local variables)
    • It includes self as %self and a return value as _return.
  • i[nfo] i or ivars or instance_variables
    • Show information about instance variables about self.
    • info ivars <expr> shows the instance variables of the result of <expr>.
  • i[nfo] c or consts or constants
    • Show information about accessible constants except toplevel constants.
    • info consts <expr> shows the constants of a class/module of the result of <expr>
  • i[nfo] g or globals or global_variables
    • Show information about global variables
  • i[nfo] th or threads
    • Show all threads (same as th[read]).
  • i[nfo] b or breakpoints or w or watchpoints
    • Show all breakpoints and watchpoints.
  • i[nfo] ... /regexp/
    • Filter the output with /regexp/.
  • o[utline] or ls
    • Show you available methods, constants, local variables, and instance variables in the current scope.
  • o[utline] <expr> or ls <expr>
    • Show you available methods and instance variables of the given object.
    • If the object is a class/module, it also lists its constants.
  • display
    • Show display setting.
  • display <expr>
    • Show the result of <expr> at every suspended timing.
  • undisplay
    • Remove all display settings.
  • undisplay <displaynum>
    • Remove a specified display setting.

Frame control

  • f[rame]
    • Show the current frame.
  • f[rame] <framenum>
    • Specify a current frame. Evaluation are run on specified frame.
  • up
    • Specify the upper frame.
  • down
    • Specify the lower frame.

Evaluate

  • p <expr>
    • Evaluate like p <expr> on the current frame.
  • pp <expr>
    • Evaluate like pp <expr> on the current frame.
  • eval <expr>
    • Evaluate <expr> on the current frame.
  • irb
    • Activate and switch to irb:rdbg console

Trace

  • trace
    • Show available tracers list.
  • trace line
    • Add a line tracer. It indicates line events.
  • trace call
    • Add a call tracer. It indicate call/return events.
  • trace exception
    • Add an exception tracer. It indicates raising exceptions.
  • trace object <expr>
    • Add an object tracer. It indicates that an object by <expr> is passed as a parameter or a receiver on method call.
  • trace ... /regexp/
    • Indicates only matched events to /regexp/.
  • trace ... into: <file>
    • Save trace information into: <file>.
  • trace off <num>
    • Disable tracer specified by <num> (use trace command to check the numbers).
  • trace off [line|call|pass]
    • Disable all tracers. If <type> is provided, disable specified type tracers.
  • record
    • Show recording status.
  • record [on|off]
    • Start/Stop recording.
  • step back
    • Start replay. Step back with the last execution log.
    • s[tep] does stepping forward with the last log.
  • step reset
    • Stop replay .

Thread control

  • th[read]
    • Show all threads.
  • th[read] <thnum>
    • Switch thread specified by <thnum>.

Configuration

  • config
    • Show all configuration with description.
  • config <name>
    • Show current configuration of .
  • config set <name> <val> or config <name> = <val>
    • Set to .
  • config append <name> <val> or config <name> << <val>
    • Append <val> to <name> if it is an array.
  • config unset <name>
    • Set to default.
  • source <file>
    • Evaluate lines in <file> as debug commands.
  • open
    • open debuggee port on UNIX domain socket and wait for attaching.
    • Note that open command is EXPERIMENTAL.
  • open [<host>:]<port>
    • open debuggee port on TCP/IP with given [<host>:]<port> and wait for attaching.
  • open vscode
    • open debuggee port for VSCode and launch VSCode if available.
  • open chrome
    • open debuggee port for Chrome and wait for attaching.

Help

  • h[elp]
    • Show help for all commands.
  • h[elp] <command>
    • Show help for the given command.

Using IRB as the Debug Console

Starting from version v1.9, you can now use IRB as the debug console. This integration brings additional features such as:

  • Autocompletion
  • Support for multi-line input
  • Access to commands not available in debug, like show_source or show_doc
  • Configurable command aliases

To switch to the IRB console, simply use the irb command in the debug console.

Once activated, you'll notice the prompt changes to:

irb:rdbg(main):001>

If you want to make IRB the default console for all sessions, configure the irb_console setting by either:

  • Setting the RUBY_DEBUG_IRB_CONSOLE=true environment variable
  • Or adding config set irb_console 1 to your ~/.rdbgrc

To disable the IRB console in the current session, execute config set irb_console 0 in the console.

Debugger API

Start debugging

Start by requiring a library

You can start debugging without rdbg command by requiring the following libraries:

  • require 'debug': Same as rdbg --nonstop --no-sigint-hook.
  • require 'debug/start': Same as rdbg.
  • require 'debug/open': Same as rdbg --open.
  • require 'debug/open_nonstop': Same as rdbg --open --nonstop.

You need to require one of them at the very beginning of the application. Using ruby -r (for example ruby -r debug/start target.rb) is another way to invoke with debugger.

NOTE: Until Ruby 3.0, there is old lib/debug.rb standard library. So that if this gem is not installed, or if Gemfile missed to list this gem and bundle exec is used, you will see the following output:

$ ruby -r debug -e0
.../2.7.3/lib/ruby/2.7.0/x86_64-linux/continuation.so: warning: callcc is obsolete; use Fiber instead
Debug.rb
Emacs support available.

.../2.7.3/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:162:    if RUBYGEMS_ACTIVATION_MONITOR.respond_to?(:mon_owned?)
(rdb:1)

lib/debug.rb was not maintained well in recent years, and the purpose of this library is to rewrite old lib/debug.rb with recent techniques.

Start by method

After loading debug/session, you can start a debug session with the following methods. They are convenient if you want to specify debug configurations in your program.

  • DEBUGGER__.start(**kw): start debug session with local console.
  • DEBUGGER__.open(**kw): open debug port with configuration (without configurations open with UNIX domain socket)
  • DEBUGGER__.open_unix(**kw): open debug port with UNIX domain socket
  • DEBUGGER__.open_tcp(**kw): open debug port with TCP/IP

For example:

require 'debug/session'
DEBUGGER__.start(no_color: true,    # disable colorize
                 log_level: 'INFO') # Change log_level to INFO

... # your application code

binding.break method

binding.break (or binding.b) set breakpoints at the written line. It also has several keywords.

If do: 'command' is specified, the debugger suspends the program, runs the command as a debug command, and continues the program. It is useful if you only want to call a debug command and don't want to stop there.

def initialize
  @a = 1
  binding.b do: 'info \n watch @a'
end

In this case, execute the info command then register a watch breakpoint for @a and continue to run. You can also use ;; instead of \n to separate your commands.

If pre: 'command' is specified, the debugger suspends the program and runs the command as a debug command, and keeps suspended. It is useful if you have operations before suspend.

def foo
  binding.b pre: 'p bar()'
  ...
end

In this case, you can see the result of bar() every time you stop there.

rdbg command help

exe/rdbg [options] -- [debuggee options]

Debug console mode:
    -n, --nonstop                    Do not stop at the beginning of the script.
    -e DEBUG_COMMAND                 Execute debug command at the beginning of the script.
    -x, --init-script=FILE           Execute debug command in the FILE.
        --no-rc                      Ignore ~/.rdbgrc
        --no-color                   Disable colorize
        --no-sigint-hook             Disable to trap SIGINT
    -c, --command                    Enable command mode.
                                     The first argument should be a command name in $PATH.
                                     Example: 'rdbg -c bundle exec rake test'

    -O, --open=[FRONTEND]            Start remote debugging with opening the network port.
                                     If TCP/IP options are not given, a UNIX domain socket will be used.
                                     If FRONTEND is given, prepare for the FRONTEND.
                                     Now rdbg, vscode and chrome is supported.
        --sock-path=SOCK_PATH        UNIX Domain socket path
        --port=PORT                  Listening TCP/IP port
        --host=HOST                  Listening TCP/IP host
        --cookie=COOKIE              Set a cookie for connection
        --session-name=NAME          Session name

  Debug console mode runs Ruby program with the debug console.

  'rdbg target.rb foo bar'                starts like 'ruby target.rb foo bar'.
  'rdbg -- -r foo -e bar'                 starts like 'ruby -r foo -e bar'.
  'rdbg -c rake test'                     starts like 'rake test'.
  'rdbg -c -- rake test -t'               starts like 'rake test -t'.
  'rdbg -c bundle exec rake test'         starts like 'bundle exec rake test'.
  'rdbg -O target.rb foo bar'             starts and accepts attaching with UNIX domain socket.
  'rdbg -O --port 1234 target.rb foo bar' starts accepts attaching with TCP/IP localhost:1234.
  'rdbg -O --port 1234 -- -r foo -e bar'  starts accepts attaching with TCP/IP localhost:1234.
  'rdbg target.rb -O chrome --port 1234'  starts and accepts connecting from Chrome Devtools with localhost:1234.

Attach mode:
    -A, --attach                     Attach to debuggee process.

  Attach mode attaches the remote debug console to the debuggee process.

  'rdbg -A'           tries to connect via UNIX domain socket.
                      If there are multiple processes are waiting for the
                      debugger connection, list possible debuggee names.
  'rdbg -A path'      tries to connect via UNIX domain socket with given path name.
  'rdbg -A port'      tries to connect to localhost:port via TCP/IP.
  'rdbg -A host port' tries to connect to host:port via TCP/IP.

Other options:
    -v                               Show version number
        --version                    Show version number and exit
    -h, --help                       Print help
        --util=NAME                  Utility mode (used by tools)
        --stop-at-load               Stop immediately when the debugging feature is loaded.

NOTE
  All messages communicated between a debugger and a debuggee are *NOT* encrypted.
  Please use the remote debugging feature carefully.

Additional Resources

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/debug. This debugger is not mature so your feedback will help us.

Please also check the contributing guideline.

Acknowledgement

debug's People

Contributors

adam12 avatar adisonlampert avatar ahangarha avatar akr avatar andyw8 avatar byroot avatar drbrain avatar eregon avatar hsbt avatar ioquatix avatar josegomezr avatar k0kubun avatar ko1 avatar makenowjust avatar mame avatar marianosimone avatar nagachika avatar nobu avatar olleolleolle avatar ono-max avatar peterzhu2118 avatar sampatbadhe avatar shunichi avatar st0012 avatar vinistock avatar willhalto avatar y-yagi avatar yamashush avatar ydah avatar znz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

debug's Issues

next doesn't work

if str = nil
  if str
  end
end

p 1

actual:

[master]$ exe/rdbg target.rb
target.rb:1: warning: found `= literal' in conditional, should be ==
[1, 9] in target.rb
=>    1| if str = nil
      2|   if str
      3|   end
      4| end
      5|
      6| p 1
      7|
      8|
      9| __END__
=>#0    <main> at target.rb:1

(rdbg) n
1

but it should stop at line 6.

Strange `Errno::ENOENT` of ARGV value

I'm not sure how to even start debugging this one. I thought it was related to console being an overused term and perhaps the filename conflicting, but I'm not sure that's the reason (hence you see console2 as I try to identify if it's the cause).

It could be application specific and I haven't been able to reproduce it outside of my app.

app@96b8c3cf79c2:/workspace$ ruby app.rb console2
DEBUGGER: Session start (pid: 57884)
[2021-07-09] [14:03:56.994] β€Ί β„Ή info    Subscribing Listeners::PaymentIntent
[2021-07-09] [14:03:57.534] β€Ί β„Ή info    Subscribing Listeners::Email
[1, 8] in lib/cli/console.rb
      1| name "console2"
      2| summary "runs console in application context"
      3| 
      4| run do |opts, args|
      5|   Container[:db].logger = Logger.new($stdout)
=>    6|   binding.bp
      7|   # Pry.start
      8| end
=>#0    block {|opts={}, args=#<Cri::ArgumentList:0x0000aaaafb9294c8 @...|} in define at lib/cli/console.rb:6
  #1    Cri::Command#run_this(opts_and_args=[], parent_opts={}) at /usr/local/bundle/gems/cri-2.15.11/lib/cri/command.rb:362
  # and 4 frames (use `bt' command for all frames)

(rdbg) q
Really quit? [Y/n] [REPL ERROR] #<Errno::ENOENT: No such file or directory @ rb_sysopen - console2>
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/console.rb:39:in `gets'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/console.rb:39:in `gets'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/console.rb:39:in `block in ask'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/console.rb:103:in `setup_interrupt'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/console.rb:37:in `ask'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:663:in `ask'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:301:in `process_command'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:247:in `wait_command'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:211:in `block (2 levels) in wait_command_loop'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:210:in `loop'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:210:in `block in wait_command_loop'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:892:in `block in stop_all_threads'
  <internal:trace_point>:196:in `enable'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:891:in `stop_all_threads'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:209:in `wait_command_loop'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:141:in `session_server_main'
  /usr/local/bundle/gems/debug-1.0.0.beta6/lib/debug/session.rb:88:in `block in initialize'

(rdbg) q
Really quit? [Y/n] app@96b8c3cf79c2:/workspace$ 

It's not obvious, so here's my text input (I'm never at the Y/n prompt to provide input):

q
q

Attached are script output and timing, if it helps. Use scriptreplay --timing timing.txt script.txt to replay.

timing.txt
script.txt

debug (1.0.0.beta6)
ruby 2.7.3p183 (2021-04-05 revision 6847ee089d) [aarch64-linux]

Trace exception rescuing with ExceptionTracer?

In addition to seeing where an exception is raised, it'll also help a lot to know when an exception is rescued. This will be very useful for debugging Rails applications or Rack middlewares. In these applications, rescue/re-raise exceptions based on different options is common. For example: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L36-L40

So it'd really help if ExceptionTracer can also trace rescuing events. But I'm not sure if this would be technically possible when such events are not supported by TracePoint?

Allow specifying inspection mode when using the info command

Current info command pretty inspects everything, which could be an issue when used in a Rails application. For example, a Rails controller usually has many instance variables that contain big objects. And calling inspect on them creates noise.
ζˆͺεœ– 2021-07-07 δΈ‹εˆ3 27 32

(more than 2000 lines after the screenshot)

So it'd be great to provide something like info -s as simple mode (calling to_s instead of inspect)

When specify a class name with `test`, test builder doesn't work.

@ko1 pointed out that test builder raise an Error when specify a class name with test.

$ bin/gentest target.rb -c StepTest
DEBUGGER: Session start (pid: 76842)
[1, 10] in ~/workspace/debug/target.rb
=>    1| module Foo
      2|   class Bar
      3|     def self.a
      4|       "hello"
      5|     end
      6|
      7|     def b(n)
      8|       2.times do
      9|         n
     10|       end
=>#0	<main> at ~/workspace/debug/target.rb:1
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:1","line":1}
(rdbg)s
 s
[1, 10] in ~/workspace/debug/target.rb
      1| module Foo
=>    2|   class Bar
      3|     def self.a
      4|       "hello"
      5|     end
      6|
      7|     def b(n)
      8|       2.times do
      9|         n
     10|       end
=>#0	<module:Foo> at ~/workspace/debug/target.rb:2
  #1	<main> at ~/workspace/debug/target.rb:1
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:2","line":2}
(rdbg)q!
 q!
/Users/naotto/workspace/debug/test/tool/test_builder.rb:188:in `create_file': undefined method `sub' for nil:NilClass (NoMethodError)
	from /Users/naotto/workspace/debug/test/tool/test_builder.rb:21:in `start'
	from bin/gentest:22:in `<main>'

Improve the watch command

Description copied from #41:

I found several things when playing with the watch command:

  • It mainly uses line events to detect the change instead of the return events (I'm not sure why though). This causes the program to always stop a line after the change. So in tests we need to add an extra line after the changed line. Otherwise it doesn't stop.
  • It always evaluates the expression from top-level scope. So it's not able to track an instance variable inside an object like watch @name and users need to use watch obj.name instead. I think we should improve this.

REPL_RPOMPT doesn't match line

A piece of code in utils.rb

Timeout.timeout(timeout_sec) do
            while (line = read.gets)
              debug_print line
              case line.chomp
              when /INTERNAL_INFO:\s(.*)/
                @internal_info = JSON.parse(Regexp.last_match(1))
                cmd = @queue.pop
                if cmd.is_a?(Proc)
                  cmd.call
                  cmd = @queue.pop
                end
                if ask_cmd.include?(cmd)
                  write.puts(cmd)
                  cmd = @queue.pop
                end
                write.puts(cmd)
                next # INTERNAL_INFO shouldn't be pushed into @backlog and @last_backlog
              when REPL_RPOMPT
                p :foo
                # check if the previous command breaks the debugger before continuing
                if error_index = @last_backlog.index { |l| l.match?(/REPL ERROR/) }
                  raise "Debugger terminated because of: #{@last_backlog[error_index..-1].join}"
                end
              end

              @backlog.push(line)
              @last_backlog.push(line)
            end

            assert_empty_queue
          end

It should output :foo, but it doesn't.

$ ruby test/debug/catch_test.rb
Loaded suite test/debug/catch_test
Started
.....
Finished in 0.91524 seconds.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
5 tests, 12 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

Inconsistent binding in CatchBreakpoint with primitive exceptions

When stopped at a breakpoint, evaluating binding should reflect the current scope's binding.

ζˆͺεœ– 2021-07-07 δΈ‹εˆ12 45 54

But it's not the case when stopped at CatchBreakpoint with some primitive exceptions like ZeroDivisionError.

ζˆͺεœ– 2021-07-07 δΈ‹εˆ12 45 38

It's because those exceptions are raised in C source code, which's frame doesn't have a binding. So the debugger falls back to its own frame.

b = current_frame.binding
result = if b
f, _l = b.source_location
b.eval(src, "(rdbg)/#{f}")
else
frame_self = current_frame.self
frame_self.instance_eval(src)
end

I'm not sure if this edge case counts as a bug. But it'd be nice if the debugger can fall back to the nearest Ruby-level frame (with binding) in such cases.

break command like `bp(command:)`

I want to introduce same functionality of bp(command:)
The problem is it is hard to define the syntax because of combination of if.

break ... do <command>
# break 10 do p lvar

break ... if <expr> do <command>
# break 10 if lvar > 100 do p lvar

for eample?

undefined method `unix_domain_socket_basedir' for DEBUGGER__:Module (NoMethodError)

$ exe/rdbg -A foo
/Users/naotto/workspace/debug/lib/debug/client.rb:68:in `connect_unix': undefined method `unix_domain_socket_basedir' for DEBUGGER__:Module (NoMethodError)
Did you mean?  unix_domain_socket_dir
	from /Users/naotto/workspace/debug/lib/debug/client.rb:37:in `initialize'
	from /Users/naotto/workspace/debug/lib/debug/client.rb:152:in `new'
	from /Users/naotto/workspace/debug/lib/debug/client.rb:152:in `connect'
	from exe/rdbg:31:in `<main>'

Test framework doesn't support assert method after ask command

# frozen_string_literal: true

require_relative '../support/test_case'

module DEBUGGER__
  class Quit < TestCase
    def program
      <<~RUBY
        1| a=1

      RUBY
    end
    
    def test_quit_does_not_quit_debugger_process_if_not_confirmed
      debug_code(program) do
        type 'q'
        assert_line_text(/Really quit\? \[Y\/n\]/)
        type 'n'
        type 'q!'
      end
    end

    def test_quit_with_exclamation_mark_quits_immediately_debugger_process
      debug_code(program) do
        type 'q!'
      end
    end
  end
end
$ ruby test/debug/quit_test.rb
Tests on local and remote. You can disable remote tests with RUBY_DEBUG_TEST_NO_REMOTE=1.
Loaded suite test/debug/quit_test
Started
F
================================================================================================================================================================================================================================================================================
Failure: test_quit_does_not_quit_debugger_process_if_not_confirmed(DEBUGGER__::Quit):
  expect all commands/assertions to be executed. still have 1 left.
  <#<Thread::Queue:0x00007f85eaaa2608>> was expected to be empty.
/Users/naotto/workspace/debug/test/support/utils.rb:209:in `assert_empty_queue'
/Users/naotto/workspace/debug/test/support/utils.rb:132:in `block (2 levels) in run_test_scenario'
/Users/naotto/.rbenv/versions/3.0.0/lib/ruby/3.0.0/timeout.rb:97:in `block in timeout'
/Users/naotto/.rbenv/versions/3.0.0/lib/ruby/3.0.0/timeout.rb:35:in `block in catch'
/Users/naotto/.rbenv/versions/3.0.0/lib/ruby/3.0.0/timeout.rb:35:in `catch'
/Users/naotto/.rbenv/versions/3.0.0/lib/ruby/3.0.0/timeout.rb:35:in `catch'
/Users/naotto/.rbenv/versions/3.0.0/lib/ruby/3.0.0/timeout.rb:112:in `timeout'
/Users/naotto/workspace/debug/test/support/utils.rb:103:in `block in run_test_scenario'
/Users/naotto/workspace/debug/test/support/utils.rb:99:in `spawn'
/Users/naotto/workspace/debug/test/support/utils.rb:99:in `run_test_scenario'
/Users/naotto/workspace/debug/test/support/utils.rb:76:in `debug_on_local'
/Users/naotto/workspace/debug/test/support/utils.rb:46:in `debug_code'
test/debug/quit_test.rb:23:in `test_quit_does_not_quit_debugger_process_if_not_confirmed'
     20:     # end
     21:
     22:     def test_quit_does_not_quit_debugger_process_if_not_confirmed
  => 23:       debug_code(program) do
     24:         type 'q'
     25:         assert_line_text(/Really quit\? \[Y\/n\]/)
     26:         type 'n'
================================================================================================================================================================================================================================================================================
.
Finished in 1.066488 seconds.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2 tests, 4 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
50% passed
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1.88 tests/s, 3.75 assertions/s

Exceptions inside frame inspection are fatal

I'm really not sure if this is a bug, but I presume it is. The example is contrived, but I was able to trigger this by putting URI in a strange state (for which I was debugging πŸ˜‚).

Target

class Baz
  Error = Class.new(StandardError)

  def initialize
    raise Error
  end

  def to_s
    raise
  end

  def inspect
    to_s
  end
end

Baz.new

Command

exe/rdbg -e 'catch Baz::Error ;; c ;; up ;; i' -e c target.rb

I expect the frame information to be shown, but instead my original exception is raised again and the debugger exits.

I'm really not sure what the best solution is here. Rescue then display rescued SomeException when inspecting? Or perhaps debug can have it's own object here that's used in it's place?

The patch below solves the issue.

diff --git a/lib/debug/thread_client.rb b/lib/debug/thread_client.rb
index 16cd2b7..808341b 100644
--- a/lib/debug/thread_client.rb
+++ b/lib/debug/thread_client.rb
@@ -29,6 +29,8 @@ module DEBUGGER__
       else
         obj.pretty_inspect
       end
+    rescue => ex
+      colored_inspect("(rescued #{ex.inspect} during inspection)")
     end
 
     def colorize_cyan(str)

Allow using binding.bp to start a debug session

As stated in the readme, the correct way to start a debug session in the middle of a program is:

require "debug"
DEBUGGER__.console

And then the user can freely use binding.bp to insert new breakpoints.

This is a fine workflow, but I think we can make it more simpler. Since we already have the binding.bp method, which just looks like byebug and binding.pry, why don't we make it work like them?

require "pry"
binding.pry

require "byebug"
byebug

require "debug"
binding.bp

Then the users will have one less thing to learn πŸ™‚

(I have to admit that it took me a while to remember to call DEBUGGER__.console, which I still forget from time to time πŸ˜‚)

@last_backlog should not be same as @backlog

Pieces of codes in utils.rb

DEBUG_MODE = true

    def debug_print msg
      print msg if DEBUG_MODE
    end

    RUBY = RbConfig.ruby
    REPL_RPOMPT = '(rdbg)'

    def create_pseudo_terminal(boot_options: "-r debug/run")
      inject_lib_to_load_path
      ENV['RUBY_DEBUG_USE_COLORIZE'] = "false"
      ENV['RUBY_DEBUG_TEST_MODE'] = 'true'

      timeout_sec = (ENV['RUBY_DEBUG_TIMEOUT_SEC'] || 10).to_i

      PTY.spawn("#{RUBY} #{boot_options} #{temp_file_path}") do |read, write, pid|
        @backlog = []
        @last_backlog = []
        ask_cmd = ['quit', 'delete', 'kill']
        begin
          Timeout.timeout(timeout_sec) do
            while (line = read.gets)
              debug_print line
              case line.chomp
              when /INTERNAL_INFO:\s(.*)/
                p ":last_backlog #{@last_backlog}"
                p ":backlog #{@backlog}"
                @internal_info = JSON.parse(Regexp.last_match(1))
                cmd = @queue.pop
                if cmd.is_a?(Proc)
                  cmd.call
                  cmd = @queue.pop

Pieces of output in terminal.

Now

$ ruby test/debug/catch_test.rb
Loaded suite test/debug/catch_test
Started
[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb
=>    1| a = 1
      2| b = 2
      3|
      4| 1/0
=>#0	<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1
INTERNAL_INFO: {"location":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1","line":1}
":last_backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1\\r\\n\"]"
":backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1\\r\\n\"]"
catch StandardError

(rdbg) catch StandardError
#0  BP - Catch  "StandardError"
INTERNAL_INFO: {"location":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1","line":1}
":last_backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1\\r\\n\", \"catch StandardError\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) catch StandardError\\r\\n\", \"\\e[?2004l\\r#0  BP - Catch  \\\"StandardError\\\"\\r\\n\"]"
":backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1\\r\\n\", \"catch StandardError\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) catch StandardError\\r\\n\", \"\\e[?2004l\\r#0  BP - Catch  \\\"StandardError\\\"\\r\\n\"]"

(rdbg) continue
# No sourcefile available for /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb
=>#0	[C] Integer#/ at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:4
  #1	<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:4

Stop by #0  BP - Catch  "StandardError"
INTERNAL_INFO: {"location":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:4","line":4}
":last_backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1\\r\\n\", \"catch StandardError\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) catch StandardError\\r\\n\", \"\\e[?2004l\\r#0  BP - Catch  \\\"StandardError\\\"\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) continue\\r\\n\", \"\\e[?2004l\\r# No sourcefile available for /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb\\r\\n\", \"=>#0\\t[C] Integer#/ at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:4\\r\\n\", \"  #1\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:4\\r\\n\", \"\\r\\n\", \"Stop by #0  BP - Catch  \\\"StandardError\\\"\\r\\n\"]"
":backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:1\\r\\n\", \"catch StandardError\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) catch StandardError\\r\\n\", \"\\e[?2004l\\r#0  BP - Catch  \\\"StandardError\\\"\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) continue\\r\\n\", \"\\e[?2004l\\r# No sourcefile available for /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb\\r\\n\", \"=>#0\\t[C] Integer#/ at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:4\\r\\n\", \"  #1\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66658-19rnmp.rb:4\\r\\n\", \"\\r\\n\", \"Stop by #0  BP - Catch  \\\"StandardError\\\"\\r\\n\"]"

(rdbg) q!

Expected

$ ruby test/debug/catch_test.rb
Loaded suite test/debug/catch_test
Started
[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb
=>    1| a = 1
      2| b = 2
      3|
      4| 1/0
=>#0	<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:1
INTERNAL_INFO: {"location":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:1","line":1}
":last_backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:1\\r\\n\"]"
":backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:1\\r\\n\"]"
catch StandardError

(rdbg) catch StandardError
#0  BP - Catch  "StandardError"
INTERNAL_INFO: {"location":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:1","line":1}
":last_backlog [\"catch StandardError\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) catch StandardError\\r\\n\", \"\\e[?2004l\\r#0  BP - Catch  \\\"StandardError\\\"\\r\\n\"]"
":backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:1\\r\\n\", \"catch StandardError\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) catch StandardError\\r\\n\", \"\\e[?2004l\\r#0  BP - Catch  \\\"StandardError\\\"\\r\\n\"]"

(rdbg) continue
# No sourcefile available for /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb
=>#0	[C] Integer#/ at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:4
  #1	<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:4

Stop by #0  BP - Catch  "StandardError"
INTERNAL_INFO: {"location":"/var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:4","line":4}
":last_backlog [\"\\e[?2004h\\r\\n\", \"(rdbg) continue\\r\\n\", \"\\e[?2004l\\r# No sourcefile available for /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb\\r\\n\", \"=>#0\\t[C] Integer#/ at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:4\\r\\n\", \"  #1\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:4\\r\\n\", \"\\r\\n\", \"Stop by #0  BP - Catch  \\\"StandardError\\\"\\r\\n\"]"
":backlog [\"[1, 4] in /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb\\r\\n\", \"=>    1| a = 1\\r\\n\", \"      2| b = 2\\r\\n\", \"      3| \\r\\n\", \"      4| 1/0\\r\\n\", \"=>#0\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:1\\r\\n\", \"catch StandardError\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) catch StandardError\\r\\n\", \"\\e[?2004l\\r#0  BP - Catch  \\\"StandardError\\\"\\r\\n\", \"\\e[?2004h\\r\\n\", \"(rdbg) continue\\r\\n\", \"\\e[?2004l\\r# No sourcefile available for /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb\\r\\n\", \"=>#0\\t[C] Integer#/ at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:4\\r\\n\", \"  #1\\t<main> at /var/folders/kv/w1k6nh1x5fl7vx47b2pd005w0000gn/T/debugger20210619-66836-kkigg1.rb:4\\r\\n\", \"\\r\\n\", \"Stop by #0  BP - Catch  \\\"StandardError\\\"\\r\\n\"]"

(rdbg) q!

Test builder doesn't work well.

Due to the recent changes in the debugger, test builder doesn't work well.

$ bin/gentest target.rb
]DEBUGGER: Session start (pid: 69779)
[1, 10] in ~/workspace/debug/target.rb
=>    1| module Foo
      2|   class Bar
      3|     def self.a
      4|       "hello"
      5|     end
      6|
      7|     def b(n)
      8|       2.times do
      9|         n
     10|       end
=>#0	<main> at ~/workspace/debug/target.rb:1
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:1","line":1}
(rdbg) s
# Stuck here.

Should not suspend on detached mode

Now, detached debuggee encounters the suspend point (like binding.bp), the debuggee will suspend and wait for remote console connection like that:

p 1
binding.bp
[master]$ exe/rdbg -O target.rb -n
DEBUGGER: Session start (pid: 11257)
DEBUGGER: Debugger can attach via UNIX domain socket (/home/ko1/.ruby-debug-sock/ruby-debug-ko1-11257)
1
DEBUGGER: wait for debuger connection...

However, other deubgers like gdb doesn't stop (from the beginning, the detached process is not under control by gdb).

I think there is a case to wait remote console connection, but not sure which is majority.

Make a configuration?

  • global configuration (which should be a default?)
  • binding.bp option (wait_attach: true/false)

Open to extensions?

I'm thinking about building a rdbg-rails gem to provide Rails integration. This includes:

  • rails commands, for example:
    • rails configs - list all Rails config options
    • rails initializers - list all initializers
    • rails instrumentation /sql.active_record/ [on|off] - similar to trace [on|off] but print ActiveSupport` instrumentation.
    • This relies on debugger to provide APIs for register new commands
  • Apply Rails' backtrace_cleaner to backtrace and flow control commands to skip certain frames.
    • This also relies on debugger to provide related APIs

So my question is, is the debugger open to these extension APIs?

Introduce something similar to Pry's ls command

The ls command is one of my favorites when using Pry. It gives users a quick overview on the objects with information like:

  • Methods & their defined classes
  • Constants
  • Instance variables
  • Locals

pry ls

Usage

[4] pry(main)> ls -h
Usage: ls [-m|-M|-p|-pM] [-q|-v] [-c|-i] [Object]
       ls [-g] [-l]

ls shows you which methods, constants and variables are accessible to Pry. By
default it shows you the local variables defined in the current shell, and any
public methods or instance variables defined on the current object.

# .....

Also check out `find-method` command (run `help find-method`).

    -m, --methods               Show public methods defined on the Object
    -M, --instance-methods      Show public methods defined in a Module or Class
    -p, --ppp                   Show public, protected (in yellow) and private (in green) methods
    -q, --quiet                 Show only methods defined on object.singleton_class and object.class
    -v, --verbose               Show methods and constants on all super-classes (ignores Pry.config.ls.ceiling)
    -g, --globals               Show global variables, including those builtin to Ruby (in cyan)
    -l, --locals                Show hash of local vars, sorted by descending size
    -c, --constants             Show constants, highlighting classes (in blue), and exceptions (in purple).
                                Constants that are pending autoload? are also shown (in yellow)
    -i, --ivars                 Show instance variables (in blue) and class variables (in bright blue)
    -G, --grep                  Filter output by regular expression
    -d, --dconstants            Show deprecated constants
    -h, --help                  Show this message.

Implementation Options

Add a new command

If we go for this, we should come up with a new name though. Because this project already has the l[ist] command for listing source code, naming this command ls could be confusing.

Enhance the info command

The info command is designed to show information about the current frame. So it doesn't exactly match what Pry's ls does. But if we can add the current functionality to a info f[rame] subcommand, info can be more generic and perform something like the ls does.

The default debugger for Ruby?

I'm really interested in this gem and I'd like to contribute to it. But I also need to learn more about it to make the correct contribution. So here are my questions:

  • Is the goal to make it Ruby's default debugger?
  • What's its difference between tools like pry and byebug? Like, are we replacing them and porting some of their useful features here? Or we're going to make them configurable options for some of the features (like breakpoint)?
  • Is there a plan to add test cases to it? I may be able to help with this at the beginning.

binding.bp with commands

Many developers, including me, usually enter debugging session with a method call like byebug or binding.pry. So we'd mostly use binding.bp instead of the rdbg executable when using this tool.

But unlike the rdbg executable, which can take a series of commands with the -e option, binding.bp doesn't take any arguments or options. So I'm wondering if we can support passing commands to it like:

binding.bp(command:  "c ;; bt ;; info")
binding.bp(command_file: "foo.rb")

RUBY_DEBUG_TEST_MODE causes tracing to hang

I guess the loop looks somewhat like:

  1. debugger prints TP event
  2. test console serializes internal info with JSON.generate
  3. because 2 triggers new TP events, back to 1

This makes it impossible to write tests for the trace command.

$ RUBY_DEBUG_TEST_MODE=true exe/rdbg target.rb
[1, 10] in target.rb
=>    1| class Foo
      2|   def first_call
      3|     second_call(20)
      4|   end
      5|
      6|   def second_call(num)
      7|     tap do
      8|       third_call_with_block do |ten|
      9|         forth_call(num, ten)
     10|       end
=>#0    <main> at target.rb:1
INTERNAL_INFO: {"location":"target.rb:1","line":1}

(rdbg)
trace on # <======= trace command 
Trace on
Tracing:                call JSON.generate at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:296
Tracing:                 line at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 c_call Module#=== at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 c_return Module#=== => false at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 line at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:300
Tracing:                 c_call Class#new at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:300
Tracing:                  c_call JSON::Ext::Generator::State#initialize at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:300
Tracing:                  c_return JSON::Ext::Generator::State#initialize => #<JSON::Ext::Generator::State:0x00007f7f2f0ceaa8> at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common
.rb:300
Tracing:                 c_return Class#new => #<JSON::Ext::Generator::State:0x00007f7f2f0ceaa8> at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:300
Tracing:                 line at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:302
Tracing:                 line at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:312
Tracing:                 c_call JSON::Ext::Generator::State#generate at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:312
Tracing:                 c_return JSON::Ext::Generator::State#generate => "{\"location\":\"target.rb:1\",\"line\":1}" at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:312
Tracing:                return JSON.generate => "{\"location\":\"target.rb:1\",\"line\":1}" at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:313
INTERNAL_INFO: {"location":"target.rb:1","line":1}

(rdbg)
Trace on
Tracing:                call JSON.generate at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:296
Tracing:                call JSON.generate at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:296
Tracing:                 line at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 line at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 c_call Module#=== at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 c_call Module#=== at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 c_return Module#=== => false at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 c_return Module#=== => false at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:297
Tracing:                 line at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:300
Tracing:                 line at /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/json-2.5.1/lib/json/common.rb:300

Breakpoint won't fire if console last statement

From the README:

# target.rb
require 'debug/session'  # introduce the functionality
DEBUGGER__.console       # and start the debug console

I'd expect this to work, but it doesn't.

root@5614f7e87baa:/workspace# bundle exec ruby target.rb 
root@5614f7e87baa:/workspace# echo $?
0

Adding a statement following the console call will trigger the breakpoint. It can be a no-op line like nil.

require 'debug/session'  # introduce the functionality
DEBUGGER__.console       # and start the debug console

nil
root@5614f7e87baa:/workspace# bundle exec ruby target.rb 
[1, 4] in target.rb
      1| require 'debug/session'  # introduce the functionality
      2| DEBUGGER__.console       # and start the debug console
      3| 
=>    4| nil
=>#0    <main> at target.rb:4

(rdbg) 

I'm fairly sure the breakpoint being setup here is 1 line off in this scenario, but I'm not sure how to fix it at first glance.

add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false

Quick exit command

Currently, all exit/quit/kill commands require an additional confirmation step.

debug/lib/debug/session.rb

Lines 236 to 251 in 4a3046b

when 'q', 'quit', 'exit'
if ask 'Really quit?'
@ui.quit arg.to_i
@tc << :continue
else
return :retry
end
# * `kill` or `q[uit]!`
# * Stop the debuggee process.
when 'kill', 'quit!', 'q!'
if ask 'Really kill?'
exit! (arg || 1).to_i
else
return :retry
end

❯ rdbg foo.rb
[1, 2] in foo.rb
=>    1| a = 1
      2| puts(a)
--> #0  foo.rb:1:in `<main>'

(rdbg) exit
Really quit? [Y/n] Y
❯ rdbg foo.rb
[1, 2] in foo.rb
=>    1| a = 1
      2| puts(a)
--> #0  foo.rb:1:in `<main>'

(rdbg) quit!
Really kill? [Y/n] Y

Without changing any existing commands, I wonder if it's ok to add the exit! command? It'll allow users to quickly exit the current session without hitting Y to confirm it.

❯ exe/rdbg foo.rb
[1, 1] in foo.rb
=>    1| puts(1)
=>#0    <main> at foo.rb:1

(rdbg) exit!

Test builder doesn't work.

Because of current changes, test builder doesn't work. Maybe the reason is 94596d3

$ bin/gentest target.rb
DEBUGGER: Session start (pid: 7977)
[1, 9] in ~/workspace/debug/target.rb
=>   1| module Foo
     2|   class Bar
     3|     def self.a
     4|       "hello"
     5|     end
     6|   end
     7|   Bar.a
     8|   bar = Bar.new
     9| end
=>#0	<main> at ~/workspace/debug/target.rb:1
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:1","line":1}
# stuck here

^C/Users/naotto/.rbenv/versions/3.0.0/lib/ruby/3.0.0/expect.rb:47:in `select': Interrupt
	from /Users/naotto/.rbenv/versions/3.0.0/lib/ruby/3.0.0/expect.rb:47:in `expect'
	from /Users/naotto/workspace/debug/test/tool/test_builder.rb:84:in `block in create_pseudo_terminal'
	from /Users/naotto/workspace/debug/test/tool/test_builder.rb:80:in `spawn'
	from /Users/naotto/workspace/debug/test/tool/test_builder.rb:80:in `create_pseudo_terminal'
	from /Users/naotto/workspace/debug/test/tool/test_builder.rb:24:in `start'
	from bin/gentest:22:in `<main>'

Pending MethodBreakpoint

When we set a method breakpoint with break C.foo and If the C or C.foo is not defined, it will be a pending breakpoint and when C.foo is activated, it will be activated.

We can set breakpoint with any expression like:

[1, 7] in target.rb
      1|
      2| def (o = '').s
      3|   :s
      4| end
      5|
=>    6| p o.s
      7| __END__
=>#0    <main> at target.rb:6
(rdbg:commands) b o.s
#0  BP - Method  o.s at target.rb:2

(rdbg) c
[1, 7] in target.rb
      1|
      2| def (o = '').s
=>    3|   :s
      4| end
      5|
      6| p o.s
      7| __END__
=>#0    .s at target.rb:3
  #1    <main> at target.rb:6

Stop by #0  BP - Method  o.s at target.rb:2

In this case, user can set a breakpoint with current context (lvar o).

The problem is, if we write an expression which does not valid, the breakpoint will be registered as a pending breakpoint.

(rdbg) b not_lvar.foo
undefined local variable or method `not_lvar' for "":String
#1  BP - Method (pending)  not_lvar.foo

Maybe it will not be activated in future.

This is useful for singleton method:

[master]$ exe/rdbg target.rb  -e 'b C.foo'
DEBUGGER: Session start (pid: 19219)
[1, 7] in target.rb
      1|
=>    2| class C
      3|   def self.foo
      4|   end
      5| end
      6|
      7| __END__
=>#0    <main> at target.rb:2
(rdbg:commands) b C.foo
uninitialized constant C
#0  BP - Method (pending)  C.foo

(rdbg) c
DEBUGGER:  BP - Method  C.foo at target.rb:3 is activated.

At first, C is not defined so it will be a pending breakpoint. After that C.foo is defined and the breakpoint will be activated.

b expr.method has two usecases:

  • (1) want to set breakpoint to the current context
  • (2) want to set singleton class

For (1), it should not make a pending breakpoint.
For (2),, it should.

We can not recognize the purpose for (1) and (2) now.

Ideas:

  • If the expression is like Constant, allow to make pending.
    • It saves (2).
    • Not complete for (1)
  • Add option to recognize 1 and 2.
    • break --allow-pending C.foo for (2).
    • Too long...

auto continue on `.rdbginit` and `binding.bp(command:)`

.rdbginit is a file to put configurations such as breakpoints and so on (now there is set command, it is another TODO). So it should be:

set ...
set ...
break file:line
break Foo#bar

Now, this commands are invoked at the first suspend event. In general, at the beginning of script. However, I'm planning to kick this script at loading debug.rb. So it should only configuration, it should not stop at the end of the rc file. However, now continue is needed.

So I'm planning to add these rule:

  • prevent continue on rc file
  • run continue at last

It is more useful, I guess.


On the other hands, binding.bp(command:) can pass the commands, and I think it has similar characteristic like rc files.

So I'm thinking to apply same rule with rc files.

  • prevent continue on given command(s).
  • run continue at the end of given command(s).
  • add nonstop: new keyword
    • binding.bp() -> binding.bp(nonstop: false)
    • binding.bp(command: cmd) -> binding.bp(command: cmd, nonstop: true)
    • You can specify binding.bp(command: cmd, nonstop: false) explictly.

@st0012 what do you think about it?

binding.break(do:) doesn't continue in exe/rdbg

I expect binding.break(do:) to continue regardless how I start the debug session. But currently it always stops when used with exe/rdbg.

# issue.rb
binding.b(do: "p '!!!!!!!!!!!!!!'")

exe/rdbg -e 'c ' issue.rb

❯ exe/rdbg -e 'c ' issue.rb
DEBUGGER: Session start (pid: 41207)
[1, 2] in issue.rb
=>    1| binding.b(do: "p '!!!!!!!!!!!!!!'")
      2|
=>#0    <main> at issue.rb:1
(rdbg:commands) c 
[1, 2] in issue.rb
=>    1| binding.b(do: "p '!!!!!!!!!!!!!!'")
      2|
=>#0    <main> at issue.rb:1
(rdbg:commands) p '!!!!!!!!!!!!!!'
=> "!!!!!!!!!!!!!!" 
(rdbg) c # it shouldn't stop here

ruby -Ilib -r debug issue.rb

❯ ruby -Ilib -r debug issue.rb
DEBUGGER: Session start (pid: 41237)
[1, 2] in issue.rb
=>    1| binding.b(do: "p '!!!!!!!!!!!!!!'")
      2|
=>#0    <main> at issue.rb:1
(rdbg:binding.break) p '!!!!!!!!!!!!!!'
=> "!!!!!!!!!!!!!!"

I guess it's caused by the PresetCommand being set not to auto continue when the session is started by exe/rdbg. But I don't fully understand the preset command system so I can't fix it.

irb command doesn't work in test mode

The irb command would hang without any output in the test mode:

❯ ruby test/debug/irb_test.rb
Loaded suite test/debug/irb_test
Started
[1, 2] in /var/folders/yg/hnbymwxd5pn7v94_clc59y6r0000gn/T/debugger20210624-80600-aod65v.rb
=>    1| a = 1
      2| b = 2
=>#0    <main> at /var/folders/yg/hnbymwxd5pn7v94_clc59y6r0000gn/T/debugger20210624-80600-aod65v.rb:1
INTERNAL_INFO: {"location":"/var/folders/yg/hnbymwxd5pn7v94_clc59y6r0000gn/T/debugger20210624-80600-aod65v.rb:1","line":1}
irb

(rdbg) irb
# then timeout

I suspect it's because the test framework doesn't support such cases yet.

Example test:

require_relative '../support/test_case'

module DEBUGGER__
  class IRBCommandTest < TestCase
    def program
      <<~RUBY
      a = 1
      b = 2
      RUBY
    end

    def test_irb_starts_an_irb_session
      debug_code(program) do
        type 'irb'
        assert_line_text(/irb\(main\)/)
        type 'exit'
        assert_line_text(/rdbg/)
        type 'q!'
      end
    end
  end
end

Proposal: Adding do, pre, and if options to the catch command

Use cases:

  • catch SomeException do: bt - pretty exception backtrace with locals!
    • Postmortem mode doesn't always cover this case because the target exception may not be the final one that exists the program.
  • catch SomeException if: $!.message.match?(/regexp/) - helpful for catching ArgumentError with specific messages.

`bt` shows strange information on nested block

[master]$ exe/rdbg -e 'b 3;; c;; bt' target.rb
[1, 7] in target.rb
=>    1| 1.times{
      2|   1.times{
      3|     p 1
      4|   }
      5| }
      6|
      7| __END__
=>#0    <main> at target.rb:1
(rdbg:init) b 3
#0  BP - Line  /mnt/c/ko1/src/rb/ruby-debug/target.rb:3 (line)
(rdbg:init) c
[1, 7] in target.rb
      1| 1.times{
      2|   1.times{
=>    3|     p 1
      4|   }
      5| }
      6|
      7| __END__
=>#0    block in levels) at target.rb:3
  #1    [C] Integer#times at target.rb:2
  # and 3 frames (use `bt' command for all frames)

Stop by # BP - Line  /mnt/c/ko1/src/rb/ruby-debug/target.rb:3 (line) 0
(rdbg:init) bt
=>#0    block in levels) at target.rb:3
  #1    [C] Integer#times at target.rb:2
  #2    block in <main> at target.rb:2
  #3    [C] Integer#times at target.rb:1
  #4    <main> at target.rb:1

=>#0 block in levels) at target.rb:3 seems strange.
Could you check it if you have a time? @st0012

Can not find debug.so

Thanks for rewriting this cool lib! I wanted to give it a try but got this error:

❯ rdbg foo.rb
/Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/debug-1.0.0.beta1/lib/debug/session.rb:7:in `require_relative': cannot load such file -- /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/debug-
1.0.0.beta1/lib/debug/debug.so (LoadError)
        from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/debug-1.0.0.beta1/lib/debug/session.rb:7:in `<top (required)>'
        from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/debug-1.0.0.beta1/lib/debug/console.rb:1:in `require_relative'
        from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/debug-1.0.0.beta1/lib/debug/console.rb:1:in `<top (required)>'
        from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/debug-1.0.0.beta1/lib/debug/run.rb:1:in `require_relative'
        from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/debug-1.0.0.beta1/lib/debug/run.rb:1:in `<top (required)>'
        from <internal:/Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
        from <internal:/Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/rubygems/core_ext/kernel_require.rb>:85:in `require'

Not sure if it requires any additional setup or only runs on the latest version of Ruby?

My Environment

  • OS: Darwin Kernel Version 20.3.0: Thu Jan 21 00:07:06 PST 2021; root:xnu-7195.81.3~1/RELEASE_X86_64
  • Ruby: 2.7.2 and 3.0.1 both got this error
  • rubygems: 3.2.15

Supporting debug within Rails 7.0

Hi @ko1

We'd really like to make debug the default debugging choice for Rails 7.0 and beyond. We were wondering how we can best make that happen and we've come up with a few questions that I hope you don't mind answering.

  1. Is DEBUGGER__.console the final choice for invoking the debugger? We'd like to use something more traditional within Rails 7.0 like debugger, etc. and we're happy to do this within Rails itself but were wondering the best way to achieve that.

  2. A lot of opposition people have to using debuggers in their Rails apps is that they end up stepping through gem code that they haven't written and end up either giving up in frustration or getting lost. As a result they fall back on print debugging or using a console. To help overcome that, we'd like to implement a filtering mechanism similar to how Rails cleans backtraces so that when a developer steps through the code they are restricted to their application - is this something that's possible now and if it isn't how can we help make it possible?

  3. Is the intention to ship 1.0 before Ruby 3.1 ships? We're not close to shipping 7.0 yet but were wondering whether the intention was to hold back the release until December?

Thanks for all the work you're doing on this. πŸ‘πŸ»

Testing with remote mode is not stable

When testing locally with Ruby 3.1.0-dev, I sometimes get errors like this:

Error: test_break_with_namespaced_class_method_stops_at_correct_place(DEBUGGER__::BreakAtMethodsTest): Errno::EMFILE: Too many open files - fork failed
/Users/st0012/projects/debug/test/support/utils.rb:77:in `spawn'
/Users/st0012/projects/debug/test/support/utils.rb:77:in `block in new_child_process'

Or timeout error:

Failure: test_backtrace_prints_the_return_value(DEBUGGER__::BasicBacktraceTest):
  TIMEOUT ERROR (10 sec)
  [DEBUG SESSION LOG]
  > [1, 10] in /var/folders/yg/hnbymwxd5pn7v94_clc59y6r0000gn/T/debugger20210701-59574-vo4b86.rb
  > =>    1| class Foo
  >       2|   def first_call
  >       3|     second_call(20)
  >       4|   end
  >       5|
  >       6|   def second_call(num)
  >       7|     third_call_with_block do |ten|
  >       8|       num + ten
  >       9|     end
  >      10|   end
  > =>#0        <main> at /var/folders/yg/hnbymwxd5pn7v94_clc59y6r0000gn/T/debugger20210701-59574-vo4b86.rb:1
  > b 4
  >
  > (rdb) b 4
  .
  <false> is not true.

The master build also fails for timeout error. I can't rerun it but it looks like a random failure too.

Proposal on backtrace (bt command) format

I really like the useful information added to the backtrace (like arguments, block parameters...etc.). It's something I've been looking for for a long time.

And because I saw @ko1 asking for feedback on the backtrace format in his tweet, I think I can provide my 2 cents here.

Example

This is the example script I'll use to generate the output for my proposal:

class Foo
  def first_call
    second_call(20)
  end

  def second_call(num)
    third_call_with_block do |ten|
      forth_call(num, ten)
    end
  end

  def third_call_with_block(&block)
    @ivar1 = 10; @ivar2 = 20

    yield(10)
  end

  def forth_call(num1, num2)
    require "debug" # <= breakpoint
  end
end

Foo.new.first_call

And this is the output it generates for the bt command.

=>#0    Foo#forth_call(num1=20, num2=10) at foo.rb:21 #=> true
  #1    block{|ten=10|} in second_call at foo.rb:8
  #2    Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) at foo.rb:15
  #3    Foo#second_call(num=20) at foo.rb:7
  #4    first_call at foo.rb:3
  #5    <main> at foo.rb:24

Introduction of Components

I know most people reading this issue are already contributor to it and are familiar with the information contained in the backtrace. But I still want to do a short introduction to those who're not that familiar with it yet.

Trace Type - Call With Arguments

=>#0    Foo#forth_call(num1=20, num2=10) at foo.rb:21 #=> true
  • =>#0 - prompt + frame index
  • Foo#forth_call - class + callee
  • (num1=20, num2=10) - args
  • at foo.rb:21 - location
  • #=> true - return value

Trace Type - Block Evaluation

  #1    block{|ten=10|} in second_call at foo.rb:8
  • #1 - prompt + frame index
  • block{|ten=10|} in second_call - block label & parameters
  • at foo.rb:8 - location

So, the Format

Ok, let's back to the backtrace:

=>#0    Foo#forth_call(num1=20, num2=10) at foo.rb:21 #=> true
  #1    block{|ten=10|} in second_call at foo.rb:8
  #2    Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) at foo.rb:15
  #3    Foo#second_call(num=20) at foo.rb:7
  #4    first_call at foo.rb:3
  #5    <main> at foo.rb:24

To me, the main drawback of the current format is: it's hard to read the full trace at once.

This is the normal Ruby backtrace for the same script:

foo.rb:8:in `block in second_call'
foo.rb:15:in `third_call_with_block'
foo.rb:7:in `second_call'
foo.rb:3:in `first_call'
foo.rb:24:in `<main>'

As a Rubyist, I'm used to having the call site upfront so I can read them directly, from top to bottom.

But in the new format, call site is placed after information like class, callee, and args. So it'll appear in the different column in different lines.

This means I need to perform a search on each line:

  • Searching for the pattern of a trace.
    • Sometime this doesn't work that well. For example, the #2 frame has 2 traces. One of the Proc object and one for the actual call location.
  • Or searching for the at keyword.

To improve this, I want to propose the following options as a start:

Option 1 - Don't Change Information Order. Just Replace at With Different Symbols

We're better at distinguishing symbols then letters in a pile of texts. So if we don't want to change the order of information, replacing at with a symbol should help locating the call site too. Some examples:

|

=>#0    Foo#forth_call(num1=20, num2=10) | foo.rb:21 #=> true
  #1    block{|ten=10|} in second_call | foo.rb:8
  #2    Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) | foo.rb:15
  #3    Foo#second_call(num=20) | foo.rb:7
  #4    first_call | foo.rb:3
  #5    <main> | foo.rb:24

@

=>#0    Foo#forth_call(num1=20, num2=10) @ foo.rb:21 #=> true
  #1    block{|ten=10|} in second_call @ foo.rb:8
  #2    Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) @ foo.rb:15
  #3    Foo#second_call(num=20) @ foo.rb:7
  #4    first_call @ foo.rb:3
  #5    <main> @ foo.rb:24

#

=>#0    Foo#forth_call(num1=20, num2=10) # foo.rb:21 #=> true
  #1    block{|ten=10|} in second_call # foo.rb:8
  #2    Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) # foo.rb:15
  #3    Foo#second_call(num=20) # foo.rb:7
  #4    first_call # foo.rb:3
  #5    <main> # foo.rb:24

Options 2 - Display Call Site Upfront

With Separator (Not Aligned)

=>#0    foo.rb:21 | Foo#forth_call(num1=20, num2=10) #=> true
  #1    foo.rb:8 | block{|ten=10|} in second_call
  #2    foo.rb:15 | Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
  #3    foo.rb:7 | Foo#second_call(num=20)
  #4    foo.rb:3 | first_call
  #5    foo.rb:24 | <main>

With Separator (Aligned)

=>#0    foo.rb:21 | Foo#forth_call(num1=20, num2=10) #=> true
  #1    foo.rb:8  | block{|ten=10|} in second_call
  #2    foo.rb:15 | Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
  #3    foo.rb:7  | Foo#second_call(num=20)
  #4    foo.rb:3  | first_call
  #5    foo.rb:24 | <main>

This looks good but not really practical because the length of call site also can vary a lot, like

from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/rubygems_integration.rb:390:in `block in replace_bin_path'
from /Users/st0012/.rbenv/versions/3.0.1/bin/pry:23:in `<top (required)>'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:63:in `load'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:63:in `kernel_load'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:28:in `run'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli.rb:494:in `exec'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli.rb:30:in `dispatch'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/base.rb:485:in `start'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli.rb:24:in `start'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/bundler-2.2.15/libexec/bundle:49:in `block in <top (required)>'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/friendly_errors.rb:130:in `with_friendly_errors'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/bundler-2.2.15/libexec/bundle:37:in `<top (required)>'
from /Users/st0012/.rbenv/versions/3.0.1/bin/bundle:23:in `load'
from /Users/st0012/.rbenv/versions/3.0.1/bin/bundle:23:in `<main>'

2 Lines

With the amount of information to display, I think having 2 lines for each trace is a reasonable option too.

With Separator

=>#0    foo.rb:21 
          Foo#forth_call(num1=20, num2=10) #=> true
  #1    foo.rb:8
          block{|ten=10|} in second_call
  #2    foo.rb:15
          Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
  #3    foo.rb:7 
          Foo#second_call(num=20)
  #4    foo.rb:3 
          first_call
  #5    foo.rb:24 
          <main>

With a 2nd-line indicator (my preference)

I find having an indicator helps me read the lines better. Among all the options, this is my favorite.

=>#0    foo.rb:21 
          | Foo#forth_call(num1=20, num2=10) #=> true
  #1    foo.rb:8
          | block{|ten=10|} in second_call
  #2    foo.rb:15 
          | Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
  #3    foo.rb:7 
          | Foo#second_call(num=20)
  #4    foo.rb:3 
          | first_call
  #5    foo.rb:24 
          | <main>

Other Options

This issue is just to continue the discussion with some of my ideas. And I believe there are many better options out there. So if you also have any thoughts on this, please leave a comment too πŸ˜„


Update

@ko1 and I have colorized the backtrace and it now looks a lot better πŸ™‚

ζˆͺεœ– 2021-05-26 上午11 21 56

ζˆͺεœ– 2021-05-26 上午11 24 16


Some commands shouldn't be automatically repeated

The debugger repeats the previous command when the user gives an empty input. This is neat for flow or frame control commands. But for commands (especially breakpoint commands) it's not necessary.

So here's the list of commands I think we should exclude:

  • all the breakpoint commands - doesn't make sense
  • eval
  • irb - it's annoying to accidentally enter the irb session by hitting <enter>

catch command doesn't work.

When I tried to use catch command, an error occurred.

terminal

$ ruby target.rb 
[1, 10] in target.rb
   1| require_relative β€˜debug/lib/debug/session’
   2| DEBUGGER__.console
=>  3| module DEBUGGER__
   4|  #
   5|  # Toy class to test stepping
   6|  #
   7|  class ExampleClass
   8|   def self.add_four
   9|    # num += 4
   10|    # num
=>#0	<main> at target.rb:3
(rdbg) catch NoMethodError
[REPL ERROR] #<NameError: undefined local variable or method `pat’ for #<DEBUGGER__::CatchBreakpoint:0x00007fd1ac90c8d0 @pat=β€œNoMethodError”, @key=[:catch, β€œNoMethodError”], @deleted=false, @tp=#<TracePoint:enabled>>
Did you mean? @pat>
 /Users/[username]/workspace/debug/lib/debug/breakpoint.rb:212:in `block (2 levels) in setup’
 /Users/[username]/workspace/debug/lib/debug/breakpoint.rb:211:in `each’
 /Users/[username]/workspace/debug/lib/debug/breakpoint.rb:211:in `block in setup’
 /Users/[username]/workspace/debug/lib/debug/session.rb:855:in `add_catch_breakpoint’
 /Users/[username]/workspace/debug/lib/debug/session.rb:316:in `wait_command’
 /Users/[username]/workspace/debug/lib/debug/session.rb:165:in `block (2 levels) in wait_command_loop’
 /Users/[username]/workspace/debug/lib/debug/session.rb:164:in `loop’
 /Users/[username]/workspace/debug/lib/debug/session.rb:164:in `block in wait_command_loop’
 /Users/[username]/workspace/debug/lib/debug/session.rb:803:in `block in stop_all_threads’
 <internal:trace_point>:196:in `enable’
 /Users/[username]/workspace/debug/lib/debug/session.rb:802:in `stop_all_threads’
 /Users/[username]/workspace/debug/lib/debug/session.rb:163:in `wait_command_loop’
 /Users/[username]/workspace/debug/lib/debug/session.rb:97:in `block in initialize’

file

require 'debug/run'
module DEBUGGER__ 
#
# Toy class to test stepping.
#
  class #{example_class}
    def self.add_four(num)
      num += 4
      num += 2
      num
    end
  end

  res = #{example_class}.add_four(7)
  res + 1
end

Failure message doesn't show which mode.

@ko1 pointed out that there is no information about which mode the test failed in.

Now

Failure: test_finish_leaves_blocks_right_away(DEBUGGER__::BlockControlFlowTest):
  TIMEOUT ERROR (10 sec)
  [DEBUG SESSION LOG]
  > [1, 4] in /tmp/debugger20210701-1565-h1h29k.rb
  > =>    1| 2.times do |n|
  >       2|   n
  >       3| end
  >       4| a += 1
  > =>#0	<main> at /tmp/debugger20210701-1565-h1h29k.rb:1
  > step
  > 
  > (rdb) step
  .
  <false> is not true.
/home/runner/work/debug/debug/test/support/utils.rb:128:in `rescue in block in create_pseudo_terminal'
/home/runner/work/debug/debug/test/support/utils.rb:123:in `block in create_pseudo_terminal'
/home/runner/work/debug/debug/test/support/utils.rb:88:in `spawn'
/home/runner/work/debug/debug/test/support/utils.rb:88:in `create_pseudo_terminal'
/home/runner/work/debug/debug/test/support/utils.rb:61:in `setup_terminal'
/home/runner/work/debug/debug/test/support/utils.rb:27:in `debug_code'
/home/runner/work/debug/debug/test/debug/control_flow_commands_test.rb:144:in `test_finish_leaves_blocks_right_away'
     141:     end
     142: 
     143:     def test_finish_leaves_blocks_right_away
  => 144:       debug_code(program) do
     145:         type 'step'
     146:         assert_line_num 2
     147:         type 'next'

Expected

Failure: test_finish_leaves_blocks_right_away(DEBUGGER__::BlockControlFlowTest):
  TIMEOUT ERROR (10 sec) on TCP/IP mode
  [DEBUG SESSION LOG]
  > [1, 4] in /tmp/debugger20210701-1565-h1h29k.rb
  > =>    1| 2.times do |n|
  >       2|   n
  >       3| end
  >       4| a += 1
  > =>#0	<main> at /tmp/debugger20210701-1565-h1h29k.rb:1
  > step
  > 
  > (rdb) step
  .
  <false> is not true.
/home/runner/work/debug/debug/test/support/utils.rb:128:in `rescue in block in create_pseudo_terminal'
/home/runner/work/debug/debug/test/support/utils.rb:123:in `block in create_pseudo_terminal'
/home/runner/work/debug/debug/test/support/utils.rb:88:in `spawn'
/home/runner/work/debug/debug/test/support/utils.rb:88:in `create_pseudo_terminal'
/home/runner/work/debug/debug/test/support/utils.rb:61:in `setup_terminal'
/home/runner/work/debug/debug/test/support/utils.rb:27:in `debug_code'
/home/runner/work/debug/debug/test/debug/control_flow_commands_test.rb:144:in `test_finish_leaves_blocks_right_away'
     141:     end
     142: 
     143:     def test_finish_leaves_blocks_right_away
  => 144:       debug_code(program) do
     145:         type 'step'
     146:         assert_line_num 2
     147:         type 'next'

Rename trace subcommands

I'm writing an article about the debugger's tracer feature. And I think we can rename some of the commands to make the concept easier to explain to users.

The current trace commands are:

  • trace line - trace <noun>
  • trace call - trace <noun>
  • trace raise - trace <verb>
  • trace pass - trace <verb>
    • I guess pass means "trace how the object is passed in the program" here. So I assume it's used as verb.

As you can see, the parts of speech of subcommands are inconsistent.

Also, each command has its own Tracer class, like LineTracer. But in the context of naming a class, having a "VerbNoun" combination is usually weird because it feels like the verb is applied to the latter noun. So RaiseTracer looks a bit funny to me and I think ExceptionTracer would be a better name.

And here are the names I want to propose:

  • trace line & LineTracer
    • When the line is evaluated.
  • trace call & CallTracer
    • When any method is called.
  • trace exception & ExceptionTracer
    • When the exception is raised.
  • trace object & ObjectTracer
    • When the object is passed into a method call as an argument.
    • When the object receives a method call.

In the new names, subcommands are the subject to be traced.
I know trace object doesn't convey the passed as an argument behavior clearly. But it's not hard to understand either. Also, because no other major Ruby tools have this feature, this project can define what "trace an object" means πŸ™‚

improve `binding.bp(command:)`

Some ideas from #148 (comment)

command passing

binding.bp(command: 'A ;; B ;; C') will be use frequently, so we can improve it.

  • command: -> do:
    • binding.bp do: 'A ;; B ;; C'
    • shorter than command:
    • same as break do: command
  • Accept command without keyword
    • binding.bp('A ;; B ;; C')
    • But it is too cryptic (bp may be already cryptic :p)
  • accept Array
    • binding.bp do: %w(A B C)

nonstop control

Now binding.bp(command: 'info', nonstop: false) will stop after info command.
We can introduce shorter way.

  • Different keyword
    • binding.bp(do: ...) for nonstop (by default)
    • binding.bp(before: ...) for stop, but run given command before stopping
    • binding.bp(pre: ...) for stop, but run given command as pre-process

I think do: and pre: seems nice.

better method name

Now debug command is b or break. Renaming binding.bp to binding.break is one idea. And we can make an alias binding.b. I'm not sure we can introduce as default, but we can define it in ~/.rdbgrc.rb.

MethodBreakpoint stops when a sibling class calls the target method

Given the script

class Foo
  def self.bar
  end
end

class A < Foo
end

class B < Foo
end

binding.b(do: "b A.bar")

B.bar

And the command: ruby -Ilib -r debug target.rb

The debugger stops when B.bar is called, even though the command specified A.bar.

❯ ruby -Ilib -r debug target.rb
DEBUGGER: Session start (pid: 41486)
[7, 14] in target.rb
      7| end
      8|
      9| class B < Foo
     10| end
     11|
=>   12| binding.b(do: "b A.bar")
     13|
     14| B.bar
=>#0    <main> at target.rb:12
(rdbg:binding.break) b A.bar
#0  BP - Method  A.bar at target.rb:2
[1, 10] in target.rb
      1| class Foo
=>    2|   def self.bar
      3|   end
      4| end
      5|
      6| class A < Foo
      7| end
      8|
      9| class B < Foo
     10| end
=>#0    #<Class:Foo>#bar at target.rb:2
  #1    <main> at target.rb:14

Stop by #0  BP - Method  A.bar at target.rb:2

I expect B.bar not to trigger the breakpoint.

After the investigation, I think MethodBreakpoint captures the correct method:

From: /Users/st0012/projects/debug/lib/debug/breakpoint.rb @ line 399 :

    394:       eval_class_name
    395:       search_method
    396:
    397:       begin
    398:         retried = false
 => 399:         binding.irb
    400:         @tp.enable(target: @method)
    401:         DEBUGGER__.warn "#{self} is activated." if added
    402:
    403:       rescue ArgumentError
    404:         raise if retried

irb( BP - Method  A.bar at target.rb:2):001:0> @method
=> #<Method: A.bar() target.rb:2>

But the TracePoint is enabled anyway. So I'm not sure if this is a bug of debugger or TracePoint.

CatchBreakpoint stops at incorrect frame

Given this file:

# target.rb

a = 1
b = 2

1/0 # will trigger ZeroDivisionError

The catch command appears stopping at the wrong frame

❯ exe/rdbg -e 'catch ZeroDivisionError ;; c' target.rb
[1, 5] in target.rb
=>    1| a = 1
      2| b = 2
      3|
      4| 1/0
      5|
=>#0    <main> at target.rb:1
(rdbg:init) catch ZeroDivisionError
#0 catch bp "ZeroDivisionError"
(rdbg:init) c
# No sourcefile available for /Users/st0012/projects/debug/lib/debug/breakpoint.rb
=>#0    [C] Array#each at ~/projects/debug/lib/debug/breakpoint.rb:211
  #1    [C] Integer#/ at target.rb:4
  #2    <main> at target.rb:4 # shouldn't it stop at this frame?

Stop by #0 catch bp "ZeroDivisionError"

(rdbg)

The TracePoint that catches the exception has the correct path though: target.rb. So I think maybe it needs to adjust the frame index in ThreadClient#on_suspend?

proposal: Add frame, value decorators

When I worked on GDB I came to feel that GDB was made so that GDB developers could debug GDB. This was how it was for a long time. But when we added with the Python API to GDB, along with Pretty Printing, frame decorators and filters, we handed back data representation to the user. Don't like how we print this object? Well, here's an API, and the ability to write your own, and hooks back into the display component to write your own representation. What parts of an object or value are important to the developer? It's going to depend on what that developer is trying to solve. So the default GDB information displays were, I think, more or less dependent on the feature implementor: usually what a GDB developer wanted to see when debugging their project (GDB).

So I know this is a super wishy washy proposal right now. But every value printed, every stack frame annotated, every backtrace printed, and so on, should first be offered to any extensions that want to work on them. That way a user can display any value they way they want to display it. If they don't care, well, they'll get the default version. Hopefully I've painted a rough picture of what I would love to see. Any more details, please ask! And if I can find some time, I hope to start putting some patches together.

Unable to quit gracefully after triggering CatchBreakpoint

The debugger seems to be unable to quit without exception once a catch bp is triggered. I'm not able to debug the cause nor see anything from the stacktrace. So I'm demonstrating the issue with a few scenarios.

The script

class Foo
  def self.bar; end
end
Foo.bar


begin
  1/0 # raise "foo" should cause the same issue
rescue
  binding.bp
end

Scenarios

Quit from a method bp

Command: exe/rdbg -e 'b Foo.bar ;; c ;; q!' target.rb

✦ ❯ exe/rdbg -e 'b Foo.bar ;; c ;; q!' target.rb
DEBUGGER: Session start (pid: 23368)
[1, 10] in target.rb
=>    1| class Foo
      2|   def self.bar; end
      3| end
      4| Foo.bar
      5|
      6|
      7| begin
      8|   1/0
      9| rescue
     10|   binding.bp
=>#0    <main> at target.rb:1
(rdbg:commands) b Foo.bar
uninitialized constant Foo
#0  BP - Method (pending)  Foo.bar
(rdbg:commands) c
[1, 10] in target.rb
      1| class Foo
=>    2|   def self.bar; end
      3| end
      4| Foo.bar
      5|
      6|
      7| begin
      8|   1/0
      9| rescue
     10|   binding.bp
=>#0    Foo.bar at target.rb:2
  #1    <main> at target.rb:4

Stop by #0  BP - Method  Foo.bar
(rdbg:commands) q!

Quit from a line bp

Command: exe/rdbg -e 'b 3 ;; c ;; q!' target.rb

✦ ❯ exe/rdbg -e 'b 3 ;; c ;; q!' target.rb
DEBUGGER: Session start (pid: 23448)
[1, 10] in target.rb
=>    1| class Foo
      2|   def self.bar; end
      3| end
      4| Foo.bar
      5|
      6|
      7| begin
      8|   1/0
      9| rescue
     10|   binding.bp
=>#0    <main> at target.rb:1
(rdbg:commands) b 3
#0  BP - Line  /Users/st0012/projects/debug/target.rb:3 (end)
(rdbg:commands) c
[1, 10] in target.rb
      1| class Foo
      2|   def self.bar; end
=>    3| end
      4| Foo.bar
      5|
      6|
      7| begin
      8|   1/0
      9| rescue
     10|   binding.bp
=>#0    <class:Foo> at target.rb:3
  #1    <main> at target.rb:1

Stop by #0  BP - Line  /Users/st0012/projects/debug/target.rb:3 (end)
(rdbg:commands) q!

Quit from a catch bp

Command: exe/rdbg -e 'catch Exception ;; c ;; q!' target.rb

✦ ❯ exe/rdbg -e 'catch Exception ;; c ;; q!' target.rb
DEBUGGER: Session start (pid: 23524)
[1, 10] in target.rb
=>    1| class Foo
      2|   def self.bar; end
      3| end
      4| Foo.bar
      5|
      6|
      7| begin
      8|   1/0
      9| rescue
     10|   binding.bp
=>#0    <main> at target.rb:1
(rdbg:commands) catch Exception
#0  BP - Catch  "Exception"
(rdbg:commands) c
# No sourcefile available for target.rb
=>#0    [C] Integer#/ at target.rb:8
  #1    <main> at target.rb:8

Stop by #0  BP - Catch  "Exception"
(rdbg:commands) q!
["/Users/st0012/projects/debug/lib/debug/thread_client.rb",
 610,
 #<fatal: No live threads left. Deadlock?
2 threads, 2 sleeps current:0x00007fbd034499c0 main thread:0x00007fbd5340a300
* #<Thread:0x00007fbd53863a90 sleep_forever>
   rb_thread_t:0x00007fbd5340a300 native:0x000000010d9c9e00 int:0
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:251:in `block in setup'
   target.rb:8:in `/'
   target.rb:8:in `<main>'
* #<Thread:0x00007fbd53a7da60 /Users/st0012/projects/debug/lib/debug/session.rb:86 sleep_forever>
   rb_thread_t:0x00007fbd034499c0 native:0x0000700007245000 int:4
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:251:in `block in setup'
   /Users/st0012/projects/debug/lib/debug/session.rb:636:in `rescue in process_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:255:in `process_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:247:in `wait_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:211:in `block (2 levels) in wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:210:in `loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:210:in `block in wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:892:in `block in stop_all_threads'
   <internal:trace_point>:196:in `enable'
   /Users/st0012/projects/debug/lib/debug/session.rb:891:in `stop_all_threads'
   /Users/st0012/projects/debug/lib/debug/session.rb:209:in `wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:141:in `session_server_main'
   /Users/st0012/projects/debug/lib/debug/session.rb:88:in `block in initialize'
>,
 ["/Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop'",
  "/Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'",
  "/Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'",
  "/Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'",
  "/Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'",
  "/Users/st0012/projects/debug/lib/debug/breakpoint.rb:251:in `block in setup'",
  "target.rb:8:in `/'",
  "target.rb:8:in `<main>'"]]

Quit from another bp after leaving a catch bp

✦ ❯ exe/rdbg -e 'catch Exception ;; c ;; c ;; q!' target.rb
DEBUGGER: Session start (pid: 23628)
[1, 10] in target.rb
=>    1| class Foo
      2|   def self.bar; end
      3| end
      4| Foo.bar
      5|
      6|
      7| begin
      8|   1/0
      9| rescue
     10|   binding.bp
=>#0    <main> at target.rb:1
(rdbg:commands) catch Exception
#0  BP - Catch  "Exception"
(rdbg:commands) c
# No sourcefile available for target.rb
=>#0    [C] Integer#/ at target.rb:8
  #1    <main> at target.rb:8

Stop by #0  BP - Catch  "Exception"
(rdbg:commands) c
[5, 11] in target.rb
      5|
      6|
      7| begin
      8|   1/0
      9| rescue
=>   10|   binding.bp
     11| end
=>#0    rescue in <main> at target.rb:10
  #1    <main> at target.rb:7
(rdbg:commands) q!
["/Users/st0012/projects/debug/lib/debug/thread_client.rb",
 610,
 #<fatal: No live threads left. Deadlock?
2 threads, 2 sleeps current:0x00007fa092c20400 main thread:0x00007fa0e2c0a300
* #<Thread:0x00007fa0e3063a70 sleep_forever>
   rb_thread_t:0x00007fa0e2c0a300 native:0x0000000112165e00 int:0
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:108:in `block in setup'
   /Users/st0012/projects/debug/lib/debug/session.rb:1154:in `bp'
   target.rb:10:in `rescue in <main>'
   target.rb:7:in `<main>'
* #<Thread:0x00007fa0a311dbe0 /Users/st0012/projects/debug/lib/debug/session.rb:86 sleep_forever>
   rb_thread_t:0x00007fa092c20400 native:0x0000700002f31000 int:0
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:251:in `block in setup'
   /Users/st0012/projects/debug/lib/debug/session.rb:636:in `rescue in process_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:255:in `process_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:247:in `wait_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:211:in `block (2 levels) in wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:210:in `loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:210:in `block in wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:892:in `block in stop_all_threads'
   <internal:trace_point>:196:in `enable'
   /Users/st0012/projects/debug/lib/debug/session.rb:891:in `stop_all_threads'
   /Users/st0012/projects/debug/lib/debug/session.rb:209:in `wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:141:in `session_server_main'
   /Users/st0012/projects/debug/lib/debug/session.rb:88:in `block in initialize'
>,
 ["/Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop'",
  "/Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'",
  "/Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'",
  "/Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'",
  "/Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'",
  "/Users/st0012/projects/debug/lib/debug/breakpoint.rb:108:in `block in setup'",
  "/Users/st0012/projects/debug/lib/debug/session.rb:1154:in `bp'",
  "target.rb:10:in `rescue in <main>'",
  "target.rb:7:in `<main>'"]]
Traceback (most recent call last):
        12: from /Users/st0012/projects/debug/lib/debug/session.rb:88:in `block in initialize'
        11: from /Users/st0012/projects/debug/lib/debug/session.rb:141:in `session_server_main'
        10: from /Users/st0012/projects/debug/lib/debug/session.rb:209:in `wait_command_loop'
         9: from /Users/st0012/projects/debug/lib/debug/session.rb:891:in `stop_all_threads'
         8: from <internal:trace_point>:196:in `enable'
         7: from /Users/st0012/projects/debug/lib/debug/session.rb:892:in `block in stop_all_threads'
         6: from /Users/st0012/projects/debug/lib/debug/session.rb:210:in `block in wait_command_loop'
         5: from /Users/st0012/projects/debug/lib/debug/session.rb:210:in `loop'
         4: from /Users/st0012/projects/debug/lib/debug/session.rb:211:in `block (2 levels) in wait_command_loop'
         3: from /Users/st0012/projects/debug/lib/debug/session.rb:247:in `wait_command'
         2: from /Users/st0012/projects/debug/lib/debug/session.rb:311:in `process_command'
         1: from /Users/st0012/projects/debug/lib/debug/console.rb:33:in `quit'
/Users/st0012/projects/debug/lib/debug/console.rb:33:in `exit': exit (SystemExit)
        8: from target.rb:7:in `<main>'
        7: from target.rb:10:in `rescue in <main>'
        6: from /Users/st0012/projects/debug/lib/debug/session.rb:1154:in `bp'
        5: from /Users/st0012/projects/debug/lib/debug/breakpoint.rb:108:in `block in setup'
        4: from /Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'
        3: from /Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'
        2: from /Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'
        1: from /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'
/Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop': No live threads left. Deadlock? (fatal)
2 threads, 2 sleeps current:0x00007fa092c20400 main thread:0x00007fa0e2c0a300
* #<Thread:0x00007fa0e3063a70 sleep_forever>
   rb_thread_t:0x00007fa0e2c0a300 native:0x0000000112165e00 int:0
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:108:in `block in setup'
   /Users/st0012/projects/debug/lib/debug/session.rb:1154:in `bp'
   target.rb:10:in `rescue in <main>'
   target.rb:7:in `<main>'
* #<Thread:0x00007fa0a311dbe0 /Users/st0012/projects/debug/lib/debug/session.rb:86 sleep_forever>
   rb_thread_t:0x00007fa092c20400 native:0x0000700002f31000 int:0
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `pop'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:438:in `wait_next_action'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:182:in `on_suspend'
   /Users/st0012/projects/debug/lib/debug/thread_client.rb:147:in `on_breakpoint'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:52:in `suspend'
   /Users/st0012/projects/debug/lib/debug/breakpoint.rb:251:in `block in setup'
   /Users/st0012/projects/debug/lib/debug/session.rb:636:in `rescue in process_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:255:in `process_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:247:in `wait_command'
   /Users/st0012/projects/debug/lib/debug/session.rb:211:in `block (2 levels) in wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:210:in `loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:210:in `block in wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:892:in `block in stop_all_threads'
   <internal:trace_point>:196:in `enable'
   /Users/st0012/projects/debug/lib/debug/session.rb:891:in `stop_all_threads'
   /Users/st0012/projects/debug/lib/debug/session.rb:209:in `wait_command_loop'
   /Users/st0012/projects/debug/lib/debug/session.rb:141:in `session_server_main'
   /Users/st0012/projects/debug/lib/debug/session.rb:88:in `block in initialize'

Support filtering frames in backtrace command

Current backtrace always prints all frames, which could be a huge number in Rails applications (in my project it could be 170 frames). Some simple filtering would be helpful:

  1. backtrace [num] - only prints the first num frames.
  2. backtrace /controller/ - only prints frames with paths that match controller

Label breakpoint output

I've been playing breakpoints for a while. And one thing that bothers me is that it's not always easy to find the breakpoint messages. Because they are usually surrounded by debugger's prompts or other output:

Current BP messages

before bp label

But colorizing them like backtrace or frame info may just cause more confusion.

So instead, I want to use reverse coloring effect to add something called "breakpoint label", which looks like this:

Proposed messages

after bp label

It also looks great with a bright background.

ζˆͺεœ– 2021-06-03 上午11 29 08

Notes

  • This will be applied to the content of Breakpoint#to_s.
  • I also plan to colorize some breakpoint (like watch breakpoint)'s value. The color scheme will be the same as backtrace.
    • It's also possible to use colored inspect on changed values, but I'm not sure if that'd be an overkill or be too noisy? @ko1 wdyt?

Doesn't work with Spring

❯ spring rails c
DEBUGGER: Session start (pid: 37299)
Running via Spring preloader in process 37315
["DEBUGGER Exception: /Users/st0012/projects/debug/lib/debug/thread_client.rb:792",
 #<RuntimeError: DEBUGGER: stop at forked process is not supported yet.>,
 [["/Users/st0012/projects/debug/lib/debug/session.rb:1277:in `check_forked'","DEBUGGER Exception: /Users/st0012/projects/debug/lib/debug/thread_client.rb:792"
,
 "/Users/st0012/projects/debug/lib/debug/thread_client.rb:581:in `wait_next_action'"#<RuntimeError: DEBUGGER: stop at forked process is not supported yet.>,
,
   ["/Users/st0012/projects/debug/lib/debug/thread_client.rb:151:in `on_thread_begin'","/Users/st0012/projects/debug/lib/debug/session.rb:1277:in `check_forked'",

  "/Users/st0012/projects/debug/lib/debug/session.rb:104:in `block in initialize'"  "/Users/st0012/projects/debug/lib/debug/thread_client.rb:581:in `wait_next_action'",
  "/Users/st0012/projects/debug/lib/debug/thread_client.rb:156:in `on_load'"],
  ]
#<Thread:0x00007f82fe33db18 /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activerecord-6.0.3.7/lib/active_record/connection_adapters/abstract/connection_pool.rb:334 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
        3: from /Users/st0012/projects/debug/lib/debug/session.rb:104:in `block in initialize'
        2: from /Users/st0012/projects/debug/lib/debug/thread_client.rb:151:in `on_thread_begin'
        1: from /Users/st0012/projects/debug/lib/debug/thread_client.rb:581:in `wait_next_action'
/Users/st0012/projects/debug/lib/debug/session.rb:1277:in `check_forked': DEBUGGER: stop at forked process is not supported yet. (RuntimeError)
"/Users/st0012/projects/debug/lib/debug/session.rb:87:in `block in initialize'",
  "/Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.7.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:59:in `load'",
  "/Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.7.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:59:in `load'",
  "/Users/st0012/.rbenv/versions/2.7.2/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:72:in `require'",
  "/Users/st0012/.rbenv/versions/2.7.2/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:72:in `require'",
  "-e:1:in `<main>'"]]
Traceback (most recent call last):
        8: from -e:1:in `<main>'
        7: from /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:72:in `require'
        6: from /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:72:in `require'
        5: from /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.7.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:59:in `load'
        4: from /Users/st0012/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.7.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:59:in `load'
        3: from /Users/st0012/projects/debug/lib/debug/session.rb:87:in `block in initialize'
        2: from /Users/st0012/projects/debug/lib/debug/thread_client.rb:156:in `on_load'
        1: from /Users/st0012/projects/debug/lib/debug/thread_client.rb:581:in `wait_next_action'
/Users/st0012/projects/debug/lib/debug/session.rb:1277:in `check_forked': DEBUGGER: stop at forked process is not supported yet. (RuntimeError)

test builder can't finish Ctrl-d and Ctrl-d

@ko1 pointed out that test builder can't finish Ctrl-d and Ctrl-d.

$ bin/gentest target.rb
DEBUGGER: Session start (pid: 14221)
[1, 10] in ~/workspace/debug/target.rb
=>    1| module Foo
      2|   class Bar
      3|     def self.a
      4|       "hello"
      5|     end
      6|
      7|     def b(n)
      8|       2.times do
      9|         n
     10|       end
=>#0	<main> at ~/workspace/debug/target.rb:1
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:1","line":1}

(rdbg) quit
Really quit? [Y/n]Traceback (most recent call last):
	4: from bin/gentest:22:in `<main>'
	3: from /Users/ono-max/workspace/debug/test/tool/test_builder.rb:19:in `start'
	2: from /Users/ono-max/workspace/debug/test/tool/test_builder.rb:58:in `create_pseudo_terminal'
	1: from /Users/ono-max/workspace/debug/test/tool/test_builder.rb:58:in `spawn'
/Users/ono-max/workspace/debug/test/tool/test_builder.rb:75:in `block in create_pseudo_terminal': undefined method `chomp' for nil:NilClass (NoMethodError)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.