toolbox 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,295 +1,371 @@
1
1
  """Ruby execution context utilities and commands."""
2
2
 
3
3
  import debugger
4
+ import command
4
5
  import format
5
- import value
6
+ import rvalue
6
7
  import rexception
7
8
 
8
9
 
9
10
  class RubyContext:
10
- """Wrapper for Ruby execution context (rb_execution_context_t).
11
-
12
- Provides a high-level interface for working with Ruby execution contexts,
13
- including inspection, convenience variable setup, and information display.
14
-
15
- Example:
16
- ctx = RubyContext.current()
17
- if ctx:
18
- ctx.print_info(terminal)
19
- ctx.setup_convenience_variables()
20
- """
21
-
22
- def __init__(self, ec):
23
- """Create a RubyContext wrapper.
24
-
25
- Args:
26
- ec: Execution context pointer (rb_execution_context_t *)
27
- """
28
- self.ec = ec
29
- self._cfp = None
30
- self._errinfo = None
31
- self._vm_stack = None
32
- self._vm_stack_size = None
33
-
34
- @classmethod
35
- def current(cls):
36
- """Get the current execution context from the running thread.
37
-
38
- Tries multiple approaches in order of preference:
39
- 1. ruby_current_ec - TLS variable (works in GDB, some LLDB)
40
- 2. rb_current_ec_noinline() - function call (works in most cases)
41
- 3. rb_current_ec() - macOS-specific function
42
-
43
- Returns:
44
- RubyContext instance, or None if not available
45
- """
46
- # Try ruby_current_ec variable first
47
- try:
48
- ec = debugger.parse_and_eval('ruby_current_ec')
49
- if ec is not None and int(ec) != 0:
50
- return cls(ec)
51
- except debugger.Error:
52
- pass
53
-
54
- # Fallback to rb_current_ec_noinline() function
55
- try:
56
- ec = debugger.parse_and_eval('rb_current_ec_noinline()')
57
- if ec is not None and int(ec) != 0:
58
- return cls(ec)
59
- except debugger.Error:
60
- pass
61
-
62
- # Last resort: rb_current_ec() (macOS-specific)
63
- try:
64
- ec = debugger.parse_and_eval('rb_current_ec()')
65
- if ec is not None and int(ec) != 0:
66
- return cls(ec)
67
- except debugger.Error:
68
- pass
69
-
70
- return None
71
-
72
- @property
73
- def cfp(self):
74
- """Get control frame pointer (lazy load)."""
75
- if self._cfp is None:
76
- try:
77
- self._cfp = self.ec['cfp']
78
- except Exception:
79
- pass
80
- return self._cfp
81
-
82
- @property
83
- def errinfo(self):
84
- """Get exception VALUE (lazy load)."""
85
- if self._errinfo is None:
86
- try:
87
- self._errinfo = self.ec['errinfo']
88
- except Exception:
89
- pass
90
- return self._errinfo
91
-
92
- @property
93
- def has_exception(self):
94
- """Check if there's a real exception (not nil/special value)."""
95
- if self.errinfo is None:
96
- return False
97
- return rexception.is_exception(self.errinfo)
98
-
99
- @property
100
- def vm_stack(self):
101
- """Get VM stack pointer (lazy load)."""
102
- if self._vm_stack is None:
103
- try:
104
- self._vm_stack = self.ec['vm_stack']
105
- except Exception:
106
- pass
107
- return self._vm_stack
108
-
109
- @property
110
- def vm_stack_size(self):
111
- """Get VM stack size (lazy load)."""
112
- if self._vm_stack_size is None:
113
- try:
114
- self._vm_stack_size = int(self.ec['vm_stack_size'])
115
- except Exception:
116
- pass
117
- return self._vm_stack_size
118
-
119
- def setup_convenience_variables(self):
120
- """Set up convenience variables for this execution context.
121
-
122
- Sets:
123
- $ec - Execution context pointer
124
- $cfp - Control frame pointer
125
- $errinfo - Current exception (if any)
126
-
127
- Returns:
128
- dict with keys: 'ec', 'cfp', 'errinfo' (values are the set variables)
129
- """
130
- result = {}
131
-
132
- # Set $ec
133
- debugger.set_convenience_variable('ec', self.ec)
134
- result['ec'] = self.ec
135
-
136
- # Set $cfp (control frame pointer)
137
- if self.cfp is not None:
138
- debugger.set_convenience_variable('cfp', self.cfp)
139
- result['cfp'] = self.cfp
140
- else:
141
- result['cfp'] = None
142
-
143
- # Set $errinfo if there's an exception
144
- if self.has_exception:
145
- debugger.set_convenience_variable('errinfo', self.errinfo)
146
- result['errinfo'] = self.errinfo
147
- else:
148
- result['errinfo'] = None
149
-
150
- return result
151
-
152
- def print_info(self, terminal):
153
- """Print detailed information about this execution context.
154
-
155
- Args:
156
- terminal: Terminal formatter for output
157
- """
158
- print("Execution Context:")
159
- print(f" $ec = ", end='')
160
- print(terminal.print_type_tag('rb_execution_context_t', int(self.ec), None))
161
-
162
- # VM Stack info
163
- if self.vm_stack is not None and self.vm_stack_size is not None:
164
- print(f" VM Stack: ", end='')
165
- print(terminal.print_type_tag('VALUE', int(self.vm_stack), f'size={self.vm_stack_size}'))
166
- else:
167
- print(f" VM Stack: <unavailable>")
168
-
169
- # Control Frame info
170
- if self.cfp is not None:
171
- print(f" $cfp = ", end='')
172
- print(terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None))
173
- else:
174
- print(f" $cfp = <unavailable>")
175
-
176
- # Exception info
177
- if self.has_exception:
178
- print(f" $errinfo = ", end='')
179
- print(terminal.print_type_tag('VALUE', int(self.errinfo), None))
180
- print(" Exception present!")
181
- else:
182
- errinfo_int = int(self.errinfo) if self.errinfo else 0
183
- if errinfo_int == 4: # Qnil
184
- print(" Exception: None")
185
- elif errinfo_int == 0: # Qfalse
186
- print(" Exception: None (false)")
187
- else:
188
- print(f" Exception: None")
189
-
190
- # Tag info (for ensure blocks)
191
- try:
192
- tag = self.ec['tag']
193
- tag_int = int(tag)
194
- if tag_int != 0:
195
- print(f" Tag: ", end='')
196
- print(terminal.print_type_tag('rb_vm_tag', tag_int, None))
197
- try:
198
- retval = tag['retval']
199
- retval_int = int(retval)
200
- is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0
201
- if not is_retval_special:
202
- print(f" $retval available (in ensure block)")
203
- except Exception:
204
- pass
205
- except Exception:
206
- pass
11
+ """Wrapper for Ruby execution context (rb_execution_context_t).
12
+
13
+ Provides a high-level interface for working with Ruby execution contexts,
14
+ including inspection, convenience variable setup, and information display.
15
+
16
+ Example:
17
+ ctx = RubyContext.current()
18
+ if ctx:
19
+ ctx.print_info(terminal)
20
+ ctx.setup_convenience_variables()
21
+ """
22
+
23
+ def __init__(self, ec):
24
+ """Create a RubyContext wrapper.
25
+
26
+ Args:
27
+ ec: Execution context pointer (rb_execution_context_t *)
28
+ """
29
+ self.ec = ec
30
+ self._cfp = None
31
+ self._errinfo = None
32
+ self._vm_stack = None
33
+ self._vm_stack_size = None
34
+
35
+ @classmethod
36
+ def current(cls):
37
+ """Get the current execution context from the running thread.
38
+
39
+ Tries multiple approaches in order of preference:
40
+ 1. ruby_current_ec - TLS variable (works in GDB, some LLDB)
41
+ 2. rb_current_ec_noinline() - function call (works in most cases)
42
+ 3. rb_current_ec() - macOS-specific function
43
+
44
+ Returns:
45
+ RubyContext instance, or None if not available
46
+ """
47
+ # Try ruby_current_ec variable first
48
+ try:
49
+ ec = debugger.parse_and_eval('ruby_current_ec')
50
+ if ec is not None and int(ec) != 0:
51
+ return cls(ec)
52
+ except debugger.Error:
53
+ pass
54
+
55
+ # Fallback to rb_current_ec_noinline() function
56
+ try:
57
+ ec = debugger.parse_and_eval('rb_current_ec_noinline()')
58
+ if ec is not None and int(ec) != 0:
59
+ return cls(ec)
60
+ except debugger.Error:
61
+ pass
62
+
63
+ # Last resort: rb_current_ec() (macOS-specific)
64
+ try:
65
+ ec = debugger.parse_and_eval('rb_current_ec()')
66
+ if ec is not None and int(ec) != 0:
67
+ return cls(ec)
68
+ except debugger.Error:
69
+ pass
70
+
71
+ return None
72
+
73
+ @property
74
+ def cfp(self):
75
+ """Get control frame pointer (lazy load)."""
76
+ if self._cfp is None:
77
+ try:
78
+ self._cfp = self.ec['cfp']
79
+ except Exception:
80
+ pass
81
+ return self._cfp
82
+
83
+ @property
84
+ def errinfo(self):
85
+ """Get exception VALUE (lazy load)."""
86
+ if self._errinfo is None:
87
+ try:
88
+ self._errinfo = self.ec['errinfo']
89
+ except Exception:
90
+ pass
91
+ return self._errinfo
92
+
93
+ @property
94
+ def has_exception(self):
95
+ """Check if there's a real exception (not nil/special value)."""
96
+ if self.errinfo is None:
97
+ return False
98
+ return rexception.is_exception(self.errinfo)
99
+
100
+ @property
101
+ def vm_stack(self):
102
+ """Get VM stack pointer (lazy load)."""
103
+ if self._vm_stack is None:
104
+ try:
105
+ self._vm_stack = self.ec['vm_stack']
106
+ except Exception:
107
+ pass
108
+ return self._vm_stack
109
+
110
+ @property
111
+ def vm_stack_size(self):
112
+ """Get VM stack size (lazy load)."""
113
+ if self._vm_stack_size is None:
114
+ try:
115
+ self._vm_stack_size = int(self.ec['vm_stack_size'])
116
+ except Exception:
117
+ pass
118
+ return self._vm_stack_size
119
+
120
+ @property
121
+ def storage(self):
122
+ """Get fiber storage VALUE."""
123
+ try:
124
+ return self.ec['storage']
125
+ except Exception:
126
+ return None
127
+
128
+ def setup_convenience_variables(self):
129
+ """Set up convenience variables for this execution context.
130
+
131
+ Sets:
132
+ $ec - Execution context pointer
133
+ $cfp - Control frame pointer
134
+ $errinfo - Current exception (if any)
135
+
136
+ Returns:
137
+ dict with keys: 'ec', 'cfp', 'errinfo' (values are the set variables)
138
+ """
139
+ result = {}
140
+
141
+ # Set $ec
142
+ debugger.set_convenience_variable('ec', self.ec)
143
+ result['ec'] = self.ec
144
+
145
+ # Set $cfp (control frame pointer)
146
+ if self.cfp is not None:
147
+ debugger.set_convenience_variable('cfp', self.cfp)
148
+ result['cfp'] = self.cfp
149
+ else:
150
+ result['cfp'] = None
151
+
152
+ # Set $errinfo if there's an exception
153
+ if self.has_exception:
154
+ debugger.set_convenience_variable('errinfo', self.errinfo)
155
+ result['errinfo'] = self.errinfo
156
+ else:
157
+ result['errinfo'] = None
158
+
159
+ return result
160
+
161
+ def print_info(self, terminal):
162
+ """Print detailed information about this execution context.
163
+
164
+ Args:
165
+ terminal: Terminal formatter for output
166
+ """
167
+ # Cache property lookups
168
+ vm_stack = self.vm_stack
169
+ vm_stack_size = self.vm_stack_size
170
+ cfp = self.cfp
171
+ storage = self.storage
172
+ errinfo = self.errinfo
173
+ has_exception = self.has_exception
174
+
175
+ terminal.print("Execution Context:")
176
+ terminal.print(f" $ec = ", end='')
177
+ terminal.print_type_tag('rb_execution_context_t', int(self.ec), None)
178
+ terminal.print()
179
+
180
+ # VM Stack info
181
+ if vm_stack is not None and vm_stack_size is not None:
182
+ terminal.print(f" VM Stack: ", end='')
183
+ terminal.print_type_tag('VALUE', int(vm_stack))
184
+ terminal.print()
185
+ else:
186
+ terminal.print(f" VM Stack: <unavailable>")
187
+
188
+ # Control Frame info
189
+ if cfp is not None:
190
+ terminal.print(f" $cfp = ", end='')
191
+ terminal.print_type_tag('rb_control_frame_t', int(cfp), None)
192
+ terminal.print()
193
+ else:
194
+ terminal.print(f" $cfp = <unavailable>")
195
+
196
+ # Storage info
197
+ if storage is not None and not rvalue.is_nil(storage):
198
+ terminal.print(f" Storage: ", end='')
199
+ terminal.print_type_tag('VALUE', int(storage), None)
200
+ terminal.print()
201
+
202
+ # Exception info
203
+ if has_exception:
204
+ terminal.print(" $errinfo = ", end='')
205
+ terminal.print_type_tag('VALUE', int(errinfo), None)
206
+ terminal.print()
207
+ terminal.print(" Exception present!")
208
+ else:
209
+ errinfo_int = int(errinfo) if errinfo else 0
210
+ if errinfo_int == 4: # Qnil
211
+ terminal.print(" Exception: None")
212
+ elif errinfo_int == 0: # Qfalse
213
+ terminal.print(" Exception: None (false)")
214
+ else:
215
+ terminal.print(" Exception: None")
216
+
217
+ # Tag info (for ensure blocks)
218
+ try:
219
+ tag = self.ec['tag']
220
+ tag_int = int(tag)
221
+ if tag_int != 0:
222
+ terminal.print(" Tag: ", end='')
223
+ terminal.print_type_tag('rb_vm_tag', tag_int, None)
224
+ terminal.print()
225
+ try:
226
+ retval = tag['retval']
227
+ retval_int = int(retval)
228
+ is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0
229
+ if not is_retval_special:
230
+ terminal.print(" $retval available (in ensure block)")
231
+ except Exception:
232
+ pass
233
+ except Exception:
234
+ pass
207
235
 
