An Attempt at Adding Custom Commands and Aliases to ruby debugger - blitterated/groove_automator GitHub Wiki

An Attempt at Adding Custom Commands and Aliases to ruby/debug

ruby/debug command processing as of v1.9.1

How Session is created and set:

class << self
  define_method :initialize_session do |&init_ui|
    DEBUGGER__.info "Session start (pid: #{Process.pid})"
    ::DEBUGGER__.const_set(:SESSION, Session.new)
    SESSION.activate init_ui.call
    load_rc
  end
end

Definition of SessionCommand:

SessionCommand = Struct.new(:block, :repeat, :unsafe, :cancel_auto_continue, :postmortem)

Definition of @commands:

@commands = {}

Definition of register_command:

private def register_command *names,
                             repeat: false, unsafe: true, cancel_auto_continue: false, postmortem: true,
                             &b
  cmd = SessionCommand.new(b, repeat, unsafe, cancel_auto_continue, postmortem)

  names.each{|name|
    @commands[name] = cmd
  }
end

Definition of register_default_command:

# too big to list

Definition of process_command:

def process_command line
  if line.empty?
    if @repl_prev_line
      line = @repl_prev_line
    else
      return :retry
    end
  else
    @repl_prev_line = line
  end

  /([^\s]+)(?:\s+(.+))?/ =~ line
  cmd_name, cmd_arg = $1, $2

  if cmd = @commands[cmd_name]
    check_postmortem      if !cmd.postmortem
    check_unsafe          if cmd.unsafe
    cancel_auto_continue  if cmd.cancel_auto_continue
    @repl_prev_line = nil if !cmd.repeat

    cmd.block.call(cmd_arg)
  else
    @repl_prev_line = nil
    check_unsafe

    request_eval :pp, line
  end

rescue Interrupt
  return :retry
rescue SystemExit
  raise
rescue PostmortemError => e
  @ui.puts e.message
  return :retry
rescue Exception => e
  @ui.puts "[REPL ERROR] #{e.inspect}"
  @ui.puts e.backtrace.map{|e| '  ' + e}
  return :retry
end

Questions to answer:

  1. Can we get access to the current Session instance?
  2. Can we monkey patch the Session class or current instance?
  3. Can we add register a command with the current Session?

Accessing current Session instance

The Session instance gets created and stuffed into the SESSION const in the DEBUGGER__ module.

Definition of DEBUGGER__.initialize_session:

class << self
  define_method :initialize_session do |&init_ui|
    DEBUGGER__.info "Session start (pid: #{Process.pid})"
    ::DEBUGGER__.const_set(:SESSION, Session.new)
    SESSION.activate init_ui.call
    load_rc
  end
end

Test:

[1] pry(main)> DEBUGGER__::SESSION.inspect
=> "DEBUGGER__::SESSION"

👍🏻

Monkey patching DEBUGGER__::Session and command registration.

Since DEBUGGER__::Session#register_command is private, we can monkey patch a wrapper method into the class.

TEST: Add a simple method to Session and see if the instance in SESSION picks it up.

Add to .pryrc:

if defined?(DEBUGGER__)
  puts "ruby/debugger loaded."

  class DEBUGGER__::Session
    def foobar
      puts "       ,,,\n      (o o)\n---ooO-(_)-Ooo---"
      "baz wuz hur."
    end
  end
end

Run Pry and test:

ruby/debugger loaded.
[1] pry(main)> DEBUGGER__::SESSION.foobar
       ,,,
      (o o)
---ooO-(_)-Ooo---
=> "baz wuz hur."

👍🏻

TEST: Add a new command to Session.

Remove the definition of foobar from .pryrc, and add the monkey patch and a method to clone a SessionCommand.

if defined?(DEBUGGER__)
  puts "ruby/debugger loaded."

  class DEBUGGER__::Session
    def register_my_command *names, repeat:, unsafe:,
                            cancel_auto_continue:, postmortem:,
                             &b
      register_command *names, repeat, unsafe, 
                       cancel_auto_continue, postmortem,
                       &b
    end

    def clone_command(cmd_name)
      cmd = @commands[cmd_name]

      if cmd.nil?
        puts "Command \"#{cmd_name}\" is not a registered command."
        nil
      else
        #SessionCommand.new(cmd.block, cmd.repeat, cmd.unsafe, 
        #                   cmd.cancel_auto_continue, cmd.postmortem)
        cmd.clone
      end
    end
  end
end

Add to .pryrc following DEBUGGER__::Session monkey patch:

DEBUGGER__::SESSION.register_my_command 'hw', "helloworld",
                    repeat: true do |arg|
  puts "Hello world!"
end

Unfortunately, it barfs all over the place.

