CrackedRuby logo

CrackedRuby

Debugger API

Overview

Ruby's Debugger API exposes the internal debugging infrastructure through several key classes and modules. The primary interface centers around the Debug module, which provides methods for setting breakpoints, stepping through code execution, and inspecting runtime state.

The API operates through a client-server architecture where the debugger server runs within the Ruby process and accepts commands from debugging clients. The Debug::Session class manages individual debugging sessions, while Debug::ThreadClient handles thread-specific debugging operations.

require 'debug'

# Start debugging session
Debug.start

# Set a breakpoint programmatically
Debug.bp(file: "app.rb", line: 25)

# Access current session
session = Debug.session
puts session.inspect

The debugger maintains an internal state machine that tracks execution flow, breakpoint status, and variable scope. When execution hits a breakpoint, control transfers to the debugger which can examine local variables, call stack, and object state.

# Enable debugging for specific methods
class Calculator
  def add(a, b)
    Debug.bp  # Set breakpoint at this line
    result = a + b
    result
  end
end

calc = Calculator.new
calc.add(5, 3)  # Execution pauses here

The API supports both synchronous and asynchronous debugging modes. In synchronous mode, the debugger blocks program execution until commands complete. Asynchronous mode allows debugging without stopping the main program flow, which proves essential for debugging concurrent applications.

# Configure debugging session
Debug.start do |session|
  session.add_breakpoint(file: "model.rb", line: 45, condition: "user.nil?")
  session.set_exception_breakpoint(exception_class: ArgumentError)
end

Basic Usage

The Debugger API activates through the Debug.start method, which initializes the debugging environment and establishes communication channels. This method accepts a block that receives the current debugging session as a parameter.

require 'debug'

Debug.start do |session|
  # Debugging session is active within this block
  puts "Debug session ID: #{session.id}"
  puts "Available commands: #{session.commands.keys}"
end

Setting breakpoints programmatically requires specifying either a file and line number combination or a method reference. The API validates breakpoint locations and raises Debug::Error for invalid positions.

# File-based breakpoint
Debug.bp(file: "lib/user.rb", line: 42)

# Method-based breakpoint  
Debug.bp(method: User.instance_method(:authenticate))

# Conditional breakpoint
Debug.bp(file: "service.rb", line: 18, condition: "params[:debug] == true")

The debugging session provides methods for controlling execution flow. The step_over, step_into, and step_out methods correspond to common debugging operations.

Debug.start do |session|
  session.step_over  # Execute next line without entering method calls
  session.step_into  # Step into method calls
  session.step_out   # Continue until current method returns
  
  # Continue execution until next breakpoint
  session.continue
end

Variable inspection occurs through the session's evaluation capabilities. The eval method executes Ruby code within the current debugging context, providing access to local variables and object state.

Debug.start do |session|
  # Evaluate expressions in current context
  local_vars = session.eval("local_variables")
  user_name = session.eval("@user.name")
  
  # Execute complex expressions
  result = session.eval("defined?(Rails) && Rails.env.development?")
  puts "Development mode: #{result}"
end

Exception handling during debugging requires careful consideration. The debugger intercepts exceptions before they propagate, allowing inspection of error conditions and program state at the point of failure.

# Set exception breakpoints
Debug.start do |session|
  session.add_exception_breakpoint(StandardError)
  session.add_exception_breakpoint(NoMethodError, enabled: false)
end

def risky_operation
  raise StandardError, "Something went wrong"
end

risky_operation  # Debugger pauses when exception occurs

Advanced Usage

Complex debugging scenarios require advanced breakpoint management and conditional logic. The API supports breakpoint groups, enabling and disabling multiple breakpoints simultaneously.

Debug.start do |session|
  # Create breakpoint group
  auth_group = session.create_breakpoint_group("authentication")
  
  auth_group.add_breakpoint(file: "auth/login.rb", line: 15)
  auth_group.add_breakpoint(file: "auth/session.rb", line: 30)
  auth_group.add_breakpoint(method: User.instance_method(:verify_password))
  
  # Enable/disable entire group
  auth_group.disable
  auth_group.enable
end

