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