toolbox 0.1.4 → 0.2.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 +178 -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 +254 -0
  13. data/data/toolbox/constants.py +200 -0
  14. data/data/toolbox/context.py +295 -0
  15. data/data/toolbox/debugger/__init__.py +99 -0
  16. data/data/toolbox/debugger/gdb_backend.py +595 -0
  17. data/data/toolbox/debugger/lldb_backend.py +885 -0
  18. data/data/toolbox/fiber.py +885 -0
  19. data/data/toolbox/format.py +200 -0
  20. data/data/toolbox/heap.py +669 -0
  21. data/data/toolbox/init.py +85 -0
  22. data/data/toolbox/object.py +84 -0
  23. data/data/toolbox/rarray.py +124 -0
  24. data/data/toolbox/rbasic.py +103 -0
  25. data/data/toolbox/rbignum.py +52 -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 +98 -0
  30. data/data/toolbox/rhash.py +159 -0
  31. data/data/toolbox/rstring.py +234 -0
  32. data/data/toolbox/rstruct.py +157 -0
  33. data/data/toolbox/rsymbol.py +302 -0
  34. data/data/toolbox/stack.py +630 -0
  35. data/data/toolbox/value.py +183 -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 +2 -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,630 @@
1
+ """Stack inspection commands for Ruby processes."""
2
+
3
+ import debugger
4
+ import sys
5
+
6
+ # Import Ruby GDB modules
7
+ import context
8
+ import format
9
+ import value
10
+ import rstring
11
+ import rexception
12
+ import rsymbol
13
+
14
+
15
+ def print_fiber_backtrace(fiber_ptr, from_tty=True):
16
+ """Print backtrace for a Ruby fiber.
17
+
18
+ Args:
19
+ fiber_ptr: Fiber struct pointer (rb_fiber_struct *)
20
+ from_tty: Whether output is to terminal (for formatting)
21
+ """
22
+ printer = RubyStackPrinter()
23
+ printer.print_fiber_backtrace(fiber_ptr, from_tty)
24
+
25
+
26
+ def print_ec_backtrace(ec, from_tty=True):
27
+ """Print backtrace for an execution context.
28
+
29
+ Args:
30
+ ec: Execution context pointer (rb_execution_context_t *)
31
+ from_tty: Whether output is to terminal (for formatting)
32
+ """
33
+ printer = RubyStackPrinter()
34
+ printer.print_backtrace(ec, from_tty)
35
+
36
+
37
+ class RubyStackPrinter:
38
+ """Helper class for printing Ruby stack traces.
39
+
40
+ This class provides the core logic for printing backtraces that can be
41
+ used both by commands and programmatically from other modules.
42
+ """
43
+
44
+ def __init__(self):
45
+ # Cached type lookups
46
+ self._rbasic_type = None
47
+ self._value_type = None
48
+ self._cfp_type = None
49
+ self._rstring_type = None
50
+ self.show_values = False
51
+ self.terminal = None
52
+
53
+ def _initialize_types(self):
54
+ """Initialize cached type lookups."""
55
+ if self._rbasic_type is None:
56
+ self._rbasic_type = debugger.lookup_type('struct RBasic').pointer()
57
+ if self._value_type is None:
58
+ self._value_type = debugger.lookup_type('VALUE')
59
+ if self._cfp_type is None:
60
+ self._cfp_type = debugger.lookup_type('rb_control_frame_t').pointer()
61
+ if self._rstring_type is None:
62
+ self._rstring_type = debugger.lookup_type('struct RString').pointer()
63
+
64
+ def print_fiber_backtrace(self, fiber_ptr, from_tty=True):
65
+ """Print backtrace for a Ruby fiber.
66
+
67
+ Args:
68
+ fiber_ptr: Fiber struct pointer (rb_fiber_struct *)
69
+ from_tty: Whether output is to terminal (for formatting)
70
+ """
71
+ try:
72
+ self._initialize_types()
73
+ self.terminal = format.create_terminal(from_tty)
74
+
75
+ # Get execution context from fiber
76
+ ec = fiber_ptr['cont']['saved_ec'].address
77
+
78
+ print(f"Backtrace for fiber {fiber_ptr}:")
79
+ self.print_backtrace(ec, from_tty)
80
+
81
+ except (debugger.Error, RuntimeError) as e:
82
+ print(f"Error printing fiber backtrace: {e}")
83
+
84
+ def print_backtrace(self, ec, from_tty=True):
85
+ """Print backtrace for an execution context.
86
+
87
+ Args:
88
+ ec: Execution context pointer (rb_execution_context_t *)
89
+ from_tty: Whether output is to terminal (for formatting)
90
+ """
91
+ try:
92
+ self._initialize_types()
93
+ if self.terminal is None:
94
+ self.terminal = format.create_terminal(from_tty)
95
+
96
+ cfp = ec['cfp']
97
+ vm_stack = ec['vm_stack']
98
+ vm_stack_size = int(ec['vm_stack_size'])
99
+
100
+ # Check for exception
101
+ errinfo_val = ec['errinfo']
102
+ errinfo_int = int(errinfo_val)
103
+
104
+ # Check if it's a real exception object (not nil or other immediate/special value)
105
+ if not value.is_immediate(errinfo_val) and not value.is_nil(errinfo_val):
106
+ try:
107
+ exc_class = self._get_exception_class(errinfo_val)
108
+ exc_msg = self._get_exception_message(errinfo_val)
109
+
110
+ # Set as GDB convenience variable for manual inspection
111
+ debugger.set_convenience_variable('errinfo', errinfo_val)
112
+
113
+ if exc_msg:
114
+ print(f"Currently handling: {exc_class}: {exc_msg} (VALUE: 0x{errinfo_int:x}, $errinfo)")
115
+ else:
116
+ print(f"Currently handling: {exc_class} (VALUE: 0x{errinfo_int:x}, $errinfo)")
117
+ print()
118
+ except:
119
+ # Set convenience variable even if we can't decode
120
+ debugger.set_convenience_variable('errinfo', errinfo_val)
121
+ print(f"Currently handling exception (VALUE: 0x{errinfo_int:x}, $errinfo)")
122
+ print()
123
+
124
+ # Calculate end of control frames
125
+ cfpend = (vm_stack + vm_stack_size).cast(self._cfp_type) - 1
126
+
127
+ frame_num = 0
128
+ current_cfp = cfp
129
+
130
+ while current_cfp < cfpend:
131
+ try:
132
+ self._print_frame(current_cfp, frame_num)
133
+ frame_num += 1
134
+ current_cfp += 1
135
+ except (debugger.Error, RuntimeError) as e:
136
+ print(f" #{frame_num}: [error reading frame: {e}]")
137
+ break
138
+
139
+ if frame_num == 0:
140
+ print(" (no frames)")
141
+
142
+ except (debugger.Error, RuntimeError) as e:
143
+ print(f"Error printing backtrace: {e}")
144
+
145
+ def _print_frame(self, cfp, depth):
146
+ """Print a single control frame.
147
+
148
+ Args:
149
+ cfp: Control frame pointer (rb_control_frame_t *)
150
+ depth: Frame depth/number
151
+ """
152
+ iseq = cfp['iseq']
153
+
154
+ if iseq is None or int(iseq) == 0:
155
+ # C function frame - try to extract method info from EP
156
+ try:
157
+ ep = cfp['ep']
158
+
159
+ # Check if this is a valid C frame
160
+ if int(ep) != 0:
161
+ ep0 = int(ep[0])
162
+ if (ep0 & 0xffff0001) == 0x55550001:
163
+ # Valid C frame, try to extract method entry
164
+ env_me_cref = ep[-2]
165
+
166
+ try:
167
+ me_type = debugger.lookup_type('rb_callable_method_entry_t').pointer()
168
+ me = env_me_cref.cast(me_type)
169
+
170
+ # Get the C function pointer
171
+ cfunc = me['def']['body']['cfunc']['func']
172
+
173
+ # Get the method ID
174
+ method_id = me['def']['original_id']
175
+
176
+ # Try to get symbol for the C function
177
+ func_addr = int(cfunc)
178
+ func_name = debugger.lookup_symbol(func_addr)
179
+ if func_name:
180
+ func_name = f" ({func_name})"
181
+ else:
182
+ func_name = f" (0x{func_addr:x})"
183
+
184
+ # Print C frame with cyan/dimmed formatting
185
+ print(self.terminal.print(
186
+ format.metadata, f" #{depth}: ",
187
+ format.dim, "[C function", func_name, "]",
188
+ format.reset
189
+ ))
190
+ return
191
+ except:
192
+ pass
193
+ except:
194
+ pass
195
+
196
+ # Fallback if we couldn't extract info
197
+ print(self.terminal.print(
198
+ format.metadata, f" #{depth}: ",
199
+ format.dim, "[C function or native frame]",
200
+ format.reset
201
+ ))
202
+ return
203
+
204
+ pc = cfp['pc']
205
+
206
+ if int(pc) == 0:
207
+ print(self.terminal.print(
208
+ format.metadata, f" #{depth}: ",
209
+ format.error, "???:???:in '???'",
210
+ format.reset
211
+ ))
212
+ return
213
+
214
+ # Check if it's an ifunc (internal function)
215
+ RUBY_IMMEDIATE_MASK = 0x03
216
+ RUBY_FL_USHIFT = 12
217
+ RUBY_T_IMEMO = 0x1a
218
+ RUBY_IMEMO_MASK = 0x0f
219
+
220
+ iseq_val = int(iseq.cast(self._value_type))
221
+ if not (iseq_val & RUBY_IMMEDIATE_MASK):
222
+ try:
223
+ flags = int(iseq['flags'])
224
+ expected = ((RUBY_T_IMEMO << RUBY_FL_USHIFT) | RUBY_T_IMEMO)
225
+ mask = ((RUBY_IMEMO_MASK << RUBY_FL_USHIFT) | 0x1f)
226
+
227
+ if (flags & mask) == expected:
228
+ # It's an ifunc
229
+ print(self.terminal.print(
230
+ format.metadata, f" #{depth}: ",
231
+ format.dim, "[ifunc]",
232
+ format.reset
233
+ ))
234
+ return
235
+ except:
236
+ pass
237
+
238
+ try:
239
+ # Get location information
240
+ body = iseq['body']
241
+ if body is None:
242
+ print(self.terminal.print(
243
+ format.metadata, f" #{depth}: ",
244
+ format.error, "???:???:in '???'",
245
+ format.reset
246
+ ))
247
+ return
248
+
249
+ location = body['location']
250
+ if location is None:
251
+ print(self.terminal.print(
252
+ format.metadata, f" #{depth}: ",
253
+ format.error, "???:???:in '???'",
254
+ format.reset
255
+ ))
256
+ return
257
+
258
+ pathobj = location['pathobj']
259
+ label = location['label']
260
+
261
+ # Get path string - pathobj can be a string or an array [path, realpath]
262
+ path = self._extract_path_from_pathobj(pathobj)
263
+ label_str = self._value_to_string(label)
264
+
265
+ # Calculate line number
266
+ lineno = self._get_lineno(cfp)
267
+
268
+ # Print Ruby frame with highlighting
269
+ print(self.terminal.print(
270
+ format.metadata, f" #{depth}: ",
271
+ format.string, path,
272
+ format.reset, ":",
273
+ format.metadata, str(lineno),
274
+ format.reset, ":in '",
275
+ format.method, label_str,
276
+ format.reset, "'"
277
+ ))
278
+
279
+ # If --values flag is set, print stack values
280
+ if self.show_values:
281
+ self._print_stack_values(cfp, iseq)
282
+
283
+ except (debugger.Error, RuntimeError) as e:
284
+ print(self.terminal.print(
285
+ format.metadata, f" #{depth}: ",
286
+ format.error, f"[error reading frame info: {e}]",
287
+ format.reset
288
+ ))
289
+
290
+ def _print_stack_values(self, cfp, iseq):
291
+ """Print Ruby VALUEs on the control frame's stack pointer.
292
+
293
+ Args:
294
+ cfp: Control frame pointer (rb_control_frame_t *)
295
+ iseq: Instruction sequence pointer
296
+ """
297
+ try:
298
+ sp = cfp['sp']
299
+ ep = cfp['ep']
300
+
301
+ if int(sp) == 0 or int(ep) == 0:
302
+ return
303
+
304
+ # Try to get local table information for better labeling
305
+ local_names = []
306
+ local_size = 0
307
+ try:
308
+ if int(iseq) != 0:
309
+ iseq_body = iseq['body']
310
+ local_table_size = int(iseq_body['local_table_size'])
311
+
312
+ if local_table_size > 0:
313
+ local_size = local_table_size
314
+ local_table = iseq_body['local_table']
315
+
316
+ # Read local variable names (they're stored as IDs/symbols)
317
+ # Local table is stored in reverse order (last local first)
318
+ for i in range(min(local_table_size, 20)): # Cap at 20
319
+ try:
320
+ local_id = local_table[local_table_size - 1 - i]
321
+ # Try to convert ID to symbol name using RubySymbol
322
+ if int(local_id) != 0:
323
+ sym = rsymbol.RubySymbol(local_id)
324
+ name = sym.to_str()
325
+ if name:
326
+ local_names.append(name)
327
+ else:
328
+ local_names.append(f"local_{i}")
329
+ else:
330
+ local_names.append(f"local_{i}")
331
+ except:
332
+ local_names.append(f"local_{i}")
333
+ except:
334
+ pass
335
+
336
+ # Environment pointer typically points to the local variable area
337
+ # Stack grows downward, so we start from sp and go down
338
+ value_ptr = sp - 1
339
+
340
+ # Print a reasonable number of stack values
341
+ max_values = 10
342
+ values_printed = 0
343
+
344
+ print(self.terminal.print(format.dim, " Stack values:", format.reset))
345
+
346
+ # Calculate offset from ep to show position
347
+ while value_ptr >= ep and values_printed < max_values:
348
+ try:
349
+ val = value_ptr[0]
350
+ val_int = int(val)
351
+
352
+ # Calculate offset from ep for labeling
353
+ offset = int(value_ptr - ep)
354
+
355
+ # Try to determine if this is a local variable
356
+ label = f"sp[-{values_printed + 1}]"
357
+ if offset < local_size and offset < len(local_names):
358
+ label = f"{local_names[offset]} (ep[{offset}])"
359
+
360
+ # Try to get a brief representation of the value
361
+ val_str = self._format_value_brief(val)
362
+
363
+ print(self.terminal.print(
364
+ format.metadata, f" {label:20s} ",
365
+ format.dim, f"= ",
366
+ format.reset, val_str,
367
+ format.reset
368
+ ))
369
+
370
+ values_printed += 1
371
+ value_ptr -= 1
372
+ except (debugger.Error, debugger.MemoryError):
373
+ break
374
+
375
+ if values_printed == 0:
376
+ print(self.terminal.print(format.dim, " (empty stack)", format.reset))
377
+
378
+ except (debugger.Error, RuntimeError) as e:
379
+ # Silently skip if we can't read stack values
380
+ pass
381
+
382
+ def _format_value_brief(self, val):
383
+ """Get a brief string representation of a VALUE.
384
+
385
+ Args:
386
+ val: Ruby VALUE
387
+
388
+ Returns:
389
+ Brief string description
390
+ """
391
+ try:
392
+ # Use value.py's interpret function to get the typed object
393
+ obj = value.interpret(val)
394
+
395
+ # Get string representation
396
+ obj_str = str(obj)
397
+
398
+ # Truncate if too long
399
+ if len(obj_str) > 60:
400
+ return obj_str[:57] + "..."
401
+
402
+ return obj_str
403
+
404
+ except Exception as e:
405
+ return f"<error: {e}>"
406
+
407
+ def _get_lineno(self, cfp):
408
+ """Get line number for a control frame.
409
+
410
+ Args:
411
+ cfp: Control frame pointer
412
+
413
+ Returns:
414
+ Line number as int or "???" if unavailable
415
+ """
416
+ try:
417
+ iseq = cfp['iseq']
418
+ pc = cfp['pc']
419
+
420
+ if int(pc) == 0:
421
+ return "???"
422
+
423
+ iseq_body = iseq['body']
424
+ iseq_encoded = iseq_body['iseq_encoded']
425
+ iseq_size = int(iseq_body['iseq_size'])
426
+
427
+ pc_offset = int(pc - iseq_encoded)
428
+
429
+ if pc_offset < 0 or pc_offset >= iseq_size:
430
+ return "???"
431
+
432
+ # Try to get line info
433
+ insns_info = iseq_body['insns_info']
434
+ positions = insns_info['positions']
435
+
436
+ if int(positions) != 0:
437
+ position = positions[pc_offset]
438
+ lineno = int(position['lineno'])
439
+ if lineno >= 0:
440
+ return lineno
441
+
442
+ # Fall back to first_lineno
443
+ return int(iseq_body['location']['first_lineno'])
444
+
445
+ except:
446
+ return "???"
447
+
448
+ def _get_exception_class(self, exc_value):
449
+ """Get the class name of an exception object.
450
+
451
+ Delegates to rexception.RException for proper exception handling.
452
+
453
+ Args:
454
+ exc_value: Exception VALUE
455
+
456
+ Returns:
457
+ Class name as string
458
+ """
459
+ try:
460
+ exc = rexception.RException(exc_value)
461
+ return exc.class_name
462
+ except Exception:
463
+ # Fallback if RException can't be created
464
+ try:
465
+ rbasic = exc_value.cast(self._rbasic_type)
466
+ klass = rbasic['klass']
467
+ return f"Exception(klass=0x{int(klass):x})"
468
+ except:
469
+ raise
470
+
471
+ def _get_exception_message(self, exc_value):
472
+ """Get the message from an exception object.
473
+
474
+ Delegates to rexception.RException for proper exception handling.
475
+
476
+ Args:
477
+ exc_value: Exception VALUE
478
+
479
+ Returns:
480
+ Message string or None if unavailable
481
+ """
482
+ try:
483
+ exc = rexception.RException(exc_value)
484
+ return exc.message
485
+ except Exception:
486
+ # If RException can't be created, return None
487
+ return None
488
+
489
+ def _value_to_string(self, val):
490
+ """Convert a Ruby VALUE to a Python string.
491
+
492
+ Args:
493
+ val: Ruby VALUE
494
+
495
+ Returns:
496
+ String representation
497
+ """
498
+ try:
499
+ # Use the value.interpret infrastructure for proper type handling
500
+ obj = value.interpret(val)
501
+
502
+ # For strings, get the actual content
503
+ if hasattr(obj, 'to_str'):
504
+ return obj.to_str()
505
+
506
+ # For immediates and other types, convert to string
507
+ obj_str = str(obj)
508
+
509
+ # Strip the type tag if present (e.g., "<T_FIXNUM> 42" -> "42")
510
+ if obj_str.startswith('<'):
511
+ # Find the end of the type tag
512
+ end_tag = obj_str.find('>')
513
+ if end_tag != -1 and end_tag + 2 < len(obj_str):
514
+ # Return the part after the tag and space
515
+ return obj_str[end_tag + 2:]
516
+
517
+ return obj_str
518
+
519
+ except Exception as e:
520
+ return f"<error:{e}>"
521
+
522
+ def _extract_path_from_pathobj(self, pathobj):
523
+ """Extract file path from pathobj (can be string or array).
524
+
525
+ Args:
526
+ pathobj: Ruby VALUE (either T_STRING or T_ARRAY)
527
+
528
+ Returns:
529
+ File path as string
530
+ """
531
+ try:
532
+ # Interpret the pathobj to get its type
533
+ obj = value.interpret(pathobj)
534
+
535
+ # If it's an array, get the first element (the path)
536
+ if hasattr(obj, 'length') and hasattr(obj, 'get_item'):
537
+ if obj.length() > 0:
538
+ path_value = obj.get_item(0)
539
+ return self._value_to_string(path_value)
540
+
541
+ # Otherwise, treat it as a string directly
542
+ return self._value_to_string(pathobj)
543
+
544
+ except Exception as e:
545
+ return f"<error:{e}>"
546
+
547
+
548
+ class RubyStackTraceCommand(debugger.Command):
549
+ """Print combined C and Ruby backtrace for current fiber or thread.
550
+
551
+ Usage: rb-stack-trace [--values]
552
+
553
+ Shows backtrace for:
554
+ - Currently selected fiber (if rb-fiber-switch was used)
555
+ - Current thread execution context (if no fiber selected)
556
+
557
+ Options:
558
+ --values Show all Ruby VALUEs on each frame's stack pointer
559
+
560
+ The output shows both C frames and Ruby frames intermixed,
561
+ giving a complete picture of the call stack.
562
+ """
563
+
564
+ def __init__(self):
565
+ super(RubyStackTraceCommand, self).__init__("rb-stack-trace", debugger.COMMAND_USER)
566
+ self.printer = RubyStackPrinter()
567
+
568
+ def usage(self):
569
+ """Print usage information."""
570
+ print("Usage: rb-stack-trace [--values]")
571
+ print("Examples:")
572
+ print(" rb-stack-trace # Show backtrace for current fiber/thread")
573
+ print(" rb-stack-trace --values # Show backtrace with stack VALUEs")
574
+
575
+
576
+ def invoke(self, arg, from_tty):
577
+ """Execute the stack trace command."""
578
+ try:
579
+ # Parse arguments
580
+ import command
581
+ arguments = command.parse_arguments(arg if arg else "")
582
+ self.printer.show_values = arguments.has_flag('values')
583
+
584
+ # Create terminal for formatting
585
+ self.printer.terminal = format.create_terminal(from_tty)
586
+
587
+ # Check if a fiber is currently selected
588
+ # Import here to avoid circular dependency
589
+ import fiber
590
+ current_fiber = fiber.get_current_fiber()
591
+
592
+ if current_fiber:
593
+ # Use the selected fiber's execution context
594
+ print(f"Stack trace for selected fiber:")
595
+ print(f" Fiber: ", end='')
596
+ print(self.printer.terminal.print_type_tag('T_DATA', int(current_fiber.value), None))
597
+ print()
598
+
599
+ ec = current_fiber.pointer['cont']['saved_ec'].address
600
+ self.printer.print_backtrace(ec, from_tty)
601
+ else:
602
+ # Use current thread's execution context
603
+ print("Stack trace for current thread:")
604
+ print()
605
+
606
+ try:
607
+ ctx = context.RubyContext.current()
608
+
609
+ if ctx is None:
610
+ print("Error: No execution context available")
611
+ print("Either select a fiber with 'rb-fiber-switch' or ensure Ruby is running")
612
+ print("\nTroubleshooting:")
613
+ print(" - Check if Ruby symbols are loaded")
614
+ print(" - Ensure the process is stopped at a Ruby frame")
615
+ return
616
+
617
+ self.printer.print_backtrace(ctx.ec, from_tty)
618
+ except debugger.Error as e:
619
+ print(f"Error getting execution context: {e}")
620
+ print("Try selecting a fiber first with 'rb-fiber-switch'")
621
+ return
622
+
623
+ except Exception as e:
624
+ print(f"Error: {e}")
625
+ import traceback
626
+ traceback.print_exc()
627
+
628
+
629
+ # Register commands
630
+ RubyStackTraceCommand()