208
236
 
209
- class RubyContextCommand(debugger.Command):
210
- """Show current execution context and set convenience variables.
211
-
212
- This command automatically discovers the current thread's execution context
213
- and displays detailed information about it, while also setting up convenience
214
- variables for easy inspection.
215
-
216
- Usage:
217
- rb-context
218
-
219
- Displays:
220
- - Execution context pointer and details
221
- - VM stack information
222
- - Control frame pointer
223
- - Exception information (if any)
224
-
225
- Sets these convenience variables:
226
- $ec - Current execution context (rb_execution_context_t *)
227
- $cfp - Current control frame pointer
228
- $errinfo - Current exception (if any)
229
-
230
- Example:
231
- (gdb) rb-context
232
- Execution Context:
233
- $ec = <rb_execution_context_t *@0x...>
234
- VM Stack: <VALUE *@0x...> size=1024
235
- $cfp = <rb_control_frame_t *@0x...>
236
- Exception: None
237
-
238
- (gdb) rb-object-print $errinfo
239
- (gdb) rb-object-print $ec->cfp->sp[-1]
240
- """
241
-
242
- def __init__(self):
243
- super(RubyContextCommand, self).__init__("rb-context", debugger.COMMAND_USER)
244
-
245
- def invoke(self, arg, from_tty):
246
- """Execute the rb-context command."""
247
- try:
248
- terminal = format.create_terminal(from_tty)
249
-
250
- # Get current execution context
251
- ctx = RubyContext.current()
252
-
253
- if ctx is None:
254
- print("Error: Could not get current execution context")
255
- print()
256
- print("Possible reasons:")
257
- print(" • Ruby symbols not loaded (compile with debug symbols)")
258
- print(" Process not stopped at a Ruby frame")
259
- print(" Ruby not fully initialized yet")
260
- print()
261
- print("Try:")
262
- print(" • Break at a Ruby function: break rb_vm_exec")
263
- print(" • Use rb-fiber-scan-switch to switch to a fiber")
264
- print(" Ensure Ruby debug symbols are available")
265
- return
266
-
267
- # Print context information
268
- ctx.print_info(terminal)
269
-
270
- # Set convenience variables
271
- vars = ctx.setup_convenience_variables()
272
-
273
- print()
274
- print("Convenience variables set:")
275
- print(f" $ec - Execution context")
276
- if vars.get('cfp'):
277
- print(f" $cfp - Control frame pointer")
278
- if vars.get('errinfo'):
279
- print(f" $errinfo - Exception object")
280
-
281
- print()
282
- print("Now you can use:")
283
- print(" rb-object-print $errinfo")
284
- print(" rb-object-print $ec->cfp->sp[-1]")
285
- print(" rb-stack-trace")
286
-
287
- except Exception as e:
288
- print(f"Error: {e}")
289
- import traceback
290
- traceback.print_exc()
237
+ class RubyContextHandler:
238
+ """Show current execution context and set convenience variables."""
239
+
240
+ USAGE = command.Usage(
241
+ summary="Show current execution context and set convenience variables",
242
+ parameters=[],
243
+ options={},
244
+ flags=[],
245
+ examples=[
246
+ ("rb-context", "Display execution context info"),
247
+ ("rb-context; rb-print $errinfo", "Show context then print exception")
248
+ ]
249
+ )
250
+
251
+ def invoke(self, arguments, terminal):
252
+ """Execute the rb-context command."""
253
+ try:
254
+ # Get current execution context
255
+ ctx = RubyContext.current()
256
+
257
+ if ctx is None:
258
+ print("Error: Could not get current execution context")
259
+ print()
260
+ print("Possible reasons:")
261
+ print(" • Ruby symbols not loaded (compile with debug symbols)")
262
+ print(" • Process not stopped at a Ruby frame")
263
+ print(" • Ruby not fully initialized yet")
264
+ print()
265
+ print("Try:")
266
+ print(" • Break at a Ruby function: break rb_vm_exec")
267
+ print(" • Use rb-fiber-scan-switch to switch to a fiber")
268
+ print(" • Ensure Ruby debug symbols are available")
269
+ return
270
+
271
+ # Print context information
272
+ ctx.print_info(terminal)
273
+
274
+ # Set convenience variables
275
+ vars = ctx.setup_convenience_variables()
276
+
277
+ print()
278
+ print("Convenience variables set:")
279
+ print(f" $ec - Execution context")
280
+ if vars.get('cfp'):
281
+ print(f" $cfp - Control frame pointer")
282
+ if vars.get('errinfo'):
283
+ print(f" $errinfo - Exception object")
284
+
285
+ print()
286
+ print("Now you can use:")
287
+ print(" rb-print $errinfo")
288
+ print(" rb-print $ec->cfp->sp[-1]")
289
+ print(" rb-stack-trace")
290
+
291
+ except Exception as e:
292
+ print(f"Error: {e}")
293
+ import traceback
294
+ traceback.print_exc()
291
295
 