Custom debugging commands extend the debugger's functionality through the command registration system. Commands receive the current debugging context and can perform arbitrary operations.

Debug.start do |session|
  # Register custom command
  session.register_command("inspect_user") do |context, args|
    user = context.eval("@user")
    if user
      puts "User ID: #{user.id}"
      puts "User roles: #{user.roles.map(&:name)}"
      puts "Last login: #{user.last_login_at}"
    else
      puts "No user in current context"
    end
  end
  
  # Command becomes available in debugging session
  session.execute_command("inspect_user")
end

The debugger supports watch expressions that monitor variable changes across execution. Watch expressions evaluate automatically when execution pauses, providing continuous monitoring of critical values.

Debug.start do |session|
  # Add watch expressions
  session.add_watch("@total_price", notify_on_change: true)
  session.add_watch("items.length")
  session.add_watch("Time.current", format: :iso8601)
  
  # Watch expressions evaluate at each pause
  def calculate_order
    @total_price = 0
    items.each do |item|
      @total_price += item.price  # Watch expression updates here
    end
  end
end

Remote debugging capabilities allow connecting to Ruby processes running on different machines or in containerized environments. The debugger server listens on configurable ports and accepts connections from remote clients.

# Start remote debugging server
Debug.start(host: "0.0.0.0", port: 8989, wait_for_attach: true) do |session|
  puts "Debugger server listening on port 8989"
  session.wait_for_client_connection
  
  # Program execution pauses until client connects
  puts "Client connected, continuing execution"
end

Integration with development tools requires careful session management and state synchronization. The API provides hooks for editor integration and external tool communication.

class DebuggerIntegration
  def initialize
    @sessions = {}
    @breakpoint_sync = Queue.new
  end
  
  def start_session(file_path)
    Debug.start do |session|
      @sessions[file_path] = session
      
      # Synchronize breakpoints with external tools
      session.on_breakpoint_hit do |bp_info|
        @breakpoint_sync << {
          type: :hit,
          file: bp_info[:file],
          line: bp_info[:line],
          variables: session.eval("local_variables")
        }
      end
      
      yield session if block_given?
    end
  end
  
  def sync_breakpoints
    while info = @breakpoint_sync.pop(non_block: true)
      notify_external_tool(info)
    end
  rescue ThreadError
    # Queue empty
  end
end

Production Patterns

Production debugging requires careful consideration of performance impact and security implications. The debugger should remain disabled in production environments unless explicitly activated for troubleshooting specific issues.

class Application
  def self.configure_debugging
    return unless debugging_enabled?
    
    Debug.configure do |config|
      config.server_host = ENV.fetch("DEBUG_HOST", "127.0.0.1")
      config.server_port = ENV.fetch("DEBUG_PORT", "8989").to_i
      config.auth_token = ENV["DEBUG_AUTH_TOKEN"]
      config.log_level = :error  # Minimize debug output
    end
  end
  
  private
  
  def self.debugging_enabled?
    ENV["RAILS_ENV"] != "production" || ENV["ENABLE_PRODUCTION_DEBUG"] == "true"
  end
end

Conditional debugging activation allows enabling debugging features only when specific conditions occur. This approach minimizes performance overhead while maintaining debugging capabilities for error conditions.

module ConditionalDebugging
  def self.debug_on_error(&block)
    block.call
  rescue => exception
    if should_debug_exception?(exception)
      Debug.start do |session|
        puts "Exception occurred: #{exception.message}"
        puts "Backtrace: #{exception.backtrace.first(5)}"
        
        # Allow interactive debugging
        session.interactive_mode
      end
    end
    
    raise exception
  end
  
  def self.should_debug_exception?(exception)
    critical_exceptions = [NoMethodError, TypeError, ArgumentError]
    critical_exceptions.include?(exception.class) && Rails.env.development?
  end
end

# Usage in application code
ConditionalDebugging.debug_on_error do
  perform_complex_operation
end

Logging integration ensures debugging activities appear in application logs without interfering with normal log processing. The debugger supports custom log formatters and filtering.

