diff --git a/context/fiber-debugging.md b/context/fiber-debugging.md index 0a03aae..a901aff 100644 --- a/context/fiber-debugging.md +++ b/context/fiber-debugging.md @@ -161,7 +161,7 @@ After switching to a fiber with `rb-fiber-scan-switch`, you can use standard GDB (gdb) bt # Show C backtrace (gdb) frame # Switch to specific frame (gdb) info locals # Show local variables -(gdb) rb-object-print $errinfo # Print exception if present +(gdb) rb-print $errinfo # Print exception if present ~~~ The fiber switch command sets up several convenience variables: diff --git a/context/getting-started.md b/context/getting-started.md index 369478d..95b0dbc 100644 --- a/context/getting-started.md +++ b/context/getting-started.md @@ -63,7 +63,7 @@ Status: ✓ Installed Test that extensions load automatically: ~~~ bash -$ gdb --batch -ex "help rb-object-print" +$ gdb --batch -ex "help rb-print" Recursively print Ruby hash and array structures... ~~~ @@ -88,7 +88,7 @@ This removes the source line from your `~/.gdbinit` or `~/.lldbinit`. Ruby GDB provides specialized commands for debugging Ruby at multiple levels: - **Context Setup** (`rb-context`) - Get current execution context and set up convenience variables -- **Object Inspection** (`rb-object-print`) - View Ruby objects, hashes, arrays, and structs with proper formatting +- **Object Inspection** (`rb-print`) - View Ruby objects, hashes, arrays, and structs with proper formatting - **Fiber Debugging** (`rb-fiber-*`) - Scan heap for fibers, inspect state, and switch contexts - **Stack Analysis** (`rb-stack-trace`) - Examine combined VM (Ruby) and C (native) stack frames - **Heap Navigation** (`rb-heap-scan`) - Scan the Ruby heap to find objects by type @@ -132,7 +132,7 @@ Diagnose the issue (extensions load automatically if installed): (gdb) rb-fiber-scan-heap # Scan heap for all fibers (gdb) rb-fiber-scan-stack-trace-all # Show backtraces for all fibers (gdb) rb-fiber-scan-switch 0 # Switch to main fiber -(gdb) rb-object-print $errinfo --depth 2 # Print exception (now $errinfo is set) +(gdb) rb-print $errinfo --depth 2 # Print exception (now $errinfo is set) (gdb) rb-heap-scan --type RUBY_T_HASH --limit 10 # Find hashes ~~~ @@ -146,7 +146,7 @@ When a Ruby exception occurs, you can inspect it in detail: (gdb) break rb_exc_raise (gdb) run (gdb) rb-context -(gdb) rb-object-print $errinfo --depth 2 +(gdb) rb-print $errinfo --depth 2 ~~~ This shows the exception class, message, and any nested structures. The `rb-context` command displays the current execution context and sets up `$ec`, `$cfp`, and `$errinfo` convenience variables. @@ -167,7 +167,7 @@ When working with fibers, you often need to see what each fiber is doing: Ruby hashes and arrays can contain nested structures: ~~~ -(gdb) rb-object-print $some_hash --depth 2 +(gdb) rb-print $some_hash --depth 2 [ 0] K: :name V: "Alice" @@ -197,4 +197,4 @@ On macOS with LLDB and Ruby <= 3.4.x, some commands including `rb-fiber-scan-hea **Workarounds:** - Use Ruby head: `ruby-install ruby-head -- CFLAGS="-g -O0"` - Use GDB instead of LLDB (works with Ruby 3.4.x) -- Other commands like `rb-object-print`, `rb-stack-trace`, `rb-context` work fine +- Other commands like `rb-print`, `rb-stack-trace`, `rb-context` work fine diff --git a/context/object-inspection.md b/context/object-inspection.md index facc5b1..23459bc 100644 --- a/context/object-inspection.md +++ b/context/object-inspection.md @@ -1,12 +1,12 @@ # Object Inspection -This guide explains how to use `rb-object-print` to inspect Ruby objects, hashes, arrays, and structs in GDB. +This guide explains how to use `rb-print` to inspect Ruby objects, hashes, arrays, and structs in GDB. ## Why Object Inspection Matters When debugging Ruby programs or analyzing core dumps, you often need to inspect complex data structures that are difficult to read in their raw memory representation. Standard GDB commands show pointer addresses and raw memory, but not the logical structure of Ruby objects. -Use `rb-object-print` when you need: +Use `rb-print` when you need: - **Understand exception objects**: See the full exception hierarchy, message, and backtrace data - **Inspect fiber storage**: View thread-local data and fiber-specific variables @@ -15,12 +15,12 @@ Use `rb-object-print` when you need: ## Basic Usage -The `rb-object-print` command recursively prints Ruby objects in a human-readable format. +The `rb-print` command recursively prints Ruby objects in a human-readable format. ### Syntax ~~~ -rb-object-print [--depth N] [--debug] +rb-print [--depth N] [--debug] ~~~ Where: @@ -33,10 +33,10 @@ Where: Print immediate values and special constants: ~~~ -(gdb) rb-object-print 0 # -(gdb) rb-object-print 8 # -(gdb) rb-object-print 20 # -(gdb) rb-object-print 85 # 42 +(gdb) rb-print 0 # +(gdb) rb-print 8 # +(gdb) rb-print 20 # +(gdb) rb-print 85 # 42 ~~~ These work without any Ruby process running, making them useful for learning and testing. @@ -46,10 +46,10 @@ These work without any Ruby process running, making them useful for learning and Use any valid GDB expression: ~~~ -(gdb) rb-object-print $ec->errinfo # Exception object -(gdb) rb-object-print $ec->cfp->sp[-1] # Top of VM stack -(gdb) rb-object-print $ec->storage # Fiber storage hash -(gdb) rb-object-print (VALUE)0x00007f8a12345678 # Object at specific address +(gdb) rb-print $ec->errinfo # Exception object +(gdb) rb-print $ec->cfp->sp[-1] # Top of VM stack +(gdb) rb-print $ec->storage # Fiber storage hash +(gdb) rb-print (VALUE)0x00007f8a12345678 # Object at specific address ~~~ ## Inspecting Hashes @@ -61,7 +61,7 @@ Ruby hashes have two internal representations (ST table and AR table). The comma For hashes with fewer than 8 entries, Ruby uses an array-based implementation: ~~~ -(gdb) rb-object-print $some_hash +(gdb) rb-print $some_hash [ 0] K: :name V: "Alice" @@ -76,7 +76,7 @@ For hashes with fewer than 8 entries, Ruby uses an array-based implementation: For larger hashes, Ruby uses a hash table (output format is similar): ~~~ -(gdb) rb-object-print $large_hash +(gdb) rb-print $large_hash [ 0] K: :user_id V: 12345 @@ -90,12 +90,12 @@ For larger hashes, Ruby uses a hash table (output format is similar): Prevent overwhelming output from deeply nested structures: ~~~ -(gdb) rb-object-print $nested_hash --depth 1 # Only top level +(gdb) rb-print $nested_hash --depth 1 # Only top level [ 0] K: :data V: # Nested hash not expanded -(gdb) rb-object-print $nested_hash --depth 2 # Expand one level +(gdb) rb-print $nested_hash --depth 2 # Expand one level [ 0] K: :data V: @@ -114,7 +114,7 @@ Arrays also have two representations based on size: Arrays display their elements with type information: ~~~ -(gdb) rb-object-print $array +(gdb) rb-print $array [ 0] 1 [ 1] 2 @@ -124,7 +124,7 @@ Arrays display their elements with type information: For arrays with nested objects: ~~~ -(gdb) rb-object-print $array --depth 2 +(gdb) rb-print $array --depth 2 [ 0] "first item" [ 1] @@ -138,7 +138,7 @@ For arrays with nested objects: Ruby Struct objects work similarly to arrays: ~~~ -(gdb) rb-object-print $struct_instance +(gdb) rb-print $struct_instance [ 0] "John" [ 1] 25 @@ -155,7 +155,7 @@ When a fiber has an exception, inspect it: ~~~ (gdb) rb-fiber-scan-heap (gdb) rb-fiber-scan-switch 5 # Switch to fiber #5 -(gdb) rb-object-print $errinfo --depth 3 +(gdb) rb-print $errinfo --depth 3 ~~~ This reveals the full exception structure including any nested causes. After switching to a fiber, `$errinfo` and `$ec` convenience variables are automatically set. @@ -167,8 +167,8 @@ Break at a method and examine arguments on the stack: ~~~ (gdb) break some_method (gdb) run -(gdb) rb-object-print $ec->cfp->sp[-1] # Last argument -(gdb) rb-object-print $ec->cfp->sp[-2] # Second-to-last argument +(gdb) rb-print $ec->cfp->sp[-1] # Last argument +(gdb) rb-print $ec->cfp->sp[-2] # Second-to-last argument ~~~ ### Examining Fiber Storage @@ -178,17 +178,17 @@ Thread-local variables are stored in fiber storage: ~~~ (gdb) rb-fiber-scan-heap (gdb) rb-fiber-scan-switch 0 -(gdb) rb-object-print $ec->storage --depth 2 +(gdb) rb-print $ec->storage --depth 2 ~~~ This shows all thread-local variables and their values. ## Debugging with --debug Flag -When `rb-object-print` doesn't show what you expect, use `--debug`: +When `rb-print` doesn't show what you expect, use `--debug`: ~~~ -(gdb) rb-object-print $suspicious_value --debug +(gdb) rb-print $suspicious_value --debug DEBUG: Evaluated '$suspicious_value' to 0x7f8a1c567890 DEBUG: Loaded constant RUBY_T_MASK = 31 DEBUG: Object at 0x7f8a1c567890 with flags=0x20040005, type=0x5 diff --git a/context/stack-inspection.md b/context/stack-inspection.md index d1cd25d..c9b9233 100644 --- a/context/stack-inspection.md +++ b/context/stack-inspection.md @@ -140,9 +140,9 @@ See what values are on the current frame's stack: (gdb) set $sp = $ec->cfp->sp # Print values on stack -(gdb) rb-object-print *(VALUE*)($sp - 1) # Top of stack -(gdb) rb-object-print *(VALUE*)($sp - 2) # Second value -(gdb) rb-object-print *(VALUE*)($sp - 3) # Third value +(gdb) rb-print *(VALUE*)($sp - 1) # Top of stack +(gdb) rb-print *(VALUE*)($sp - 2) # Second value +(gdb) rb-print *(VALUE*)($sp - 3) # Third value ~~~ ### Tracking Fiber Switches diff --git a/data/toolbox/command.py b/data/toolbox/command.py index 8a93d3a..83c723f 100644 --- a/data/toolbox/command.py +++ b/data/toolbox/command.py @@ -19,6 +19,231 @@ def get_option(self, option_name, default=None): """Get an option value with optional default""" return self.options.get(option_name, default) + +class Usage: + """Command specification DSL for declarative command interfaces. + + Defines what parameters, options, and flags a command accepts, + enabling validation and help text generation. + + Example: + usage = Usage( + summary="Print Ruby objects with recursion", + parameters=['value'], + options={'depth': (int, 1), 'limit': (int, None)}, + flags=['debug', 'verbose'] + ) + + arguments = usage.parse("$var --depth 3 --debug") + # arguments.expressions = ['$var'] + # arguments.options = {'depth': 3} + # arguments.flags = {'debug'} + """ + + def __init__(self, summary, parameters=None, options=None, flags=None, examples=None): + """Define command interface. + + Args: + summary: One-line command description + parameters: List of parameter names or tuples (name, description) + options: Dict of {name: (type, default, description)} + flags: List of flag names or tuples (name, description) + examples: List of example command strings with descriptions + + Example: + Usage( + summary="Scan heap for objects", + parameters=[('type_name', 'Ruby type to search for')], + options={ + 'limit': (int, None, 'Maximum objects to find'), + 'depth': (int, 1, 'Recursion depth') + }, + flags=[ + ('terminated', 'Include terminated fibers'), + ('cache', 'Use cached results') + ], + examples=[ + ("rb-heap-scan --type RUBY_T_STRING", "Find all strings"), + ("rb-heap-scan --type RUBY_T_HASH --limit 10", "Find first 10 hashes") + ] + ) + """ + self.summary = summary + self.parameters = self._normalize_params(parameters or []) + self.options = options or {} + self.flags = self._normalize_flags(flags or []) + self.examples = examples or [] + + def _normalize_params(self, params): + """Normalize parameters to list of (name, description) tuples.""" + normalized = [] + for param in params: + if isinstance(param, tuple): + normalized.append(param) + else: + normalized.append((param, None)) + return normalized + + def _normalize_flags(self, flags): + """Normalize flags to list of (name, description) tuples.""" + normalized = [] + for flag in flags: + if isinstance(flag, tuple): + normalized.append(flag) + else: + normalized.append((flag, None)) + return normalized + + def parse(self, argument_string): + """Parse and validate command arguments. + + Args: + argument_string: Raw argument string from debugger + + Returns: + Arguments object with validated and type-converted values + + Raises: + ValueError: If validation fails (wrong parameter count, invalid types, etc.) + """ + # Use existing parser to extract raw arguments + arguments = parse_arguments(argument_string) + + # Validate parameter count + if len(arguments.expressions) != len(self.parameters): + if len(self.parameters) == 0 and len(arguments.expressions) > 0: + raise ValueError(f"Command takes no parameters, got {len(arguments.expressions)}") + elif len(self.parameters) == 1: + raise ValueError(f"Command requires 1 parameter, got {len(arguments.expressions)}") + else: + raise ValueError(f"Command requires {len(self.parameters)} parameters, got {len(arguments.expressions)}") + + # Validate and convert option types + converted_options = {} + for option_name, option_value in arguments.options.items(): + if option_name not in self.options: + raise ValueError(f"Unknown option: --{option_name}") + + # Unpack option spec (handle 2-tuple or 3-tuple) + opt_spec = self.options[option_name] + option_type = opt_spec[0] + option_default = opt_spec[1] + + # Convert to specified type + try: + if option_type == int: + converted_options[option_name] = int(option_value) + elif option_type == str: + converted_options[option_name] = str(option_value) + elif option_type == bool: + converted_options[option_name] = bool(option_value) + else: + # Custom type converter + converted_options[option_name] = option_type(option_value) + except (ValueError, TypeError) as e: + raise ValueError(f"Invalid value for --{option_name}: {option_value} (expected {option_type.__name__})") + + # Add defaults for missing options + for option_name, opt_spec in self.options.items(): + option_default = opt_spec[1] + if option_name not in converted_options and option_default is not None: + converted_options[option_name] = option_default + + # Validate flags + flag_names = {flag[0] for flag in self.flags} + for flag_name in arguments.flags: + if flag_name not in flag_names: + raise ValueError(f"Unknown flag: --{flag_name}") + + # Return new Arguments with converted options + return Arguments(arguments.expressions, arguments.flags, converted_options) + + def print_to(self, terminal, command_name): + """Print help text from usage specification. + + Args: + terminal: Terminal for colored output + command_name: Name of the command (e.g., "rb-print") + """ + import format as fmt + + # Summary with color + terminal.print(fmt.bold, self.summary, fmt.reset) + terminal.print() + + # Print usage line + terminal.print("Usage: ", end='') + terminal.print(fmt.bold, command_name, fmt.reset, end='') + + for param_name, _ in self.parameters: + terminal.print(' ', end='') + terminal.print(fmt.placeholder, f"<{param_name}>", fmt.reset, end='') + + # Add option placeholders + for option_name in self.options.keys(): + terminal.print(' ', end='') + terminal.print(fmt.placeholder, f"[--{option_name} N]", fmt.reset, end='') + + # Add flag placeholders + for flag_name, _ in self.flags: + terminal.print(' ', end='') + terminal.print(fmt.placeholder, f"[--{flag_name}]", fmt.reset, end='') + + terminal.print() + terminal.print() + + # Parameter descriptions + if self.parameters: + terminal.print(fmt.title, "Parameters:", fmt.reset) + + for param_name, param_desc in self.parameters: + terminal.print(" ", fmt.symbol, param_name, fmt.reset, end='') + if param_desc: + terminal.print(f" - {param_desc}") + else: + terminal.print() + terminal.print() + + # Option descriptions + if self.options: + terminal.print(fmt.title, "Options:", fmt.reset) + + for option_name, opt_spec in self.options.items(): + opt_type, opt_default = opt_spec[0], opt_spec[1] + opt_desc = opt_spec[2] if len(opt_spec) > 2 else None + + type_str = opt_type.__name__ if hasattr(opt_type, '__name__') else str(opt_type) + default_str = f" (default: {opt_default})" if opt_default is not None else "" + + terminal.print(" ", fmt.symbol, f"--{option_name}", fmt.reset, end='') + terminal.print(fmt.placeholder, f" <{type_str}>", fmt.reset, end='') + terminal.print(default_str) + + if opt_desc: + terminal.print(f" {opt_desc}") + terminal.print() + + # Flag descriptions + if self.flags: + terminal.print(fmt.title, "Flags:", fmt.reset) + + for flag_name, flag_desc in self.flags: + terminal.print(" ", fmt.symbol, f"--{flag_name}", fmt.reset, end='') + if flag_desc: + terminal.print(f" - {flag_desc}") + else: + terminal.print() + terminal.print() + + # Examples section + if self.examples: + terminal.print(fmt.title, "Examples:", fmt.reset) + + for example_cmd, example_desc in self.examples: + terminal.print(fmt.example, f" {example_cmd}", fmt.reset) + if example_desc: + terminal.print(f" {example_desc}") + class ArgumentParser: """Parse GDB command arguments handling nested brackets, quotes, and flags. diff --git a/data/toolbox/context.py b/data/toolbox/context.py index 1306c36..7031abb 100644 --- a/data/toolbox/context.py +++ b/data/toolbox/context.py @@ -1,295 +1,371 @@ """Ruby execution context utilities and commands.""" import debugger +import command import format -import value +import value as rvalue import rexception class RubyContext: - """Wrapper for Ruby execution context (rb_execution_context_t). - - Provides a high-level interface for working with Ruby execution contexts, - including inspection, convenience variable setup, and information display. - - Example: - ctx = RubyContext.current() - if ctx: - ctx.print_info(terminal) - ctx.setup_convenience_variables() - """ - - def __init__(self, ec): - """Create a RubyContext wrapper. - - Args: - ec: Execution context pointer (rb_execution_context_t *) - """ - self.ec = ec - self._cfp = None - self._errinfo = None - self._vm_stack = None - self._vm_stack_size = None - - @classmethod - def current(cls): - """Get the current execution context from the running thread. - - Tries multiple approaches in order of preference: - 1. ruby_current_ec - TLS variable (works in GDB, some LLDB) - 2. rb_current_ec_noinline() - function call (works in most cases) - 3. rb_current_ec() - macOS-specific function - - Returns: - RubyContext instance, or None if not available - """ - # Try ruby_current_ec variable first - try: - ec = debugger.parse_and_eval('ruby_current_ec') - if ec is not None and int(ec) != 0: - return cls(ec) - except debugger.Error: - pass - - # Fallback to rb_current_ec_noinline() function - try: - ec = debugger.parse_and_eval('rb_current_ec_noinline()') - if ec is not None and int(ec) != 0: - return cls(ec) - except debugger.Error: - pass - - # Last resort: rb_current_ec() (macOS-specific) - try: - ec = debugger.parse_and_eval('rb_current_ec()') - if ec is not None and int(ec) != 0: - return cls(ec) - except debugger.Error: - pass - - return None - - @property - def cfp(self): - """Get control frame pointer (lazy load).""" - if self._cfp is None: - try: - self._cfp = self.ec['cfp'] - except Exception: - pass - return self._cfp - - @property - def errinfo(self): - """Get exception VALUE (lazy load).""" - if self._errinfo is None: - try: - self._errinfo = self.ec['errinfo'] - except Exception: - pass - return self._errinfo - - @property - def has_exception(self): - """Check if there's a real exception (not nil/special value).""" - if self.errinfo is None: - return False - return rexception.is_exception(self.errinfo) - - @property - def vm_stack(self): - """Get VM stack pointer (lazy load).""" - if self._vm_stack is None: - try: - self._vm_stack = self.ec['vm_stack'] - except Exception: - pass - return self._vm_stack - - @property - def vm_stack_size(self): - """Get VM stack size (lazy load).""" - if self._vm_stack_size is None: - try: - self._vm_stack_size = int(self.ec['vm_stack_size']) - except Exception: - pass - return self._vm_stack_size - - def setup_convenience_variables(self): - """Set up convenience variables for this execution context. - - Sets: - $ec - Execution context pointer - $cfp - Control frame pointer - $errinfo - Current exception (if any) - - Returns: - dict with keys: 'ec', 'cfp', 'errinfo' (values are the set variables) - """ - result = {} - - # Set $ec - debugger.set_convenience_variable('ec', self.ec) - result['ec'] = self.ec - - # Set $cfp (control frame pointer) - if self.cfp is not None: - debugger.set_convenience_variable('cfp', self.cfp) - result['cfp'] = self.cfp - else: - result['cfp'] = None - - # Set $errinfo if there's an exception - if self.has_exception: - debugger.set_convenience_variable('errinfo', self.errinfo) - result['errinfo'] = self.errinfo - else: - result['errinfo'] = None - - return result - - def print_info(self, terminal): - """Print detailed information about this execution context. - - Args: - terminal: Terminal formatter for output - """ - print("Execution Context:") - print(f" $ec = ", end='') - print(terminal.print_type_tag('rb_execution_context_t', int(self.ec), None)) - - # VM Stack info - if self.vm_stack is not None and self.vm_stack_size is not None: - print(f" VM Stack: ", end='') - print(terminal.print_type_tag('VALUE', int(self.vm_stack), f'size={self.vm_stack_size}')) - else: - print(f" VM Stack: ") - - # Control Frame info - if self.cfp is not None: - print(f" $cfp = ", end='') - print(terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None)) - else: - print(f" $cfp = ") - - # Exception info - if self.has_exception: - print(f" $errinfo = ", end='') - print(terminal.print_type_tag('VALUE', int(self.errinfo), None)) - print(" Exception present!") - else: - errinfo_int = int(self.errinfo) if self.errinfo else 0 - if errinfo_int == 4: # Qnil - print(" Exception: None") - elif errinfo_int == 0: # Qfalse - print(" Exception: None (false)") - else: - print(f" Exception: None") - - # Tag info (for ensure blocks) - try: - tag = self.ec['tag'] - tag_int = int(tag) - if tag_int != 0: - print(f" Tag: ", end='') - print(terminal.print_type_tag('rb_vm_tag', tag_int, None)) - try: - retval = tag['retval'] - retval_int = int(retval) - is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0 - if not is_retval_special: - print(f" $retval available (in ensure block)") - except Exception: - pass - except Exception: - pass + """Wrapper for Ruby execution context (rb_execution_context_t). + + Provides a high-level interface for working with Ruby execution contexts, + including inspection, convenience variable setup, and information display. + + Example: + ctx = RubyContext.current() + if ctx: + ctx.print_info(terminal) + ctx.setup_convenience_variables() + """ + + def __init__(self, ec): + """Create a RubyContext wrapper. + + Args: + ec: Execution context pointer (rb_execution_context_t *) + """ + self.ec = ec + self._cfp = None + self._errinfo = None + self._vm_stack = None + self._vm_stack_size = None + + @classmethod + def current(cls): + """Get the current execution context from the running thread. + + Tries multiple approaches in order of preference: + 1. ruby_current_ec - TLS variable (works in GDB, some LLDB) + 2. rb_current_ec_noinline() - function call (works in most cases) + 3. rb_current_ec() - macOS-specific function + + Returns: + RubyContext instance, or None if not available + """ + # Try ruby_current_ec variable first + try: + ec = debugger.parse_and_eval('ruby_current_ec') + if ec is not None and int(ec) != 0: + return cls(ec) + except debugger.Error: + pass + + # Fallback to rb_current_ec_noinline() function + try: + ec = debugger.parse_and_eval('rb_current_ec_noinline()') + if ec is not None and int(ec) != 0: + return cls(ec) + except debugger.Error: + pass + + # Last resort: rb_current_ec() (macOS-specific) + try: + ec = debugger.parse_and_eval('rb_current_ec()') + if ec is not None and int(ec) != 0: + return cls(ec) + except debugger.Error: + pass + + return None + + @property + def cfp(self): + """Get control frame pointer (lazy load).""" + if self._cfp is None: + try: + self._cfp = self.ec['cfp'] + except Exception: + pass + return self._cfp + + @property + def errinfo(self): + """Get exception VALUE (lazy load).""" + if self._errinfo is None: + try: + self._errinfo = self.ec['errinfo'] + except Exception: + pass + return self._errinfo + + @property + def has_exception(self): + """Check if there's a real exception (not nil/special value).""" + if self.errinfo is None: + return False + return rexception.is_exception(self.errinfo) + + @property + def vm_stack(self): + """Get VM stack pointer (lazy load).""" + if self._vm_stack is None: + try: + self._vm_stack = self.ec['vm_stack'] + except Exception: + pass + return self._vm_stack + + @property + def vm_stack_size(self): + """Get VM stack size (lazy load).""" + if self._vm_stack_size is None: + try: + self._vm_stack_size = int(self.ec['vm_stack_size']) + except Exception: + pass + return self._vm_stack_size + + @property + def storage(self): + """Get fiber storage VALUE.""" + try: + return self.ec['storage'] + except Exception: + return None + + def setup_convenience_variables(self): + """Set up convenience variables for this execution context. + + Sets: + $ec - Execution context pointer + $cfp - Control frame pointer + $errinfo - Current exception (if any) + + Returns: + dict with keys: 'ec', 'cfp', 'errinfo' (values are the set variables) + """ + result = {} + + # Set $ec + debugger.set_convenience_variable('ec', self.ec) + result['ec'] = self.ec + + # Set $cfp (control frame pointer) + if self.cfp is not None: + debugger.set_convenience_variable('cfp', self.cfp) + result['cfp'] = self.cfp + else: + result['cfp'] = None + + # Set $errinfo if there's an exception + if self.has_exception: + debugger.set_convenience_variable('errinfo', self.errinfo) + result['errinfo'] = self.errinfo + else: + result['errinfo'] = None + + return result + + def print_info(self, terminal): + """Print detailed information about this execution context. + + Args: + terminal: Terminal formatter for output + """ + # Cache property lookups + vm_stack = self.vm_stack + vm_stack_size = self.vm_stack_size + cfp = self.cfp + storage = self.storage + errinfo = self.errinfo + has_exception = self.has_exception + + terminal.print("Execution Context:") + terminal.print(f" $ec = ", end='') + terminal.print_type_tag('rb_execution_context_t', int(self.ec), None) + terminal.print() + + # VM Stack info + if vm_stack is not None and vm_stack_size is not None: + terminal.print(f" VM Stack: ", end='') + terminal.print_type_tag('VALUE', int(vm_stack)) + terminal.print() + else: + terminal.print(f" VM Stack: ") + + # Control Frame info + if cfp is not None: + terminal.print(f" $cfp = ", end='') + terminal.print_type_tag('rb_control_frame_t', int(cfp), None) + terminal.print() + else: + terminal.print(f" $cfp = ") + + # Storage info + if storage is not None and not rvalue.is_nil(storage): + terminal.print(f" Storage: ", end='') + terminal.print_type_tag('VALUE', int(storage), None) + terminal.print() + + # Exception info + if has_exception: + terminal.print(" $errinfo = ", end='') + terminal.print_type_tag('VALUE', int(errinfo), None) + terminal.print() + terminal.print(" Exception present!") + else: + errinfo_int = int(errinfo) if errinfo else 0 + if errinfo_int == 4: # Qnil + terminal.print(" Exception: None") + elif errinfo_int == 0: # Qfalse + terminal.print(" Exception: None (false)") + else: + terminal.print(" Exception: None") + + # Tag info (for ensure blocks) + try: + tag = self.ec['tag'] + tag_int = int(tag) + if tag_int != 0: + terminal.print(" Tag: ", end='') + terminal.print_type_tag('rb_vm_tag', tag_int, None) + terminal.print() + try: + retval = tag['retval'] + retval_int = int(retval) + is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0 + if not is_retval_special: + terminal.print(" $retval available (in ensure block)") + except Exception: + pass + except Exception: + pass -class RubyContextCommand(debugger.Command): - """Show current execution context and set convenience variables. - - This command automatically discovers the current thread's execution context - and displays detailed information about it, while also setting up convenience - variables for easy inspection. - - Usage: - rb-context - - Displays: - - Execution context pointer and details - - VM stack information - - Control frame pointer - - Exception information (if any) - - Sets these convenience variables: - $ec - Current execution context (rb_execution_context_t *) - $cfp - Current control frame pointer - $errinfo - Current exception (if any) - - Example: - (gdb) rb-context - Execution Context: - $ec = - VM Stack: size=1024 - $cfp = - Exception: None - - (gdb) rb-object-print $errinfo - (gdb) rb-object-print $ec->cfp->sp[-1] - """ - - def __init__(self): - super(RubyContextCommand, self).__init__("rb-context", debugger.COMMAND_USER) - - def invoke(self, arg, from_tty): - """Execute the rb-context command.""" - try: - terminal = format.create_terminal(from_tty) - - # Get current execution context - ctx = RubyContext.current() - - if ctx is None: - print("Error: Could not get current execution context") - print() - print("Possible reasons:") - print(" • Ruby symbols not loaded (compile with debug symbols)") - print(" • Process not stopped at a Ruby frame") - print(" • Ruby not fully initialized yet") - print() - print("Try:") - print(" • Break at a Ruby function: break rb_vm_exec") - print(" • Use rb-fiber-scan-switch to switch to a fiber") - print(" • Ensure Ruby debug symbols are available") - return - - # Print context information - ctx.print_info(terminal) - - # Set convenience variables - vars = ctx.setup_convenience_variables() - - print() - print("Convenience variables set:") - print(f" $ec - Execution context") - if vars.get('cfp'): - print(f" $cfp - Control frame pointer") - if vars.get('errinfo'): - print(f" $errinfo - Exception object") - - print() - print("Now you can use:") - print(" rb-object-print $errinfo") - print(" rb-object-print $ec->cfp->sp[-1]") - print(" rb-stack-trace") - - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() +class RubyContextHandler: + """Show current execution context and set convenience variables.""" + + USAGE = command.Usage( + summary="Show current execution context and set convenience variables", + parameters=[], + options={}, + flags=[], + examples=[ + ("rb-context", "Display execution context info"), + ("rb-context; rb-print $errinfo", "Show context then print exception") + ] + ) + + def invoke(self, arguments, terminal): + """Execute the rb-context command.""" + try: + # Get current execution context + ctx = RubyContext.current() + + if ctx is None: + print("Error: Could not get current execution context") + print() + print("Possible reasons:") + print(" • Ruby symbols not loaded (compile with debug symbols)") + print(" • Process not stopped at a Ruby frame") + print(" • Ruby not fully initialized yet") + print() + print("Try:") + print(" • Break at a Ruby function: break rb_vm_exec") + print(" • Use rb-fiber-scan-switch to switch to a fiber") + print(" • Ensure Ruby debug symbols are available") + return + + # Print context information + ctx.print_info(terminal) + + # Set convenience variables + vars = ctx.setup_convenience_variables() + + print() + print("Convenience variables set:") + print(f" $ec - Execution context") + if vars.get('cfp'): + print(f" $cfp - Control frame pointer") + if vars.get('errinfo'): + print(f" $errinfo - Exception object") + + print() + print("Now you can use:") + print(" rb-object-print $errinfo") + print(" rb-object-print $ec->cfp->sp[-1]") + print(" rb-stack-trace") + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() -# Register command -RubyContextCommand() +class RubyContextStorageHandler: + """Print the fiber storage from the current execution context.""" + + USAGE = command.Usage( + summary="Print fiber storage from current execution context", + parameters=[], + options={'depth': (int, 1, 'Recursion depth for nested objects')}, + flags=[('debug', 'Show debug information')], + examples=[ + ("rb-context-storage", "Print storage with default depth"), + ("rb-context-storage --depth 3", "Print storage with depth 3") + ] + ) + + def invoke(self, arguments, terminal): + """Execute the rb-context-storage command.""" + try: + # Get current execution context + ctx = RubyContext.current() + + if ctx is None: + print("Error: Could not get current execution context") + print("\nTry:") + print(" • Run rb-context first to set up execution context") + print(" • Break at a Ruby function") + print(" • Use rb-fiber-scan-switch to switch to a fiber") + return + + # Get storage + storage_val = ctx.storage + + if storage_val is None: + print("Error: Could not access fiber storage") + return + + # Check if it's nil + if rvalue.is_nil(storage_val): + print("Fiber storage: nil") + return + + # Parse arguments (--depth, --debug, etc.) + arguments = command.parse_arguments(arg if arg else "") + + # Get depth flag + depth = 1 # Default depth + depth_str = arguments.get_option('depth') + if depth_str: + try: + depth = int(depth_str) + except ValueError: + print(f"Error: invalid depth '{depth_str}'") + return + + # Get debug flag + debug = arguments.has_flag('debug') + + # Use print module to print the storage + import print as print_module + printer = print_module.RubyObjectPrinter() + + # Build arguments for the printer + flags_set = {'debug'} if debug else set() + args_for_printer = command.Arguments([storage_val], flags_set, {'depth': depth}) + + printer.invoke(args_for_printer, terminal) + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +# Register commands +debugger.register("rb-context", RubyContextHandler, usage=RubyContextHandler.USAGE) +debugger.register("rb-context-storage", RubyContextStorageHandler, usage=RubyContextStorageHandler.USAGE) diff --git a/data/toolbox/debugger/__init__.py b/data/toolbox/debugger/__init__.py index 7cb735c..f767d7d 100644 --- a/data/toolbox/debugger/__init__.py +++ b/data/toolbox/debugger/__init__.py @@ -70,6 +70,7 @@ create_value = _backend.create_value create_value_from_int = _backend.create_value_from_int create_value_from_address = _backend.create_value_from_address +register = _backend.register # Constants COMMAND_DATA = _backend.COMMAND_DATA @@ -94,6 +95,7 @@ 'create_value', 'create_value_from_int', 'create_value_from_address', + 'register', 'COMMAND_DATA', 'COMMAND_USER', ] diff --git a/data/toolbox/debugger/gdb_backend.py b/data/toolbox/debugger/gdb_backend.py index e1d5f31..f378486 100644 --- a/data/toolbox/debugger/gdb_backend.py +++ b/data/toolbox/debugger/gdb_backend.py @@ -3,6 +3,7 @@ """ import gdb +import format # Command categories COMMAND_DATA = gdb.COMMAND_DATA @@ -589,6 +590,74 @@ def create_value_from_address(address, value_type): return Value(addr_val.dereference()) +def register(name, handler_class, usage=None, category=COMMAND_USER): + """Register a command with GDB using a handler class. + + This creates a wrapper Command that handles parsing, terminal setup, + and delegates to the handler class for actual command logic. + + Args: + name: Command name (e.g., "rb-object-print") + handler_class: Class to instantiate for handling the command + usage: Optional command.Usage specification for validation/help + category: Command category (COMMAND_USER, etc.) + + Example: + class PrintHandler: + def invoke(self, arguments, terminal): + depth = arguments.get_option('depth', 1) + print(f"Depth: {depth}") + + usage = command.Usage( + summary="Print something", + options={'depth': (int, 1)}, + flags=['debug'] + ) + debugger.register("my-print", PrintHandler, usage=usage) + + Returns: + The registered Command instance + """ + class RegisteredCommand(Command): + def __init__(self): + super(RegisteredCommand, self).__init__(name, category) + self.usage_spec = usage + self.handler_class = handler_class + + def invoke(self, arg, from_tty): + """GDB entry point - parses arguments and delegates to handler.""" + # Create terminal first (needed for help text) + import format + terminal = format.create_terminal(from_tty) + + try: + # Parse and validate arguments + if self.usage_spec: + arguments = self.usage_spec.parse(arg if arg else "") + else: + # Fallback to basic parsing without validation + import command + arguments = command.parse_arguments(arg if arg else "") + + # Instantiate handler and invoke + handler = self.handler_class() + handler.invoke(arguments, terminal) + + except ValueError as e: + # Validation error - show colored help + print(f"Error: {e}") + if self.usage_spec: + print() + self.usage_spec.print_to(terminal, name) + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + + # Instantiate and register the command with GDB + return RegisteredCommand() + + diff --git a/data/toolbox/debugger/lldb_backend.py b/data/toolbox/debugger/lldb_backend.py index a8dc2bb..31e9e5d 100644 --- a/data/toolbox/debugger/lldb_backend.py +++ b/data/toolbox/debugger/lldb_backend.py @@ -6,6 +6,7 @@ """ import lldb +import format # Command categories (LLDB doesn't have exact equivalents, using symbolic constants) COMMAND_DATA = 0 @@ -886,3 +887,71 @@ def create_value_from_int(int_value, value_type): return Value(result) +def register(name, handler_class, usage=None, category=COMMAND_USER): + """Register a command with LLDB using a handler class. + + This creates a wrapper Command that handles parsing, terminal setup, + and delegates to the handler class for actual command logic. + + Args: + name: Command name (e.g., "rb-object-print") + handler_class: Class to instantiate for handling the command + usage: Optional command.Usage specification for validation/help + category: Command category (COMMAND_USER, etc.) + + Example: + class PrintHandler: + def invoke(self, arguments, terminal): + depth = arguments.get_option('depth', 1) + print(f"Depth: {depth}") + + usage = command.Usage( + summary="Print something", + options={'depth': (int, 1)}, + flags=['debug'] + ) + debugger.register("my-print", PrintHandler, usage=usage) + + Returns: + The registered Command instance + """ + class RegisteredCommand(Command): + def __init__(self): + super(RegisteredCommand, self).__init__(name, category) + self.usage_spec = usage + self.handler_class = handler_class + + def invoke(self, arg, from_tty): + """LLDB entry point - parses arguments and delegates to handler.""" + # Create terminal first (needed for help text) + import format + terminal = format.create_terminal(from_tty) + + try: + # Parse and validate arguments + if self.usage_spec: + arguments = self.usage_spec.parse(arg if arg else "") + else: + # Fallback to basic parsing without validation + import command + arguments = command.parse_arguments(arg if arg else "") + + # Instantiate handler and invoke + handler = self.handler_class() + handler.invoke(arguments, terminal) + + except ValueError as e: + # Validation error - show colored help + print(f"Error: {e}") + if self.usage_spec: + print() + self.usage_spec.print_to(terminal, name) + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + + # Instantiate and register the command with LLDB + return RegisteredCommand() + + diff --git a/data/toolbox/fiber.py b/data/toolbox/fiber.py index 9015fcc..dea1382 100644 --- a/data/toolbox/fiber.py +++ b/data/toolbox/fiber.py @@ -12,7 +12,7 @@ # Import command parser import command import constants -import value +import value as rvalue import format import heap import rexception @@ -27,875 +27,851 @@ _current_fiber = None def parse_fiber_index(arg): - """Parse fiber index from argument string. - - Returns: - (index, error_message) - index is None if parsing failed - """ - if not arg or not arg.strip(): - return None, "Usage: provide " - - arguments = command.parse_arguments(arg) - - if not arguments.expressions: - return None, "Error: No index provided" - - try: - index = int(arguments.expressions[0]) - return index, None - except ValueError: - return None, f"Error: invalid index '{arguments.expressions[0]}'" + """Parse fiber index from argument string. + + Returns: + (index, error_message) - index is None if parsing failed + """ + if not arg or not arg.strip(): + return None, "Usage: provide " + + arguments = command.parse_arguments(arg) + + if not arguments.expressions: + return None, "Error: No index provided" + + try: + index = int(arguments.expressions[0]) + return index, None + except ValueError: + return None, f"Error: invalid index '{arguments.expressions[0]}'" def get_current_fiber(): - """Get the currently selected fiber (if any). - - Returns: - RubyFiber instance or None - """ - return _current_fiber + """Get the currently selected fiber (if any). + + Returns: + RubyFiber instance or None + """ + return _current_fiber def set_current_fiber(fiber): - """Set the currently selected fiber. - - Args: - fiber: RubyFiber instance or None - - Note: Should only be called by rb-fiber-switch command. - """ - global _current_fiber - _current_fiber = fiber + """Set the currently selected fiber. + + Args: + fiber: RubyFiber instance or None + + Note: Should only be called by rb-fiber-switch command. + """ + global _current_fiber + _current_fiber = fiber class RubyFiber: - """Wrapper for Ruby Fiber objects. - - Wraps a Fiber VALUE and provides high-level interface for fiber introspection. - """ - - # Fiber status constants - FIBER_STATUS = { - 0: "CREATED", - 1: "RESUMED", - 2: "SUSPENDED", - 3: "TERMINATED" - } - - def __init__(self, fiber_value): - """Initialize with a Fiber VALUE. - - Args: - fiber_value: A GDB value representing a Ruby Fiber object (VALUE) - """ - self.value = fiber_value - self._pointer = None - self._ec = None - self._exception = None - - def _extract_fiber_pointer(self): - """Extract struct rb_fiber_struct* from the Fiber VALUE.""" - if self._pointer is None: - # Cast to RTypedData and extract the data pointer - rtypeddata_type = constants.type_struct('struct RTypedData').pointer() - typed_data = self.value.cast(rtypeddata_type) - - rb_fiber_struct_type = constants.type_struct('struct rb_fiber_struct').pointer() - self._pointer = typed_data['data'].cast(rb_fiber_struct_type) - - return self._pointer - - @property - def pointer(self): - """Get the struct rb_fiber_struct* pointer.""" - return self._extract_fiber_pointer() - - @property - def address(self): - """Get the raw address of the fiber struct.""" - return int(self.pointer) - - @property - def status(self): - """Get fiber status as string (CREATED, RESUMED, etc.).""" - status_code = int(self.pointer['status']) - return self.FIBER_STATUS.get(status_code, f"UNKNOWN({status_code})") - - @property - def stack_base(self): - """Get fiber stack base pointer.""" - return self.pointer['stack']['base'] - - @property - def stack_size(self): - """Get fiber stack size.""" - return int(self.pointer['stack']['size']) - - @property - def ec(self): - """Get execution context (rb_execution_context_t*).""" - if self._ec is None: - self._ec = self.pointer['cont']['saved_ec'].address - return self._ec - - @property - def vm_stack(self): - """Get VM stack pointer.""" - return self.ec['vm_stack'] - - @property - def vm_stack_size(self): - """Get VM stack size.""" - return int(self.ec['vm_stack_size']) - - @property - def cfp(self): - """Get control frame pointer.""" - return self.ec['cfp'] - - @property - def exception(self): - """Get current exception RException object (if any). - - Returns: - RException instance or None - """ - if self._exception is None: - try: - errinfo_val = self.ec['errinfo'] - - # Only process if it's a real object (not nil or other immediate value) - if value.is_object(errinfo_val) and not value.is_nil(errinfo_val): - try: - self._exception = rexception.RException(errinfo_val) - except Exception: - # If we can't create RException, return None - pass - except Exception: - pass - - return self._exception - - @property - def exception_info(self): - """Get formatted exception string (if any). - - Returns: - Formatted exception string or None - """ - exc = self.exception - if exc: - return str(exc) - return None - - def print_info(self, terminal): - """Print summary information about this fiber. - - Args: - terminal: Terminal instance for formatted output - """ - # Print fiber VALUE and address - print(f"Fiber VALUE: ", end='') - print(terminal.print_type_tag('T_DATA', int(self.value), None)) - print(f" Address: ", end='') - print(terminal.print_type_tag('struct rb_fiber_struct', self.address, None)) - - # Print status - print(f" Status: {self.status}") - - # Print exception if present - exc_info = self.exception_info - if exc_info: - print(f" Exception: {exc_info}") - - # Print Stack with formatted pointer - stack_type = str(self.stack_base.type) - print(f" Stack: ", end='') - print(terminal.print_type_tag(stack_type, int(self.stack_base), f'size={self.stack_size}')) - - # Print VM Stack with formatted pointer - vm_stack_type = str(self.vm_stack.type) - print(f" VM Stack: ", end='') - print(terminal.print_type_tag(vm_stack_type, int(self.vm_stack), f'size={self.vm_stack_size}')) - - # Print CFP - print(f" CFP: ", end='') - print(terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None)) - - -class RubyFiberScanHeapCommand(debugger.Command): - """Scan heap and list all Ruby fibers.""" - - def __init__(self): - super(RubyFiberScanHeapCommand, self).__init__("rb-fiber-scan-heap", debugger.COMMAND_USER) - self.heap = heap.RubyHeap() - - def usage(self): - """Print usage information.""" - print("Usage: rb-fiber-scan-heap [--limit N] [--cache [filename]] [--terminated]") - print("Examples:") - print(" rb-fiber-scan-heap # Find all non-terminated fibers") - print(" rb-fiber-scan-heap --limit 10 # Find first 10 non-terminated fibers") - print(" rb-fiber-scan-heap --terminated # Include terminated fibers") - print(" rb-fiber-scan-heap --cache # Use fibers.json cache") - print(" rb-fiber-scan-heap --cache my.json # Use custom cache file") - - def save_cache(self, fiber_values, filename): - """Save fiber VALUE addresses to cache file. - - Args: - fiber_values: List of Fiber VALUEs - filename: Path to cache file - """ - try: - data = { - 'version': 1, - 'fiber_count': len(fiber_values), - 'fibers': [int(f) for f in fiber_values] # Store VALUE addresses - } - with open(filename, 'w') as f: - json.dump(data, f, indent=2) - print(f"Saved {len(fiber_values)} fiber VALUE(s) to {filename}") - return True - except Exception as e: - print(f"Warning: Failed to save cache: {e}") - return False - - def load_cache(self, filename): - """Load fiber VALUE addresses from cache file. - - Args: - filename: Path to cache file - - Returns: - List of VALUEs or None if loading failed - """ - try: - with open(filename, 'r') as f: - data = json.load(f) - - if data.get('version') != 1: - print(f"Warning: Unknown cache version, ignoring cache") - return None - - fiber_addrs = data.get('fibers', []) - print(f"Loaded {len(fiber_addrs)} fiber VALUE address(es) from {filename}") - - # Initialize heap to ensure we have type information - if not self.heap.initialize(): - return None - - # Reconstruct VALUEs from addresses - value_type = constants.type_struct('VALUE') - fibers = [] - for addr in fiber_addrs: - try: - fiber_val = debugger.create_value(addr, value_type) - fibers.append(fiber_val) - except (debugger.Error, debugger.MemoryError): - print(f"Warning: Could not access VALUE at 0x{addr:x}") - - print(f"Successfully reconstructed {len(fibers)} fiber VALUE(s)") - return fibers - - except FileNotFoundError: - return None - except Exception as e: - print(f"Warning: Failed to load cache: {e}") - return None - - def invoke(self, arg, from_tty): - global _fiber_cache - - # Create terminal for formatting - terminal = format.create_terminal(from_tty) - - # Parse arguments using the robust parser - arguments = command.parse_arguments(arg if arg else "") - - # Get limit from --limit option - limit = None - limit_str = arguments.get_option('limit') - if limit_str: - try: - limit = int(limit_str) - if limit <= 0: - print("Error: limit must be positive") - self.usage() - return - except ValueError: - print(f"Error: invalid limit '{limit_str}'") - self.usage() - return - - # Check for --cache flag - use_cache = arguments.has_flag('cache') - cache_file = arguments.get_option('cache', 'fibers.json') - - # Check for --terminated flag - include_terminated = arguments.has_flag('terminated') - - # Try to load from cache if requested - if use_cache: - loaded_fibers = self.load_cache(cache_file) - if loaded_fibers is not None: - # Filter out terminated fibers unless --terminated is specified - if not include_terminated: - filtered_fibers = [] - for fiber_val in loaded_fibers: - try: - fiber_obj = RubyFiber(fiber_val) - if fiber_obj.status != "TERMINATED": - filtered_fibers.append(fiber_val) - except: - # Keep fibers we can't inspect - filtered_fibers.append(fiber_val) - loaded_fibers = filtered_fibers - - # Successfully loaded from cache - _fiber_cache = loaded_fibers - - print(f"\nLoaded {len(loaded_fibers)} fiber(s) from cache:\n") - - for i, fiber_val in enumerate(loaded_fibers): - try: - fiber_obj = RubyFiber(fiber_val) - self._print_fiber_info(terminal, i, fiber_obj) - except: - print(f"Fiber #{i}: VALUE 0x{int(fiber_val):x}") - print(f" (error creating RubyFiber)") - print() - - print(f"Fibers cached. Use 'rb-fiber-scan-switch ' to switch to a fiber.") - return - else: - print(f"Cache file '{cache_file}' not found, proceeding with scan...") - print() - - # Initialize heap scanner - if not self.heap.initialize(): - return - - # Get fiber_data_type for matching - try: - fiber_data_type = debugger.parse_and_eval('&fiber_data_type') - if fiber_data_type is None or int(fiber_data_type) == 0: - print("Error: Could not find 'fiber_data_type' symbol") - print("\nThis usually means:") - print(" • Ruby debug symbols are not available") - print(" • Ruby version doesn't export this symbol") - print("\nTo fix:") - print(" • Install Ruby with debug symbols") - print(" • On macOS: brew install ruby (includes debug info)") - print(" • Or compile Ruby with --enable-debug-symbols") - return - except debugger.Error as e: - print(f"Error: Could not evaluate '&fiber_data_type': {e}") - print("\nRuby debug symbols may not be available.") - return - - if limit: - print(f"Scanning heap for first {limit} Fiber object(s)...", file=sys.stderr) - else: - print("Scanning heap for Fiber objects...", file=sys.stderr) - - # Use RubyHeap to find fibers (returns VALUEs) - fiber_values = self.heap.find_typed_data(fiber_data_type, limit=limit, progress=True) - - # Filter out terminated fibers unless --terminated is specified - if not include_terminated: - filtered_fibers = [] - for fiber_val in fiber_values: - try: - fiber_obj = RubyFiber(fiber_val) - if fiber_obj.status != "TERMINATED": - filtered_fibers.append(fiber_val) - except: - # Keep fibers we can't inspect - filtered_fibers.append(fiber_val) - fiber_values = filtered_fibers - - # Cache the VALUEs for later use - _fiber_cache = fiber_values - - if limit and len(fiber_values) >= limit: - print(f"Found {len(fiber_values)} fiber(s) (limit reached):\n") - else: - print(f"Found {len(fiber_values)} fiber(s):\n") - - for i, fiber_val in enumerate(fiber_values): - fiber_obj = RubyFiber(fiber_val) - self._print_fiber_info(terminal, i, fiber_obj) - - # Save to cache if requested - if use_cache and fiber_values: - self.save_cache(fiber_values, cache_file) - print() - - print(f"Fibers cached. Use 'rb-fiber-scan-switch ' to switch to a fiber.") - - def _print_fiber_info(self, terminal, index, fiber_obj): - """Print formatted fiber information. - - Args: - terminal: Terminal instance for formatting - index: Fiber index in cache - fiber_obj: RubyFiber instance - """ - # Print fiber index with VALUE and pointer - print(f"Fiber #{index}: ", end='') - print(terminal.print_type_tag('T_DATA', int(fiber_obj.value)), end='') - print(' → ', end='') - print(terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address)) - - # Print status - print(f" Status: {fiber_obj.status}") - - # Print exception if present (catch errors for terminated fibers) - try: - exc_info = fiber_obj.exception_info - if exc_info: - print(f" Exception: {exc_info}") - except Exception: - # Silently skip exception info if we can't read it - pass - - # Print Stack with formatted pointer - stack_type = str(fiber_obj.stack_base.type) - print(f" Stack: ", end='') - print(terminal.print_type_tag(stack_type, int(fiber_obj.stack_base), f'size={fiber_obj.stack_size}')) - - # Print VM Stack with formatted pointer - vm_stack_type = str(fiber_obj.vm_stack.type) - print(f" VM Stack: ", end='') - print(terminal.print_type_tag(vm_stack_type, int(fiber_obj.vm_stack), f'size={fiber_obj.vm_stack_size}')) - - # Print CFP - cfp_type = str(fiber_obj.cfp.type).replace(' *', '') # Remove pointer marker for display - print(f" CFP: ", end='') - print(terminal.print_type_tag(cfp_type, int(fiber_obj.cfp))) - print() - - -class RubyFiberScanSwitchCommand(debugger.Command): - """Switch to a fiber from the scan heap cache. - - Usage: rb-fiber-scan-switch - Example: rb-fiber-scan-switch 0 - rb-fiber-scan-switch 2 - - Note: This command requires a fiber cache populated by 'rb-fiber-scan-heap'. - """ - - def __init__(self): - super(RubyFiberScanSwitchCommand, self).__init__("rb-fiber-scan-switch", debugger.COMMAND_USER) - - def usage(self): - """Print usage information.""" - print("Usage: rb-fiber-scan-switch ") - print("Examples:") - print(" rb-fiber-scan-switch 0 # Switch to fiber #0") - print(" rb-fiber-scan-switch 2 # Switch to fiber #2") - print() - print("Note: Run 'rb-fiber-scan-heap' first to populate the fiber cache.") - - def invoke(self, arg, from_tty): - global _fiber_cache - - if not arg or not arg.strip(): - self.usage() - return - - # Check if cache is populated - if not _fiber_cache: - print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.") - return - - # Parse index - try: - index = int(arg.strip()) - except ValueError: - print(f"Error: Invalid index '{arg}'. Must be an integer.") - self.usage() - return - - # Validate index - if index < 0 or index >= len(_fiber_cache): - print(f"Error: Index {index} out of range [0, {len(_fiber_cache)-1}]") - print(f"\nRun 'rb-fiber-scan-heap' to see available fibers.") - return - - # Get fiber VALUE from cache - fiber_value = _fiber_cache[index] - - print(f"Switching to Fiber #{index}: VALUE 0x{int(fiber_value):x}") - - # Delegate to rb-fiber-switch command - # This command manages the global _current_fiber state - try: - debugger.execute(f"rb-fiber-switch 0x{int(fiber_value):x}", from_tty=from_tty) - except debugger.Error as e: - print(f"Error switching to fiber: {e}") - import traceback - traceback.print_exc() + """Wrapper for Ruby Fiber objects. + + Wraps a Fiber VALUE and provides high-level interface for fiber introspection. + """ + + # Fiber status constants + FIBER_STATUS = { + 0: "CREATED", + 1: "RESUMED", + 2: "SUSPENDED", + 3: "TERMINATED" + } + + def __init__(self, fiber_value): + """Initialize with a Fiber VALUE. + + Args: + fiber_value: A GDB value representing a Ruby Fiber object (VALUE) + """ + self.value = fiber_value + self._pointer = None + self._ec = None + self._exception = None + + def _extract_fiber_pointer(self): + """Extract struct rb_fiber_struct* from the Fiber VALUE.""" + if self._pointer is None: + # Cast to RTypedData and extract the data pointer + rtypeddata_type = constants.type_struct('struct RTypedData').pointer() + typed_data = self.value.cast(rtypeddata_type) + + rb_fiber_struct_type = constants.type_struct('struct rb_fiber_struct').pointer() + self._pointer = typed_data['data'].cast(rb_fiber_struct_type) + + return self._pointer + + @property + def pointer(self): + """Get the struct rb_fiber_struct* pointer.""" + return self._extract_fiber_pointer() + + @property + def address(self): + """Get the raw address of the fiber struct.""" + return int(self.pointer) + + @property + def status(self): + """Get fiber status as string (CREATED, RESUMED, etc.).""" + status_code = int(self.pointer['status']) + return self.FIBER_STATUS.get(status_code, f"UNKNOWN({status_code})") + + @property + def stack_base(self): + """Get fiber stack base pointer.""" + return self.pointer['stack']['base'] + + @property + def stack_size(self): + """Get fiber stack size.""" + return int(self.pointer['stack']['size']) + + @property + def ec(self): + """Get execution context (rb_execution_context_t*).""" + if self._ec is None: + self._ec = self.pointer['cont']['saved_ec'].address + return self._ec + + @property + def vm_stack(self): + """Get VM stack pointer.""" + return self.ec['vm_stack'] + + @property + def vm_stack_size(self): + """Get VM stack size.""" + return int(self.ec['vm_stack_size']) + + @property + def cfp(self): + """Get control frame pointer.""" + return self.ec['cfp'] + + @property + def exception(self): + """Get current exception RException object (if any). + + Returns: + RException instance or None + """ + if self._exception is None: + try: + errinfo_val = self.ec['errinfo'] + + # Only process if it's a real object (not nil or other immediate value) + if rvalue.is_object(errinfo_val) and not rvalue.is_nil(errinfo_val): + try: + self._exception = rexception.RException(errinfo_val) + except Exception: + # If we can't create RException, return None + pass + except Exception: + pass + + return self._exception + + @property + def exception_info(self): + """Get formatted exception string (if any). + + Returns: + Formatted exception string or None + """ + exc = self.exception + if exc: + return str(exc) + return None + + def print_info(self, terminal): + """Print summary information about this fiber. + + Args: + terminal: Terminal instance for formatted output + """ + # Print fiber VALUE and address + print(f"Fiber VALUE: ", end='') + terminal.print_type_tag('T_DATA', int(self.value), None) + print() + print(f" Address: ", end='') + terminal.print_type_tag('struct rb_fiber_struct', self.address, None) + print() + + # Print status + print(f" Status: {self.status}") + + # Print exception if present + exc_info = self.exception_info + if exc_info: + print(f" Exception: {exc_info}") + + # Print Stack with formatted pointer + stack_type = str(self.stack_base.type) + print(f" Stack: ", end='') + terminal.print_type_tag(stack_type, int(self.stack_base)) + print() + + # Print VM Stack with formatted pointer + vm_stack_type = str(self.vm_stack.type) + print(f" VM Stack: ", end='') + terminal.print_type_tag(vm_stack_type, int(self.vm_stack)) + print() + + # Print CFP + print(f" CFP: ", end='') + terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None) + print() + + +class RubyFiberScanHeapHandler: + """Scan heap and list all Ruby fibers.""" + + USAGE = command.Usage( + summary="Scan heap and list all Ruby fibers", + parameters=[], + options={ + 'limit': (int, None, 'Maximum fibers to find'), + 'cache': (str, None, 'Cache file to use (default: fibers.json)') + }, + flags=[ + ('terminated', 'Include terminated fibers in results') + ], + examples=[ + ("rb-fiber-scan-heap", "Find all non-terminated fibers"), + ("rb-fiber-scan-heap --limit 10", "Find first 10 fibers"), + ("rb-fiber-scan-heap --terminated", "Include terminated fibers"), + ("rb-fiber-scan-heap --cache my.json", "Use custom cache file") + ] + ) + + def __init__(self): + self.heap = heap.RubyHeap() + + def save_cache(self, fiber_values, filename): + """Save fiber VALUE addresses to cache file. + + Args: + fiber_values: List of Fiber VALUEs + filename: Path to cache file + """ + try: + data = { + 'version': 1, + 'fiber_count': len(fiber_values), + 'fibers': [int(f) for f in fiber_values] # Store VALUE addresses + } + with open(filename, 'w') as f: + json.dump(data, f, indent=2) + print(f"Saved {len(fiber_values)} fiber VALUE(s) to {filename}") + return True + except Exception as e: + print(f"Warning: Failed to save cache: {e}") + return False + + def load_cache(self, filename): + """Load fiber VALUE addresses from cache file. + + Args: + filename: Path to cache file + + Returns: + List of VALUEs or None if loading failed + """ + try: + with open(filename, 'r') as f: + data = json.load(f) + + if data.get('version') != 1: + print(f"Warning: Unknown cache version, ignoring cache") + return None + + fiber_addrs = data.get('fibers', []) + print(f"Loaded {len(fiber_addrs)} fiber VALUE address(es) from {filename}") + + # Initialize heap to ensure we have type information + if not self.heap.initialize(): + return None + + # Reconstruct VALUEs from addresses + value_type = constants.type_struct('VALUE') + fibers = [] + for addr in fiber_addrs: + try: + fiber_val = debugger.create_value(addr, value_type) + fibers.append(fiber_val) + except (debugger.Error, debugger.MemoryError): + print(f"Warning: Could not access VALUE at 0x{addr:x}") + + print(f"Successfully reconstructed {len(fibers)} fiber VALUE(s)") + return fibers + + except FileNotFoundError: + return None + except Exception as e: + print(f"Warning: Failed to load cache: {e}") + return None + + def invoke(self, arguments, terminal): + global _fiber_cache + + # Get limit from --limit option + limit = None + limit_str = arguments.get_option('limit') + if limit_str: + try: + limit = int(limit_str) + if limit <= 0: + print("Error: limit must be positive") + self.usage() + return + except ValueError: + print(f"Error: invalid limit '{limit_str}'") + self.usage() + return + + # Check for --cache flag + use_cache = arguments.has_flag('cache') + cache_file = arguments.get_option('cache', 'fibers.json') + + # Check for --terminated flag + include_terminated = arguments.has_flag('terminated') + + # Try to load from cache if requested + if use_cache: + loaded_fibers = self.load_cache(cache_file) + if loaded_fibers is not None: + # Filter out terminated fibers unless --terminated is specified + if not include_terminated: + filtered_fibers = [] + for fiber_val in loaded_fibers: + try: + fiber_obj = RubyFiber(fiber_val) + if fiber_obj.status != "TERMINATED": + filtered_fibers.append(fiber_val) + except: + # Keep fibers we can't inspect + filtered_fibers.append(fiber_val) + loaded_fibers = filtered_fibers + + # Successfully loaded from cache + _fiber_cache = loaded_fibers + + print(f"\nLoaded {len(loaded_fibers)} fiber(s) from cache:\n") + + for i, fiber_val in enumerate(loaded_fibers): + try: + fiber_obj = RubyFiber(fiber_val) + self._print_fiber_info(terminal, i, fiber_obj) + except: + print(f"Fiber #{i}: VALUE 0x{int(fiber_val):x}") + print(f" (error creating RubyFiber)") + print() + + print(f"Fibers cached. Use 'rb-fiber-scan-switch ' to switch to a fiber.") + return + else: + print(f"Cache file '{cache_file}' not found, proceeding with scan...") + print() + + # Initialize heap scanner + if not self.heap.initialize(): + return + + # Get fiber_data_type for matching + try: + fiber_data_type = debugger.parse_and_eval('&fiber_data_type') + if fiber_data_type is None or int(fiber_data_type) == 0: + print("Error: Could not find 'fiber_data_type' symbol") + print("\nThis usually means:") + print(" • Ruby debug symbols are not available") + print(" • Ruby version doesn't export this symbol") + print("\nTo fix:") + print(" • Install Ruby with debug symbols") + print(" • On macOS: brew install ruby (includes debug info)") + print(" • Or compile Ruby with --enable-debug-symbols") + return + except debugger.Error as e: + print(f"Error: Could not evaluate '&fiber_data_type': {e}") + print("\nRuby debug symbols may not be available.") + return + + if limit: + print(f"Scanning heap for first {limit} Fiber object(s)...", file=sys.stderr) + else: + print("Scanning heap for Fiber objects...", file=sys.stderr) + + # Use RubyHeap to find fibers (returns VALUEs) + fiber_values = self.heap.find_typed_data(fiber_data_type, limit=limit, progress=True) + + # Filter out terminated fibers unless --terminated is specified + if not include_terminated: + filtered_fibers = [] + for fiber_val in fiber_values: + try: + fiber_obj = RubyFiber(fiber_val) + if fiber_obj.status != "TERMINATED": + filtered_fibers.append(fiber_val) + except: + # Keep fibers we can't inspect + filtered_fibers.append(fiber_val) + fiber_values = filtered_fibers + + # Cache the VALUEs for later use + _fiber_cache = fiber_values + + if limit and len(fiber_values) >= limit: + print(f"Found {len(fiber_values)} fiber(s) (limit reached):\n") + else: + print(f"Found {len(fiber_values)} fiber(s):\n") + + for i, fiber_val in enumerate(fiber_values): + fiber_obj = RubyFiber(fiber_val) + self._print_fiber_info(terminal, i, fiber_obj) + + # Save to cache if requested + if use_cache and fiber_values: + self.save_cache(fiber_values, cache_file) + print() + + print(f"Fibers cached. Use 'rb-fiber-scan-switch ' to switch to a fiber.") + + def _print_fiber_info(self, terminal, index, fiber_obj): + """Print formatted fiber information. + + Args: + terminal: Terminal instance for formatting + index: Fiber index in cache + fiber_obj: RubyFiber instance + """ + # Print fiber index with VALUE and pointer + print(f"Fiber #{index}: ", end='') + terminal.print_type_tag('T_DATA', int(fiber_obj.value)) + print(' → ', end='') + terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address) + print() + + # Print status + print(f" Status: {fiber_obj.status}") + + # Print exception if present (catch errors for terminated fibers) + try: + exc_info = fiber_obj.exception_info + if exc_info: + print(f" Exception: {exc_info}") + except Exception: + # Silently skip exception info if we can't read it + pass + + # Print Stack with formatted pointer + stack_type = str(fiber_obj.stack_base.type) + print(f" Stack: ", end='') + terminal.print_type_tag(stack_type, int(fiber_obj.stack_base)) + print() + + # Print VM Stack with formatted pointer + vm_stack_type = str(fiber_obj.vm_stack.type) + print(f" VM Stack: ", end='') + terminal.print_type_tag(vm_stack_type, int(fiber_obj.vm_stack)) + print() + + # Print CFP + cfp_type = str(fiber_obj.cfp.type).replace(' *', '') # Remove pointer marker for display + print(f" CFP: ", end='') + terminal.print_type_tag(cfp_type, int(fiber_obj.cfp)) + print() + print() + + +class RubyFiberScanSwitchHandler: + """Switch to a fiber from the scan heap cache.""" + + USAGE = command.Usage( + summary="Switch to a fiber from scan cache by index", + parameters=[('index', 'Fiber index from rb-fiber-scan-heap')], + options={}, + flags=[], + examples=[ + ("rb-fiber-scan-switch 0", "Switch to first fiber"), + ("rb-fiber-scan-switch 2", "Switch to third fiber") + ] + ) + + def invoke(self, arguments, terminal): + global _fiber_cache + + if not arguments.expressions or not arguments.expressions[0].strip(): + command.print_usage(RubyFiberScanSwitchHandler.USAGE, terminal) + return + + # Check if cache is populated + if not _fiber_cache: + print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.") + return + + # Parse index + try: + index = int(arguments.expressions[0].strip()) + except ValueError: + print(f"Error: Invalid index '{arguments.expressions[0]}'. Must be an integer.") + command.print_usage(RubyFiberScanSwitchHandler.USAGE, terminal) + return + + # Validate index + if index < 0 or index >= len(_fiber_cache): + print(f"Error: Index {index} out of range [0, {len(_fiber_cache)-1}]") + print(f"\nRun 'rb-fiber-scan-heap' to see available fibers.") + return + + # Get fiber VALUE from cache + fiber_value = _fiber_cache[index] + + print(f"Switching to Fiber #{index}: VALUE 0x{int(fiber_value):x}") + + # Delegate to rb-fiber-switch command + # This command manages the global _current_fiber state + try: + RubyFiberSwitchHandler().invoke(command.Arguments([f"0x{int(fiber_value):x}"], {}, []), terminal) + except debugger.Error as e: + print(f"Error switching to fiber: {e}") + import traceback + traceback.print_exc() # GDB-specific unwinder class - only available when running under GDB if debugger.DEBUGGER_NAME == 'gdb': - class RubyFiberUnwinder(gdb.unwinder.Unwinder): - """Custom unwinder for Ruby fibers. - - This allows GDB to unwind a fiber's stack even in a core dump, - by extracting saved register state from the fiber's jmp_buf. - - Based on similar technique from Facebook Folly: - https://github.com/facebook/folly/blob/main/folly/fibers/scripts/gdb.py - """ - - def __init__(self): - super(RubyFiberUnwinder, self).__init__("Ruby Fiber Unwinder") - self.active_fiber = None - self.unwound_first_frame = False - - def __call__(self, pending_frame): - """Called by GDB when unwinding frames.""" - # Only unwind if we have an active fiber set - if not self.active_fiber: - return None - - # Only unwind the first frame, then let GDB continue normally - if self.unwound_first_frame: - return None - - try: - # Ruby uses its own coroutine implementation, not setjmp/longjmp! - # Registers are saved in fiber->context.stack_pointer - # See coroutine/amd64/Context.S for the layout - - coroutine_ctx = self.active_fiber['context'] - stack_ptr = coroutine_ctx['stack_pointer'] - - # The stack_pointer points to the saved register area - # From Context.S (x86-64): - # [stack_pointer + 0] = R15 - # [stack_pointer + 8] = R14 - # [stack_pointer + 16] = R13 - # [stack_pointer + 24] = R12 - # [stack_pointer + 32] = RBX - # [stack_pointer + 40] = RBP - # [stack_pointer + 48] = Return address (RIP) - - if int(stack_ptr) == 0: - return None - - # Cast to uint64 pointer to read saved registers - uint64_ptr = stack_ptr.cast(gdb.lookup_type('uint64_t').pointer()) - - # Read saved registers (keep as gdb.Value) - r15 = uint64_ptr[0] - r14 = uint64_ptr[1] - r13 = uint64_ptr[2] - r12 = uint64_ptr[3] - rbx = uint64_ptr[4] - rbp = uint64_ptr[5] - - # After coroutine_transfer executes 'addq $48, %rsp', RSP points to the return address - # After 'ret' pops the return address, RSP = stack_ptr + 48 + 8 - # We want to create an unwind frame AS IF we're in the caller of coroutine_transfer - # So RSP should be pointing AFTER the return address was popped - rsp_value = int(stack_ptr) + 48 + 8 - rsp = gdb.Value(rsp_value).cast(gdb.lookup_type('uint64_t')) - - # The return address (RIP) is at [stack_ptr + 48] - # This is what 'ret' will pop and jump to - rip_ptr = gdb.Value(int(stack_ptr) + 48).cast(gdb.lookup_type('uint64_t').pointer()) - rip = rip_ptr.dereference() - - # Sanity check - if int(rsp) == 0 or int(rip) == 0: - return None - - # Create frame ID - frame_id = gdb.unwinder.FrameId(int(rsp), int(rip)) - - # Create unwind info - unwind_info = pending_frame.create_unwind_info(frame_id) - - # Add saved registers - unwind_info.add_saved_register("rip", rip) - unwind_info.add_saved_register("rsp", rsp) - unwind_info.add_saved_register("rbp", rbp) - unwind_info.add_saved_register("rbx", rbx) - unwind_info.add_saved_register("r12", r12) - unwind_info.add_saved_register("r13", r13) - unwind_info.add_saved_register("r14", r14) - unwind_info.add_saved_register("r15", r15) - - # Mark that we've unwound the first frame - self.unwound_first_frame = True - - return unwind_info - - except (gdb.error, gdb.MemoryError) as e: - # If we can't read the fiber context, bail - return None - - def activate_fiber(self, fiber): - """Activate unwinding for a specific fiber.""" - self.active_fiber = fiber - self.unwound_first_frame = False - gdb.invalidate_cached_frames() - - def deactivate(self): - """Deactivate fiber unwinding.""" - self.active_fiber = None - self.unwound_first_frame = False - gdb.invalidate_cached_frames() - - -class RubyFiberSwitchCommand(debugger.Command): - """Switch debugger's stack view to a specific fiber. - - Usage: rb-fiber-switch - rb-fiber-switch off - - Examples: - rb-fiber-switch 0x7fffdc409ca8 # VALUE address - rb-fiber-switch $fiber_val # Debugger variable - rb-fiber-switch off # Deactivate unwinder (GDB only) - - This uses a custom unwinder (GDB only) to make the debugger follow the fiber's saved - stack, allowing you to use 'bt', 'up', 'down', 'frame', etc. - Works even with core dumps! - - Based on technique from Facebook Folly fibers. - """ - - def __init__(self): - super(RubyFiberSwitchCommand, self).__init__("rb-fiber-switch", debugger.COMMAND_USER) - if debugger.DEBUGGER_NAME == 'gdb': - self._ensure_unwinder() - - def usage(self): - """Print usage information.""" - print("Usage: rb-fiber-switch ") - print(" rb-fiber-switch off") - print("Examples:") - print(" rb-fiber-switch 0x7fffdc409ca8 # VALUE address") - print(" rb-fiber-switch $fiber # Debugger variable") - print(" rb-fiber-switch off # Deactivate unwinder (GDB only)") - print() - print("After switching, you can use: bt, up, down, frame, info locals, etc.") - if debugger.DEBUGGER_NAME != 'gdb': - print("Note: Stack unwinding is only supported in GDB") - - def _ensure_unwinder(self): - """Ensure the fiber unwinder is registered (GDB only).""" - global _fiber_unwinder - if _fiber_unwinder is None: - _fiber_unwinder = RubyFiberUnwinder() - gdb.unwinder.register_unwinder(None, _fiber_unwinder, replace=True) - - def invoke(self, arg, from_tty): - global _fiber_unwinder - - if not arg: - self.usage() - return - - # Check for deactivate - if arg and arg.lower() in ('off', 'none', 'deactivate'): - if debugger.DEBUGGER_NAME == 'gdb' and _fiber_unwinder: - _fiber_unwinder.deactivate() - set_current_fiber(None) - print("Fiber unwinder deactivated. Switched back to normal stack view.") - print("Try: bt") - return - - # Parse the argument as a VALUE - try: - # Evaluate the expression to get a VALUE - fiber_value = debugger.parse_and_eval(arg) - - # Ensure it's cast to VALUE type - try: - value_type = constants.type_struct('VALUE') - except debugger.Error as lookup_err: - print(f"Error: Could not lookup type 'VALUE': {lookup_err}") - print("This usually means Ruby symbols aren't fully loaded yet.") - print(f"Try running the process further or checking symbol loading.") - return - - fiber_value = fiber_value.cast(value_type) - - except (debugger.Error, RuntimeError) as e: - print(f"Error: Could not evaluate '{arg}' as a VALUE") - print(f"Details: {e}") - import traceback - traceback.print_exc() - print() - self.usage() - return - - # Create RubyFiber wrapper - try: - fiber_obj = RubyFiber(fiber_value) - except Exception as e: - print(f"Error: Could not create RubyFiber from VALUE 0x{int(fiber_value):x}") - print(f"Details: {e}") - import traceback - traceback.print_exc() - return - - # Check if fiber is in a switchable state - if fiber_obj.status in ('CREATED', 'TERMINATED'): - print(f"Warning: Fiber is {fiber_obj.status}, may not have valid saved context") - print() - - # Update global current fiber state - set_current_fiber(fiber_obj) - - # Get the fiber pointer for unwinder - fiber_ptr = fiber_obj.pointer - - # Activate the unwinder for this fiber (GDB only) - if debugger.DEBUGGER_NAME == 'gdb' and _fiber_unwinder: - _fiber_unwinder.activate_fiber(fiber_ptr) - - # Set convenience variables for the fiber context - ec = fiber_ptr['cont']['saved_ec'].address - debugger.set_convenience_variable('fiber', fiber_value) - debugger.set_convenience_variable('fiber_ptr', fiber_ptr) - debugger.set_convenience_variable('ec', ec) - - # Set errinfo if present (check for real object, not special constant) - errinfo_val = ec['errinfo'] - errinfo_int = int(errinfo_val) - is_special = (errinfo_int & 0x03) != 0 or errinfo_int == 0 - if not is_special: - debugger.set_convenience_variable('errinfo', errinfo_val) - - # Create terminal for formatting - terminal = format.create_terminal(from_tty) - - # Print switch confirmation - print(f"Switched to Fiber: ", end='') - print(terminal.print_type_tag('T_DATA', int(fiber_value), None), end='') - print(' → ', end='') - print(terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address, None)) - print(f" Status: {fiber_obj.status}") - - # Print exception if present (catch errors for terminated fibers) - try: - exc_info = fiber_obj.exception_info - if exc_info: - print(f" Exception: {exc_info}") - except Exception: - # Silently skip exception info if we can't read it - pass - print() - - # Set tag retval if present - tag = None - is_retval_special = True - try: - tag = ec['tag'] - if int(tag) != 0: - tag_retval = tag['retval'] - tag_state = int(tag['state']) - retval_int = int(tag_retval) - is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0 - if not is_retval_special: - debugger.set_convenience_variable('retval', tag_retval) - except: - tag = None - is_retval_special = True - - print("Convenience variables set:") - print(f" $fiber = Current fiber VALUE") - print(f" $fiber_ptr = Current fiber pointer (struct rb_fiber_struct *)") - print(f" $ec = Execution context (rb_execution_context_t *)") - if not is_special: - print(f" $errinfo = Exception being handled (VALUE)") - if tag and not is_retval_special: - print(f" $retval = Return value from 'return' (VALUE)") - print() - print("Now try:") - print(" bt # Show C backtrace of fiber") - print(" frame # Switch to frame N") - print(" up/down # Move up/down frames") - print(" info locals # Show local variables") - if not is_special: - print(" rp $errinfo # Pretty print exception") - if tag and not is_retval_special: - print(" rp $retval # Pretty print return value (in ensure blocks)") - print() - print("Useful VALUES to inspect:") - print(" $ec->tag->retval # Return value (in ensure after 'return')") - print(" $ec->cfp->sp[-1] # Top of VM stack") - print(" $fiber_ptr->cont.value # Fiber yield/return value") - print() - print("NOTE: Frame #0 is synthetic (created by the unwinder) and may look odd.") - print(" The real fiber context starts at frame #1.") - print(" Use 'frame 1' to skip to the actual fiber_setcontext frame.") - print() - print("To switch back:") - print(" rb-fiber-switch off") - - -class RubyFiberScanStackTraceAllCommand(debugger.Command): - """Print stack traces for all fibers in the scan cache. - - Usage: rb-fiber-scan-stack-trace-all - - This command prints the Ruby stack trace for each fiber that was - found by 'rb-fiber-scan-heap'. Run that command first to populate - the fiber cache. - """ - - def __init__(self): - super(RubyFiberScanStackTraceAllCommand, self).__init__("rb-fiber-scan-stack-trace-all", debugger.COMMAND_USER) - - def usage(self): - """Print usage information.""" - print("Usage: rb-fiber-scan-stack-trace-all") - print() - print("Note: Run 'rb-fiber-scan-heap' first to populate the fiber cache.") - - def invoke(self, arg, from_tty): - global _fiber_cache - - # Check if cache is populated - if not _fiber_cache: - print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.") - return - - # Import stack module to use print_fiber_backtrace - import stack - - print(f"Printing stack traces for {len(_fiber_cache)} fiber(s)\n") - print("=" * 80) - - for i, fiber_value in enumerate(_fiber_cache): - try: - # Create RubyFiber wrapper to get fiber info - fiber_obj = RubyFiber(fiber_value) - - print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x} → {fiber_obj.status}") - print("-" * 80) - - # Use stack.print_fiber_backtrace with the fiber pointer - stack.print_fiber_backtrace(fiber_obj.pointer, from_tty=from_tty) - - except Exception as e: - print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x}") - print("-" * 80) - print(f"Error printing backtrace: {e}") - import traceback - traceback.print_exc() - - print() + class RubyFiberUnwinder(gdb.unwinder.Unwinder): + """Custom unwinder for Ruby fibers. + + This allows GDB to unwind a fiber's stack even in a core dump, + by extracting saved register state from the fiber's jmp_buf. + + Based on similar technique from Facebook Folly: + https://github.com/facebook/folly/blob/main/folly/fibers/scripts/gdb.py + """ + + def __init__(self): + super(RubyFiberUnwinder, self).__init__("Ruby Fiber Unwinder") + self.active_fiber = None + self.unwound_first_frame = False + + def __call__(self, pending_frame): + """Called by GDB when unwinding frames.""" + # Only unwind if we have an active fiber set + if not self.active_fiber: + return None + + # Only unwind the first frame, then let GDB continue normally + if self.unwound_first_frame: + return None + + try: + # Ruby uses its own coroutine implementation, not setjmp/longjmp! + # Registers are saved in fiber->context.stack_pointer + # See coroutine/amd64/Context.S for the layout + + coroutine_ctx = self.active_fiber['context'] + stack_ptr = coroutine_ctx['stack_pointer'] + + # The stack_pointer points to the saved register area + # From Context.S (x86-64): + # [stack_pointer + 0] = R15 + # [stack_pointer + 8] = R14 + # [stack_pointer + 16] = R13 + # [stack_pointer + 24] = R12 + # [stack_pointer + 32] = RBX + # [stack_pointer + 40] = RBP + # [stack_pointer + 48] = Return address (RIP) + + if int(stack_ptr) == 0: + return None + + # Cast to uint64 pointer to read saved registers + uint64_ptr = stack_ptr.cast(gdb.lookup_type('uint64_t').pointer()) + + # Read saved registers (keep as gdb.Value) + r15 = uint64_ptr[0] + r14 = uint64_ptr[1] + r13 = uint64_ptr[2] + r12 = uint64_ptr[3] + rbx = uint64_ptr[4] + rbp = uint64_ptr[5] + + # After coroutine_transfer executes 'addq $48, %rsp', RSP points to the return address + # After 'ret' pops the return address, RSP = stack_ptr + 48 + 8 + # We want to create an unwind frame AS IF we're in the caller of coroutine_transfer + # So RSP should be pointing AFTER the return address was popped + rsp_value = int(stack_ptr) + 48 + 8 + rsp = gdb.Value(rsp_value).cast(gdb.lookup_type('uint64_t')) + + # The return address (RIP) is at [stack_ptr + 48] + # This is what 'ret' will pop and jump to + rip_ptr = gdb.Value(int(stack_ptr) + 48).cast(gdb.lookup_type('uint64_t').pointer()) + rip = rip_ptr.dereference() + + # Sanity check + if int(rsp) == 0 or int(rip) == 0: + return None + + # Create frame ID + frame_id = gdb.unwinder.FrameId(int(rsp), int(rip)) + + # Create unwind info + unwind_info = pending_frame.create_unwind_info(frame_id) + + # Add saved registers + unwind_info.add_saved_register("rip", rip) + unwind_info.add_saved_register("rsp", rsp) + unwind_info.add_saved_register("rbp", rbp) + unwind_info.add_saved_register("rbx", rbx) + unwind_info.add_saved_register("r12", r12) + unwind_info.add_saved_register("r13", r13) + unwind_info.add_saved_register("r14", r14) + unwind_info.add_saved_register("r15", r15) + + # Mark that we've unwound the first frame + self.unwound_first_frame = True + + return unwind_info + + except (gdb.error, gdb.MemoryError) as e: + # If we can't read the fiber context, bail + return None + + def activate_fiber(self, fiber): + """Activate unwinding for a specific fiber.""" + self.active_fiber = fiber + self.unwound_first_frame = False + gdb.invalidate_cached_frames() + + def deactivate(self): + """Deactivate fiber unwinding.""" + self.active_fiber = None + self.unwound_first_frame = False + gdb.invalidate_cached_frames() + + +class RubyFiberSwitchHandler: + """Switch debugger's stack view to a specific fiber.""" + + USAGE = command.Usage( + summary="Switch debugger stack view to a specific fiber", + parameters=[('fiber', 'Fiber VALUE/address or "off" to deactivate')], + options={}, + flags=[], + examples=[ + ("rb-fiber-switch 0x7fffdc409ca8", "Switch to fiber at address"), + ("rb-fiber-switch $fiber", "Switch using debugger variable"), + ("rb-fiber-switch off", "Deactivate unwinder (GDB only)") + ] + ) + + def __init__(self): + if debugger.DEBUGGER_NAME == 'gdb': + self._ensure_unwinder() + + def _ensure_unwinder(self): + """Ensure the fiber unwinder is registered (GDB only).""" + global _fiber_unwinder + if _fiber_unwinder is None: + _fiber_unwinder = RubyFiberUnwinder() + gdb.unwinder.register_unwinder(None, _fiber_unwinder, replace=True) + + def invoke(self, arguments, terminal): + global _fiber_unwinder + + # Check for deactivate + arg = arguments.expressions[0] if arguments.expressions else None + if not arg: + print("Error: fiber parameter required") + return + + if arg.lower() in ('off', 'none', 'deactivate'): + if debugger.DEBUGGER_NAME == 'gdb' and _fiber_unwinder: + _fiber_unwinder.deactivate() + set_current_fiber(None) + print("Fiber unwinder deactivated. Switched back to normal stack view.") + print("Try: bt") + return + + # Parse the argument as a VALUE + try: + # Evaluate the expression to get a VALUE + fiber_value = debugger.parse_and_eval(arg) + + # Ensure it's cast to VALUE type + try: + value_type = constants.type_struct('VALUE') + except debugger.Error as lookup_err: + print(f"Error: Could not lookup type 'VALUE': {lookup_err}") + print("This usually means Ruby symbols aren't fully loaded yet.") + print(f"Try running the process further or checking symbol loading.") + return + + fiber_value = fiber_value.cast(value_type) + + except (debugger.Error, RuntimeError) as e: + print(f"Error: Could not evaluate '{arg}' as a VALUE") + print(f"Details: {e}") + import traceback + traceback.print_exc() + print() + self.usage() + return + + # Create RubyFiber wrapper + try: + fiber_obj = RubyFiber(fiber_value) + except Exception as e: + print(f"Error: Could not create RubyFiber from VALUE 0x{int(fiber_value):x}") + print(f"Details: {e}") + import traceback + traceback.print_exc() + return + + # Check if fiber is in a switchable state + if fiber_obj.status in ('CREATED', 'TERMINATED'): + print(f"Warning: Fiber is {fiber_obj.status}, may not have valid saved context") + print() + + # Update global current fiber state + set_current_fiber(fiber_obj) + + # Get the fiber pointer for unwinder + fiber_ptr = fiber_obj.pointer + + # Activate the unwinder for this fiber (GDB only) + if debugger.DEBUGGER_NAME == 'gdb' and _fiber_unwinder: + _fiber_unwinder.activate_fiber(fiber_ptr) + + # Set convenience variables for the fiber context + ec = fiber_ptr['cont']['saved_ec'].address + debugger.set_convenience_variable('fiber', fiber_value) + debugger.set_convenience_variable('fiber_ptr', fiber_ptr) + debugger.set_convenience_variable('ec', ec) + + # Set errinfo if present (check for real object, not special constant) + errinfo_val = ec['errinfo'] + errinfo_int = int(errinfo_val) + is_special = (errinfo_int & 0x03) != 0 or errinfo_int == 0 + if not is_special: + debugger.set_convenience_variable('errinfo', errinfo_val) + + # Print switch confirmation + print(f"Switched to Fiber: ", end='') + terminal.print_type_tag('T_DATA', int(fiber_value), None) + print(' → ', end='') + terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address, None) + print() + print(f" Status: {fiber_obj.status}") # Print exception if present (catch errors for terminated fibers) + try: + exc_info = fiber_obj.exception_info + if exc_info: + print(f" Exception: {exc_info}") + except Exception: + # Silently skip exception info if we can't read it + pass + print() + + # Set tag retval if present + tag = None + is_retval_special = True + try: + tag = ec['tag'] + if int(tag) != 0: + tag_retval = tag['retval'] + tag_state = int(tag['state']) + retval_int = int(tag_retval) + is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0 + if not is_retval_special: + debugger.set_convenience_variable('retval', tag_retval) + except: + tag = None + is_retval_special = True + + print("Convenience variables set:") + print(f" $fiber = Current fiber VALUE") + print(f" $fiber_ptr = Current fiber pointer (struct rb_fiber_struct *)") + print(f" $ec = Execution context (rb_execution_context_t *)") + if not is_special: + print(f" $errinfo = Exception being handled (VALUE)") + if tag and not is_retval_special: + print(f" $retval = Return value from 'return' (VALUE)") + print() + print("Now try:") + print(" bt # Show C backtrace of fiber") + print(" frame # Switch to frame N") + print(" up/down # Move up/down frames") + print(" info locals # Show local variables") + if not is_special: + print(" rp $errinfo # Pretty print exception") + if tag and not is_retval_special: + print(" rp $retval # Pretty print return value (in ensure blocks)") + print() + print("Useful VALUES to inspect:") + print(" $ec->tag->retval # Return value (in ensure after 'return')") + print(" $ec->cfp->sp[-1] # Top of VM stack") + print(" $fiber_ptr->cont.value # Fiber yield/return value") + print() + print("NOTE: Frame #0 is synthetic (created by the unwinder) and may look odd.") + print(" The real fiber context starts at frame #1.") + print(" Use 'frame 1' to skip to the actual fiber_setcontext frame.") + print() + print("To switch back:") + print(" rb-fiber-switch off") + + +class RubyFiberScanStackTraceAllHandler: + """Print stack traces for all fibers in the scan cache.""" + + USAGE = command.Usage( + summary="Print stack traces for all cached fibers", + parameters=[], + options={}, + flags=[], + examples=[ + ("rb-fiber-scan-heap; rb-fiber-scan-stack-trace-all", "Scan fibers then show all backtraces") + ] + ) + + def invoke(self, arguments, terminal): + global _fiber_cache + + # Check if cache is populated + if not _fiber_cache: + print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.") + return + + # Import stack module to use print_fiber_backtrace + import stack + + print(f"Printing stack traces for {len(_fiber_cache)} fiber(s)\n") + print("=" * 80) + + for i, fiber_value in enumerate(_fiber_cache): + try: + # Create RubyFiber wrapper to get fiber info + fiber_obj = RubyFiber(fiber_value) + + print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x} → {fiber_obj.status}") + print("-" * 80) + + # Use stack.print_fiber_backtrace with the fiber pointer + stack.print_fiber_backtrace(fiber_obj.pointer) + + except Exception as e: + print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x}") + print("-" * 80) + print(f"Error printing backtrace: {e}") + import traceback + traceback.print_exc() + + print() # Register commands -RubyFiberScanHeapCommand() -RubyFiberScanSwitchCommand() -RubyFiberSwitchCommand() -RubyFiberScanStackTraceAllCommand() +debugger.register("rb-fiber-scan-heap", RubyFiberScanHeapHandler, usage=RubyFiberScanHeapHandler.USAGE) +debugger.register("rb-fiber-scan-switch", RubyFiberScanSwitchHandler, usage=RubyFiberScanSwitchHandler.USAGE) +debugger.register("rb-fiber-switch", RubyFiberSwitchHandler, usage=RubyFiberSwitchHandler.USAGE) +debugger.register("rb-fiber-scan-stack-trace-all", RubyFiberScanStackTraceAllHandler, usage=RubyFiberScanStackTraceAllHandler.USAGE) diff --git a/data/toolbox/format.py b/data/toolbox/format.py index 32c831e..4618696 100644 --- a/data/toolbox/format.py +++ b/data/toolbox/format.py @@ -6,6 +6,7 @@ """ import sys +import value as rvalue class Style: """Sentinel object representing a style.""" @@ -28,66 +29,60 @@ def __repr__(self): error = Style('error') bold = Style('bold') dim = Style('dim') +title = Style('title') # For section headers in help text +placeholder = Style('placeholder') # For help text placeholders (parameters, options) +example = Style('example') # For example commands in help text class Text: """Plain text output without any formatting.""" - def print(self, *args): - """Print arguments, skipping style sentinels.""" - result = [] - for arg in args: - # Skip Style sentinels - if not isinstance(arg, Style): - # If arg has a print_to method, use it - if hasattr(arg, 'print_to'): - result.append(arg.print_to(self)) - else: - result.append(str(arg)) - return "".join(result) - - def print_type_tag(self, type_name, address_val=None, details=None): + def __init__(self, output=None): + """Initialize text terminal. + + Args: + output: Output stream (default: sys.stdout) + """ + self.output = output or sys.stdout + + def print(self, *args, end='\n'): + """Print arguments to output stream. + + Args: + *args: Arguments to print (strings, Style sentinels, or objects with print_to) + end: String appended after the last arg (default: newline) """ - Format a type tag like . + for arg in args: + if isinstance(arg, Style): + # Skip style sentinels in plain text mode + continue + elif hasattr(arg, 'print_to'): + # Let object print itself + arg.print_to(self) + else: + self.output.write(str(arg)) + self.output.write(end) + + def print_type_tag(self, type_name, addr=None, details=None): + """Print a type tag like . Arguments: type_name: Type name (e.g., "T_ARRAY", "void *") - address_val: Optional hex address (as integer or hex string without 0x) + addr: Optional hex address (as integer or hex string without 0x) details: Optional details string (e.g., "embedded length=3") - - Returns: - Formatted string with appropriate styling """ - if isinstance(address_val, int): - address_val = f"{address_val:x}" + if isinstance(addr, int): + addr = f"{addr:x}" - parts = [metadata, '<', type, type_name] + self.print(metadata, '<', reset, type, type_name, reset, end='') - if address_val: - parts.extend([metadata, f'@0x{address_val}']) + if addr: + # @ symbol in dim, address in magenta + self.print(metadata, '@', reset, address, f'0x{addr}', reset, end='') if details: - parts.extend([f' {details}']) - - parts.extend([metadata, '>', reset]) + self.print(metadata, f' {details}', reset, end='') - return self.print(*parts) - - def print_value_with_tag(self, type_tag, val=None, value_style=value): - """ - Format a complete value with type tag and optional value. - - Arguments: - type_tag: Formatted type tag string - val: Optional value to display - value_style: Style sentinel to use for the value - - Returns: - Complete formatted string - """ - if val is not None: - return f"{type_tag} {self.print(value_style, str(val), reset)}" - else: - return type_tag + self.print(metadata, '>', reset, end='') class XTerm(Text): """ANSI color/style output for terminal.""" @@ -112,36 +107,49 @@ class XTerm(Text): BRIGHT_MAGENTA = '\033[95m' BRIGHT_CYAN = '\033[96m' - def __init__(self): + def __init__(self, output=None): + """Initialize ANSI terminal. + + Args: + output: Output stream (default: sys.stdout) + """ + super().__init__(output) # Map style sentinels to ANSI codes self.style_map = { reset: self.RESET, - metadata: self.DIM, - address: self.DIM, - type: '', - value: '', + metadata: self.DIM, # Type tag brackets <> + address: self.MAGENTA, # Memory addresses in type tags + type: self.CYAN, # Type names (T_ARRAY, VALUE, etc.) + value_style: '', string: self.GREEN, number: self.CYAN, symbol: self.YELLOW, - method: self.YELLOW, # Same as symbol + method: self.YELLOW, # Same as symbol error: self.RED, bold: self.BOLD, dim: self.DIM, + title: self.BOLD, # Section headers in help text + placeholder: self.BLUE, # Help text placeholders + example: self.GREEN, # Example commands in help text } - def print(self, *args): - """Print arguments, replacing style sentinels with ANSI codes.""" - result = [] + def print(self, *args, end='\n'): + """Print arguments to output stream with ANSI formatting. + + Args: + *args: Arguments to print (strings, Style sentinels, or objects with print_to) + end: String appended after the last arg (default: newline) + """ for arg in args: if isinstance(arg, Style): - result.append(self.style_map.get(arg, '')) + # Print ANSI code for style + self.output.write(self.style_map.get(arg, '')) + elif hasattr(arg, 'print_to'): + # Let object print itself + arg.print_to(self) else: - # If arg has a print_to method, use it - if hasattr(arg, 'print_to'): - result.append(arg.print_to(self)) - else: - result.append(str(arg)) - return "".join(result) + self.output.write(str(arg)) + self.output.write(end) # Helper for creating the appropriate terminal based on TTY status def create_terminal(from_tty): @@ -166,7 +174,7 @@ def debug(self, message): def print(self, *args): """Print arguments using the terminal's formatting.""" - print(self.terminal.print(*args)) + self.terminal.print(*args) def print_indent(self, depth): """Print indentation based on depth.""" @@ -194,7 +202,5 @@ def print_value(self, ruby_value, depth): value_int = int(ruby_value) self.debug(f"print_value: value=0x{value_int:x}, depth={depth}") - # Use interpret to get the appropriate wrapper and let it print itself - import value - ruby_object = value.interpret(ruby_value) + ruby_object = rvalue.interpret(ruby_value) ruby_object.print_recursive(self, depth) diff --git a/data/toolbox/heap.py b/data/toolbox/heap.py index 8d62566..7e0a01f 100644 --- a/data/toolbox/heap.py +++ b/data/toolbox/heap.py @@ -1,6 +1,8 @@ import debugger import sys +import command import constants +import format # Constants RBASIC_FLAGS_TYPE_MASK = 0x1f @@ -471,7 +473,7 @@ def find_typed_data(self, data_type, limit=None, progress=False): return objects -class RubyHeapScanCommand(debugger.Command): +class RubyHeapScanHandler: """Scan the Ruby heap for objects, optionally filtered by type. Usage: rb-heap-scan [--type TYPE] [--limit N] [--from $heap] @@ -501,23 +503,21 @@ class RubyHeapScanCommand(debugger.Command): rb-heap-scan --from $heap # Continue from last scan """ - def __init__(self): - super(RubyHeapScanCommand, self).__init__("rb-heap-scan", debugger.COMMAND_USER) - - def usage(self): - """Print usage information.""" - print("Usage: rb-heap-scan [--type TYPE] [--limit N] [--from $heap]") - print("Examples:") - print(" rb-heap-scan --type RUBY_T_STRING # Find up to 10 strings") - print(" rb-heap-scan --type RUBY_T_ARRAY --limit 5 # Find up to 5 arrays") - print(" rb-heap-scan --type 0x05 --limit 100 # Find up to 100 T_STRING objects") - print(" rb-heap-scan --limit 20 # Scan 20 objects (any type)") - print(" rb-heap-scan --type RUBY_T_STRING --from $heap # Continue from last scan") - print() - print("Pagination:") - print(" The address of the last object is saved to $heap for pagination:") - print(" rb-heap-scan --type RUBY_T_STRING --limit 10 # First page") - print(" rb-heap-scan --type RUBY_T_STRING --from $heap # Next page") + USAGE = command.Usage( + summary="Scan the Ruby heap for objects, optionally filtered by type", + parameters=[], + options={ + 'type': (str, None, 'Filter by Ruby type (e.g., RUBY_T_STRING, RUBY_T_ARRAY, or 0x05)'), + 'limit': (int, 10, 'Maximum objects to find'), + 'from': (str, None, 'Start address for pagination (use $heap)') + }, + flags=[], + examples=[ + ("rb-heap-scan --type RUBY_T_STRING", "Find up to 10 strings"), + ("rb-heap-scan --type RUBY_T_ARRAY --limit 20", "Find first 20 arrays"), + ("rb-heap-scan --from $heap", "Continue from last scan (pagination)") + ] + ) def _parse_type(self, type_arg): """Parse a type argument and return the type value. @@ -551,13 +551,9 @@ def _parse_type(self, type_arg): return type_value - def invoke(self, arg, from_tty): + def invoke(self, arguments, terminal): """Execute the heap scan command.""" try: - # Parse arguments - import command - arguments = command.parse_arguments(arg if arg else "") - # Check if we're continuing from a previous scan from_option = arguments.get_option('from') if from_option is not None: @@ -616,20 +612,14 @@ def invoke(self, arg, from_tty): print("(You may have reached the end of the heap)") return - # Import format for terminal output - import format - terminal = format.create_terminal(from_tty) - - # Import value module for interpretation import value as value_module print(f"Found {len(objects)} object(s):") print() for i, obj in enumerate(objects): - obj_int = int(obj) - # Set as convenience variable + obj_int = int(obj) var_name = f"heap{i}" debugger.set_convenience_variable(var_name, obj) @@ -637,48 +627,48 @@ def invoke(self, arg, from_tty): try: interpreted = value_module.interpret(obj) - print(terminal.print( + terminal.print( format.metadata, f" [{i}] ", format.dim, f"${var_name} = ", format.reset, interpreted - )) + ) except Exception as e: - print(terminal.print( + terminal.print( format.metadata, f" [{i}] ", format.dim, f"${var_name} = ", format.error, f"" - )) + ) print() - print(terminal.print( + terminal.print( format.dim, f"Objects saved in $heap0 through $heap{len(objects)-1}", format.reset - )) + ) # Save next address to $heap for pagination if next_address is not None: # Save the next address to continue from void_ptr_type = constants.type_struct('void').pointer() debugger.set_convenience_variable('heap', debugger.create_value(next_address, void_ptr_type)) - print(terminal.print( + terminal.print( format.dim, f"Next scan address saved to $heap: 0x{next_address:016x}", format.reset - )) - print(terminal.print( + ) + terminal.print( format.dim, f"Run 'rb-heap-scan --type {type_option if type_option else '...'} --from $heap' for next page", format.reset - )) + ) else: # Reached the end of the heap - unset $heap so next scan starts fresh debugger.set_convenience_variable('heap', None) - print(terminal.print( + terminal.print( format.dim, f"Reached end of heap (no more objects to scan)", format.reset - )) + ) except Exception as e: print(f"Error: {e}") @@ -687,4 +677,4 @@ def invoke(self, arg, from_tty): # Register commands -RubyHeapScanCommand() +debugger.register("rb-heap-scan", RubyHeapScanHandler, usage=RubyHeapScanHandler.USAGE) diff --git a/data/toolbox/init.py b/data/toolbox/init.py index 10c4164..550ae77 100644 --- a/data/toolbox/init.py +++ b/data/toolbox/init.py @@ -24,8 +24,8 @@ # Try to load each extension individually extensions_to_load = [ - ('object', 'rb-object-print'), - ('context', 'rb-context'), + ('print', 'rb-print'), + ('context', 'rb-context, rb-context-storage'), ('fiber', 'rb-fiber-scan-heap, rb-fiber-switch'), ('stack', 'rb-stack-trace'), ('heap', 'rb-heap-scan'), @@ -33,8 +33,8 @@ for module_name, commands in extensions_to_load: try: - if module_name == 'object': - import object + if module_name == 'print': + import print as print_module elif module_name == 'context': import context elif module_name == 'fiber': @@ -44,8 +44,12 @@ elif module_name == 'heap': import heap loaded_extensions.append((module_name, commands)) - except ImportError as e: + except Exception as e: + # Catch all exceptions during module load failed_extensions.append((module_name, str(e))) + import traceback + print(f"Failed to load {module_name}: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) # Silently load - no status messages printed by default # Users can run 'help' to see available commands diff --git a/data/toolbox/object.py b/data/toolbox/object.py deleted file mode 100644 index 3ea7e89..0000000 --- a/data/toolbox/object.py +++ /dev/null @@ -1,84 +0,0 @@ -import debugger -import sys - -# Import utilities -import command -import constants -import value -import rstring -import rarray -import rhash -import rsymbol -import rstruct -import rfloat -import rbignum -import rbasic -import format - -class RubyObjectPrintCommand(debugger.Command): - """Recursively print Ruby hash and array structures. - Usage: rb-object-print [max_depth] [--debug] - Examples: - rb-object-print $errinfo # Print exception object - rb-object-print $ec->storage # Print fiber storage - rb-object-print 0x7f7a12345678 # Print object at address - rb-object-print $var 2 # Print with max depth 2 - - Default max_depth is 1 if not specified. - Add --debug flag to enable debug output.""" - - def __init__(self): - super(RubyObjectPrintCommand, self).__init__("rb-object-print", debugger.COMMAND_DATA) - - def usage(self): - """Print usage information.""" - print("Usage: rb-object-print [--depth N] [--debug]") - print("Examples:") - print(" rb-object-print $errinfo") - print(" rb-object-print $ec->storage --depth 2") - print(" rb-object-print foo + 10") - print(" rb-object-print $ec->cfp->sp[-1] --depth 3 --debug") - - def invoke(self, argument, from_tty): - # Parse arguments using the robust parser - arguments = command.parse_arguments(argument if argument else "") - - # Validate that we have at least one expression - if not arguments.expressions: - self.usage() - return - - # Apply flags - debug_mode = arguments.has_flag('debug') - - # Apply options - max_depth = arguments.get_option('depth', 1) - - # Validate depth - if max_depth < 1: - print("Error: --depth must be >= 1") - return - - # Create terminal and printer - terminal = format.create_terminal(from_tty) - printer = format.Printer(terminal, max_depth, debug_mode) - - # Process each expression - for expression in arguments.expressions: - try: - # Evaluate the expression - ruby_value = debugger.parse_and_eval(expression) - - # Interpret the value and let it print itself recursively - ruby_object = value.interpret(ruby_value) - ruby_object.print_recursive(printer, max_depth) - except debugger.Error as e: - print(f"Error evaluating expression '{expression}': {e}") - except Exception as e: - print(f"Error processing '{expression}': {type(e).__name__}: {e}") - if debug_mode: - import traceback - traceback.print_exc(file=sys.stderr) - -# Register command -RubyObjectPrintCommand() diff --git a/data/toolbox/print.py b/data/toolbox/print.py new file mode 100644 index 0000000..a65510a --- /dev/null +++ b/data/toolbox/print.py @@ -0,0 +1,79 @@ +"""Print command for Ruby values.""" + +import debugger +import sys + +# Import utilities +import command +import constants +import value as rvalue +import rstring +import rarray +import rhash +import rsymbol +import rstruct +import rfloat +import rbignum +import rbasic +import format + + +class RubyObjectPrinter: + """Print Ruby objects with recursive descent into nested structures.""" + + USAGE = command.Usage( + summary="Print Ruby objects with recursive inspection", + parameters=[('value', 'VALUE or expression to print')], + options={ + 'depth': (int, 1, 'Maximum recursion depth for nested objects') + }, + flags=[ + ('debug', 'Show internal structure and debug information') + ], + examples=[ + ("rb-print $errinfo", "Print exception object"), + ("rb-print $ec->storage --depth 3", "Print fiber storage with depth 3"), + ("rb-print $ec->cfp->sp[-1] --debug", "Print top of stack with debug info") + ] + ) + + def invoke(self, arguments, terminal): + """Execute the print command. + + Args: + arguments: Parsed Arguments object + terminal: Terminal formatter (already configured for TTY/non-TTY) + """ + # Get options + max_depth = arguments.get_option('depth', 1) + debug_mode = arguments.has_flag('debug') + + # Validate depth + if max_depth < 1: + print("Error: --depth must be >= 1") + return + + # Create printer + printer = format.Printer(terminal, max_depth, debug_mode) + + # Process each expression + for expression in arguments.expressions: + try: + # Evaluate the expression + ruby_value = debugger.parse_and_eval(expression) + + # Interpret the value and let it print itself recursively + ruby_object = rvalue.interpret(ruby_value) + ruby_object.print_recursive(printer, max_depth) + except debugger.Error as e: + print(f"Error evaluating expression '{expression}': {e}") + except Exception as e: + print(f"Error processing '{expression}': {type(e).__name__}: {e}") + if debug_mode: + import traceback + traceback.print_exc(file=sys.stderr) + + +# Register command using new interface +debugger.register("rb-print", RubyObjectPrinter, usage=RubyObjectPrinter.USAGE) + diff --git a/data/toolbox/rarray.py b/data/toolbox/rarray.py index 04a01bb..2ae8004 100644 --- a/data/toolbox/rarray.py +++ b/data/toolbox/rarray.py @@ -77,12 +77,8 @@ def __str__(self): def print_to(self, terminal): """Print this array with formatting.""" addr = int(self.value) - return terminal.print( - format.metadata, '<', - format.type, 'T_ARRAY', - format.metadata, f'@0x{addr:x} embedded length={len(self)}>', - format.reset - ) + details = f"embedded length={len(self)}" + terminal.print_type_tag('T_ARRAY', addr, details) class RArrayHeap(RArrayBase): """Heap array (larger arrays with separate memory allocation).""" @@ -101,12 +97,8 @@ def __str__(self): def print_to(self, terminal): """Print this array with formatting.""" addr = int(self.value) - return terminal.print( - format.metadata, '<', - format.type, 'T_ARRAY', - format.metadata, f'@0x{addr:x} heap length={len(self)}>', - format.reset - ) + details = f"heap length={len(self)}" + terminal.print_type_tag('T_ARRAY', addr, details) def RArray(value): """Factory function that returns the appropriate RArray variant. diff --git a/data/toolbox/rbasic.py b/data/toolbox/rbasic.py index 4b13dcd..e113f29 100644 --- a/data/toolbox/rbasic.py +++ b/data/toolbox/rbasic.py @@ -88,15 +88,13 @@ def __str__(self): return f"<{type_str}:0x{int(self.value):x}>" def print_to(self, terminal): - """Return formatted basic object representation.""" + """Print formatted basic object representation.""" type_str = type_name(self.value) addr = int(self.value) - return terminal.print( - format.metadata, '<', - format.type, type_str, - format.metadata, f':0x{addr:x}>', - format.reset - ) + # Note: Using : instead of @ for basic objects + import format as fmt + terminal.print(fmt.metadata, '<', fmt.reset, fmt.type, type_str, fmt.reset, end='') + terminal.print(fmt.metadata, f':0x{addr:x}>', fmt.reset, end='') def print_recursive(self, printer, depth): """Print this basic object (no recursion).""" diff --git a/data/toolbox/rbignum.py b/data/toolbox/rbignum.py index 363f04f..fb48048 100644 --- a/data/toolbox/rbignum.py +++ b/data/toolbox/rbignum.py @@ -31,15 +31,11 @@ def __str__(self): return f"" def print_to(self, terminal): - """Return formatted bignum representation.""" + """Print formatted bignum representation.""" addr = int(self.value) storage = "embedded" if self.is_embedded() else "heap" - return terminal.print( - format.metadata, '<', - format.type, 'T_BIGNUM', - format.metadata, f'@0x{addr:x} {storage} length={len(self)}>', - format.reset - ) + details = f"{storage} length={len(self)}" + terminal.print_type_tag('T_BIGNUM', addr, details) def print_recursive(self, printer, depth): """Print this bignum (no recursion needed).""" diff --git a/data/toolbox/rclass.py b/data/toolbox/rclass.py index dc88435..df8d975 100644 --- a/data/toolbox/rclass.py +++ b/data/toolbox/rclass.py @@ -1,6 +1,6 @@ import debugger import constants -import value +import value as rvalue import rstring class RClass: @@ -103,9 +103,9 @@ def _get_class_name(self): except: classpath_val = None - if classpath_val and int(classpath_val) != 0 and not value.is_nil(classpath_val): + if classpath_val and int(classpath_val) != 0 and not rvalue.is_nil(classpath_val): # Decode the classpath string - class_name_obj = value.interpret(classpath_val) + class_name_obj = rvalue.interpret(classpath_val) if hasattr(class_name_obj, 'to_str'): class_name = class_name_obj.to_str() if class_name and not class_name.startswith('<'): diff --git a/data/toolbox/readme.md b/data/toolbox/readme.md index e535f18..a8e5e3f 100644 --- a/data/toolbox/readme.md +++ b/data/toolbox/readme.md @@ -14,7 +14,7 @@ data/toolbox/ │ └── *.md # Documentation │ # Ruby debugging extensions (currently GDB-specific, migrating to abstraction) -├── object.py # Object inspection (rb-object-print) +├── object.py # Object inspection (rb-print) ├── fiber.py # Fiber debugging (rb-fiber-scan-heap, rb-fiber-switch) ├── heap.py # Heap scanning (rb-heap-scan) ├── stack.py # Stack inspection @@ -113,7 +113,7 @@ command script import /path/to/gem/data/toolbox/init.py ## Available Commands ### Object Inspection -- `rb-object-print [--depth N]` - Print Ruby objects with recursion +- `rb-print [--depth N]` - Print Ruby objects with recursion ### Fiber Debugging - `rb-fiber-scan-heap [--limit N]` - Scan heap for fiber objects @@ -151,14 +151,14 @@ command script import /path/to/gem/data/toolbox/init.py Test with GDB: ```bash gdb -q ruby -(gdb) help rb-object-print +(gdb) help rb-print (gdb) help rb-fiber-scan-heap ``` Test with LLDB (once migrated): ```bash lldb ruby -(lldb) help rb-object-print +(lldb) help rb-print (lldb) help rb-fiber-scan-heap ``` @@ -192,7 +192,7 @@ if toolbox_dir not in sys.path: This allows: ```python import debugger # data/toolbox/debugger.py -import object # data/toolbox/object.py +import print # data/toolbox/print.py import fiber # data/toolbox/fiber.py from debugger import gdb # data/toolbox/debugger/gdb.py ``` diff --git a/data/toolbox/rexception.py b/data/toolbox/rexception.py index 68b7ed4..08e7f8b 100644 --- a/data/toolbox/rexception.py +++ b/data/toolbox/rexception.py @@ -1,7 +1,7 @@ import debugger import constants import format -import value +import value as rvalue import rstring import rclass @@ -21,7 +21,7 @@ def __init__(self, exception_value): self._message = None # Validate it's an object - if value.is_immediate(exception_value): + if rvalue.is_immediate(exception_value): raise ValueError("Exception VALUE cannot be an immediate value") @property @@ -116,7 +116,7 @@ def is_exception(val): Returns: True if the value appears to be an exception object, False otherwise """ - if not value.is_object(val): + if not rvalue.is_object(val): return False try: diff --git a/data/toolbox/rfloat.py b/data/toolbox/rfloat.py index c3bc675..19f3c37 100644 --- a/data/toolbox/rfloat.py +++ b/data/toolbox/rfloat.py @@ -32,15 +32,10 @@ def __str__(self): return f" {self.float_value()}" def print_to(self, terminal): - """Return formatted float representation.""" - tag = terminal.print( - format.metadata, '<', - format.type, 'T_FLOAT', - format.metadata, '>', - format.reset - ) - num_val = terminal.print(format.number, str(self.float_value()), format.reset) - return f"{tag} {num_val}" + """Print formatted float representation.""" + terminal.print_type_tag('T_FLOAT') + terminal.print(' ', end='') + terminal.print(format.number, str(self.float_value()), format.reset, end='') def print_recursive(self, printer, depth): """Print this float (no recursion needed).""" @@ -59,16 +54,11 @@ def __str__(self): return f" {self.float_value()}" def print_to(self, terminal): - """Return formatted float representation.""" + """Print formatted float representation.""" addr = int(self.value.address) - tag = terminal.print( - format.metadata, '<', - format.type, 'T_FLOAT', - format.metadata, f'@0x{addr:x}>', - format.reset - ) - num_val = terminal.print(format.number, str(self.float_value()), format.reset) - return f"{tag} {num_val}" + terminal.print_type_tag('T_FLOAT', addr) + terminal.print(' ', end='') + terminal.print(format.number, str(self.float_value()), format.reset, end='') def print_recursive(self, printer, depth): """Print this float (no recursion needed).""" diff --git a/data/toolbox/rhash.py b/data/toolbox/rhash.py index eeaf630..1253249 100644 --- a/data/toolbox/rhash.py +++ b/data/toolbox/rhash.py @@ -51,12 +51,8 @@ def __str__(self): def print_to(self, terminal): """Print this hash with formatting.""" addr = int(self.value) - return terminal.print( - format.metadata, '<', - format.type, 'T_HASH', - format.metadata, f'@0x{addr:x} ST-Table entries={self.size()}>', - format.reset - ) + details = f"ST-Table entries={self.size()}" + terminal.print_type_tag('T_HASH', addr, details) def print_recursive(self, printer, depth): """Print this hash recursively.""" @@ -121,12 +117,8 @@ def __str__(self): def print_to(self, terminal): """Print this hash with formatting.""" addr = int(self.value) - return terminal.print( - format.metadata, '<', - format.type, 'T_HASH', - format.metadata, f'@0x{addr:x} AR-Table size={self.size()} bound={self.bound()}>', - format.reset - ) + details = f"AR-Table size={self.size()} bound={self.bound()}" + terminal.print_type_tag('T_HASH', addr, details) def print_recursive(self, printer, depth): """Print this hash recursively.""" diff --git a/data/toolbox/rstring.py b/data/toolbox/rstring.py index 009ee68..d912574 100644 --- a/data/toolbox/rstring.py +++ b/data/toolbox/rstring.py @@ -71,15 +71,11 @@ def print_to(self, terminal): addr = int(self.value) storage = "embedded" if self._is_embedded() else "heap" content = self.to_str() - tag = terminal.print( - format.metadata, '<', - format.type, 'T_STRING', - format.metadata, f'@0x{addr:x} {storage} length={self.length()}>', - format.reset - ) + details = f"{storage} length={self.length()}" + terminal.print_type_tag('T_STRING', addr, details) + terminal.print(' ', end='') # Use repr() to properly escape quotes, newlines, etc. - string_val = terminal.print(format.string, repr(content), format.reset) - return f"{tag} {string_val}" + terminal.print(format.string, repr(content), format.reset, end='') def print_recursive(self, printer, depth): """Print this string (no recursion needed for strings).""" diff --git a/data/toolbox/rstruct.py b/data/toolbox/rstruct.py index a00b640..5bd3723 100644 --- a/data/toolbox/rstruct.py +++ b/data/toolbox/rstruct.py @@ -60,14 +60,10 @@ def __str__(self): return f"" def print_to(self, terminal): - """Return formatted struct representation.""" + """Print formatted struct representation.""" addr = int(self.value) - return terminal.print( - format.metadata, '<', - format.type, 'T_STRUCT', - format.metadata, f'@0x{addr:x} embedded length={len(self)}>', - format.reset - ) + details = f"embedded length={len(self)}" + terminal.print_type_tag('T_STRUCT', addr, details) def print_recursive(self, printer, depth): """Print this struct recursively.""" @@ -101,14 +97,10 @@ def __str__(self): return f"" def print_to(self, terminal): - """Return formatted struct representation.""" + """Print formatted struct representation.""" addr = int(self.value) - return terminal.print( - format.metadata, '<', - format.type, 'T_STRUCT', - format.metadata, f'@0x{addr:x} heap length={len(self)}>', - format.reset - ) + details = f"heap length={len(self)}" + terminal.print_type_tag('T_STRUCT', addr, details) def print_recursive(self, printer, depth): """Print this struct recursively.""" diff --git a/data/toolbox/rsymbol.py b/data/toolbox/rsymbol.py index 113b44a..230f89f 100644 --- a/data/toolbox/rsymbol.py +++ b/data/toolbox/rsymbol.py @@ -138,25 +138,13 @@ def __str__(self): def print_to(self, terminal): """Return formatted symbol representation.""" + terminal.print_type_tag('T_SYMBOL') + terminal.print(' ', end='') name = self.to_str() if name: - tag = terminal.print( - format.metadata, '<', - format.type, 'T_SYMBOL', - format.metadata, '>', - format.reset - ) - symbol_val = terminal.print(format.symbol, f':{name}', format.reset) - return f"{tag} {symbol_val}" + terminal.print(format.symbol, f':{name}', format.reset, end='') else: - tag = terminal.print( - format.metadata, '<', - format.type, 'T_SYMBOL', - format.metadata, '>', - format.reset - ) - symbol_val = terminal.print(format.symbol, f':id_0x{self.id():x}', format.reset) - return f"{tag} {symbol_val}" + terminal.print(format.symbol, f':id_0x{self.id():x}', format.reset, end='') def print_recursive(self, printer, depth): """Print this symbol (no recursion needed).""" @@ -208,28 +196,16 @@ def __str__(self): return f" :" def print_to(self, terminal): - """Return formatted symbol representation.""" + """Print formatted symbol representation.""" name = self.to_str() addr = int(self.value) + terminal.print_type_tag('T_SYMBOL', addr) + terminal.print(' ', end='') if name: - tag = terminal.print( - format.metadata, '<', - format.type, 'T_SYMBOL', - format.metadata, f'@0x{addr:x}>', - format.reset - ) - symbol_val = terminal.print(format.symbol, f':{name}', format.reset) - return f"{tag} {symbol_val}" + terminal.print(format.symbol, f':{name}', format.reset, end='') else: fstr_val = self.fstr() - tag = terminal.print( - format.metadata, '<', - format.type, 'T_SYMBOL', - format.metadata, f'@0x{addr:x}>', - format.reset - ) - symbol_val = terminal.print(format.symbol, f':', format.reset) - return f"{tag} {symbol_val}" + terminal.print(format.symbol, f':', format.reset, end='') def print_recursive(self, printer, depth): """Print this symbol (no recursion needed).""" diff --git a/data/toolbox/stack.py b/data/toolbox/stack.py index b7bf26e..d7d89e4 100644 --- a/data/toolbox/stack.py +++ b/data/toolbox/stack.py @@ -4,628 +4,617 @@ import sys # Import Ruby GDB modules +import command import constants import context import format -import value +import value as rvalue import rstring import rexception import rsymbol def print_fiber_backtrace(fiber_ptr, from_tty=True): - """Print backtrace for a Ruby fiber. - - Args: - fiber_ptr: Fiber struct pointer (rb_fiber_struct *) - from_tty: Whether output is to terminal (for formatting) - """ - printer = RubyStackPrinter() - printer.print_fiber_backtrace(fiber_ptr, from_tty) + """Print backtrace for a Ruby fiber. + + Args: + fiber_ptr: Fiber struct pointer (rb_fiber_struct *) + from_tty: Whether output is to terminal (for formatting) + """ + printer = RubyStackPrinter() + printer.print_fiber_backtrace(fiber_ptr, from_tty) def print_ec_backtrace(ec, from_tty=True): - """Print backtrace for an execution context. - - Args: - ec: Execution context pointer (rb_execution_context_t *) - from_tty: Whether output is to terminal (for formatting) - """ - printer = RubyStackPrinter() - printer.print_backtrace(ec, from_tty) + """Print backtrace for an execution context. + + Args: + ec: Execution context pointer (rb_execution_context_t *) + from_tty: Whether output is to terminal (for formatting) + """ + printer = RubyStackPrinter() + printer.print_backtrace(ec, from_tty) class RubyStackPrinter: - """Helper class for printing Ruby stack traces. - - This class provides the core logic for printing backtraces that can be - used both by commands and programmatically from other modules. - """ - - def __init__(self): - # Cached type lookups - self._rbasic_type = None - self._value_type = None - self._cfp_type = None - self._rstring_type = None - self.show_values = False - self.terminal = None - - def _initialize_types(self): - """Initialize cached type lookups.""" - if self._rbasic_type is None: - self._rbasic_type = constants.type_struct('struct RBasic').pointer() - if self._value_type is None: - self._value_type = constants.type_struct('VALUE') - if self._cfp_type is None: - self._cfp_type = constants.type_struct('rb_control_frame_t').pointer() - if self._rstring_type is None: - self._rstring_type = constants.type_struct('struct RString').pointer() - - def print_fiber_backtrace(self, fiber_ptr, from_tty=True): - """Print backtrace for a Ruby fiber. - - Args: - fiber_ptr: Fiber struct pointer (rb_fiber_struct *) - from_tty: Whether output is to terminal (for formatting) - """ - try: - self._initialize_types() - self.terminal = format.create_terminal(from_tty) - - # Get execution context from fiber - ec = fiber_ptr['cont']['saved_ec'].address - - print(f"Backtrace for fiber {fiber_ptr}:") - self.print_backtrace(ec, from_tty) - - except (debugger.Error, RuntimeError) as e: - print(f"Error printing fiber backtrace: {e}") - - def print_backtrace(self, ec, from_tty=True): - """Print backtrace for an execution context. - - Args: - ec: Execution context pointer (rb_execution_context_t *) - from_tty: Whether output is to terminal (for formatting) - """ - try: - self._initialize_types() - if self.terminal is None: - self.terminal = format.create_terminal(from_tty) - - cfp = ec['cfp'] - vm_stack = ec['vm_stack'] - vm_stack_size = int(ec['vm_stack_size']) - - # Check for exception - errinfo_val = ec['errinfo'] - errinfo_int = int(errinfo_val) - - # Check if it's a real exception object (not nil or other immediate/special value) - if not value.is_immediate(errinfo_val) and not value.is_nil(errinfo_val): - try: - exc_class = self._get_exception_class(errinfo_val) - exc_msg = self._get_exception_message(errinfo_val) - - # Set as GDB convenience variable for manual inspection - debugger.set_convenience_variable('errinfo', errinfo_val) - - if exc_msg: - print(f"Currently handling: {exc_class}: {exc_msg} (VALUE: 0x{errinfo_int:x}, $errinfo)") - else: - print(f"Currently handling: {exc_class} (VALUE: 0x{errinfo_int:x}, $errinfo)") - print() - except: - # Set convenience variable even if we can't decode - debugger.set_convenience_variable('errinfo', errinfo_val) - print(f"Currently handling exception (VALUE: 0x{errinfo_int:x}, $errinfo)") - print() - - # Calculate end of control frames - cfpend = (vm_stack + vm_stack_size).cast(self._cfp_type) - 1 - - frame_num = 0 - current_cfp = cfp - - while current_cfp < cfpend: - try: - self._print_frame(current_cfp, frame_num) - frame_num += 1 - current_cfp += 1 - except (debugger.Error, RuntimeError) as e: - print(f" #{frame_num}: [error reading frame: {e}]") - break - - if frame_num == 0: - print(" (no frames)") - - except (debugger.Error, RuntimeError) as e: - print(f"Error printing backtrace: {e}") - - def _print_frame(self, cfp, depth): - """Print a single control frame. - - Args: - cfp: Control frame pointer (rb_control_frame_t *) - depth: Frame depth/number - """ - iseq = cfp['iseq'] - - if iseq is None or int(iseq) == 0: - # C function frame - try to extract method info from EP - try: - ep = cfp['ep'] - - # Check if this is a valid C frame - if int(ep) != 0: - ep0 = int(ep[0]) - if (ep0 & 0xffff0001) == 0x55550001: - # Valid C frame, try to extract method entry - env_me_cref = ep[-2] - - try: - me_type = constants.type_struct('rb_callable_method_entry_t').pointer() - me = env_me_cref.cast(me_type) - - # Get the C function pointer - cfunc = me['def']['body']['cfunc']['func'] - - # Get the method ID - method_id = me['def']['original_id'] - - # Try to get symbol for the C function - func_addr = int(cfunc) - func_name = debugger.lookup_symbol(func_addr) - if func_name: - func_name = f" ({func_name})" - else: - func_name = f" (0x{func_addr:x})" - - # Print C frame with cyan/dimmed formatting - print(self.terminal.print( - format.metadata, f" #{depth}: ", - format.dim, "[C function", func_name, "]", - format.reset - )) - return - except: - pass - except: - pass - - # Fallback if we couldn't extract info - print(self.terminal.print( - format.metadata, f" #{depth}: ", - format.dim, "[C function or native frame]", - format.reset - )) - return - - pc = cfp['pc'] - - if int(pc) == 0: - print(self.terminal.print( - format.metadata, f" #{depth}: ", - format.error, "???:???:in '???'", - format.reset - )) - return - - # Check if it's an ifunc (internal function) - RUBY_IMMEDIATE_MASK = 0x03 - RUBY_FL_USHIFT = 12 - RUBY_T_IMEMO = 0x1a - RUBY_IMEMO_MASK = 0x0f - - iseq_val = int(iseq.cast(self._value_type)) - if not (iseq_val & RUBY_IMMEDIATE_MASK): - try: - flags = int(iseq['flags']) - expected = ((RUBY_T_IMEMO << RUBY_FL_USHIFT) | RUBY_T_IMEMO) - mask = ((RUBY_IMEMO_MASK << RUBY_FL_USHIFT) | 0x1f) - - if (flags & mask) == expected: - # It's an ifunc - print(self.terminal.print( - format.metadata, f" #{depth}: ", - format.dim, "[ifunc]", - format.reset - )) - return - except: - pass - - try: - # Get location information - body = iseq['body'] - if body is None: - print(self.terminal.print( - format.metadata, f" #{depth}: ", - format.error, "???:???:in '???'", - format.reset - )) - return - - location = body['location'] - if location is None: - print(self.terminal.print( - format.metadata, f" #{depth}: ", - format.error, "???:???:in '???'", - format.reset - )) - return - - pathobj = location['pathobj'] - label = location['label'] - - # Get path string - pathobj can be a string or an array [path, realpath] - path = self._extract_path_from_pathobj(pathobj) - label_str = self._value_to_string(label) - - # Calculate line number - lineno = self._get_lineno(cfp) - - # Print Ruby frame with highlighting - print(self.terminal.print( - format.metadata, f" #{depth}: ", - format.string, path, - format.reset, ":", - format.metadata, str(lineno), - format.reset, ":in '", - format.method, label_str, - format.reset, "'" - )) - - # If --values flag is set, print stack values - if self.show_values: - self._print_stack_values(cfp, iseq) - - except (debugger.Error, RuntimeError) as e: - print(self.terminal.print( - format.metadata, f" #{depth}: ", - format.error, f"[error reading frame info: {e}]", - format.reset - )) - - def _print_stack_values(self, cfp, iseq): - """Print Ruby VALUEs on the control frame's stack pointer. - - Args: - cfp: Control frame pointer (rb_control_frame_t *) - iseq: Instruction sequence pointer - """ - try: - sp = cfp['sp'] - ep = cfp['ep'] - - if int(sp) == 0 or int(ep) == 0: - return - - # Try to get local table information for better labeling - local_names = [] - local_size = 0 - try: - if int(iseq) != 0: - iseq_body = iseq['body'] - local_table_size = int(iseq_body['local_table_size']) - - if local_table_size > 0: - local_size = local_table_size - local_table = iseq_body['local_table'] - - # Read local variable names (they're stored as IDs/symbols) - # Local table is stored in reverse order (last local first) - for i in range(min(local_table_size, 20)): # Cap at 20 - try: - local_id = local_table[local_table_size - 1 - i] - # Try to convert ID to symbol name using RubySymbol - if int(local_id) != 0: - sym = rsymbol.RubySymbol(local_id) - name = sym.to_str() - if name: - local_names.append(name) - else: - local_names.append(f"local_{i}") - else: - local_names.append(f"local_{i}") - except: - local_names.append(f"local_{i}") - except: - pass - - # Environment pointer typically points to the local variable area - # Stack grows downward, so we start from sp and go down - value_ptr = sp - 1 - - # Print a reasonable number of stack values - max_values = 10 - values_printed = 0 - - print(self.terminal.print(format.dim, " Stack values:", format.reset)) - - # Calculate offset from ep to show position - while value_ptr >= ep and values_printed < max_values: - try: - val = value_ptr[0] - val_int = int(val) - - # Calculate offset from ep for labeling - offset = int(value_ptr - ep) - - # Try to determine if this is a local variable - label = f"sp[-{values_printed + 1}]" - if offset < local_size and offset < len(local_names): - label = f"{local_names[offset]} (ep[{offset}])" - - # Try to get a brief representation of the value - val_str = self._format_value_brief(val) - - print(self.terminal.print( - format.metadata, f" {label:20s} ", - format.dim, f"= ", - format.reset, val_str, - format.reset - )) - - values_printed += 1 - value_ptr -= 1 - except (debugger.Error, debugger.MemoryError): - break - - if values_printed == 0: - print(self.terminal.print(format.dim, " (empty stack)", format.reset)) - - except (debugger.Error, RuntimeError) as e: - # Silently skip if we can't read stack values - pass - - def _format_value_brief(self, val): - """Get a brief string representation of a VALUE. - - Args: - val: Ruby VALUE - - Returns: - Brief string description - """ - try: - # Use value.py's interpret function to get the typed object - obj = value.interpret(val) - - # Get string representation - obj_str = str(obj) - - # Truncate if too long - if len(obj_str) > 60: - return obj_str[:57] + "..." - - return obj_str - - except Exception as e: - return f"" - - def _get_lineno(self, cfp): - """Get line number for a control frame. - - Args: - cfp: Control frame pointer - - Returns: - Line number as int or "???" if unavailable - """ - try: - iseq = cfp['iseq'] - pc = cfp['pc'] - - if int(pc) == 0: - return "???" - - iseq_body = iseq['body'] - iseq_encoded = iseq_body['iseq_encoded'] - iseq_size = int(iseq_body['iseq_size']) - - pc_offset = int(pc - iseq_encoded) - - if pc_offset < 0 or pc_offset >= iseq_size: - return "???" - - # Try to get line info - insns_info = iseq_body['insns_info'] - positions = insns_info['positions'] - - if int(positions) != 0: - position = positions[pc_offset] - lineno = int(position['lineno']) - if lineno >= 0: - return lineno - - # Fall back to first_lineno - return int(iseq_body['location']['first_lineno']) - - except: - return "???" - - def _get_exception_class(self, exc_value): - """Get the class name of an exception object. - - Delegates to rexception.RException for proper exception handling. - - Args: - exc_value: Exception VALUE - - Returns: - Class name as string - """ - try: - exc = rexception.RException(exc_value) - return exc.class_name - except Exception: - # Fallback if RException can't be created - try: - rbasic = exc_value.cast(self._rbasic_type) - klass = rbasic['klass'] - return f"Exception(klass=0x{int(klass):x})" - except: - raise - - def _get_exception_message(self, exc_value): - """Get the message from an exception object. - - Delegates to rexception.RException for proper exception handling. - - Args: - exc_value: Exception VALUE - - Returns: - Message string or None if unavailable - """ - try: - exc = rexception.RException(exc_value) - return exc.message - except Exception: - # If RException can't be created, return None - return None - - def _value_to_string(self, val): - """Convert a Ruby VALUE to a Python string. - - Args: - val: Ruby VALUE - - Returns: - String representation - """ - try: - # Use the value.interpret infrastructure for proper type handling - obj = value.interpret(val) - - # For strings, get the actual content - if hasattr(obj, 'to_str'): - return obj.to_str() - - # For immediates and other types, convert to string - obj_str = str(obj) - - # Strip the type tag if present (e.g., " 42" -> "42") - if obj_str.startswith('<'): - # Find the end of the type tag - end_tag = obj_str.find('>') - if end_tag != -1 and end_tag + 2 < len(obj_str): - # Return the part after the tag and space - return obj_str[end_tag + 2:] - - return obj_str - - except Exception as e: - return f"" - - def _extract_path_from_pathobj(self, pathobj): - """Extract file path from pathobj (can be string or array). - - Args: - pathobj: Ruby VALUE (either T_STRING or T_ARRAY) - - Returns: - File path as string - """ - try: - # Interpret the pathobj to get its type - obj = value.interpret(pathobj) - - # If it's an array, get the first element (the path) - if hasattr(obj, 'length') and hasattr(obj, 'get_item'): - if obj.length() > 0: - path_value = obj.get_item(0) - return self._value_to_string(path_value) - - # Otherwise, treat it as a string directly - return self._value_to_string(pathobj) - - except Exception as e: - return f"" + """Helper class for printing Ruby stack traces. + + This class provides the core logic for printing backtraces that can be + used both by commands and programmatically from other modules. + """ + + def __init__(self): + # Cached type lookups + self._rbasic_type = None + self._value_type = None + self._cfp_type = None + self._rstring_type = None + self.show_values = False + self.terminal = None + + def _initialize_types(self): + """Initialize cached type lookups.""" + if self._rbasic_type is None: + self._rbasic_type = constants.type_struct('struct RBasic').pointer() + if self._value_type is None: + self._value_type = constants.type_struct('VALUE') + if self._cfp_type is None: + self._cfp_type = constants.type_struct('rb_control_frame_t').pointer() + if self._rstring_type is None: + self._rstring_type = constants.type_struct('struct RString').pointer() + + def print_fiber_backtrace(self, fiber_ptr, from_tty=True): + """Print backtrace for a Ruby fiber. + + Args: + fiber_ptr: Fiber struct pointer (rb_fiber_struct *) + from_tty: Whether output is to terminal (for formatting) + """ + try: + self._initialize_types() + self.terminal = format.create_terminal(from_tty) + + # Get execution context from fiber + ec = fiber_ptr['cont']['saved_ec'].address + + print(f"Backtrace for fiber {fiber_ptr}:") + self.print_backtrace(ec, from_tty) + + except (debugger.Error, RuntimeError) as e: + print(f"Error printing fiber backtrace: {e}") + + def print_backtrace(self, ec, from_tty=True): + """Print backtrace for an execution context. + + Args: + ec: Execution context pointer (rb_execution_context_t *) + from_tty: Whether output is to terminal (for formatting) + """ + try: + self._initialize_types() + if self.terminal is None: + self.terminal = format.create_terminal(from_tty) + + cfp = ec['cfp'] + vm_stack = ec['vm_stack'] + vm_stack_size = int(ec['vm_stack_size']) + + # Check for exception + errinfo_val = ec['errinfo'] + errinfo_int = int(errinfo_val) + + # Check if it's a real exception object (not nil or other immediate/special value) + if not rvalue.is_immediate(errinfo_val) and not rvalue.is_nil(errinfo_val): + try: + exc_class = self._get_exception_class(errinfo_val) + exc_msg = self._get_exception_message(errinfo_val) + + # Set as GDB convenience variable for manual inspection + debugger.set_convenience_variable('errinfo', errinfo_val) + + if exc_msg: + print(f"Currently handling: {exc_class}: {exc_msg} (VALUE: 0x{errinfo_int:x}, $errinfo)") + else: + print(f"Currently handling: {exc_class} (VALUE: 0x{errinfo_int:x}, $errinfo)") + print() + except: + # Set convenience variable even if we can't decode + debugger.set_convenience_variable('errinfo', errinfo_val) + print(f"Currently handling exception (VALUE: 0x{errinfo_int:x}, $errinfo)") + print() + + # Calculate end of control frames + cfpend = (vm_stack + vm_stack_size).cast(self._cfp_type) - 1 + + frame_num = 0 + current_cfp = cfp + + while current_cfp < cfpend: + try: + self._print_frame(current_cfp, frame_num) + frame_num += 1 + current_cfp += 1 + except (debugger.Error, RuntimeError) as e: + print(f" #{frame_num}: [error reading frame: {e}]") + break + + if frame_num == 0: + print(" (no frames)") + + except (debugger.Error, RuntimeError) as e: + print(f"Error printing backtrace: {e}") + + def _print_frame(self, cfp, depth): + """Print a single control frame. + + Args: + cfp: Control frame pointer (rb_control_frame_t *) + depth: Frame depth/number + """ + iseq = cfp['iseq'] + + if iseq is None or int(iseq) == 0: + # C function frame - try to extract method info from EP + try: + ep = cfp['ep'] + + # Check if this is a valid C frame + if int(ep) != 0: + ep0 = int(ep[0]) + if (ep0 & 0xffff0001) == 0x55550001: + # Valid C frame, try to extract method entry + env_me_cref = ep[-2] + + try: + me_type = constants.type_struct('rb_callable_method_entry_t').pointer() + me = env_me_cref.cast(me_type) + + # Get the C function pointer + cfunc = me['def']['body']['cfunc']['func'] + + # Get the method ID + method_id = me['def']['original_id'] + + # Try to get symbol for the C function + func_addr = int(cfunc) + func_name = debugger.lookup_symbol(func_addr) + if func_name: + func_name = f" ({func_name})" + else: + func_name = f" (0x{func_addr:x})" + + # Print C frame with cyan/dimmed formatting + self.terminal.print( + format.metadata, f" #{depth}: ", + format.dim, "[C function", func_name, "]", + format.reset + ) + return + except: + pass + except: + pass + + # Fallback if we couldn't extract info + self.terminal.print( + format.metadata, f" #{depth}: ", + format.dim, "[C function or native frame]", + format.reset + ) + return + + pc = cfp['pc'] + + if int(pc) == 0: + self.terminal.print( + format.metadata, f" #{depth}: ", + format.error, "???:???:in '???'", + format.reset + ) + return + + # Check if it's an ifunc (internal function) + RUBY_IMMEDIATE_MASK = 0x03 + RUBY_FL_USHIFT = 12 + RUBY_T_IMEMO = 0x1a + RUBY_IMEMO_MASK = 0x0f + + iseq_val = int(iseq.cast(self._value_type)) + if not (iseq_val & RUBY_IMMEDIATE_MASK): + try: + flags = int(iseq['flags']) + expected = ((RUBY_T_IMEMO << RUBY_FL_USHIFT) | RUBY_T_IMEMO) + mask = ((RUBY_IMEMO_MASK << RUBY_FL_USHIFT) | 0x1f) + + if (flags & mask) == expected: + # It's an ifunc + self.terminal.print( + format.metadata, f" #{depth}: ", + format.dim, "[ifunc]", + format.reset + ) + return + except: + pass + + try: + # Get location information + body = iseq['body'] + if body is None: + self.terminal.print( + format.metadata, f" #{depth}: ", + format.error, "???:???:in '???'", + format.reset + ) + return + + location = body['location'] + if location is None: + self.terminal.print( + format.metadata, f" #{depth}: ", + format.error, "???:???:in '???'", + format.reset + ) + return + + pathobj = location['pathobj'] + label = location['label'] + + # Get path string - pathobj can be a string or an array [path, realpath] + path = self._extract_path_from_pathobj(pathobj) + label_str = self._value_to_string(label) + + # Calculate line number + lineno = self._get_lineno(cfp) + + # Print Ruby frame with highlighting + self.terminal.print( + format.metadata, f" #{depth}: ", + format.string, path, + format.reset, ":", + format.metadata, str(lineno), + format.reset, ":in '", + format.method, label_str, + format.reset, "'" + ) + + # If --values flag is set, print stack values + if self.show_values: + self._print_stack_values(cfp, iseq) + + except (debugger.Error, RuntimeError) as e: + self.terminal.print( + format.metadata, f" #{depth}: ", + format.error, f"[error reading frame info: {e}]", + format.reset + ) + + def _print_stack_values(self, cfp, iseq): + """Print Ruby VALUEs on the control frame's stack pointer. + + Args: + cfp: Control frame pointer (rb_control_frame_t *) + iseq: Instruction sequence pointer + """ + try: + sp = cfp['sp'] + ep = cfp['ep'] + + if int(sp) == 0 or int(ep) == 0: + return + + # Try to get local table information for better labeling + local_names = [] + local_size = 0 + try: + if int(iseq) != 0: + iseq_body = iseq['body'] + local_table_size = int(iseq_body['local_table_size']) + + if local_table_size > 0: + local_size = local_table_size + local_table = iseq_body['local_table'] + + # Read local variable names (they're stored as IDs/symbols) + # Local table is stored in reverse order (last local first) + for i in range(min(local_table_size, 20)): # Cap at 20 + try: + local_id = local_table[local_table_size - 1 - i] + # Try to convert ID to symbol name using RubySymbol + if int(local_id) != 0: + sym = rsymbol.RubySymbol(local_id) + name = sym.to_str() + if name: + local_names.append(name) + else: + local_names.append(f"local_{i}") + else: + local_names.append(f"local_{i}") + except: + local_names.append(f"local_{i}") + except: + pass + + # Environment pointer typically points to the local variable area + # Stack grows downward, so we start from sp and go down + value_ptr = sp - 1 + + # Print a reasonable number of stack values + max_values = 10 + values_printed = 0 + + self.terminal.print(format.dim, " Stack values:", format.reset) + + # Calculate offset from ep to show position + while value_ptr >= ep and values_printed < max_values: + try: + val = value_ptr[0] + val_int = int(val) + + # Calculate offset from ep for labeling + offset = int(value_ptr - ep) + + # Try to determine if this is a local variable + label = f"sp[-{values_printed + 1}]" + if offset < local_size and offset < len(local_names): + label = f"{local_names[offset]} (ep[{offset}])" + + # Try to get a brief representation of the value + val_str = self._format_value_brief(val) + + self.terminal.print( + format.metadata, f" {label:20s} ", + format.dim, f"= ", + format.reset, val_str, + format.reset + ) + + values_printed += 1 + value_ptr -= 1 + except (debugger.Error, debugger.MemoryError): + break + + if values_printed == 0: + self.terminal.print(format.dim, " (empty stack)", format.reset) + + except (debugger.Error, RuntimeError) as e: + # Silently skip if we can't read stack values + pass + + def _format_value_brief(self, val): + """Get a brief string representation of a VALUE. + + Args: + val: Ruby VALUE + + Returns: + Brief string description + """ + try: + # Use value.py's interpret function to get the typed object + obj = rvalue.interpret(val) + + # Get string representation + obj_str = str(obj) + + # Truncate if too long + if len(obj_str) > 60: + return obj_str[:57] + "..." + + return obj_str + + except Exception as e: + return f"" + + def _get_lineno(self, cfp): + """Get line number for a control frame. + + Args: + cfp: Control frame pointer + + Returns: + Line number as int or "???" if unavailable + """ + try: + iseq = cfp['iseq'] + pc = cfp['pc'] + + if int(pc) == 0: + return "???" + + iseq_body = iseq['body'] + iseq_encoded = iseq_body['iseq_encoded'] + iseq_size = int(iseq_body['iseq_size']) + + pc_offset = int(pc - iseq_encoded) + + if pc_offset < 0 or pc_offset >= iseq_size: + return "???" + + # Try to get line info + insns_info = iseq_body['insns_info'] + positions = insns_info['positions'] + + if int(positions) != 0: + position = positions[pc_offset] + lineno = int(position['lineno']) + if lineno >= 0: + return lineno + + # Fall back to first_lineno + return int(iseq_body['location']['first_lineno']) + + except: + return "???" + + def _get_exception_class(self, exc_value): + """Get the class name of an exception object. + + Delegates to rexception.RException for proper exception handling. + + Args: + exc_value: Exception VALUE + + Returns: + Class name as string + """ + try: + exc = rexception.RException(exc_value) + return exc.class_name + except Exception: + # Fallback if RException can't be created + try: + rbasic = exc_value.cast(self._rbasic_type) + klass = rbasic['klass'] + return f"Exception(klass=0x{int(klass):x})" + except: + raise + + def _get_exception_message(self, exc_value): + """Get the message from an exception object. + + Delegates to rexception.RException for proper exception handling. + + Args: + exc_value: Exception VALUE + + Returns: + Message string or None if unavailable + """ + try: + exc = rexception.RException(exc_value) + return exc.message + except Exception: + # If RException can't be created, return None + return None + + def _value_to_string(self, val): + """Convert a Ruby VALUE to a Python string. + + Args: + val: Ruby VALUE + + Returns: + String representation + """ + try: + # Use the rvalue.interpret infrastructure for proper type handling + obj = rvalue.interpret(val) + + # For strings, get the actual content + if hasattr(obj, 'to_str'): + return obj.to_str() + + # For immediates and other types, convert to string + obj_str = str(obj) + + # Strip the type tag if present (e.g., " 42" -> "42") + if obj_str.startswith('<'): + # Find the end of the type tag + end_tag = obj_str.find('>') + if end_tag != -1 and end_tag + 2 < len(obj_str): + # Return the part after the tag and space + return obj_str[end_tag + 2:] + + return obj_str + + except Exception as e: + return f"" + + def _extract_path_from_pathobj(self, pathobj): + """Extract file path from pathobj (can be string or array). + + Args: + pathobj: Ruby VALUE (either T_STRING or T_ARRAY) + + Returns: + File path as string + """ + try: + # Interpret the pathobj to get its type + obj = rvalue.interpret(pathobj) + + # If it's an array, get the first element (the path) + if hasattr(obj, 'length') and hasattr(obj, 'get_item'): + if obj.length() > 0: + path_value = obj.get_item(0) + return self._value_to_string(path_value) + + # Otherwise, treat it as a string directly + return self._value_to_string(pathobj) + + except Exception as e: + return f"" -class RubyStackTraceCommand(debugger.Command): - """Print combined C and Ruby backtrace for current fiber or thread. - - Usage: rb-stack-trace [--values] - - Shows backtrace for: - - Currently selected fiber (if rb-fiber-switch was used) - - Current thread execution context (if no fiber selected) - - Options: - --values Show all Ruby VALUEs on each frame's stack pointer - - The output shows both C frames and Ruby frames intermixed, - giving a complete picture of the call stack. - """ - - def __init__(self): - super(RubyStackTraceCommand, self).__init__("rb-stack-trace", debugger.COMMAND_USER) - self.printer = RubyStackPrinter() - - def usage(self): - """Print usage information.""" - print("Usage: rb-stack-trace [--values]") - print("Examples:") - print(" rb-stack-trace # Show backtrace for current fiber/thread") - print(" rb-stack-trace --values # Show backtrace with stack VALUEs") - - - def invoke(self, arg, from_tty): - """Execute the stack trace command.""" - try: - # Parse arguments - import command - arguments = command.parse_arguments(arg if arg else "") - self.printer.show_values = arguments.has_flag('values') - - # Create terminal for formatting - self.printer.terminal = format.create_terminal(from_tty) - - # Check if a fiber is currently selected - # Import here to avoid circular dependency - import fiber - current_fiber = fiber.get_current_fiber() - - if current_fiber: - # Use the selected fiber's execution context - print(f"Stack trace for selected fiber:") - print(f" Fiber: ", end='') - print(self.printer.terminal.print_type_tag('T_DATA', int(current_fiber.value), None)) - print() - - ec = current_fiber.pointer['cont']['saved_ec'].address - self.printer.print_backtrace(ec, from_tty) - else: - # Use current thread's execution context - print("Stack trace for current thread:") - print() - - try: - ctx = context.RubyContext.current() - - if ctx is None: - print("Error: No execution context available") - print("Either select a fiber with 'rb-fiber-switch' or ensure Ruby is running") - print("\nTroubleshooting:") - print(" - Check if Ruby symbols are loaded") - print(" - Ensure the process is stopped at a Ruby frame") - return - - self.printer.print_backtrace(ctx.ec, from_tty) - except debugger.Error as e: - print(f"Error getting execution context: {e}") - print("Try selecting a fiber first with 'rb-fiber-switch'") - return - - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() +class RubyStackTraceHandler: + """Print combined C and Ruby backtrace for current fiber or thread.""" + + USAGE = command.Usage( + summary="Print combined C and Ruby backtrace", + parameters=[], + options={}, + flags=[('values', 'Show stack VALUEs in addition to backtrace')], + examples=[ + ("rb-stack-trace", "Show backtrace for current fiber/thread"), + ("rb-stack-trace --values", "Show backtrace with stack VALUEs") + ] + ) + + def __init__(self): + self.printer = RubyStackPrinter() + + def invoke(self, arguments, terminal): + """Execute the stack trace command.""" + try: + # Get flags + self.printer.show_values = arguments.has_flag('values') + + # Set terminal for formatting + self.printer.terminal = terminal + + # Check if a fiber is currently selected + # Import here to avoid circular dependency + import fiber + current_fiber = fiber.get_current_fiber() + + if current_fiber: + # Use the selected fiber's execution context + print(f"Stack trace for selected fiber:") + print(f" Fiber: ", end='') + self.printer.terminal.print_type_tag('T_DATA', int(current_fiber.value), None) + print() + print() + + ec = current_fiber.pointer['cont']['saved_ec'].address + self.printer.print_backtrace(ec, True) + else: + # Use current thread's execution context + print("Stack trace for current thread:") + print() + + try: + ctx = context.RubyContext.current() + + if ctx is None: + print("Error: No execution context available") + print("Either select a fiber with 'rb-fiber-switch' or ensure Ruby is running") + print("\nTroubleshooting:") + print(" - Check if Ruby symbols are loaded") + print(" - Ensure the process is stopped at a Ruby frame") + return + + self.printer.print_backtrace(ctx.ec, True) + except debugger.Error as e: + print(f"Error getting execution context: {e}") + print("Try selecting a fiber first with 'rb-fiber-switch'") + return + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() # Register commands -RubyStackTraceCommand() +debugger.register("rb-stack-trace", RubyStackTraceHandler, usage=RubyStackTraceHandler.USAGE) diff --git a/data/toolbox/value.py b/data/toolbox/value.py index cc2abf4..d73f086 100644 --- a/data/toolbox/value.py +++ b/data/toolbox/value.py @@ -1,7 +1,8 @@ -import debugger +import sys import constants import format + import rbasic import rfloat import rsymbol @@ -35,19 +36,19 @@ def __str__(self): def print_to(self, terminal): """Print this value with formatting to the given terminal.""" if self.val_int == 0: - return terminal.print(format.metadata, '<', format.type, 'T_FALSE', format.metadata, '>', format.reset) + terminal.print_type_tag('T_FALSE') elif self.val_int == 0x04 or self.val_int == 0x08: - return terminal.print(format.metadata, '<', format.type, 'T_NIL', format.metadata, '>', format.reset) + terminal.print_type_tag('T_NIL') elif self.val_int == 0x14: - return terminal.print(format.metadata, '<', format.type, 'T_TRUE', format.metadata, '>', format.reset) + terminal.print_type_tag('T_TRUE') elif (self.val_int & 0x01) != 0: # Fixnum - shift right to get actual value - tag = terminal.print(format.metadata, '<', format.type, 'T_FIXNUM', format.metadata, '>', format.reset) - num = terminal.print(format.number, str(self.val_int >> 1), format.reset) - return f"{tag} {num}" + terminal.print_type_tag('T_FIXNUM') + terminal.print(' ', end='') + terminal.print(format.number, str(self.val_int >> 1), format.reset, end='') else: # Unknown immediate - return f"" + terminal.print_type_tag('Immediate', self.val_int) def print_recursive(self, printer, depth): """Print this immediate value (no recursion needed).""" @@ -138,7 +139,6 @@ def interpret(value): Returns: An instance of the appropriate type class (never None) """ - import sys val_int = int(value) # Check for immediate flonum (must be before fixnum check) diff --git a/fixtures/toolbox/gdb/fiber/can_use_cache.gdb b/fixtures/toolbox/gdb/fiber/can_use_cache.gdb index ca4703b..eb328c7 100644 --- a/fixtures/toolbox/gdb/fiber/can_use_cache.gdb +++ b/fixtures/toolbox/gdb/fiber/can_use_cache.gdb @@ -18,8 +18,6 @@ run echo ===TOOLBOX-OUTPUT-START===\n rb-fiber-scan-heap --cache test_fibers.json echo \n -shell cat test_fibers.json -shell rm test_fibers.json echo ===TOOLBOX-OUTPUT-END===\n quit diff --git a/fixtures/toolbox/gdb/object/can_handle_bignum.gdb b/fixtures/toolbox/gdb/object/can_handle_bignum.gdb index 2b453f9..aaed6af 100644 --- a/fixtures/toolbox/gdb/object/can_handle_bignum.gdb +++ b/fixtures/toolbox/gdb/object/can_handle_bignum.gdb @@ -8,6 +8,6 @@ break ruby_process_options run set $value = rb_eval_string("[123456789012345678901234567890, -999999999999999999999999999999]") echo ===TOOLBOX-OUTPUT-START===\n -rb-object-print $value +rb-print $value echo ===TOOLBOX-OUTPUT-END===\n quit diff --git a/fixtures/toolbox/gdb/object/can_handle_debug_flag.gdb b/fixtures/toolbox/gdb/object/can_handle_debug_flag.gdb index 204a4a2..be5216a 100644 --- a/fixtures/toolbox/gdb/object/can_handle_debug_flag.gdb +++ b/fixtures/toolbox/gdb/object/can_handle_debug_flag.gdb @@ -2,6 +2,6 @@ source data/toolbox/init.py -rb-object-print 85 --debug +rb-print 85 --debug quit diff --git a/fixtures/toolbox/gdb/object/can_handle_depth_flag.gdb b/fixtures/toolbox/gdb/object/can_handle_depth_flag.gdb index 6f99d76..a2b30c1 100644 --- a/fixtures/toolbox/gdb/object/can_handle_depth_flag.gdb +++ b/fixtures/toolbox/gdb/object/can_handle_depth_flag.gdb @@ -2,6 +2,6 @@ source data/toolbox/init.py -rb-object-print 85 --depth 3 +rb-print 85 --depth 3 quit diff --git a/fixtures/toolbox/gdb/object/can_handle_float.gdb b/fixtures/toolbox/gdb/object/can_handle_float.gdb index 1b34492..8e335cf 100644 --- a/fixtures/toolbox/gdb/object/can_handle_float.gdb +++ b/fixtures/toolbox/gdb/object/can_handle_float.gdb @@ -8,6 +8,6 @@ break ruby_process_options run set $value = rb_eval_string("[3.14, -2.718, 0.0]") echo ===TOOLBOX-OUTPUT-START===\n -rb-object-print $value +rb-print $value echo ===TOOLBOX-OUTPUT-END===\n quit diff --git a/fixtures/toolbox/gdb/object/can_print_fixnum_42.gdb b/fixtures/toolbox/gdb/object/can_print_fixnum_42.gdb index 5ca210b..a4b7336 100644 --- a/fixtures/toolbox/gdb/object/can_print_fixnum_42.gdb +++ b/fixtures/toolbox/gdb/object/can_print_fixnum_42.gdb @@ -2,6 +2,6 @@ source data/toolbox/init.py -rb-object-print 85 +rb-print 85 quit diff --git a/fixtures/toolbox/gdb/object/can_print_hash.gdb b/fixtures/toolbox/gdb/object/can_print_hash.gdb index 4a6c93a..be50a00 100644 --- a/fixtures/toolbox/gdb/object/can_print_hash.gdb +++ b/fixtures/toolbox/gdb/object/can_print_hash.gdb @@ -21,7 +21,7 @@ set $hash = argv[0] # Print the hash echo ===TOOLBOX-OUTPUT-START===\n -rb-object-print $hash +rb-print $hash echo ===TOOLBOX-OUTPUT-END===\n quit diff --git a/fixtures/toolbox/gdb/object/can_print_long_string.gdb b/fixtures/toolbox/gdb/object/can_print_long_string.gdb index 2ca29e8..6df7e0e 100644 --- a/fixtures/toolbox/gdb/object/can_print_long_string.gdb +++ b/fixtures/toolbox/gdb/object/can_print_long_string.gdb @@ -8,6 +8,6 @@ break ruby_process_options run set $value = rb_eval_string("'This is a much longer string that will be heap allocated instead of embedded in the RString structure'") echo ===TOOLBOX-OUTPUT-START===\n -rb-object-print $value +rb-print $value echo ===TOOLBOX-OUTPUT-END===\n quit diff --git a/fixtures/toolbox/gdb/object/can_print_qfalse.gdb b/fixtures/toolbox/gdb/object/can_print_qfalse.gdb index 8763af4..44690da 100644 --- a/fixtures/toolbox/gdb/object/can_print_qfalse.gdb +++ b/fixtures/toolbox/gdb/object/can_print_qfalse.gdb @@ -2,6 +2,6 @@ source data/toolbox/init.py -rb-object-print 0 +rb-print 0 quit diff --git a/fixtures/toolbox/gdb/object/can_print_qnil.gdb b/fixtures/toolbox/gdb/object/can_print_qnil.gdb index 10963de..a095166 100644 --- a/fixtures/toolbox/gdb/object/can_print_qnil.gdb +++ b/fixtures/toolbox/gdb/object/can_print_qnil.gdb @@ -2,6 +2,6 @@ source data/toolbox/init.py -rb-object-print 8 +rb-print 8 quit diff --git a/fixtures/toolbox/gdb/object/can_print_qtrue.gdb b/fixtures/toolbox/gdb/object/can_print_qtrue.gdb index 2b8bd81..b14b4b3 100644 --- a/fixtures/toolbox/gdb/object/can_print_qtrue.gdb +++ b/fixtures/toolbox/gdb/object/can_print_qtrue.gdb @@ -2,6 +2,6 @@ source data/toolbox/init.py -rb-object-print 20 +rb-print 20 quit diff --git a/fixtures/toolbox/gdb/object/can_print_short_string.gdb b/fixtures/toolbox/gdb/object/can_print_short_string.gdb index 8916c73..64ec1c7 100644 --- a/fixtures/toolbox/gdb/object/can_print_short_string.gdb +++ b/fixtures/toolbox/gdb/object/can_print_short_string.gdb @@ -8,6 +8,6 @@ break ruby_process_options run set $value = rb_eval_string("'Hello'") echo ===TOOLBOX-OUTPUT-START===\n -rb-object-print $value +rb-print $value echo ===TOOLBOX-OUTPUT-END===\n quit diff --git a/fixtures/toolbox/gdb/object/can_print_short_symbol.gdb b/fixtures/toolbox/gdb/object/can_print_short_symbol.gdb index 48e39f9..9b2eaa6 100644 --- a/fixtures/toolbox/gdb/object/can_print_short_symbol.gdb +++ b/fixtures/toolbox/gdb/object/can_print_short_symbol.gdb @@ -8,6 +8,6 @@ break ruby_process_options run set $value = rb_eval_string(":hello") echo ===TOOLBOX-OUTPUT-START===\n -rb-object-print $value +rb-print $value echo ===TOOLBOX-OUTPUT-END===\n quit diff --git a/fixtures/toolbox/lldb/object/can_handle_depth_flag.lldb b/fixtures/toolbox/lldb/object/can_handle_depth_flag.lldb index 596d834..d5f3639 100644 --- a/fixtures/toolbox/lldb/object/can_handle_depth_flag.lldb +++ b/fixtures/toolbox/lldb/object/can_handle_depth_flag.lldb @@ -3,7 +3,7 @@ command script import data/toolbox/init.py script print("===TOOLBOX-OUTPUT-START===") -rb-object-print 85 --depth 3 +rb-print 85 --depth 3 script print("===TOOLBOX-OUTPUT-END===") quit diff --git a/fixtures/toolbox/lldb/object/can_print_fixnum_42.lldb b/fixtures/toolbox/lldb/object/can_print_fixnum_42.lldb index cd1be88..c0d7a0d 100644 --- a/fixtures/toolbox/lldb/object/can_print_fixnum_42.lldb +++ b/fixtures/toolbox/lldb/object/can_print_fixnum_42.lldb @@ -1,7 +1,7 @@ command script import data/toolbox/init.py script print("===TOOLBOX-OUTPUT-START===") -rb-object-print 85 +rb-print 85 script print("===TOOLBOX-OUTPUT-END===") quit diff --git a/fixtures/toolbox/lldb/object/can_print_qfalse.lldb b/fixtures/toolbox/lldb/object/can_print_qfalse.lldb index 8ea97b9..f177ca6 100644 --- a/fixtures/toolbox/lldb/object/can_print_qfalse.lldb +++ b/fixtures/toolbox/lldb/object/can_print_qfalse.lldb @@ -3,7 +3,7 @@ command script import data/toolbox/init.py script print("===TOOLBOX-OUTPUT-START===") -rb-object-print 0 +rb-print 0 script print("===TOOLBOX-OUTPUT-END===") quit diff --git a/fixtures/toolbox/lldb/object/can_print_qnil.lldb b/fixtures/toolbox/lldb/object/can_print_qnil.lldb index 6c74b1f..362d8d2 100644 --- a/fixtures/toolbox/lldb/object/can_print_qnil.lldb +++ b/fixtures/toolbox/lldb/object/can_print_qnil.lldb @@ -3,7 +3,7 @@ command script import data/toolbox/init.py script print("===TOOLBOX-OUTPUT-START===") -rb-object-print 8 +rb-print 8 script print("===TOOLBOX-OUTPUT-END===") quit diff --git a/fixtures/toolbox/lldb/object/can_print_qtrue.lldb b/fixtures/toolbox/lldb/object/can_print_qtrue.lldb index 881a055..10df85c 100644 --- a/fixtures/toolbox/lldb/object/can_print_qtrue.lldb +++ b/fixtures/toolbox/lldb/object/can_print_qtrue.lldb @@ -3,7 +3,7 @@ command script import data/toolbox/init.py script print("===TOOLBOX-OUTPUT-START===") -rb-object-print 20 +rb-print 20 script print("===TOOLBOX-OUTPUT-END===") quit diff --git a/fixtures/toolbox/lldb/object/can_print_short_symbol.lldb b/fixtures/toolbox/lldb/object/can_print_short_symbol.lldb index a8b3d89..1f9ea91 100644 --- a/fixtures/toolbox/lldb/object/can_print_short_symbol.lldb +++ b/fixtures/toolbox/lldb/object/can_print_short_symbol.lldb @@ -8,7 +8,7 @@ breakpoint set --name ruby_process_options run expr unsigned long $value = (unsigned long)rb_eval_string(":hello") script print("===TOOLBOX-OUTPUT-START===") -rb-object-print $value +rb-print $value script print("===TOOLBOX-OUTPUT-END===") quit diff --git a/guides/fiber-debugging/readme.md b/guides/fiber-debugging/readme.md index 0a03aae..a901aff 100644 --- a/guides/fiber-debugging/readme.md +++ b/guides/fiber-debugging/readme.md @@ -161,7 +161,7 @@ After switching to a fiber with `rb-fiber-scan-switch`, you can use standard GDB (gdb) bt # Show C backtrace (gdb) frame # Switch to specific frame (gdb) info locals # Show local variables -(gdb) rb-object-print $errinfo # Print exception if present +(gdb) rb-print $errinfo # Print exception if present ~~~ The fiber switch command sets up several convenience variables: diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index 369478d..95b0dbc 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -63,7 +63,7 @@ Status: ✓ Installed Test that extensions load automatically: ~~~ bash -$ gdb --batch -ex "help rb-object-print" +$ gdb --batch -ex "help rb-print" Recursively print Ruby hash and array structures... ~~~ @@ -88,7 +88,7 @@ This removes the source line from your `~/.gdbinit` or `~/.lldbinit`. Ruby GDB provides specialized commands for debugging Ruby at multiple levels: - **Context Setup** (`rb-context`) - Get current execution context and set up convenience variables -- **Object Inspection** (`rb-object-print`) - View Ruby objects, hashes, arrays, and structs with proper formatting +- **Object Inspection** (`rb-print`) - View Ruby objects, hashes, arrays, and structs with proper formatting - **Fiber Debugging** (`rb-fiber-*`) - Scan heap for fibers, inspect state, and switch contexts - **Stack Analysis** (`rb-stack-trace`) - Examine combined VM (Ruby) and C (native) stack frames - **Heap Navigation** (`rb-heap-scan`) - Scan the Ruby heap to find objects by type @@ -132,7 +132,7 @@ Diagnose the issue (extensions load automatically if installed): (gdb) rb-fiber-scan-heap # Scan heap for all fibers (gdb) rb-fiber-scan-stack-trace-all # Show backtraces for all fibers (gdb) rb-fiber-scan-switch 0 # Switch to main fiber -(gdb) rb-object-print $errinfo --depth 2 # Print exception (now $errinfo is set) +(gdb) rb-print $errinfo --depth 2 # Print exception (now $errinfo is set) (gdb) rb-heap-scan --type RUBY_T_HASH --limit 10 # Find hashes ~~~ @@ -146,7 +146,7 @@ When a Ruby exception occurs, you can inspect it in detail: (gdb) break rb_exc_raise (gdb) run (gdb) rb-context -(gdb) rb-object-print $errinfo --depth 2 +(gdb) rb-print $errinfo --depth 2 ~~~ This shows the exception class, message, and any nested structures. The `rb-context` command displays the current execution context and sets up `$ec`, `$cfp`, and `$errinfo` convenience variables. @@ -167,7 +167,7 @@ When working with fibers, you often need to see what each fiber is doing: Ruby hashes and arrays can contain nested structures: ~~~ -(gdb) rb-object-print $some_hash --depth 2 +(gdb) rb-print $some_hash --depth 2 [ 0] K: :name V: "Alice" @@ -197,4 +197,4 @@ On macOS with LLDB and Ruby <= 3.4.x, some commands including `rb-fiber-scan-hea **Workarounds:** - Use Ruby head: `ruby-install ruby-head -- CFLAGS="-g -O0"` - Use GDB instead of LLDB (works with Ruby 3.4.x) -- Other commands like `rb-object-print`, `rb-stack-trace`, `rb-context` work fine +- Other commands like `rb-print`, `rb-stack-trace`, `rb-context` work fine diff --git a/guides/object-inspection/readme.md b/guides/object-inspection/readme.md index facc5b1..23459bc 100644 --- a/guides/object-inspection/readme.md +++ b/guides/object-inspection/readme.md @@ -1,12 +1,12 @@ # Object Inspection -This guide explains how to use `rb-object-print` to inspect Ruby objects, hashes, arrays, and structs in GDB. +This guide explains how to use `rb-print` to inspect Ruby objects, hashes, arrays, and structs in GDB. ## Why Object Inspection Matters When debugging Ruby programs or analyzing core dumps, you often need to inspect complex data structures that are difficult to read in their raw memory representation. Standard GDB commands show pointer addresses and raw memory, but not the logical structure of Ruby objects. -Use `rb-object-print` when you need: +Use `rb-print` when you need: - **Understand exception objects**: See the full exception hierarchy, message, and backtrace data - **Inspect fiber storage**: View thread-local data and fiber-specific variables @@ -15,12 +15,12 @@ Use `rb-object-print` when you need: ## Basic Usage -The `rb-object-print` command recursively prints Ruby objects in a human-readable format. +The `rb-print` command recursively prints Ruby objects in a human-readable format. ### Syntax ~~~ -rb-object-print [--depth N] [--debug] +rb-print [--depth N] [--debug] ~~~ Where: @@ -33,10 +33,10 @@ Where: Print immediate values and special constants: ~~~ -(gdb) rb-object-print 0 # -(gdb) rb-object-print 8 # -(gdb) rb-object-print 20 # -(gdb) rb-object-print 85 # 42 +(gdb) rb-print 0 # +(gdb) rb-print 8 # +(gdb) rb-print 20 # +(gdb) rb-print 85 # 42 ~~~ These work without any Ruby process running, making them useful for learning and testing. @@ -46,10 +46,10 @@ These work without any Ruby process running, making them useful for learning and Use any valid GDB expression: ~~~ -(gdb) rb-object-print $ec->errinfo # Exception object -(gdb) rb-object-print $ec->cfp->sp[-1] # Top of VM stack -(gdb) rb-object-print $ec->storage # Fiber storage hash -(gdb) rb-object-print (VALUE)0x00007f8a12345678 # Object at specific address +(gdb) rb-print $ec->errinfo # Exception object +(gdb) rb-print $ec->cfp->sp[-1] # Top of VM stack +(gdb) rb-print $ec->storage # Fiber storage hash +(gdb) rb-print (VALUE)0x00007f8a12345678 # Object at specific address ~~~ ## Inspecting Hashes @@ -61,7 +61,7 @@ Ruby hashes have two internal representations (ST table and AR table). The comma For hashes with fewer than 8 entries, Ruby uses an array-based implementation: ~~~ -(gdb) rb-object-print $some_hash +(gdb) rb-print $some_hash [ 0] K: :name V: "Alice" @@ -76,7 +76,7 @@ For hashes with fewer than 8 entries, Ruby uses an array-based implementation: For larger hashes, Ruby uses a hash table (output format is similar): ~~~ -(gdb) rb-object-print $large_hash +(gdb) rb-print $large_hash [ 0] K: :user_id V: 12345 @@ -90,12 +90,12 @@ For larger hashes, Ruby uses a hash table (output format is similar): Prevent overwhelming output from deeply nested structures: ~~~ -(gdb) rb-object-print $nested_hash --depth 1 # Only top level +(gdb) rb-print $nested_hash --depth 1 # Only top level [ 0] K: :data V: # Nested hash not expanded -(gdb) rb-object-print $nested_hash --depth 2 # Expand one level +(gdb) rb-print $nested_hash --depth 2 # Expand one level [ 0] K: :data V: @@ -114,7 +114,7 @@ Arrays also have two representations based on size: Arrays display their elements with type information: ~~~ -(gdb) rb-object-print $array +(gdb) rb-print $array [ 0] 1 [ 1] 2 @@ -124,7 +124,7 @@ Arrays display their elements with type information: For arrays with nested objects: ~~~ -(gdb) rb-object-print $array --depth 2 +(gdb) rb-print $array --depth 2 [ 0] "first item" [ 1] @@ -138,7 +138,7 @@ For arrays with nested objects: Ruby Struct objects work similarly to arrays: ~~~ -(gdb) rb-object-print $struct_instance +(gdb) rb-print $struct_instance [ 0] "John" [ 1] 25 @@ -155,7 +155,7 @@ When a fiber has an exception, inspect it: ~~~ (gdb) rb-fiber-scan-heap (gdb) rb-fiber-scan-switch 5 # Switch to fiber #5 -(gdb) rb-object-print $errinfo --depth 3 +(gdb) rb-print $errinfo --depth 3 ~~~ This reveals the full exception structure including any nested causes. After switching to a fiber, `$errinfo` and `$ec` convenience variables are automatically set. @@ -167,8 +167,8 @@ Break at a method and examine arguments on the stack: ~~~ (gdb) break some_method (gdb) run -(gdb) rb-object-print $ec->cfp->sp[-1] # Last argument -(gdb) rb-object-print $ec->cfp->sp[-2] # Second-to-last argument +(gdb) rb-print $ec->cfp->sp[-1] # Last argument +(gdb) rb-print $ec->cfp->sp[-2] # Second-to-last argument ~~~ ### Examining Fiber Storage @@ -178,17 +178,17 @@ Thread-local variables are stored in fiber storage: ~~~ (gdb) rb-fiber-scan-heap (gdb) rb-fiber-scan-switch 0 -(gdb) rb-object-print $ec->storage --depth 2 +(gdb) rb-print $ec->storage --depth 2 ~~~ This shows all thread-local variables and their values. ## Debugging with --debug Flag -When `rb-object-print` doesn't show what you expect, use `--debug`: +When `rb-print` doesn't show what you expect, use `--debug`: ~~~ -(gdb) rb-object-print $suspicious_value --debug +(gdb) rb-print $suspicious_value --debug DEBUG: Evaluated '$suspicious_value' to 0x7f8a1c567890 DEBUG: Loaded constant RUBY_T_MASK = 31 DEBUG: Object at 0x7f8a1c567890 with flags=0x20040005, type=0x5 diff --git a/guides/stack-inspection/readme.md b/guides/stack-inspection/readme.md index d1cd25d..c9b9233 100644 --- a/guides/stack-inspection/readme.md +++ b/guides/stack-inspection/readme.md @@ -140,9 +140,9 @@ See what values are on the current frame's stack: (gdb) set $sp = $ec->cfp->sp # Print values on stack -(gdb) rb-object-print *(VALUE*)($sp - 1) # Top of stack -(gdb) rb-object-print *(VALUE*)($sp - 2) # Second value -(gdb) rb-object-print *(VALUE*)($sp - 3) # Third value +(gdb) rb-print *(VALUE*)($sp - 1) # Top of stack +(gdb) rb-print *(VALUE*)($sp - 2) # Second value +(gdb) rb-print *(VALUE*)($sp - 3) # Third value ~~~ ### Tracking Fiber Switches diff --git a/readme.md b/readme.md index cb1c943..91bc11e 100644 --- a/readme.md +++ b/readme.md @@ -19,7 +19,7 @@ Please see the [project documentation](https://socketry.github.io/toolbox/) for - [Getting Started](https://socketry.github.io/toolbox/guides/getting-started/index) - This guide explains how to install and use Toolbox for debugging Ruby programs and core dumps with GDB or LLDB. - - [Object Inspection](https://socketry.github.io/toolbox/guides/object-inspection/index) - This guide explains how to use `rb-object-print` to inspect Ruby objects, hashes, arrays, and structs in GDB. + - [Object Inspection](https://socketry.github.io/toolbox/guides/object-inspection/index) - This guide explains how to use `rb-print` to inspect Ruby objects, hashes, arrays, and structs in GDB. - [Stack Inspection](https://socketry.github.io/toolbox/guides/stack-inspection/index) - This guide explains how to inspect both Ruby VM stacks and native C stacks when debugging Ruby programs.