292
296
 
293
- # Register command
294
- RubyContextCommand()
297
+ class RubyContextStorageHandler:
298
+ """Print the fiber storage from the current execution context."""
299
+
300
+ USAGE = command.Usage(
301
+ summary="Print fiber storage from current execution context",
302
+ parameters=[],
303
+ options={'depth': (int, 1, 'Recursion depth for nested objects')},
304
+ flags=[('debug', 'Show debug information')],
305
+ examples=[
306
+ ("rb-context-storage", "Print storage with default depth"),
307
+ ("rb-context-storage --depth 3", "Print storage with depth 3")
308
+ ]
309
+ )
310
+
311
+ def invoke(self, arguments, terminal):
312
+ """Execute the rb-context-storage command."""
313
+ try:
314
+ # Get current execution context
315
+ ctx = RubyContext.current()
316
+
317
+ if ctx is None:
318
+ print("Error: Could not get current execution context")
319
+ print("\nTry:")
320
+ print(" • Run rb-context first to set up execution context")
321
+ print(" • Break at a Ruby function")
322
+ print(" • Use rb-fiber-scan-switch to switch to a fiber")
323
+ return
324
+
325
+ # Get storage
326
+ storage_val = ctx.storage
327
+
328
+ if storage_val is None:
329
+ print("Error: Could not access fiber storage")
330
+ return
331
+
332
+ # Check if it's nil
333
+ if rvalue.is_nil(storage_val):
334
+ print("Fiber storage: nil")
335
+ return
336
+
337
+ # Parse arguments (--depth, --debug, etc.)
338
+ arguments = command.parse_arguments(arg if arg else "")
339
+
340
+ # Get depth flag
341
+ depth = 1 # Default depth
342
+ depth_str = arguments.get_option('depth')
343
+ if depth_str:
344
+ try:
345
+ depth = int(depth_str)
346
+ except ValueError:
347
+ print(f"Error: invalid depth '{depth_str}'")
348
+ return
349
+
350
+ # Get debug flag
351
+ debug = arguments.has_flag('debug')
352
+
353
+ # Use print module to print the storage
354
+ import print as print_module
355
+ printer = print_module.RubyObjectPrinter()
356
+
357
+ # Build arguments for the printer
358
+ flags_set = {'debug'} if debug else set()
359
+ args_for_printer = command.Arguments([storage_val], flags_set, {'depth': depth})
360
+
361
+ printer.invoke(args_for_printer, terminal)
362
+
363
+ except Exception as e:
364
+ print(f"Error: {e}")
365
+ import traceback
366
+ traceback.print_exc()
295
367
 
368
+
369
+ # Register commands
370
+ debugger.register("rb-context", RubyContextHandler, usage=RubyContextHandler.USAGE)
371
+ debugger.register("rb-context-storage", RubyContextStorageHandler, usage=RubyContextStorageHandler.USAGE)