class DebuggerLogger
  def initialize(base_logger)
    @base_logger = base_logger
    @debug_events = []
  end
  
  def log_debug_event(event_type, details)
    timestamp = Time.current
    
    event = {
      timestamp: timestamp,
      type: event_type,
      details: details,
      thread_id: Thread.current.object_id
    }
    
    @debug_events << event
    @base_logger.debug("[DEBUGGER] #{event_type}: #{details}")
    
    # Limit event history
    @debug_events.shift if @debug_events.length > 1000
  end
  
  def debug_session_summary
    return "No debug events recorded" if @debug_events.empty?
    
    summary = {
      total_events: @debug_events.length,
      event_types: @debug_events.group_by { |e| e[:type] }.transform_values(&:count),
      first_event: @debug_events.first[:timestamp],
      last_event: @debug_events.last[:timestamp]
    }
    
    @base_logger.info("[DEBUGGER] Session summary: #{summary}")
  end
end

Container debugging requires specific configuration for Docker and Kubernetes environments. The debugger server must bind to appropriate interfaces and handle container networking constraints.

# docker-compose.yml equivalent configuration
class ContainerDebugConfig
  def self.configure_for_container
    Debug.configure do |config|
      # Bind to all interfaces in container
      config.server_host = "0.0.0.0"
      config.server_port = ENV.fetch("DEBUG_PORT", "8989").to_i
      
      # Handle container shutdown gracefully
      config.on_shutdown do
        puts "Debugger shutting down gracefully"
        save_debug_session_state
      end
      
      # Container-specific logging
      config.logger = ContainerLogger.new
    end
  end
  
  def self.save_debug_session_state
    state = {
      breakpoints: Debug.session&.breakpoints,
      watch_expressions: Debug.session&.watch_expressions,
      timestamp: Time.current
    }
    
    File.write("/tmp/debug_state.json", JSON.pretty_generate(state))
  end
end

Common Pitfalls

Performance degradation represents the most significant risk when using the Debugger API inappropriately. Leaving breakpoints active in production code or enabling verbose logging can severely impact application performance.

# BAD: Breakpoint left active in production
class OrderProcessor
  def process_order(order)
    Debug.bp  # This will pause execution in production!
    order.items.each { |item| calculate_tax(item) }
  end
end

# GOOD: Conditional debugging
class OrderProcessor
  def process_order(order)
    Debug.bp if Rails.env.development? && ENV["DEBUG_ORDERS"]
    order.items.each { |item| calculate_tax(item) }
  end
end

Memory leaks occur when debugging sessions accumulate without proper cleanup. Long-running applications must manage session lifecycle carefully to prevent memory exhaustion.

# BAD: Sessions never cleaned up
class LeakyDebugger
  def initialize
    @sessions = []
  end
  
  def debug_method(method_name)
    @sessions << Debug.start  # Memory leak!
    # Session objects accumulate indefinitely
  end
end

# GOOD: Proper session management
class ManagedDebugger
  def initialize
    @current_session = nil
  end
  
  def debug_method(method_name)
    cleanup_session if @current_session
    
    @current_session = Debug.start do |session|
      yield session if block_given?
    end
  ensure
    cleanup_session
  end
  
  private
  
  def cleanup_session
    @current_session&.terminate
    @current_session = nil
  end
end

Thread safety issues arise when multiple threads access the same debugging session simultaneously. The debugger API requires synchronization for concurrent access patterns.

# BAD: Race condition on session access
class UnsafeDebugger
  def initialize
    @session = Debug.start
  end
  
  def debug_in_thread
    Thread.new do
      @session.eval("some_expression")  # Race condition!
    end
  end
end

# GOOD: Thread-safe debugging
class ThreadSafeDebugger
  def initialize
    @session = Debug.start
    @session_mutex = Mutex.new
  end
  
  def debug_in_thread
    Thread.new do
      @session_mutex.synchronize do
        @session.eval("some_expression")
      end
    end
  end
end

Security vulnerabilities emerge when debugging interfaces expose sensitive information or accept unvalidated input. Remote debugging servers require authentication and input validation.

