|
| 1 | +"""Ruby execution context utilities and commands.""" |
| 2 | + |
| 3 | +import debugger |
| 4 | +import format |
| 5 | +import value |
| 6 | + |
| 7 | + |
| 8 | +class RubyContext: |
| 9 | + """Wrapper for Ruby execution context (rb_execution_context_t). |
| 10 | + |
| 11 | + Provides a high-level interface for working with Ruby execution contexts, |
| 12 | + including inspection, convenience variable setup, and information display. |
| 13 | + |
| 14 | + Example: |
| 15 | + ctx = RubyContext.current() |
| 16 | + if ctx: |
| 17 | + ctx.print_info(terminal) |
| 18 | + ctx.setup_convenience_variables() |
| 19 | + """ |
| 20 | + |
| 21 | + def __init__(self, ec): |
| 22 | + """Create a RubyContext wrapper. |
| 23 | + |
| 24 | + Args: |
| 25 | + ec: Execution context pointer (rb_execution_context_t *) |
| 26 | + """ |
| 27 | + self.ec = ec |
| 28 | + self._cfp = None |
| 29 | + self._errinfo = None |
| 30 | + self._vm_stack = None |
| 31 | + self._vm_stack_size = None |
| 32 | + |
| 33 | + @classmethod |
| 34 | + def current(cls): |
| 35 | + """Get the current execution context from the running thread. |
| 36 | + |
| 37 | + Tries multiple approaches in order of preference: |
| 38 | + 1. ruby_current_ec - TLS variable (works in GDB, some LLDB) |
| 39 | + 2. rb_current_ec_noinline() - function call (works in most cases) |
| 40 | + 3. rb_current_ec() - macOS-specific function |
| 41 | + |
| 42 | + Returns: |
| 43 | + RubyContext instance, or None if not available |
| 44 | + """ |
| 45 | + # Try ruby_current_ec variable first |
| 46 | + try: |
| 47 | + ec = debugger.parse_and_eval('ruby_current_ec') |
| 48 | + if ec is not None and int(ec) != 0: |
| 49 | + return cls(ec) |
| 50 | + except debugger.Error: |
| 51 | + pass |
| 52 | + |
| 53 | + # Fallback to rb_current_ec_noinline() function |
| 54 | + try: |
| 55 | + ec = debugger.parse_and_eval('rb_current_ec_noinline()') |
| 56 | + if ec is not None and int(ec) != 0: |
| 57 | + return cls(ec) |
| 58 | + except debugger.Error: |
| 59 | + pass |
| 60 | + |
| 61 | + # Last resort: rb_current_ec() (macOS-specific) |
| 62 | + try: |
| 63 | + ec = debugger.parse_and_eval('rb_current_ec()') |
| 64 | + if ec is not None and int(ec) != 0: |
| 65 | + return cls(ec) |
| 66 | + except debugger.Error: |
| 67 | + pass |
| 68 | + |
| 69 | + return None |
| 70 | + |
| 71 | + @property |
| 72 | + def cfp(self): |
| 73 | + """Get control frame pointer (lazy load).""" |
| 74 | + if self._cfp is None: |
| 75 | + try: |
| 76 | + self._cfp = self.ec['cfp'] |
| 77 | + except Exception: |
| 78 | + pass |
| 79 | + return self._cfp |
| 80 | + |
| 81 | + @property |
| 82 | + def errinfo(self): |
| 83 | + """Get exception VALUE (lazy load).""" |
| 84 | + if self._errinfo is None: |
| 85 | + try: |
| 86 | + self._errinfo = self.ec['errinfo'] |
| 87 | + except Exception: |
| 88 | + pass |
| 89 | + return self._errinfo |
| 90 | + |
| 91 | + @property |
| 92 | + def has_exception(self): |
| 93 | + """Check if there's a real exception (not nil/special value).""" |
| 94 | + if self.errinfo is None: |
| 95 | + return False |
| 96 | + try: |
| 97 | + errinfo_int = int(self.errinfo) |
| 98 | + # Check if it's a real object (not nil or other special constant) |
| 99 | + is_special = (errinfo_int & 0x03) != 0 or errinfo_int == 0 |
| 100 | + return not is_special |
| 101 | + except Exception: |
| 102 | + return False |
| 103 | + |
| 104 | + @property |
| 105 | + def vm_stack(self): |
| 106 | + """Get VM stack pointer (lazy load).""" |
| 107 | + if self._vm_stack is None: |
| 108 | + try: |
| 109 | + self._vm_stack = self.ec['vm_stack'] |
| 110 | + except Exception: |
| 111 | + pass |
| 112 | + return self._vm_stack |
| 113 | + |
| 114 | + @property |
| 115 | + def vm_stack_size(self): |
| 116 | + """Get VM stack size (lazy load).""" |
| 117 | + if self._vm_stack_size is None: |
| 118 | + try: |
| 119 | + self._vm_stack_size = int(self.ec['vm_stack_size']) |
| 120 | + except Exception: |
| 121 | + pass |
| 122 | + return self._vm_stack_size |
| 123 | + |
| 124 | + def setup_convenience_variables(self): |
| 125 | + """Set up convenience variables for this execution context. |
| 126 | + |
| 127 | + Sets: |
| 128 | + $ec - Execution context pointer |
| 129 | + $cfp - Control frame pointer |
| 130 | + $errinfo - Current exception (if any) |
| 131 | + |
| 132 | + Returns: |
| 133 | + dict with keys: 'ec', 'cfp', 'errinfo' (values are the set variables) |
| 134 | + """ |
| 135 | + result = {} |
| 136 | + |
| 137 | + # Set $ec |
| 138 | + debugger.set_convenience_variable('ec', self.ec) |
| 139 | + result['ec'] = self.ec |
| 140 | + |
| 141 | + # Set $cfp (control frame pointer) |
| 142 | + if self.cfp is not None: |
| 143 | + debugger.set_convenience_variable('cfp', self.cfp) |
| 144 | + result['cfp'] = self.cfp |
| 145 | + else: |
| 146 | + result['cfp'] = None |
| 147 | + |
| 148 | + # Set $errinfo if there's an exception |
| 149 | + if self.has_exception: |
| 150 | + debugger.set_convenience_variable('errinfo', self.errinfo) |
| 151 | + result['errinfo'] = self.errinfo |
| 152 | + else: |
| 153 | + result['errinfo'] = None |
| 154 | + |
| 155 | + return result |
| 156 | + |
| 157 | + def print_info(self, terminal): |
| 158 | + """Print detailed information about this execution context. |
| 159 | + |
| 160 | + Args: |
| 161 | + terminal: Terminal formatter for output |
| 162 | + """ |
| 163 | + print("Execution Context:") |
| 164 | + print(f" $ec = ", end='') |
| 165 | + print(terminal.print_type_tag('rb_execution_context_t', int(self.ec), None)) |
| 166 | + |
| 167 | + # VM Stack info |
| 168 | + if self.vm_stack is not None and self.vm_stack_size is not None: |
| 169 | + print(f" VM Stack: ", end='') |
| 170 | + print(terminal.print_type_tag('VALUE', int(self.vm_stack), f'size={self.vm_stack_size}')) |
| 171 | + else: |
| 172 | + print(f" VM Stack: <unavailable>") |
| 173 | + |
| 174 | + # Control Frame info |
| 175 | + if self.cfp is not None: |
| 176 | + print(f" $cfp = ", end='') |
| 177 | + print(terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None)) |
| 178 | + else: |
| 179 | + print(f" $cfp = <unavailable>") |
| 180 | + |
| 181 | + # Exception info |
| 182 | + if self.has_exception: |
| 183 | + print(f" $errinfo = ", end='') |
| 184 | + print(terminal.print_type_tag('VALUE', int(self.errinfo), None)) |
| 185 | + print(" Exception present!") |
| 186 | + else: |
| 187 | + errinfo_int = int(self.errinfo) if self.errinfo else 0 |
| 188 | + if errinfo_int == 4: # Qnil |
| 189 | + print(" Exception: None") |
| 190 | + elif errinfo_int == 0: # Qfalse |
| 191 | + print(" Exception: None (false)") |
| 192 | + else: |
| 193 | + print(f" Exception: None") |
| 194 | + |
| 195 | + # Tag info (for ensure blocks) |
| 196 | + try: |
| 197 | + tag = self.ec['tag'] |
| 198 | + tag_int = int(tag) |
| 199 | + if tag_int != 0: |
| 200 | + print(f" Tag: ", end='') |
| 201 | + print(terminal.print_type_tag('rb_vm_tag', tag_int, None)) |
| 202 | + try: |
| 203 | + retval = tag['retval'] |
| 204 | + retval_int = int(retval) |
| 205 | + is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0 |
| 206 | + if not is_retval_special: |
| 207 | + print(f" $retval available (in ensure block)") |
| 208 | + except Exception: |
| 209 | + pass |
| 210 | + except Exception: |
| 211 | + pass |
| 212 | + |
| 213 | + |
| 214 | +class RubyContextCommand(debugger.Command): |
| 215 | + """Show current execution context and set convenience variables. |
| 216 | + |
| 217 | + This command automatically discovers the current thread's execution context |
| 218 | + and displays detailed information about it, while also setting up convenience |
| 219 | + variables for easy inspection. |
| 220 | + |
| 221 | + Usage: |
| 222 | + rb-context |
| 223 | + |
| 224 | + Displays: |
| 225 | + - Execution context pointer and details |
| 226 | + - VM stack information |
| 227 | + - Control frame pointer |
| 228 | + - Exception information (if any) |
| 229 | + |
| 230 | + Sets these convenience variables: |
| 231 | + $ec - Current execution context (rb_execution_context_t *) |
| 232 | + $cfp - Current control frame pointer |
| 233 | + $errinfo - Current exception (if any) |
| 234 | + |
| 235 | + Example: |
| 236 | + (gdb) rb-context |
| 237 | + Execution Context: |
| 238 | + $ec = <rb_execution_context_t *@0x...> |
| 239 | + VM Stack: <VALUE *@0x...> size=1024 |
| 240 | + $cfp = <rb_control_frame_t *@0x...> |
| 241 | + Exception: None |
| 242 | + |
| 243 | + (gdb) rb-object-print $errinfo |
| 244 | + (gdb) rb-object-print $ec->cfp->sp[-1] |
| 245 | + """ |
| 246 | + |
| 247 | + def __init__(self): |
| 248 | + super(RubyContextCommand, self).__init__("rb-context", debugger.COMMAND_USER) |
| 249 | + |
| 250 | + def invoke(self, arg, from_tty): |
| 251 | + """Execute the rb-context command.""" |
| 252 | + try: |
| 253 | + terminal = format.create_terminal(from_tty) |
| 254 | + |
| 255 | + # Get current execution context |
| 256 | + ctx = RubyContext.current() |
| 257 | + |
| 258 | + if ctx is None: |
| 259 | + print("Error: Could not get current execution context") |
| 260 | + print() |
| 261 | + print("Possible reasons:") |
| 262 | + print(" • Ruby symbols not loaded (compile with debug symbols)") |
| 263 | + print(" • Process not stopped at a Ruby frame") |
| 264 | + print(" • Ruby not fully initialized yet") |
| 265 | + print() |
| 266 | + print("Try:") |
| 267 | + print(" • Break at a Ruby function: break rb_vm_exec") |
| 268 | + print(" • Use rb-fiber-scan-switch to switch to a fiber") |
| 269 | + print(" • Ensure Ruby debug symbols are available") |
| 270 | + return |
| 271 | + |
| 272 | + # Print context information |
| 273 | + ctx.print_info(terminal) |
| 274 | + |
| 275 | + # Set convenience variables |
| 276 | + vars = ctx.setup_convenience_variables() |
| 277 | + |
| 278 | + print() |
| 279 | + print("Convenience variables set:") |
| 280 | + print(f" $ec - Execution context") |
| 281 | + if vars.get('cfp'): |
| 282 | + print(f" $cfp - Control frame pointer") |
| 283 | + if vars.get('errinfo'): |
| 284 | + print(f" $errinfo - Exception object") |
| 285 | + |
| 286 | + print() |
| 287 | + print("Now you can use:") |
| 288 | + print(" rb-object-print $errinfo") |
| 289 | + print(" rb-object-print $ec->cfp->sp[-1]") |
| 290 | + print(" rb-stack-trace") |
| 291 | + |
| 292 | + except Exception as e: |
| 293 | + print(f"Error: {e}") |
| 294 | + import traceback |
| 295 | + traceback.print_exc() |
| 296 | + |
| 297 | + |
| 298 | +# Register command |
| 299 | +RubyContextCommand() |
| 300 | + |
0 commit comments