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.
@@ -11,7 +11,8 @@ if debugger.DEBUGGER_NAME == 'gdb':
11
11
 
12
12
  # Import command parser
13
13
  import command
14
- import value
14
+ import constants
15
+ import rvalue
15
16
  import format
16
17
  import heap
17
18
  import rexception
@@ -26,860 +27,851 @@ _fiber_unwinder = None
26
27
  _current_fiber = None
27
28
 
28
29
  def parse_fiber_index(arg):
29
- """Parse fiber index from argument string.
30
-
31
- Returns:
32
- (index, error_message) - index is None if parsing failed
33
- """
34
- if not arg or not arg.strip():
35
- return None, "Usage: provide <index>"
36
-
37
- arguments = command.parse_arguments(arg)
38
-
39
- if not arguments.expressions:
40
- return None, "Error: No index provided"
41
-
42
- try:
43
- index = int(arguments.expressions[0])
44
- return index, None
45
- except ValueError:
46
- return None, f"Error: invalid index '{arguments.expressions[0]}'"
30
+ """Parse fiber index from argument string.
31
+
32
+ Returns:
33
+ (index, error_message) - index is None if parsing failed
34
+ """
35
+ if not arg or not arg.strip():
36
+ return None, "Usage: provide <index>"
37
+
38
+ arguments = command.parse_arguments(arg)
39
+
40
+ if not arguments.expressions:
41
+ return None, "Error: No index provided"
42
+
43
+ try:
44
+ index = int(arguments.expressions[0])
45
+ return index, None
46
+ except ValueError:
47
+ return None, f"Error: invalid index '{arguments.expressions[0]}'"
47
48
 
48
49
  def get_current_fiber():
49
- """Get the currently selected fiber (if any).
50
-
51
- Returns:
52
- RubyFiber instance or None
53
- """
54
- return _current_fiber
50
+ """Get the currently selected fiber (if any).
51
+
52
+ Returns:
53
+ RubyFiber instance or None
54
+ """
55
+ return _current_fiber
55
56
 
56
57
  def set_current_fiber(fiber):
57
- """Set the currently selected fiber.
58
-
59
- Args:
60
- fiber: RubyFiber instance or None
61
-
62
- Note: Should only be called by rb-fiber-switch command.
63
- """
64
- global _current_fiber
65
- _current_fiber = fiber
58
+ """Set the currently selected fiber.
59
+
60
+ Args:
61
+ fiber: RubyFiber instance or None
62
+
63
+ Note: Should only be called by rb-fiber-switch command.
64
+ """
65
+ global _current_fiber
66
+ _current_fiber = fiber
66
67
 
67
68
  class RubyFiber:
68
- """Wrapper for Ruby Fiber objects.
69
-
70
- Wraps a Fiber VALUE and provides high-level interface for fiber introspection.
71
- """
72
-
73
- # Fiber status constants
74
- FIBER_STATUS = {
75
- 0: "CREATED",
76
- 1: "RESUMED",
77
- 2: "SUSPENDED",
78
- 3: "TERMINATED"
79
- }
80
-
81
- def __init__(self, fiber_value):
82
- """Initialize with a Fiber VALUE.
83
-
84
- Args:
85
- fiber_value: A GDB value representing a Ruby Fiber object (VALUE)
86
- """
87
- self.value = fiber_value
88
- self._pointer = None
89
- self._ec = None
90
- self._exception = None
91
-
92
- def _extract_fiber_pointer(self):
93
- """Extract struct rb_fiber_struct* from the Fiber VALUE."""
94
- if self._pointer is None:
95
- # Cast to RTypedData and extract the data pointer
96
- rtypeddata_type = debugger.lookup_type('struct RTypedData').pointer()
97
- typed_data = self.value.cast(rtypeddata_type)
98
-
99
- rb_fiber_struct_type = debugger.lookup_type('struct rb_fiber_struct').pointer()
100
- self._pointer = typed_data['data'].cast(rb_fiber_struct_type)
101
-
102
- return self._pointer
103
-
104
- @property
105
- def pointer(self):
106
- """Get the struct rb_fiber_struct* pointer."""
107
- return self._extract_fiber_pointer()
108
-
109
- @property
110
- def address(self):
111
- """Get the raw address of the fiber struct."""
112
- return int(self.pointer)
113
-
114
- @property
115
- def status(self):
116
- """Get fiber status as string (CREATED, RESUMED, etc.)."""
117
- status_code = int(self.pointer['status'])
118
- return self.FIBER_STATUS.get(status_code, f"UNKNOWN({status_code})")
119
-
120
- @property
121
- def stack_base(self):
122
- """Get fiber stack base pointer."""
123
- return self.pointer['stack']['base']
124
-
125
- @property
126
- def stack_size(self):
127
- """Get fiber stack size."""
128
- return int(self.pointer['stack']['size'])
129
-
130
- @property
131
- def ec(self):
132
- """Get execution context (rb_execution_context_t*)."""
133
- if self._ec is None:
134
- self._ec = self.pointer['cont']['saved_ec'].address
135
- return self._ec
136
-
137
- @property
138
- def vm_stack(self):
139
- """Get VM stack pointer."""
140
- return self.ec['vm_stack']
141
-
142
- @property
143
- def vm_stack_size(self):
144
- """Get VM stack size."""
145
- return int(self.ec['vm_stack_size'])
146
-
147
- @property
148
- def cfp(self):
149
- """Get control frame pointer."""
150
- return self.ec['cfp']
151
-
152
- @property
153
- def exception(self):
154
- """Get current exception RException object (if any).
155
-
156
- Returns:
157
- RException instance or None
158
- """
159
- if self._exception is None:
160
- try:
161
- errinfo_val = self.ec['errinfo']
162
-
163
- # Only process if it's a real object (not nil or other immediate value)
164
- if value.is_object(errinfo_val) and not value.is_nil(errinfo_val):
165
- try:
166
- self._exception = rexception.RException(errinfo_val)
167
- except Exception:
168
- # If we can't create RException, return None
169
- pass
170
- except Exception:
171
- pass
172
-
173
- return self._exception
174
-
175
- @property
176
- def exception_info(self):
177
- """Get formatted exception string (if any).
178
-
179
- Returns:
180
- Formatted exception string or None
181
- """
182
- exc = self.exception
183
- if exc:
184
- return str(exc)
185
- return None
186
-
187
- def print_info(self, terminal):
188
- """Print summary information about this fiber.
189
-
190
- Args:
191
- terminal: Terminal instance for formatted output
192
- """
193
- # Print fiber VALUE and address
194
- print(f"Fiber VALUE: ", end='')
195
- print(terminal.print_type_tag('T_DATA', int(self.value), None))
196
- print(f" Address: ", end='')
197
- print(terminal.print_type_tag('struct rb_fiber_struct', self.address, None))
198
-
199
- # Print status
200
- print(f" Status: {self.status}")
201
-
202
- # Print exception if present
203
- exc_info = self.exception_info
204
- if exc_info:
205
- print(f" Exception: {exc_info}")
206
-
207
- # Print Stack with formatted pointer
208
- stack_type = str(self.stack_base.type)
209
- print(f" Stack: ", end='')
210
- print(terminal.print_type_tag(stack_type, int(self.stack_base), f'size={self.stack_size}'))
211
-
212
- # Print VM Stack with formatted pointer
213
- vm_stack_type = str(self.vm_stack.type)
214
- print(f" VM Stack: ", end='')
215
- print(terminal.print_type_tag(vm_stack_type, int(self.vm_stack), f'size={self.vm_stack_size}'))
216
-
217
- # Print CFP
218
- print(f" CFP: ", end='')
219
- print(terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None))
220
-
221
-
222
- class RubyFiberScanHeapCommand(debugger.Command):
223
- """Scan heap and list all Ruby fibers."""
224
-
225
- def __init__(self):
226
- super(RubyFiberScanHeapCommand, self).__init__("rb-fiber-scan-heap", debugger.COMMAND_USER)
227
- self.heap = heap.RubyHeap()
228
-
229
- def usage(self):
230
- """Print usage information."""
231
- print("Usage: rb-fiber-scan-heap [--limit N] [--cache [filename]] [--terminated]")
232
- print("Examples:")
233
- print(" rb-fiber-scan-heap # Find all non-terminated fibers")
234
- print(" rb-fiber-scan-heap --limit 10 # Find first 10 non-terminated fibers")
235
- print(" rb-fiber-scan-heap --terminated # Include terminated fibers")
236
- print(" rb-fiber-scan-heap --cache # Use fibers.json cache")
237
- print(" rb-fiber-scan-heap --cache my.json # Use custom cache file")
238
-
239
- def save_cache(self, fiber_values, filename):
240
- """Save fiber VALUE addresses to cache file.
241
-
242
- Args:
243
- fiber_values: List of Fiber VALUEs
244
- filename: Path to cache file
245
- """
246
- try:
247
- data = {
248
- 'version': 1,
249
- 'fiber_count': len(fiber_values),
250
- 'fibers': [int(f) for f in fiber_values] # Store VALUE addresses
251
- }
252
- with open(filename, 'w') as f:
253
- json.dump(data, f, indent=2)
254
- print(f"Saved {len(fiber_values)} fiber VALUE(s) to {filename}")
255
- return True
256
- except Exception as e:
257
- print(f"Warning: Failed to save cache: {e}")
258
- return False
259
-
260
- def load_cache(self, filename):
261
- """Load fiber VALUE addresses from cache file.
262
-
263
- Args:
264
- filename: Path to cache file
265
-
266
- Returns:
267
- List of VALUEs or None if loading failed
268
- """
269
- try:
270
- with open(filename, 'r') as f:
271
- data = json.load(f)
272
-
273
- if data.get('version') != 1:
274
- print(f"Warning: Unknown cache version, ignoring cache")
275
- return None
276
-
277
- fiber_addrs = data.get('fibers', [])
278
- print(f"Loaded {len(fiber_addrs)} fiber VALUE address(es) from {filename}")
279
-
280
- # Initialize heap to ensure we have type information
281
- if not self.heap.initialize():
282
- return None
283
-
284
- # Reconstruct VALUEs from addresses
285
- value_type = debugger.lookup_type('VALUE')
286
- fibers = []
287
- for addr in fiber_addrs:
288
- try:
289
- fiber_val = debugger.create_value(addr, value_type)
290
- fibers.append(fiber_val)
291
- except (debugger.Error, debugger.MemoryError):
292
- print(f"Warning: Could not access VALUE at 0x{addr:x}")
293
-
294
- print(f"Successfully reconstructed {len(fibers)} fiber VALUE(s)")
295
- return fibers
296
-
297
- except FileNotFoundError:
298
- return None
299
- except Exception as e:
300
- print(f"Warning: Failed to load cache: {e}")
301
- return None
302
-
303
- def invoke(self, arg, from_tty):
304
- global _fiber_cache
305
-
306
- # Create terminal for formatting
307
- terminal = format.create_terminal(from_tty)
308
-
309
- # Parse arguments using the robust parser
310
- arguments = command.parse_arguments(arg if arg else "")
311
-
312
- # Get limit from --limit option
313
- limit = None
314
- limit_str = arguments.get_option('limit')
315
- if limit_str:
316
- try:
317
- limit = int(limit_str)
318
- if limit <= 0:
319
- print("Error: limit must be positive")
320
- self.usage()
321
- return
322
- except ValueError:
323
- print(f"Error: invalid limit '{limit_str}'")
324
- self.usage()
325
- return
326
-
327
- # Check for --cache flag
328
- use_cache = arguments.has_flag('cache')
329
- cache_file = arguments.get_option('cache', 'fibers.json')
330
-
331
- # Check for --terminated flag
332
- include_terminated = arguments.has_flag('terminated')
333
-
334
- # Try to load from cache if requested
335
- if use_cache:
336
- loaded_fibers = self.load_cache(cache_file)
337
- if loaded_fibers is not None:
338
- # Filter out terminated fibers unless --terminated is specified
339
- if not include_terminated:
340
- filtered_fibers = []
341
- for fiber_val in loaded_fibers:
342
- try:
343
- fiber_obj = RubyFiber(fiber_val)
344
- if fiber_obj.status != "TERMINATED":
345
- filtered_fibers.append(fiber_val)
346
- except:
347
- # Keep fibers we can't inspect
348
- filtered_fibers.append(fiber_val)
349
- loaded_fibers = filtered_fibers
350
-
351
- # Successfully loaded from cache
352
- _fiber_cache = loaded_fibers
353
-
354
- print(f"\nLoaded {len(loaded_fibers)} fiber(s) from cache:\n")
355
-
356
- for i, fiber_val in enumerate(loaded_fibers):
357
- try:
358
- fiber_obj = RubyFiber(fiber_val)
359
- self._print_fiber_info(terminal, i, fiber_obj)
360
- except:
361
- print(f"Fiber #{i}: VALUE 0x{int(fiber_val):x}")
362
- print(f" (error creating RubyFiber)")
363
- print()
364
-
365
- print(f"Fibers cached. Use 'rb-fiber-scan-switch <index>' to switch to a fiber.")
366
- return
367
- else:
368
- print(f"Cache file '{cache_file}' not found, proceeding with scan...")
369
- print()
370
-
371
- # Initialize heap scanner
372
- if not self.heap.initialize():
373
- return
374
-
375
- # Get fiber_data_type for matching
376
- fiber_data_type = debugger.parse_and_eval('&fiber_data_type')
377
-
378
- if limit:
379
- print(f"Scanning heap for first {limit} Fiber object(s)...", file=sys.stderr)
380
- else:
381
- print("Scanning heap for Fiber objects...", file=sys.stderr)
382
-
383
- # Use RubyHeap to find fibers (returns VALUEs)
384
- fiber_values = self.heap.find_typed_data(fiber_data_type, limit=limit, progress=True)
385
-
386
- # Filter out terminated fibers unless --terminated is specified
387
- if not include_terminated:
388
- filtered_fibers = []
389
- for fiber_val in fiber_values:
390
- try:
391
- fiber_obj = RubyFiber(fiber_val)
392
- if fiber_obj.status != "TERMINATED":
393
- filtered_fibers.append(fiber_val)
394
- except:
395
- # Keep fibers we can't inspect
396
- filtered_fibers.append(fiber_val)
397
- fiber_values = filtered_fibers
398
-
399
- # Cache the VALUEs for later use
400
- _fiber_cache = fiber_values
401
-
402
- if limit and len(fiber_values) >= limit:
403
- print(f"Found {len(fiber_values)} fiber(s) (limit reached):\n")
404
- else:
405
- print(f"Found {len(fiber_values)} fiber(s):\n")
406
-
407
- for i, fiber_val in enumerate(fiber_values):
408
- fiber_obj = RubyFiber(fiber_val)
409
- self._print_fiber_info(terminal, i, fiber_obj)
410
-
411
- # Save to cache if requested
412
- if use_cache and fiber_values:
413
- self.save_cache(fiber_values, cache_file)
414
- print()
415
-
416
- print(f"Fibers cached. Use 'rb-fiber-scan-switch <index>' to switch to a fiber.")
417
-
418
- def _print_fiber_info(self, terminal, index, fiber_obj):
419
- """Print formatted fiber information.
420
-
421
- Args:
422
- terminal: Terminal instance for formatting
423
- index: Fiber index in cache
424
- fiber_obj: RubyFiber instance
425
- """
426
- # Print fiber index with VALUE and pointer
427
- print(f"Fiber #{index}: ", end='')
428
- print(terminal.print_type_tag('T_DATA', int(fiber_obj.value)), end='')
429
- print(' ', end='')
430
- print(terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address))
431
-
432
- # Print status
433
- print(f" Status: {fiber_obj.status}")
434
-
435
- # Print exception if present (catch errors for terminated fibers)
436
- try:
437
- exc_info = fiber_obj.exception_info
438
- if exc_info:
439
- print(f" Exception: {exc_info}")
440
- except Exception:
441
- # Silently skip exception info if we can't read it
442
- pass
443
-
444
- # Print Stack with formatted pointer
445
- stack_type = str(fiber_obj.stack_base.type)
446
- print(f" Stack: ", end='')
447
- print(terminal.print_type_tag(stack_type, int(fiber_obj.stack_base), f'size={fiber_obj.stack_size}'))
448
-
449
- # Print VM Stack with formatted pointer
450
- vm_stack_type = str(fiber_obj.vm_stack.type)
451
- print(f" VM Stack: ", end='')
452
- print(terminal.print_type_tag(vm_stack_type, int(fiber_obj.vm_stack), f'size={fiber_obj.vm_stack_size}'))
453
-
454
- # Print CFP
455
- cfp_type = str(fiber_obj.cfp.type).replace(' *', '') # Remove pointer marker for display
456
- print(f" CFP: ", end='')
457
- print(terminal.print_type_tag(cfp_type, int(fiber_obj.cfp)))
458
- print()
459
-
460
-
461
- class RubyFiberScanSwitchCommand(debugger.Command):
462
- """Switch to a fiber from the scan heap cache.
463
-
464
- Usage: rb-fiber-scan-switch <index>
465
- Example: rb-fiber-scan-switch 0
466
- rb-fiber-scan-switch 2
467
-
468
- Note: This command requires a fiber cache populated by 'rb-fiber-scan-heap'.
469
- """
470
-
471
- def __init__(self):
472
- super(RubyFiberScanSwitchCommand, self).__init__("rb-fiber-scan-switch", debugger.COMMAND_USER)
473
-
474
- def usage(self):
475
- """Print usage information."""
476
- print("Usage: rb-fiber-scan-switch <index>")
477
- print("Examples:")
478
- print(" rb-fiber-scan-switch 0 # Switch to fiber #0")
479
- print(" rb-fiber-scan-switch 2 # Switch to fiber #2")
480
- print()
481
- print("Note: Run 'rb-fiber-scan-heap' first to populate the fiber cache.")
482
-
483
- def invoke(self, arg, from_tty):
484
- global _fiber_cache
485
-
486
- if not arg or not arg.strip():
487
- self.usage()
488
- return
489
-
490
- # Check if cache is populated
491
- if not _fiber_cache:
492
- print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.")
493
- return
494
-
495
- # Parse index
496
- try:
497
- index = int(arg.strip())
498
- except ValueError:
499
- print(f"Error: Invalid index '{arg}'. Must be an integer.")
500
- self.usage()
501
- return
502
-
503
- # Validate index
504
- if index < 0 or index >= len(_fiber_cache):
505
- print(f"Error: Index {index} out of range [0, {len(_fiber_cache)-1}]")
506
- print(f"\nRun 'rb-fiber-scan-heap' to see available fibers.")
507
- return
508
-
509
- # Get fiber VALUE from cache
510
- fiber_value = _fiber_cache[index]
511
-
512
- print(f"Switching to Fiber #{index}: VALUE 0x{int(fiber_value):x}")
513
-
514
- # Delegate to rb-fiber-switch command
515
- # This command manages the global _current_fiber state
516
- try:
517
- debugger.execute(f"rb-fiber-switch 0x{int(fiber_value):x}", from_tty=from_tty)
518
- except debugger.Error as e:
519
- print(f"Error switching to fiber: {e}")
520
- import traceback
521
- traceback.print_exc()
69
+ """Wrapper for Ruby Fiber objects.
70
+
71
+ Wraps a Fiber VALUE and provides high-level interface for fiber introspection.
72
+ """
73
+
74
+ # Fiber status constants
75
+ FIBER_STATUS = {
76
+ 0: "CREATED",
77
+ 1: "RESUMED",
78
+ 2: "SUSPENDED",
79
+ 3: "TERMINATED"
80
+ }
81
+
82
+ def __init__(self, fiber_value):
83
+ """Initialize with a Fiber VALUE.
84
+
85
+ Args:
86
+ fiber_value: A GDB value representing a Ruby Fiber object (VALUE)
87
+ """
88
+ self.value = fiber_value
89
+ self._pointer = None
90
+ self._ec = None
91
+ self._exception = None
92
+
93
+ def _extract_fiber_pointer(self):
94
+ """Extract struct rb_fiber_struct* from the Fiber VALUE."""
95
+ if self._pointer is None:
96
+ # Cast to RTypedData and extract the data pointer
97
+ rtypeddata_type = constants.type_struct('struct RTypedData').pointer()
98
+ typed_data = self.value.cast(rtypeddata_type)
99
+
100
+ rb_fiber_struct_type = constants.type_struct('struct rb_fiber_struct').pointer()
101
+ self._pointer = typed_data['data'].cast(rb_fiber_struct_type)
102
+
103
+ return self._pointer
104
+
105
+ @property
106
+ def pointer(self):
107
+ """Get the struct rb_fiber_struct* pointer."""
108
+ return self._extract_fiber_pointer()
109
+
110
+ @property
111
+ def address(self):
112
+ """Get the raw address of the fiber struct."""
113
+ return int(self.pointer)
114
+
115
+ @property
116
+ def status(self):
117
+ """Get fiber status as string (CREATED, RESUMED, etc.)."""
118
+ status_code = int(self.pointer['status'])
119
+ return self.FIBER_STATUS.get(status_code, f"UNKNOWN({status_code})")
120
+
121
+ @property
122
+ def stack_base(self):
123
+ """Get fiber stack base pointer."""
124
+ return self.pointer['stack']['base']
125
+
126
+ @property
127
+ def stack_size(self):
128
+ """Get fiber stack size."""
129
+ return int(self.pointer['stack']['size'])
130
+
131
+ @property
132
+ def ec(self):
133
+ """Get execution context (rb_execution_context_t*)."""
134
+ if self._ec is None:
135
+ self._ec = self.pointer['cont']['saved_ec'].address
136
+ return self._ec
137
+
138
+ @property
139
+ def vm_stack(self):
140
+ """Get VM stack pointer."""
141
+ return self.ec['vm_stack']
142
+
143
+ @property
144
+ def vm_stack_size(self):
145
+ """Get VM stack size."""
146
+ return int(self.ec['vm_stack_size'])
147
+
148
+ @property
149
+ def cfp(self):
150
+ """Get control frame pointer."""
151
+ return self.ec['cfp']
152
+
153
+ @property
154
+ def exception(self):
155
+ """Get current exception RException object (if any).
156
+
157
+ Returns:
158
+ RException instance or None
159
+ """
160
+ if self._exception is None:
161
+ try:
162
+ errinfo_val = self.ec['errinfo']
163
+
164
+ # Only process if it's a real object (not nil or other immediate value)
165
+ if rvalue.is_object(errinfo_val) and not rvalue.is_nil(errinfo_val):
166
+ try:
167
+ self._exception = rexception.RException(errinfo_val)
168
+ except Exception:
169
+ # If we can't create RException, return None
170
+ pass
171
+ except Exception:
172
+ pass
173
+
174
+ return self._exception
175
+
176
+ @property
177
+ def exception_info(self):
178
+ """Get formatted exception string (if any).
179
+
180
+ Returns:
181
+ Formatted exception string or None
182
+ """
183
+ exc = self.exception
184
+ if exc:
185
+ return str(exc)
186
+ return None
187
+
188
+ def print_info(self, terminal):
189
+ """Print summary information about this fiber.
190
+
191
+ Args:
192
+ terminal: Terminal instance for formatted output
193
+ """
194
+ # Print fiber VALUE and address
195
+ print(f"Fiber VALUE: ", end='')
196
+ terminal.print_type_tag('T_DATA', int(self.value), None)
197
+ print()
198
+ print(f" Address: ", end='')
199
+ terminal.print_type_tag('struct rb_fiber_struct', self.address, None)
200
+ print()
201
+
202
+ # Print status
203
+ print(f" Status: {self.status}")
204
+
205
+ # Print exception if present
206
+ exc_info = self.exception_info
207
+ if exc_info:
208
+ print(f" Exception: {exc_info}")
209
+
210
+ # Print Stack with formatted pointer
211
+ stack_type = str(self.stack_base.type)
212
+ print(f" Stack: ", end='')
213
+ terminal.print_type_tag(stack_type, int(self.stack_base))
214
+ print()
215
+
216
+ # Print VM Stack with formatted pointer
217
+ vm_stack_type = str(self.vm_stack.type)
218
+ print(f" VM Stack: ", end='')
219
+ terminal.print_type_tag(vm_stack_type, int(self.vm_stack))
220
+ print()
221
+
222
+ # Print CFP
223
+ print(f" CFP: ", end='')
224
+ terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None)
225
+ print()
226
+
227
+
228
+ class RubyFiberScanHeapHandler:
229
+ """Scan heap and list all Ruby fibers."""
230
+
231
+ USAGE = command.Usage(
232
+ summary="Scan heap and list all Ruby fibers",
233
+ parameters=[],
234
+ options={
235
+ 'limit': (int, None, 'Maximum fibers to find'),
236
+ 'cache': (str, None, 'Cache file to use (default: fibers.json)')
237
+ },
238
+ flags=[
239
+ ('terminated', 'Include terminated fibers in results')
240
+ ],
241
+ examples=[
242
+ ("rb-fiber-scan-heap", "Find all non-terminated fibers"),
243
+ ("rb-fiber-scan-heap --limit 10", "Find first 10 fibers"),
244
+ ("rb-fiber-scan-heap --terminated", "Include terminated fibers"),
245
+ ("rb-fiber-scan-heap --cache my.json", "Use custom cache file")
246
+ ]
247
+ )
248
+
249
+ def __init__(self):
250
+ self.heap = heap.RubyHeap()
251
+
252
+ def save_cache(self, fiber_values, filename):
253
+ """Save fiber VALUE addresses to cache file.
254
+
255
+ Args:
256
+ fiber_values: List of Fiber VALUEs
257
+ filename: Path to cache file
258
+ """
259
+ try:
260
+ data = {
261
+ 'version': 1,
262
+ 'fiber_count': len(fiber_values),
263
+ 'fibers': [int(f) for f in fiber_values] # Store VALUE addresses
264
+ }
265
+ with open(filename, 'w') as f:
266
+ json.dump(data, f, indent=2)
267
+ print(f"Saved {len(fiber_values)} fiber VALUE(s) to {filename}")
268
+ return True
269
+ except Exception as e:
270
+ print(f"Warning: Failed to save cache: {e}")
271
+ return False
272
+
273
+ def load_cache(self, filename):
274
+ """Load fiber VALUE addresses from cache file.
275
+
276
+ Args:
277
+ filename: Path to cache file
278
+
279
+ Returns:
280
+ List of VALUEs or None if loading failed
281
+ """
282
+ try:
283
+ with open(filename, 'r') as f:
284
+ data = json.load(f)
285
+
286
+ if data.get('version') != 1:
287
+ print(f"Warning: Unknown cache version, ignoring cache")
288
+ return None
289
+
290
+ fiber_addrs = data.get('fibers', [])
291
+ print(f"Loaded {len(fiber_addrs)} fiber VALUE address(es) from {filename}")
292
+
293
+ # Initialize heap to ensure we have type information
294
+ if not self.heap.initialize():
295
+ return None
296
+
297
+ # Reconstruct VALUEs from addresses
298
+ value_type = constants.type_struct('VALUE')
299
+ fibers = []
300
+ for addr in fiber_addrs:
301
+ try:
302
+ fiber_val = debugger.create_value(addr, value_type)
303
+ fibers.append(fiber_val)
304
+ except (debugger.Error, debugger.MemoryError):
305
+ print(f"Warning: Could not access VALUE at 0x{addr:x}")
306
+
307
+ print(f"Successfully reconstructed {len(fibers)} fiber VALUE(s)")
308
+ return fibers
309
+
310
+ except FileNotFoundError:
311
+ return None
312
+ except Exception as e:
313
+ print(f"Warning: Failed to load cache: {e}")
314
+ return None
315
+
316
+ def invoke(self, arguments, terminal):
317
+ global _fiber_cache
318
+
319
+ # Get limit from --limit option
320
+ limit = None
321
+ limit_str = arguments.get_option('limit')
322
+ if limit_str:
323
+ try:
324
+ limit = int(limit_str)
325
+ if limit <= 0:
326
+ print("Error: limit must be positive")
327
+ self.usage()
328
+ return
329
+ except ValueError:
330
+ print(f"Error: invalid limit '{limit_str}'")
331
+ self.usage()
332
+ return
333
+
334
+ # Check for --cache flag
335
+ use_cache = arguments.has_flag('cache')
336
+ cache_file = arguments.get_option('cache', 'fibers.json')
337
+
338
+ # Check for --terminated flag
339
+ include_terminated = arguments.has_flag('terminated')
340
+
341
+ # Try to load from cache if requested
342
+ if use_cache:
343
+ loaded_fibers = self.load_cache(cache_file)
344
+ if loaded_fibers is not None:
345
+ # Filter out terminated fibers unless --terminated is specified
346
+ if not include_terminated:
347
+ filtered_fibers = []
348
+ for fiber_val in loaded_fibers:
349
+ try:
350
+ fiber_obj = RubyFiber(fiber_val)
351
+ if fiber_obj.status != "TERMINATED":
352
+ filtered_fibers.append(fiber_val)
353
+ except:
354
+ # Keep fibers we can't inspect
355
+ filtered_fibers.append(fiber_val)
356
+ loaded_fibers = filtered_fibers
357
+
358
+ # Successfully loaded from cache
359
+ _fiber_cache = loaded_fibers
360
+
361
+ print(f"\nLoaded {len(loaded_fibers)} fiber(s) from cache:\n")
362
+
363
+ for i, fiber_val in enumerate(loaded_fibers):
364
+ try:
365
+ fiber_obj = RubyFiber(fiber_val)
366
+ self._print_fiber_info(terminal, i, fiber_obj)
367
+ except:
368
+ print(f"Fiber #{i}: VALUE 0x{int(fiber_val):x}")
369
+ print(f" (error creating RubyFiber)")
370
+ print()
371
+
372
+ print(f"Fibers cached. Use 'rb-fiber-scan-switch <index>' to switch to a fiber.")
373
+ return
374
+ else:
375
+ print(f"Cache file '{cache_file}' not found, proceeding with scan...")
376
+ print()
377
+
378
+ # Initialize heap scanner
379
+ if not self.heap.initialize():
380
+ return
381
+
382
+ # Get fiber_data_type for matching
383
+ try:
384
+ fiber_data_type = debugger.parse_and_eval('&fiber_data_type')
385
+ if fiber_data_type is None or int(fiber_data_type) == 0:
386
+ print("Error: Could not find 'fiber_data_type' symbol")
387
+ print("\nThis usually means:")
388
+ print(" • Ruby debug symbols are not available")
389
+ print(" • Ruby version doesn't export this symbol")
390
+ print("\nTo fix:")
391
+ print(" • Install Ruby with debug symbols")
392
+ print(" • On macOS: brew install ruby (includes debug info)")
393
+ print(" • Or compile Ruby with --enable-debug-symbols")
394
+ return
395
+ except debugger.Error as e:
396
+ print(f"Error: Could not evaluate '&fiber_data_type': {e}")
397
+ print("\nRuby debug symbols may not be available.")
398
+ return
399
+
400
+ if limit:
401
+ print(f"Scanning heap for first {limit} Fiber object(s)...", file=sys.stderr)
402
+ else:
403
+ print("Scanning heap for Fiber objects...", file=sys.stderr)
404
+
405
+ # Use RubyHeap to find fibers (returns VALUEs)
406
+ fiber_values = self.heap.find_typed_data(fiber_data_type, limit=limit, progress=True)
407
+
408
+ # Filter out terminated fibers unless --terminated is specified
409
+ if not include_terminated:
410
+ filtered_fibers = []
411
+ for fiber_val in fiber_values:
412
+ try:
413
+ fiber_obj = RubyFiber(fiber_val)
414
+ if fiber_obj.status != "TERMINATED":
415
+ filtered_fibers.append(fiber_val)
416
+ except:
417
+ # Keep fibers we can't inspect
418
+ filtered_fibers.append(fiber_val)
419
+ fiber_values = filtered_fibers
420
+
421
+ # Cache the VALUEs for later use
422
+ _fiber_cache = fiber_values
423
+
424
+ if limit and len(fiber_values) >= limit:
425
+ print(f"Found {len(fiber_values)} fiber(s) (limit reached):\n")
426
+ else:
427
+ print(f"Found {len(fiber_values)} fiber(s):\n")
428
+
429
+ for i, fiber_val in enumerate(fiber_values):
430
+ fiber_obj = RubyFiber(fiber_val)
431
+ self._print_fiber_info(terminal, i, fiber_obj)
432
+
433
+ # Save to cache if requested
434
+ if use_cache and fiber_values:
435
+ self.save_cache(fiber_values, cache_file)
436
+ print()
437
+
438
+ print(f"Fibers cached. Use 'rb-fiber-scan-switch <index>' to switch to a fiber.")
439
+
440
+ def _print_fiber_info(self, terminal, index, fiber_obj):
441
+ """Print formatted fiber information.
442
+
443
+ Args:
444
+ terminal: Terminal instance for formatting
445
+ index: Fiber index in cache
446
+ fiber_obj: RubyFiber instance
447
+ """
448
+ # Print fiber index with VALUE and pointer
449
+ print(f"Fiber #{index}: ", end='')
450
+ terminal.print_type_tag('T_DATA', int(fiber_obj.value))
451
+ print(' → ', end='')
452
+ terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address)
453
+ print()
454
+
455
+ # Print status
456
+ print(f" Status: {fiber_obj.status}")
457
+
458
+ # Print exception if present (catch errors for terminated fibers)
459
+ try:
460
+ exc_info = fiber_obj.exception_info
461
+ if exc_info:
462
+ print(f" Exception: {exc_info}")
463
+ except Exception:
464
+ # Silently skip exception info if we can't read it
465
+ pass
466
+
467
+ # Print Stack with formatted pointer
468
+ stack_type = str(fiber_obj.stack_base.type)
469
+ print(f" Stack: ", end='')
470
+ terminal.print_type_tag(stack_type, int(fiber_obj.stack_base))
471
+ print()
472
+
473
+ # Print VM Stack with formatted pointer
474
+ vm_stack_type = str(fiber_obj.vm_stack.type)
475
+ print(f" VM Stack: ", end='')
476
+ terminal.print_type_tag(vm_stack_type, int(fiber_obj.vm_stack))
477
+ print()
478
+
479
+ # Print CFP
480
+ cfp_type = str(fiber_obj.cfp.type).replace(' *', '') # Remove pointer marker for display
481
+ print(f" CFP: ", end='')
482
+ terminal.print_type_tag(cfp_type, int(fiber_obj.cfp))
483
+ print()
484
+ print()
485
+
486
+
487
+ class RubyFiberScanSwitchHandler:
488
+ """Switch to a fiber from the scan heap cache."""
489
+
490
+ USAGE = command.Usage(
491
+ summary="Switch to a fiber from scan cache by index",
492
+ parameters=[('index', 'Fiber index from rb-fiber-scan-heap')],
493
+ options={},
494
+ flags=[],
495
+ examples=[
496
+ ("rb-fiber-scan-switch 0", "Switch to first fiber"),
497
+ ("rb-fiber-scan-switch 2", "Switch to third fiber")
498
+ ]
499
+ )
500
+
501
+ def invoke(self, arguments, terminal):
502
+ global _fiber_cache
503
+
504
+ if not arguments.expressions or not arguments.expressions[0].strip():
505
+ command.print_usage(RubyFiberScanSwitchHandler.USAGE, terminal)
506
+ return
507
+
508
+ # Check if cache is populated
509
+ if not _fiber_cache:
510
+ print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.")
511
+ return
512
+
513
+ # Parse index
514
+ try:
515
+ index = int(arguments.expressions[0].strip())
516
+ except ValueError:
517
+ print(f"Error: Invalid index '{arguments.expressions[0]}'. Must be an integer.")
518
+ command.print_usage(RubyFiberScanSwitchHandler.USAGE, terminal)
519
+ return
520
+
521
+ # Validate index
522
+ if index < 0 or index >= len(_fiber_cache):
523
+ print(f"Error: Index {index} out of range [0, {len(_fiber_cache)-1}]")
524
+ print(f"\nRun 'rb-fiber-scan-heap' to see available fibers.")
525
+ return
526
+
527
+ # Get fiber VALUE from cache
528
+ fiber_value = _fiber_cache[index]
529
+
530
+ print(f"Switching to Fiber #{index}: VALUE 0x{int(fiber_value):x}")
531
+
532
+ # Delegate to rb-fiber-switch command
533
+ # This command manages the global _current_fiber state
534
+ try:
535
+ RubyFiberSwitchHandler().invoke(command.Arguments([f"0x{int(fiber_value):x}"], {}, []), terminal)
536
+ except debugger.Error as e:
537
+ print(f"Error switching to fiber: {e}")
538
+ import traceback
539
+ traceback.print_exc()
522
540
 