# BAD: Unrestricted remote debugging
Debug.start(host: "0.0.0.0", port: 8989) do |session|
  # Anyone can connect and execute arbitrary code!
  session.interactive_mode
end

# GOOD: Secured remote debugging
class SecureRemoteDebugger
  def self.start_secure_session
    return unless authorized_environment?
    
    Debug.start(
      host: "127.0.0.1",  # Localhost only
      port: secure_port,
      auth_token: generate_session_token
    ) do |session|
      session.set_eval_filter(method(:safe_eval_filter))
      yield session if block_given?
    end
  end
  
  private
  
  def self.safe_eval_filter(code)
    dangerous_methods = %w[system exec ` eval instance_eval class_eval]
    
    dangerous_methods.any? { |method| code.include?(method) } ? nil : code
  end
  
  def self.generate_session_token
    SecureRandom.hex(32)
  end
end

Exception handling complexity increases when debugging code itself raises exceptions. Proper error handling prevents debugging operations from masking original application errors.

# Handle debugging errors gracefully
module SafeDebugging
  def self.safe_breakpoint(location_info = {})
    Debug.bp(**location_info)
  rescue Debug::InvalidLocationError => e
    Rails.logger.warn("Invalid breakpoint location: #{e.message}")
  rescue Debug::SessionError => e
    Rails.logger.error("Debugging session error: #{e.message}")
  end
  
  def self.safe_eval(expression, default_value = nil)
    Debug.session&.eval(expression)
  rescue NameError, SyntaxError => e
    Rails.logger.debug("Evaluation failed: #{expression} - #{e.message}")
    default_value
  rescue Debug::EvaluationError => e
    Rails.logger.warn("Debug evaluation error: #{e.message}")
    default_value
  end
end

Reference

Core Classes and Modules

Class/Module Purpose Key Methods
Debug Main debugger interface start, bp, session, configure
Debug::Session Individual debugging session eval, step_over, continue, add_breakpoint
Debug::ThreadClient Thread-specific debugging pause, resume, backtrace
Debug::Breakpoint Breakpoint representation enable, disable, condition=

Primary Methods

Method Parameters Returns Description
Debug.start(**opts, &block) host (String), port (Integer), block Debug::Session Starts debugging session
Debug.bp(**location) file (String), line (Integer), condition (String) Debug::Breakpoint Sets breakpoint
session.eval(code) code (String) Object Evaluates code in current context
session.step_over None nil Steps over next line
session.step_into None nil Steps into method calls
session.continue None nil Continues execution

Configuration Options

Option Type Default Description
server_host String "127.0.0.1" Debugging server bind address
server_port Integer 8989 Debugging server port
auth_token String nil Authentication token for remote access
log_level Symbol :info Logging verbosity level
wait_for_attach Boolean false Wait for client connection

Breakpoint Types

Type Location Syntax Example
Line file:, line: file: "app.rb", line: 42
Method method: method: User.instance_method(:save)
Exception exception_class: exception_class: ArgumentError
Conditional condition: condition: "user.admin?"

Session States

State Description Available Operations
inactive No debugging session Start session only
paused Execution paused at breakpoint Step, continue, evaluate
running Normal execution with breakpoints active Pause, add/remove breakpoints
terminated Session ended None

Exception Classes

Exception Inheritance Raised When
Debug::Error StandardError General debugging errors
Debug::SessionError Debug::Error Session management failures
Debug::InvalidLocationError Debug::Error Invalid breakpoint location
Debug::EvaluationError Debug::Error Code evaluation failures
Debug::ConnectionError Debug::Error Remote debugging connection issues

Command Categories

Category Commands Description
Execution Control continue, step, next, finish Control program flow
Breakpoints break, clear, info breakpoints Manage breakpoints
Inspection print, pp, info locals Examine program state
Navigation up, down, backtrace Navigate call stack
Session quit, restart, help Manage debugging session

Integration Patterns

Pattern Use Case Implementation
Conditional Activation Development-only debugging Environment variable checks
Exception Debugging Error investigation Exception-triggered breakpoints
Remote Debugging Container/production debugging Network-accessible sessions
Test Integration Debugging test failures Test-specific breakpoint placement