toolbox 0.1.4 → 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.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/ruby/gdb.rb +135 -0
  4. data/bake/toolbox/gdb.rb +137 -0
  5. data/bake/toolbox/lldb.rb +137 -0
  6. data/context/fiber-debugging.md +171 -0
  7. data/context/getting-started.md +200 -0
  8. data/context/heap-debugging.md +351 -0
  9. data/context/index.yaml +28 -0
  10. data/context/object-inspection.md +208 -0
  11. data/context/stack-inspection.md +188 -0
  12. data/data/toolbox/command.py +479 -0
  13. data/data/toolbox/constants.py +200 -0
  14. data/data/toolbox/context.py +371 -0
  15. data/data/toolbox/debugger/__init__.py +101 -0
  16. data/data/toolbox/debugger/gdb_backend.py +664 -0
  17. data/data/toolbox/debugger/lldb_backend.py +986 -0
  18. data/data/toolbox/fiber.py +877 -0
  19. data/data/toolbox/format.py +205 -0
  20. data/data/toolbox/heap.py +679 -0
  21. data/data/toolbox/init.py +89 -0
  22. data/data/toolbox/print.py +79 -0
  23. data/data/toolbox/rarray.py +116 -0
  24. data/data/toolbox/rbasic.py +99 -0
  25. data/data/toolbox/rbignum.py +48 -0
  26. data/data/toolbox/rclass.py +136 -0
  27. data/data/toolbox/readme.md +214 -0
  28. data/data/toolbox/rexception.py +150 -0
  29. data/data/toolbox/rfloat.py +88 -0
  30. data/data/toolbox/rhash.py +151 -0
  31. data/data/toolbox/rstring.py +230 -0
  32. data/data/toolbox/rstruct.py +149 -0
  33. data/data/toolbox/rsymbol.py +278 -0
  34. data/data/toolbox/rvalue.py +183 -0
  35. data/data/toolbox/stack.py +620 -0
  36. data/lib/toolbox/gdb.rb +21 -0
  37. data/lib/toolbox/lldb.rb +21 -0
  38. data/lib/toolbox/version.rb +7 -1
  39. data/lib/toolbox.rb +9 -24
  40. data/license.md +21 -0
  41. data/readme.md +64 -0
  42. data/releases.md +9 -0
  43. data.tar.gz.sig +0 -0
  44. metadata +95 -165
  45. metadata.gz.sig +0 -0
  46. data/Rakefile +0 -61
  47. data/lib/dirs.rb +0 -9
  48. data/lib/toolbox/config.rb +0 -211
  49. data/lib/toolbox/default_controller.rb +0 -393
  50. data/lib/toolbox/helpers.rb +0 -11
  51. data/lib/toolbox/rendering.rb +0 -413
  52. data/lib/toolbox/searching.rb +0 -85
  53. data/lib/toolbox/session_params.rb +0 -63
  54. data/lib/toolbox/sorting.rb +0 -74
  55. data/locale/de/LC_MESSAGES/toolbox.mo +0 -0
  56. data/public/images/add.png +0 -0
  57. data/public/images/arrow_down.gif +0 -0
  58. data/public/images/arrow_up.gif +0 -0
  59. data/public/images/close.png +0 -0
  60. data/public/images/edit.gif +0 -0
  61. data/public/images/email.png +0 -0
  62. data/public/images/page.png +0 -0
  63. data/public/images/page_acrobat.png +0 -0
  64. data/public/images/page_add.png +0 -0
  65. data/public/images/page_copy.png +0 -0
  66. data/public/images/page_delete.png +0 -0
  67. data/public/images/page_edit.png +0 -0
  68. data/public/images/page_excel.png +0 -0
  69. data/public/images/page_list.png +0 -0
  70. data/public/images/page_save.png +0 -0
  71. data/public/images/page_word.png +0 -0
  72. data/public/images/remove.png +0 -0
  73. data/public/images/show.gif +0 -0
  74. data/public/images/spinner.gif +0 -0
  75. data/public/javascripts/popup.js +0 -498
  76. data/public/javascripts/toolbox.js +0 -18
  77. data/public/stylesheets/context_menu.css +0 -168
  78. data/public/stylesheets/popup.css +0 -30
  79. data/public/stylesheets/toolbox.css +0 -107
  80. data/view/toolbox/_collection.html.erb +0 -24
  81. data/view/toolbox/_collection_header.html.erb +0 -7
  82. data/view/toolbox/_context_menu.html.erb +0 -17
  83. data/view/toolbox/_dialogs.html.erb +0 -6
  84. data/view/toolbox/_form.html.erb +0 -30
  85. data/view/toolbox/_form_collection_row.html.erb +0 -18
  86. data/view/toolbox/_form_fieldset.html.erb +0 -30
  87. data/view/toolbox/_form_fieldset_row.html.erb +0 -19
  88. data/view/toolbox/_list.html.erb +0 -25
  89. data/view/toolbox/_list_row.html.erb +0 -10
  90. data/view/toolbox/_menu.html.erb +0 -7
  91. data/view/toolbox/_search_field.html.erb +0 -8
  92. data/view/toolbox/_show.html.erb +0 -12
  93. data/view/toolbox/_show_collection_row.html.erb +0 -6
  94. data/view/toolbox/_show_fieldset.html.erb +0 -21
  95. data/view/toolbox/edit.html.erb +0 -5
  96. data/view/toolbox/index.html.erb +0 -3
  97. data/view/toolbox/new.html.erb +0 -9
  98. data/view/toolbox/show.html.erb +0 -39
@@ -0,0 +1,877 @@
1
+ import debugger
2
+ import re
3
+ import struct
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ # Import GDB unwinder only if running under GDB
9
+ if debugger.DEBUGGER_NAME == 'gdb':
10
+ import gdb.unwinder
11
+
12
+ # Import command parser
13
+ import command
14
+ import constants
15
+ import rvalue
16
+ import format
17
+ import heap
18
+ import rexception
19
+
20
+ # Global cache of fibers
21
+ _fiber_cache = []
22
+
23
+ # Global fiber unwinder instance
24
+ _fiber_unwinder = None
25
+
26
+ # Global current fiber (managed by rb-fiber-switch command)
27
+ _current_fiber = None
28
+
29
+ def parse_fiber_index(arg):
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]}'"
48
+
49
+ def get_current_fiber():
50
+ """Get the currently selected fiber (if any).
51
+
52
+ Returns:
53
+ RubyFiber instance or None
54
+ """
55
+ return _current_fiber
56
+
57
+ def set_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
67
+
68
+ class RubyFiber:
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()
540
+
541
+
542
+ # GDB-specific unwinder class - only available when running under GDB
543
+ if debugger.DEBUGGER_NAME == 'gdb':
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()
871
+
872
+
873
+ # Register commands
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)