523
541
 
524
542
  # GDB-specific unwinder class - only available when running under GDB
525
543
  if debugger.DEBUGGER_NAME == 'gdb':
526
- class RubyFiberUnwinder(gdb.unwinder.Unwinder):
527
- """Custom unwinder for Ruby fibers.
528
-
529
- This allows GDB to unwind a fiber's stack even in a core dump,
530
- by extracting saved register state from the fiber's jmp_buf.
531
-
532
- Based on similar technique from Facebook Folly:
533
- https://github.com/facebook/folly/blob/main/folly/fibers/scripts/gdb.py
534
- """
535
-
536
- def __init__(self):
537
- super(RubyFiberUnwinder, self).__init__("Ruby Fiber Unwinder")
538
- self.active_fiber = None
539
- self.unwound_first_frame = False
540
-
541
- def __call__(self, pending_frame):
542
- """Called by GDB when unwinding frames."""
543
- # Only unwind if we have an active fiber set
544
- if not self.active_fiber:
545
- return None
546
-
547
- # Only unwind the first frame, then let GDB continue normally
548
- if self.unwound_first_frame:
549
- return None
550
-
551
- try:
552
- # Ruby uses its own coroutine implementation, not setjmp/longjmp!
553
- # Registers are saved in fiber->context.stack_pointer
554
- # See coroutine/amd64/Context.S for the layout
555
-
556
- coroutine_ctx = self.active_fiber['context']
557
- stack_ptr = coroutine_ctx['stack_pointer']
558
-
559
- # The stack_pointer points to the saved register area
560
- # From Context.S (x86-64):
561
- # [stack_pointer + 0] = R15
562
- # [stack_pointer + 8] = R14
563
- # [stack_pointer + 16] = R13
564
- # [stack_pointer + 24] = R12
565
- # [stack_pointer + 32] = RBX
566
- # [stack_pointer + 40] = RBP
567
- # [stack_pointer + 48] = Return address (RIP)
568
-
569
- if int(stack_ptr) == 0:
570
- return None
571
-
572
- # Cast to uint64 pointer to read saved registers
573
- uint64_ptr = stack_ptr.cast(gdb.lookup_type('uint64_t').pointer())
574
-
575
- # Read saved registers (keep as gdb.Value)
576
- r15 = uint64_ptr[0]
577
- r14 = uint64_ptr[1]
578
- r13 = uint64_ptr[2]
579
- r12 = uint64_ptr[3]
580
- rbx = uint64_ptr[4]
581
- rbp = uint64_ptr[5]
582
-
583
- # After coroutine_transfer executes 'addq $48, %rsp', RSP points to the return address
584
- # After 'ret' pops the return address, RSP = stack_ptr + 48 + 8
585
- # We want to create an unwind frame AS IF we're in the caller of coroutine_transfer
586
- # So RSP should be pointing AFTER the return address was popped
587
- rsp_value = int(stack_ptr) + 48 + 8
588
- rsp = gdb.Value(rsp_value).cast(gdb.lookup_type('uint64_t'))
589
-
590
- # The return address (RIP) is at [stack_ptr + 48]
591
- # This is what 'ret' will pop and jump to
592
- rip_ptr = gdb.Value(int(stack_ptr) + 48).cast(gdb.lookup_type('uint64_t').pointer())
593
- rip = rip_ptr.dereference()
594
-
595
- # Sanity check
596
- if int(rsp) == 0 or int(rip) == 0:
597
- return None
598
-
599
- # Create frame ID
600
- frame_id = gdb.unwinder.FrameId(int(rsp), int(rip))
601
-
602
- # Create unwind info
603
- unwind_info = pending_frame.create_unwind_info(frame_id)
604
-
605
- # Add saved registers
606
- unwind_info.add_saved_register("rip", rip)
607
- unwind_info.add_saved_register("rsp", rsp)
608
- unwind_info.add_saved_register("rbp", rbp)
609
- unwind_info.add_saved_register("rbx", rbx)
610
- unwind_info.add_saved_register("r12", r12)
611
- unwind_info.add_saved_register("r13", r13)
612
- unwind_info.add_saved_register("r14", r14)
613
- unwind_info.add_saved_register("r15", r15)
614
-
615
- # Mark that we've unwound the first frame
616
- self.unwound_first_frame = True
617
-
618
- return unwind_info
619
-
620
- except (gdb.error, gdb.MemoryError) as e:
621
- # If we can't read the fiber context, bail
622
- return None
623
-
624
- def activate_fiber(self, fiber):
625
- """Activate unwinding for a specific fiber."""
626
- self.active_fiber = fiber
627
- self.unwound_first_frame = False
628
- gdb.invalidate_cached_frames()
629
-
630
- def deactivate(self):
631
- """Deactivate fiber unwinding."""
632
- self.active_fiber = None
633
- self.unwound_first_frame = False
634
- gdb.invalidate_cached_frames()
635
-
636
-
637
- class RubyFiberSwitchCommand(debugger.Command):
638
- """Switch debugger's stack view to a specific fiber.
639
-
640
- Usage: rb-fiber-switch <fiber_value_or_address>
641
- rb-fiber-switch off
642
-
643
- Examples:
644
- rb-fiber-switch 0x7fffdc409ca8 # VALUE address
645
- rb-fiber-switch $fiber_val # Debugger variable
646
- rb-fiber-switch off # Deactivate unwinder (GDB only)
647
-
648
- This uses a custom unwinder (GDB only) to make the debugger follow the fiber's saved
649
- stack, allowing you to use 'bt', 'up', 'down', 'frame', etc.
650
- Works even with core dumps!
651
-
652
- Based on technique from Facebook Folly fibers.
653
- """
654
-
655
- def __init__(self):
656
- super(RubyFiberSwitchCommand, self).__init__("rb-fiber-switch", debugger.COMMAND_USER)
657
- if debugger.DEBUGGER_NAME == 'gdb':
658
- self._ensure_unwinder()
659
-
660
- def usage(self):
661
- """Print usage information."""
662
- print("Usage: rb-fiber-switch <fiber_value_or_address>")
663
- print(" rb-fiber-switch off")
664
- print("Examples:")
665
- print(" rb-fiber-switch 0x7fffdc409ca8 # VALUE address")
666
- print(" rb-fiber-switch $fiber # Debugger variable")
667
- print(" rb-fiber-switch off # Deactivate unwinder (GDB only)")
668
- print()
669
- print("After switching, you can use: bt, up, down, frame, info locals, etc.")
670
- if debugger.DEBUGGER_NAME != 'gdb':
671
- print("Note: Stack unwinding is only supported in GDB")
672
-
673
- def _ensure_unwinder(self):
674
- """Ensure the fiber unwinder is registered (GDB only)."""
675
- global _fiber_unwinder
676
- if _fiber_unwinder is None:
677
- _fiber_unwinder = RubyFiberUnwinder()
678
- gdb.unwinder.register_unwinder(None, _fiber_unwinder, replace=True)
679
-
680
- def invoke(self, arg, from_tty):
681
- global _fiber_unwinder
682
-
683
- if not arg:
684
- self.usage()
685
- return
686
-
687
- # Check for deactivate
688
- if arg and arg.lower() in ('off', 'none', 'deactivate'):
689
- if debugger.DEBUGGER_NAME == 'gdb' and _fiber_unwinder:
690
- _fiber_unwinder.deactivate()
691
- set_current_fiber(None)
692
- print("Fiber unwinder deactivated. Switched back to normal stack view.")
693
- print("Try: bt")
694
- return
695
-
696
- # Parse the argument as a VALUE
697
- try:
698
- # Evaluate the expression to get a VALUE
699
- fiber_value = debugger.parse_and_eval(arg)
700
-
701
- # Ensure it's cast to VALUE type
702
- try:
703
- value_type = debugger.lookup_type('VALUE')
704
- except debugger.Error as lookup_err:
705
- print(f"Error: Could not lookup type 'VALUE': {lookup_err}")
706
- print("This usually means Ruby symbols aren't fully loaded yet.")
707
- print(f"Try running the process further or checking symbol loading.")
708
- return
709
-
710
- fiber_value = fiber_value.cast(value_type)
711
-
712
- except (debugger.Error, RuntimeError) as e:
713
- print(f"Error: Could not evaluate '{arg}' as a VALUE")
714
- print(f"Details: {e}")
715
- import traceback
716
- traceback.print_exc()
717
- print()
718
- self.usage()
719
- return
720
-
721
- # Create RubyFiber wrapper
722
- try:
723
- fiber_obj = RubyFiber(fiber_value)
724
- except Exception as e:
725
- print(f"Error: Could not create RubyFiber from VALUE 0x{int(fiber_value):x}")
726
- print(f"Details: {e}")
727
- import traceback
728
- traceback.print_exc()
729
- return
730
-
731
- # Check if fiber is in a switchable state
732
- if fiber_obj.status in ('CREATED', 'TERMINATED'):
733
- print(f"Warning: Fiber is {fiber_obj.status}, may not have valid saved context")
734
- print()
735
-
736
- # Update global current fiber state
737
- set_current_fiber(fiber_obj)
738
-
739
- # Get the fiber pointer for unwinder
740
- fiber_ptr = fiber_obj.pointer
741
-
742
- # Activate the unwinder for this fiber (GDB only)
743
- if debugger.DEBUGGER_NAME == 'gdb' and _fiber_unwinder:
744
- _fiber_unwinder.activate_fiber(fiber_ptr)
745
-
746
- # Set convenience variables for the fiber context
747
- ec = fiber_ptr['cont']['saved_ec'].address
748
- debugger.set_convenience_variable('fiber', fiber_value)
749
- debugger.set_convenience_variable('fiber_ptr', fiber_ptr)
750
- debugger.set_convenience_variable('ec', ec)
751
-
752
- # Set errinfo if present (check for real object, not special constant)
753
- errinfo_val = ec['errinfo']
754
- errinfo_int = int(errinfo_val)
755
- is_special = (errinfo_int & 0x03) != 0 or errinfo_int == 0
756
- if not is_special:
757
- debugger.set_convenience_variable('errinfo', errinfo_val)
758
-
759
- # Create terminal for formatting
760
- terminal = format.create_terminal(from_tty)
761
-
762
- # Print switch confirmation
763
- print(f"Switched to Fiber: ", end='')
764
- print(terminal.print_type_tag('T_DATA', int(fiber_value), None), end='')
765
- print(' ', end='')
766
- print(terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address, None))
767
- print(f" Status: {fiber_obj.status}")
768
-
769
- # Print exception if present (catch errors for terminated fibers)
770
- try:
771
- exc_info = fiber_obj.exception_info
772
- if exc_info:
773
- print(f" Exception: {exc_info}")
774
- except Exception:
775
- # Silently skip exception info if we can't read it
776
- pass
777
- print()
778
-
779
- # Set tag retval if present
780
- tag = None
781
- is_retval_special = True
782
- try:
783
- tag = ec['tag']
784
- if int(tag) != 0:
785
- tag_retval = tag['retval']
786
- tag_state = int(tag['state'])
787
- retval_int = int(tag_retval)
788
- is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0
789
- if not is_retval_special:
790
- debugger.set_convenience_variable('retval', tag_retval)
791
- except:
792
- tag = None
793
- is_retval_special = True
794
-
795
- print("Convenience variables set:")
796
- print(f" $fiber = Current fiber VALUE")
797
- print(f" $fiber_ptr = Current fiber pointer (struct rb_fiber_struct *)")
798
- print(f" $ec = Execution context (rb_execution_context_t *)")
799
- if not is_special:
800
- print(f" $errinfo = Exception being handled (VALUE)")
801
- if tag and not is_retval_special:
802
- print(f" $retval = Return value from 'return' (VALUE)")
803
- print()
804
- print("Now try:")
805
- print(" bt # Show C backtrace of fiber")
806
- print(" frame <n> # Switch to frame N")
807
- print(" up/down # Move up/down frames")
808
- print(" info locals # Show local variables")
809
- if not is_special:
810
- print(" rp $errinfo # Pretty print exception")
811
- if tag and not is_retval_special:
812
- print(" rp $retval # Pretty print return value (in ensure blocks)")
813
- print()
814
- print("Useful VALUES to inspect:")
815
- print(" $ec->tag->retval # Return value (in ensure after 'return')")
816
- print(" $ec->cfp->sp[-1] # Top of VM stack")
817
- print(" $fiber_ptr->cont.value # Fiber yield/return value")
818
- print()
819
- print("NOTE: Frame #0 is synthetic (created by the unwinder) and may look odd.")
820
- print(" The real fiber context starts at frame #1.")
821
- print(" Use 'frame 1' to skip to the actual fiber_setcontext frame.")
822
- print()
823
- print("To switch back:")
824
- print(" rb-fiber-switch off")
825
-
826
-
827
- class RubyFiberScanStackTraceAllCommand(debugger.Command):
828
- """Print stack traces for all fibers in the scan cache.
829
-
830
- Usage: rb-fiber-scan-stack-trace-all
831
-
832
- This command prints the Ruby stack trace for each fiber that was
833
- found by 'rb-fiber-scan-heap'. Run that command first to populate
834
- the fiber cache.
835
- """
836
-
837
- def __init__(self):
838
- super(RubyFiberScanStackTraceAllCommand, self).__init__("rb-fiber-scan-stack-trace-all", debugger.COMMAND_USER)
839
-
840
- def usage(self):
841
- """Print usage information."""
842
- print("Usage: rb-fiber-scan-stack-trace-all")
843
- print()
844
- print("Note: Run 'rb-fiber-scan-heap' first to populate the fiber cache.")
845
-
846
- def invoke(self, arg, from_tty):
847
- global _fiber_cache
848
-
849
- # Check if cache is populated
850
- if not _fiber_cache:
851
- print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.")
852
- return
853
-
854
- # Import stack module to use print_fiber_backtrace
855
- import stack
856
-
857
- print(f"Printing stack traces for {len(_fiber_cache)} fiber(s)\n")
858
- print("=" * 80)
859
-
860
- for i, fiber_value in enumerate(_fiber_cache):
861
- try:
862
- # Create RubyFiber wrapper to get fiber info
863
- fiber_obj = RubyFiber(fiber_value)
864
-
865
- print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x} → {fiber_obj.status}")
866
- print("-" * 80)
867
-
868
- # Use stack.print_fiber_backtrace with the fiber pointer
869
- stack.print_fiber_backtrace(fiber_obj.pointer, from_tty=from_tty)
870
-
871
- except Exception as e:
872
- print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x}")
873
- print("-" * 80)
874
- print(f"Error printing backtrace: {e}")
875
- import traceback
876
- traceback.print_exc()
877
-
878
- print()
544
+ class RubyFiberUnwinder(gdb.unwinder.Unwinder):
545
+ """Custom unwinder for Ruby fibers.
546
+
547
+ This allows GDB to unwind a fiber's stack even in a core dump,
548
+ by extracting saved register state from the fiber's jmp_buf.
549
+
550
+ Based on similar technique from Facebook Folly:
551
+ https://github.com/facebook/folly/blob/main/folly/fibers/scripts/gdb.py
552
+ """
553
+
554
+ def __init__(self):
555
+ super(RubyFiberUnwinder, self).__init__("Ruby Fiber Unwinder")
556
+ self.active_fiber = None
557
+ self.unwound_first_frame = False
558
+
559
+ def __call__(self, pending_frame):
560
+ """Called by GDB when unwinding frames."""
561
+ # Only unwind if we have an active fiber set
562
+ if not self.active_fiber:
563
+ return None
564
+
565
+ # Only unwind the first frame, then let GDB continue normally
566
+ if self.unwound_first_frame:
567
+ return None
568
+
569
+ try:
570
+ # Ruby uses its own coroutine implementation, not setjmp/longjmp!
571
+ # Registers are saved in fiber->context.stack_pointer
572
+ # See coroutine/amd64/Context.S for the layout
573
+
574
+ coroutine_ctx = self.active_fiber['context']
575
+ stack_ptr = coroutine_ctx['stack_pointer']
576
+
577
+ # The stack_pointer points to the saved register area
578
+ # From Context.S (x86-64):
579
+ # [stack_pointer + 0] = R15
580
+ # [stack_pointer + 8] = R14
581
+ # [stack_pointer + 16] = R13
582
+ # [stack_pointer + 24] = R12
583
+ # [stack_pointer + 32] = RBX
584
+ # [stack_pointer + 40] = RBP
585
+ # [stack_pointer + 48] = Return address (RIP)
586
+
587
+ if int(stack_ptr) == 0:
588
+ return None
589
+
590
+ # Cast to uint64 pointer to read saved registers
591
+ uint64_ptr = stack_ptr.cast(gdb.lookup_type('uint64_t').pointer())
592
+
593
+ # Read saved registers (keep as gdb.Value)
594
+ r15 = uint64_ptr[0]
595
+ r14 = uint64_ptr[1]
596
+ r13 = uint64_ptr[2]
597
+ r12 = uint64_ptr[3]
598
+ rbx = uint64_ptr[4]
599
+ rbp = uint64_ptr[5]
600
+
601
+ # After coroutine_transfer executes 'addq $48, %rsp', RSP points to the return address
602
+ # After 'ret' pops the return address, RSP = stack_ptr + 48 + 8
603
+ # We want to create an unwind frame AS IF we're in the caller of coroutine_transfer
604
+ # So RSP should be pointing AFTER the return address was popped
605
+ rsp_value = int(stack_ptr) + 48 + 8
606
+ rsp = gdb.Value(rsp_value).cast(gdb.lookup_type('uint64_t'))
607
+
608
+ # The return address (RIP) is at [stack_ptr + 48]
609
+ # This is what 'ret' will pop and jump to
610
+ rip_ptr = gdb.Value(int(stack_ptr) + 48).cast(gdb.lookup_type('uint64_t').pointer())
611
+ rip = rip_ptr.dereference()
612
+
613
+ # Sanity check
614
+ if int(rsp) == 0 or int(rip) == 0:
615
+ return None
616
+
617
+ # Create frame ID
618
+ frame_id = gdb.unwinder.FrameId(int(rsp), int(rip))
619
+
620
+ # Create unwind info
621
+ unwind_info = pending_frame.create_unwind_info(frame_id)
622
+
623
+ # Add saved registers
624
+ unwind_info.add_saved_register("rip", rip)
625
+ unwind_info.add_saved_register("rsp", rsp)
626
+ unwind_info.add_saved_register("rbp", rbp)
627
+ unwind_info.add_saved_register("rbx", rbx)
628
+ unwind_info.add_saved_register("r12", r12)
629
+ unwind_info.add_saved_register("r13", r13)
630
+ unwind_info.add_saved_register("r14", r14)
631
+ unwind_info.add_saved_register("r15", r15)
632
+
633
+ # Mark that we've unwound the first frame
634
+ self.unwound_first_frame = True
635
+
636
+ return unwind_info
637
+
638
+ except (gdb.error, gdb.MemoryError) as e:
639
+ # If we can't read the fiber context, bail
640
+ return None
641
+
642
+ def activate_fiber(self, fiber):
643
+ """Activate unwinding for a specific fiber."""
644
+ self.active_fiber = fiber
645
+ self.unwound_first_frame = False
646
+ gdb.invalidate_cached_frames()
647
+
648
+ def deactivate(self):
649
+ """Deactivate fiber unwinding."""
650
+ self.active_fiber = None
651
+ self.unwound_first_frame = False
652
+ gdb.invalidate_cached_frames()
653
+
654
+
655
+ class RubyFiberSwitchHandler:
656
+ """Switch debugger's stack view to a specific fiber."""
657
+
658
+ USAGE = command.Usage(
659
+ summary="Switch debugger stack view to a specific fiber",
660
+ parameters=[('fiber', 'Fiber VALUE/address or "off" to deactivate')],
661
+ options={},
662
+ flags=[],
663
+ examples=[
664
+ ("rb-fiber-switch 0x7fffdc409ca8", "Switch to fiber at address"),
665
+ ("rb-fiber-switch $fiber", "Switch using debugger variable"),
666
+ ("rb-fiber-switch off", "Deactivate unwinder (GDB only)")
667
+ ]
668
+ )
669
+
670
+ def __init__(self):
671
+ if debugger.DEBUGGER_NAME == 'gdb':
672
+ self._ensure_unwinder()
673
+
674
+ def _ensure_unwinder(self):
675
+ """Ensure the fiber unwinder is registered (GDB only)."""
676
+ global _fiber_unwinder
677
+ if _fiber_unwinder is None:
678
+ _fiber_unwinder = RubyFiberUnwinder()
679
+ gdb.unwinder.register_unwinder(None, _fiber_unwinder, replace=True)
680
+
681
+ def invoke(self, arguments, terminal):
682
+ global _fiber_unwinder
683
+
684
+ # Check for deactivate
685
+ arg = arguments.expressions[0] if arguments.expressions else None
686
+ if not arg:
687
+ print("Error: fiber parameter required")
688
+ return
689
+
690
+ if arg.lower() in ('off', 'none', 'deactivate'):
691
+ if debugger.DEBUGGER_NAME == 'gdb' and _fiber_unwinder:
692
+ _fiber_unwinder.deactivate()
693
+ set_current_fiber(None)
694
+ print("Fiber unwinder deactivated. Switched back to normal stack view.")
695
+ print("Try: bt")
696
+ return
697
+
698
+ # Parse the argument as a VALUE
699
+ try:
700
+ # Evaluate the expression to get a VALUE
701
+ fiber_value = debugger.parse_and_eval(arg)
702
+
703
+ # Ensure it's cast to VALUE type
704
+ try:
705
+ value_type = constants.type_struct('VALUE')
706
+ except debugger.Error as lookup_err:
707
+ print(f"Error: Could not lookup type 'VALUE': {lookup_err}")
708
+ print("This usually means Ruby symbols aren't fully loaded yet.")
709
+ print(f"Try running the process further or checking symbol loading.")
710
+ return
711
+
712
+ fiber_value = fiber_value.cast(value_type)
713
+
714
+ except (debugger.Error, RuntimeError) as e:
715
+ print(f"Error: Could not evaluate '{arg}' as a VALUE")
716
+ print(f"Details: {e}")
717
+ import traceback
718
+ traceback.print_exc()
719
+ print()
720
+ self.usage()
721
+ return
722
+
723
+ # Create RubyFiber wrapper
724
+ try:
725
+ fiber_obj = RubyFiber(fiber_value)
726
+ except Exception as e:
727
+ print(f"Error: Could not create RubyFiber from VALUE 0x{int(fiber_value):x}")
728
+ print(f"Details: {e}")
729
+ import traceback
730
+ traceback.print_exc()
731
+ return
732
+
733
+ # Check if fiber is in a switchable state
734
+ if fiber_obj.status in ('CREATED', 'TERMINATED'):
735
+ print(f"Warning: Fiber is {fiber_obj.status}, may not have valid saved context")
736
+ print()
737
+
738
+ # Update global current fiber state
739
+ set_current_fiber(fiber_obj)
740
+
741
+ # Get the fiber pointer for unwinder
742
+ fiber_ptr = fiber_obj.pointer
743
+
744
+ # Activate the unwinder for this fiber (GDB only)
745
+ if debugger.DEBUGGER_NAME == 'gdb' and _fiber_unwinder:
746
+ _fiber_unwinder.activate_fiber(fiber_ptr)
747
+
748
+ # Set convenience variables for the fiber context
749
+ ec = fiber_ptr['cont']['saved_ec'].address
750
+ debugger.set_convenience_variable('fiber', fiber_value)
751
+ debugger.set_convenience_variable('fiber_ptr', fiber_ptr)
752
+ debugger.set_convenience_variable('ec', ec)
753
+
754
+ # Set errinfo if present (check for real object, not special constant)
755
+ errinfo_val = ec['errinfo']
756
+ errinfo_int = int(errinfo_val)
757
+ is_special = (errinfo_int & 0x03) != 0 or errinfo_int == 0
758
+ if not is_special:
759
+ debugger.set_convenience_variable('errinfo', errinfo_val)
760
+
761
+ # Print switch confirmation
762
+ print(f"Switched to Fiber: ", end='')
763
+ terminal.print_type_tag('T_DATA', int(fiber_value), None)
764
+ print(' ', end='')
765
+ terminal.print_type_tag('struct rb_fiber_struct', fiber_obj.address, None)
766
+ print()
767
+ print(f" Status: {fiber_obj.status}") # Print exception if present (catch errors for terminated fibers)
768
+ try:
769
+ exc_info = fiber_obj.exception_info
770
+ if exc_info:
771
+ print(f" Exception: {exc_info}")
772
+ except Exception:
773
+ # Silently skip exception info if we can't read it
774
+ pass
775
+ print()
776
+
777
+ # Set tag retval if present
778
+ tag = None
779
+ is_retval_special = True
780
+ try:
781
+ tag = ec['tag']
782
+ if int(tag) != 0:
783
+ tag_retval = tag['retval']
784
+ tag_state = int(tag['state'])
785
+ retval_int = int(tag_retval)
786
+ is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0
787
+ if not is_retval_special:
788
+ debugger.set_convenience_variable('retval', tag_retval)
789
+ except:
790
+ tag = None
791
+ is_retval_special = True
792
+
793
+ print("Convenience variables set:")
794
+ print(f" $fiber = Current fiber VALUE")
795
+ print(f" $fiber_ptr = Current fiber pointer (struct rb_fiber_struct *)")
796
+ print(f" $ec = Execution context (rb_execution_context_t *)")
797
+ if not is_special:
798
+ print(f" $errinfo = Exception being handled (VALUE)")
799
+ if tag and not is_retval_special:
800
+ print(f" $retval = Return value from 'return' (VALUE)")
801
+ print()
802
+ print("Now try:")
803
+ print(" bt # Show C backtrace of fiber")
804
+ print(" frame <n> # Switch to frame N")
805
+ print(" up/down # Move up/down frames")
806
+ print(" info locals # Show local variables")
807
+ if not is_special:
808
+ print(" rp $errinfo # Pretty print exception")
809
+ if tag and not is_retval_special:
810
+ print(" rp $retval # Pretty print return value (in ensure blocks)")
811
+ print()
812
+ print("Useful VALUES to inspect:")
813
+ print(" $ec->tag->retval # Return value (in ensure after 'return')")
814
+ print(" $ec->cfp->sp[-1] # Top of VM stack")
815
+ print(" $fiber_ptr->cont.value # Fiber yield/return value")
816
+ print()
817
+ print("NOTE: Frame #0 is synthetic (created by the unwinder) and may look odd.")
818
+ print(" The real fiber context starts at frame #1.")
819
+ print(" Use 'frame 1' to skip to the actual fiber_setcontext frame.")
820
+ print()
821
+ print("To switch back:")
822
+ print(" rb-fiber-switch off")
823
+
824
+
825
+ class RubyFiberScanStackTraceAllHandler:
826
+ """Print stack traces for all fibers in the scan cache."""
827
+
828
+ USAGE = command.Usage(
829
+ summary="Print stack traces for all cached fibers",
830
+ parameters=[],
831
+ options={},
832
+ flags=[],
833
+ examples=[
834
+ ("rb-fiber-scan-heap; rb-fiber-scan-stack-trace-all", "Scan fibers then show all backtraces")
835
+ ]
836
+ )
837
+
838
+ def invoke(self, arguments, terminal):
839
+ global _fiber_cache
840
+
841
+ # Check if cache is populated
842
+ if not _fiber_cache:
843
+ print("Error: No fibers in cache. Run 'rb-fiber-scan-heap' first.")
844
+ return
845
+
846
+ # Import stack module to use print_fiber_backtrace
847
+ import stack
848
+
849
+ print(f"Printing stack traces for {len(_fiber_cache)} fiber(s)\n")
850
+ print("=" * 80)
851
+
852
+ for i, fiber_value in enumerate(_fiber_cache):
853
+ try:
854
+ # Create RubyFiber wrapper to get fiber info
855
+ fiber_obj = RubyFiber(fiber_value)
856
+
857
+ print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x} → {fiber_obj.status}")
858
+ print("-" * 80)
859
+
860
+ # Use stack.print_fiber_backtrace with the fiber pointer
861
+ stack.print_fiber_backtrace(fiber_obj.pointer)
862
+
863
+ except Exception as e:
864
+ print(f"\nFiber #{i}: VALUE 0x{int(fiber_value):x}")
865
+ print("-" * 80)
866
+ print(f"Error printing backtrace: {e}")
867
+ import traceback
868
+ traceback.print_exc()
869
+
870
+ print()
879
871
 
880
872
 
881
873
  # Register commands
882
- RubyFiberScanHeapCommand()
883
- RubyFiberScanSwitchCommand()
884
- RubyFiberSwitchCommand()
885
- RubyFiberScanStackTraceAllCommand()
874
+ debugger.register("rb-fiber-scan-heap", RubyFiberScanHeapHandler, usage=RubyFiberScanHeapHandler.USAGE)
875
+ debugger.register("rb-fiber-scan-switch", RubyFiberScanSwitchHandler, usage=RubyFiberScanSwitchHandler.USAGE)
876
+ debugger.register("rb-fiber-switch", RubyFiberSwitchHandler, usage=RubyFiberSwitchHandler.USAGE)
877
+ debugger.register("rb-fiber-scan-stack-trace-all", RubyFiberScanStackTraceAllHandler, usage=RubyFiberScanStackTraceAllHandler.USAGE)