ruby / debug

Debugging functionality for Ruby
BSD 2-Clause "Simplified" License
1.13k stars 127 forks source link

Open to extensions? #151

Open st0012 opened 3 years ago

st0012 commented 3 years ago

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

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

ko1 commented 3 years ago

What kind of extension do you want to make?

st0012 commented 3 years ago
  1. Allow registering new commands
  2. Support frame filtering in debugger and allow configuring the pattern
ko1 commented 3 years ago

For 1, now I'm negative because the debugger's structure is complex and is not stable yet.

For 2, we can introduce some configurations.

ko1 commented 3 years ago

For 1, "alias" functionality is acceptable, to call Ruby's method and so on.

st0012 commented 3 years ago

For 1, the API should provide access to the scope of ThreadClient, so the extension can also format and colorize the output.

For 2, we can introduce some configurations.

Ok cool, I'll make a PR to experiment this idea 😄

st0012 commented 3 years ago

For 1, the API should provide access to the scope of ThreadClient, so the extension can also format and colorize the output.

Actually this is not necessary. I think being able to register the command to session is good enough.

ko1 commented 3 years ago

Could you give me a more concrete example? and we can discuss on it.

st0012 commented 3 years ago

@ko1 I'm thinking something like https://github.com/ruby/debug/pull/158

ko1 commented 3 years ago

I want to ask what kind of extension do you want to make. Not the extension design. We can provide a command to eval Ruby code in session context, like direct_eval <expr>. Combine with alias foo <debug command> https://github.com/ruby/debug/issues/151#issuecomment-876994542 we can make new command:

$ alias foo direct_eval some_method
ko1 commented 3 years ago

In general, design extension API is too difficult and we need to consider user's examples. It is what we Ruby interpreter developers do usually.

st0012 commented 3 years ago

An example is to get Rails application's config:

(rdbg) rails config active_job

The simplest implementation would be:

Rails.configuration.send(arg)
ko1 commented 3 years ago

how about that? alias rails direct_eval rails_command_helper(__args__)

where __args__ are expanded to the given parameters. if rails foo bar is called, __args__ will be "foo bar" and rails_command_helper will parse it.

(NOTE: the names direct_eval and __args__ are not considered carefully now)

Disadvantage of this approach is, we can not define complement logic.

st0012 commented 3 years ago

How can the extension insert the rails_command_helper method to either Session or ThreadClient?

ko1 commented 3 years ago

eval rails_command_helper command run the method on ThreadClient. direct_eval rails_command_helper is on Session.

st0012 commented 3 years ago

@ko1 does that mean the extension gem needs to define rails_command_helper at the top level? can we support namespacing with modules?

for example, alias rails direct_eval RDBG::Rails.execute_command(arguments)

this will allow extensions to organize commands more easily:

module RDBG
  module Rails
    class << self
      def execute_command(arguments)
        # ...
      end
    end
  end

  module Foo
    class << self
      def execute_command(arguments)
        # ...
      end
    end
  end
end
st0012 commented 2 years ago

I now want to revisit this issue. I think the key here is to allow extensions to register their command logic in a ractor-safe way.

So using proc like:

DEBUGGER__.register_command("rails") { the_logic }

won't be acceptable, as we won't know if the block is isolated or not.

But how about classes? Will this work?

class MyCommand
  def self.process_command(args)
    # ...
  end
end

DEBUGGER__.register_command("cmd", MyCommand)

Or if we take modules and extend/include the methods instead?

# in the debugger
module DEBUGGER__
  module ExtensionContainer
  end

  def self.regsiter_command(module)
    ExtensionContainer.extend module
  end

  class Session
    def process_command line
      # ....
      else
        if ExtensionContainer.respond_to?(cmd)
          ExtensionContainer.send(cmd, arg)
          return :retry
        else
          @tc << [:eval, :pp, line]
        end
      end
  end
end

# in extension
module RailsExtension
  def rails(input)
    # logic
  end
end

DEBUGGER__.register_command(RailsExtension)

Of course, with this approach we'll need to worry about the name collision issue. Not just the main command method but also how extensions name their helper methods.

st0012 commented 2 years ago

Update: the current plan is to introduce Ractor support (no due date) first so we can define better interfaces for extensions.