(rdbg) hw
Hello world!
nil
#<fatal:"No live threads left. Deadlock?\n2 threads, 2 sleeps current:0x0000aaaab0d53bf0 main thread:0x0000aaaab029f2a0\n* #<Thread:0x0000ffff6ec8aa28 sleep_forever>\n   rb_thread_t:0x0000aaaab029f2a0 native:0x0000ffff8855d4c0 int:0\n   \n* #<Thread:0x0000ffff6cde2d50@DEBUGGER__::SESSION@server /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/session.rb:179 sleep_forever>\n   rb_thread_t:0x0000aaaab0d53bf0 native:0x0000ffff6caaf100 int:0\n   \n">
@@@ #<Thread:0x0000ffff6ec8aa28 run>
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:1251:in `backtrace'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:1251:in `block in wait_next_action_'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:1249:in `each'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:1249:in `rescue in wait_next_action_'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:1244:in `wait_next_action_'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:875:in `block in wait_next_action'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:866:in `block in fiber_blocking'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:866:in `blocking'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:866:in `fiber_blocking'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:875:in `wait_next_action'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:320:in `suspend'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/thread_client.rb:358:in `block in step_tp'
 > /src/src/groove_automator.rb:174:in `block in remove_kick_on_snare'
 > /src/src/groove_automator.rb:165:in `each'
 > /src/src/groove_automator.rb:165:in `each_with_index'
 > /src/src/groove_automator.rb:165:in `remove_kick_on_snare'
 > /src/src/groove_automator.rb:291:in `groove_345c'
 > (pry):1:in `__pry__'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:290:in `eval'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:290:in `evaluate_ruby'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:659:in `handle_line'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:261:in `block (2 levels) in eval'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:260:in `catch'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:260:in `block in eval'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:259:in `catch'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:259:in `eval'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/repl.rb:77:in `block in repl'
 > <internal:kernel>:187:in `loop'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/repl.rb:67:in `repl'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/repl.rb:38:in `block in start'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/input_lock.rb:61:in `__with_ownership'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/input_lock.rb:78:in `with_ownership'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/repl.rb:38:in `start'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/repl.rb:15:in `start'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/pry_class.rb:194:in `start'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/lib/pry/cli.rb:112:in `start'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/pry-0.14.2/bin/pry:13:in `<top (required)>'
 > /opt/rubies/ruby-3.3.0/bin/pry:25:in `load'
 > /opt/rubies/ruby-3.3.0/bin/pry:25:in `<main>'
@@@ #<Thread:0x0000ffff6cde2d50@DEBUGGER__::SESSION@server /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/session.rb:179 sleep_forever>
 > <internal:thread_sync>:18:in `pop'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/session.rb:249:in `pop_event'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/session.rb:253:in `session_server_main'
 > /opt/rubies/ruby-3.3.0/lib/ruby/gems/3.3.0/gems/debug-1.9.1/lib/debug/session.rb:212:in `block in activate'```

### `SessionCommand` properties, values, and defaults

#### `SessionCommand` properties

| Property | Definition |
| -------- | ---------- |
| `:repeat` | Command will repeat each time the user enters a new line |
| `:unsafe` | Command allowed/denied in Unsafe Context |
| `:cancel_auto_continue` | ??? |
| `:postmortem` | Command allowed/denied for [Post-mortem Debugging](https://web.archive.org/web/20070103104458/http://www.datanoise.com/articles/2006/12/20/post-mortem-debugging). Debug process after fatal exception. |

These get checked in [`Session#process_command`](https://github.com/ruby/debug/blob/9de0ff46faeea73aed27bb090ee052db8fb74c14/lib/debug/session.rb#L1157-L1163):

```ruby
if cmd = @commands[cmd_name]
   check_postmortem      if !cmd.postmortem
   check_unsafe          if cmd.unsafe
   cancel_auto_continue  if cmd.cancel_auto_continue
   @repl_prev_line = nil if !cmd.repeat

   cmd.block.call(cmd_arg)

Default SessionCommand property values as set by register_command and register_default_command:

Context :repeat :unsafe :cancel_auto_continue :postmortem
register_command param defaults false true false true
register_default_command:next true true* true false
register_default_command:info false* false false* true*

* means default is used

Register custom command

Add nl command. It combines both n[ext] and i[nfo] l[ocals]

definition: n[ext] definition: i[nfo] definition: i[nfo] l[ocals]

register_my_command 'nl',
                    repeat: true,
                    cancel_auto_continue: true,
                    postmortem: false do |arg|

  # TODO: `next` can take an int arg, and `info` can take a regex arg
  
  # Run to next line
  next_line = "next"
  next_line += " #{next_arg}" unless next_arg.nil? or next_arg.empty?
  process_command next_line

  # Show local variables
  info_locals_line = "info locals"
  info_locals_line += " #{info_locals_arg}" unless info_locals_arg.nil? or info_locals_arg.empty?
  process_command info_locals_line
end

Actual 🐵🩹

not quite there yet...

⚠️ **GitHub.com Fallback** ⚠️