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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/fiber-debugging.md +1 -1
- data/context/getting-started.md +36 -14
- data/context/index.yaml +1 -1
- data/context/object-inspection.md +25 -25
- data/context/stack-inspection.md +3 -3
- data/data/toolbox/command.py +225 -0
- data/data/toolbox/context.py +358 -282
- data/data/toolbox/debugger/__init__.py +2 -0
- data/data/toolbox/debugger/gdb_backend.py +70 -1
- data/data/toolbox/debugger/lldb_backend.py +110 -9
- data/data/toolbox/fiber.py +837 -845
- data/data/toolbox/format.py +70 -65
- data/data/toolbox/heap.py +66 -56
- data/data/toolbox/init.py +9 -5
- data/data/toolbox/print.py +79 -0
- data/data/toolbox/rarray.py +4 -12
- data/data/toolbox/rbasic.py +31 -35
- data/data/toolbox/rbignum.py +3 -7
- data/data/toolbox/rclass.py +5 -5
- data/data/toolbox/readme.md +8 -8
- data/data/toolbox/rexception.py +3 -3
- data/data/toolbox/rfloat.py +8 -18
- data/data/toolbox/rhash.py +4 -12
- data/data/toolbox/rstring.py +4 -8
- data/data/toolbox/rstruct.py +6 -14
- data/data/toolbox/rsymbol.py +9 -33
- data/data/toolbox/{value.py → rvalue.py} +9 -9
- data/data/toolbox/stack.py +595 -605
- data/lib/toolbox/version.rb +1 -1
- data/readme.md +1 -1
- data.tar.gz.sig +0 -0
- metadata +3 -3
- metadata.gz.sig +0 -0
- data/data/toolbox/object.py +0 -84
data/data/toolbox/format.py
CHANGED
|
@@ -6,6 +6,7 @@ less prominent and values more readable.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import sys
|
|
9
|
+
import rvalue
|
|
9
10
|
|
|
10
11
|
class Style:
|
|
11
12
|
"""Sentinel object representing a style."""
|
|
@@ -28,66 +29,60 @@ method = Style('method') # Alias for symbol (method names)
|
|
|
28
29
|
error = Style('error')
|
|
29
30
|
bold = Style('bold')
|
|
30
31
|
dim = Style('dim')
|
|
32
|
+
title = Style('title') # For section headers in help text
|
|
33
|
+
placeholder = Style('placeholder') # For help text placeholders (parameters, options)
|
|
34
|
+
example = Style('example') # For example commands in help text
|
|
31
35
|
|
|
32
36
|
class Text:
|
|
33
37
|
"""Plain text output without any formatting."""
|
|
34
38
|
|
|
35
|
-
def
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
def __init__(self, output=None):
|
|
40
|
+
"""Initialize text terminal.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
output: Output stream (default: sys.stdout)
|
|
44
|
+
"""
|
|
45
|
+
self.output = output or sys.stdout
|
|
46
|
+
|
|
47
|
+
def print(self, *args, end='\n'):
|
|
48
|
+
"""Print arguments to output stream.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
*args: Arguments to print (strings, Style sentinels, or objects with print_to)
|
|
52
|
+
end: String appended after the last arg (default: newline)
|
|
49
53
|
"""
|
|
50
|
-
|
|
54
|
+
for arg in args:
|
|
55
|
+
if isinstance(arg, Style):
|
|
56
|
+
# Skip style sentinels in plain text mode
|
|
57
|
+
continue
|
|
58
|
+
elif hasattr(arg, 'print_to'):
|
|
59
|
+
# Let object print itself
|
|
60
|
+
arg.print_to(self)
|
|
61
|
+
else:
|
|
62
|
+
self.output.write(str(arg))
|
|
63
|
+
self.output.write(end)
|
|
64
|
+
|
|
65
|
+
def print_type_tag(self, type_name, addr=None, details=None):
|
|
66
|
+
"""Print a type tag like <T_ARRAY@0xABCD embedded length=3>.
|
|
51
67
|
|
|
52
68
|
Arguments:
|
|
53
69
|
type_name: Type name (e.g., "T_ARRAY", "void *")
|
|
54
|
-
|
|
70
|
+
addr: Optional hex address (as integer or hex string without 0x)
|
|
55
71
|
details: Optional details string (e.g., "embedded length=3")
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Formatted string with appropriate styling
|
|
59
72
|
"""
|
|
60
|
-
if isinstance(
|
|
61
|
-
|
|
73
|
+
if isinstance(addr, int):
|
|
74
|
+
addr = f"{addr:x}"
|
|
62
75
|
|
|
63
|
-
|
|
76
|
+
self.print(metadata, '<', reset, type, type_name, reset, end='')
|
|
64
77
|
|
|
65
|
-
if
|
|
66
|
-
|
|
78
|
+
if addr:
|
|
79
|
+
# @ symbol in dim, address in magenta
|
|
80
|
+
self.print(metadata, '@', reset, address, f'0x{addr}', reset, end='')
|
|
67
81
|
|
|
68
82
|
if details:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
parts.extend([metadata, '>', reset])
|
|
83
|
+
self.print(metadata, f' {details}', reset, end='')
|
|
72
84
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def print_value_with_tag(self, type_tag, val=None, value_style=value):
|
|
76
|
-
"""
|
|
77
|
-
Format a complete value with type tag and optional value.
|
|
78
|
-
|
|
79
|
-
Arguments:
|
|
80
|
-
type_tag: Formatted type tag string
|
|
81
|
-
val: Optional value to display
|
|
82
|
-
value_style: Style sentinel to use for the value
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
Complete formatted string
|
|
86
|
-
"""
|
|
87
|
-
if val is not None:
|
|
88
|
-
return f"{type_tag} {self.print(value_style, str(val), reset)}"
|
|
89
|
-
else:
|
|
90
|
-
return type_tag
|
|
85
|
+
self.print(metadata, '>', reset, end='')
|
|
91
86
|
|
|
92
87
|
class XTerm(Text):
|
|
93
88
|
"""ANSI color/style output for terminal."""
|
|
@@ -112,36 +107,48 @@ class XTerm(Text):
|
|
|
112
107
|
BRIGHT_MAGENTA = '\033[95m'
|
|
113
108
|
BRIGHT_CYAN = '\033[96m'
|
|
114
109
|
|
|
115
|
-
def __init__(self):
|
|
110
|
+
def __init__(self, output=None):
|
|
111
|
+
"""Initialize ANSI terminal.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
output: Output stream (default: sys.stdout)
|
|
115
|
+
"""
|
|
116
|
+
super().__init__(output)
|
|
116
117
|
# Map style sentinels to ANSI codes
|
|
117
118
|
self.style_map = {
|
|
118
119
|
reset: self.RESET,
|
|
119
|
-
metadata: self.DIM,
|
|
120
|
-
address: self.
|
|
121
|
-
type:
|
|
122
|
-
value: '',
|
|
120
|
+
metadata: self.DIM, # Type tag brackets <>
|
|
121
|
+
address: self.MAGENTA, # Memory addresses in type tags
|
|
122
|
+
type: self.CYAN, # Type names (T_ARRAY, VALUE, etc.)
|
|
123
123
|
string: self.GREEN,
|
|
124
124
|
number: self.CYAN,
|
|
125
125
|
symbol: self.YELLOW,
|
|
126
|
-
method: self.YELLOW,
|
|
126
|
+
method: self.YELLOW, # Same as symbol
|
|
127
127
|
error: self.RED,
|
|
128
128
|
bold: self.BOLD,
|
|
129
129
|
dim: self.DIM,
|
|
130
|
+
title: self.BOLD, # Section headers in help text
|
|
131
|
+
placeholder: self.BLUE, # Help text placeholders
|
|
132
|
+
example: self.GREEN, # Example commands in help text
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
def print(self, *args):
|
|
133
|
-
"""Print arguments
|
|
134
|
-
|
|
135
|
+
def print(self, *args, end='\n'):
|
|
136
|
+
"""Print arguments to output stream with ANSI formatting.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
*args: Arguments to print (strings, Style sentinels, or objects with print_to)
|
|
140
|
+
end: String appended after the last arg (default: newline)
|
|
141
|
+
"""
|
|
135
142
|
for arg in args:
|
|
136
143
|
if isinstance(arg, Style):
|
|
137
|
-
|
|
144
|
+
# Print ANSI code for style
|
|
145
|
+
self.output.write(self.style_map.get(arg, ''))
|
|
146
|
+
elif hasattr(arg, 'print_to'):
|
|
147
|
+
# Let object print itself
|
|
148
|
+
arg.print_to(self)
|
|
138
149
|
else:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
result.append(arg.print_to(self))
|
|
142
|
-
else:
|
|
143
|
-
result.append(str(arg))
|
|
144
|
-
return "".join(result)
|
|
150
|
+
self.output.write(str(arg))
|
|
151
|
+
self.output.write(end)
|
|
145
152
|
|
|
146
153
|
# Helper for creating the appropriate terminal based on TTY status
|
|
147
154
|
def create_terminal(from_tty):
|
|
@@ -166,7 +173,7 @@ class Printer:
|
|
|
166
173
|
|
|
167
174
|
def print(self, *args):
|
|
168
175
|
"""Print arguments using the terminal's formatting."""
|
|
169
|
-
|
|
176
|
+
self.terminal.print(*args)
|
|
170
177
|
|
|
171
178
|
def print_indent(self, depth):
|
|
172
179
|
"""Print indentation based on depth."""
|
|
@@ -194,7 +201,5 @@ class Printer:
|
|
|
194
201
|
value_int = int(ruby_value)
|
|
195
202
|
self.debug(f"print_value: value=0x{value_int:x}, depth={depth}")
|
|
196
203
|
|
|
197
|
-
|
|
198
|
-
import value
|
|
199
|
-
ruby_object = value.interpret(ruby_value)
|
|
204
|
+
ruby_object = rvalue.interpret(ruby_value)
|
|
200
205
|
ruby_object.print_recursive(self, depth)
|
data/data/toolbox/heap.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import debugger
|
|
2
2
|
import sys
|
|
3
|
+
import command
|
|
4
|
+
import constants
|
|
5
|
+
import format
|
|
6
|
+
import rvalue
|
|
3
7
|
|
|
4
8
|
# Constants
|
|
5
9
|
RBASIC_FLAGS_TYPE_MASK = 0x1f
|
|
@@ -66,15 +70,15 @@ class RubyHeap:
|
|
|
66
70
|
return False
|
|
67
71
|
|
|
68
72
|
# Cache commonly used type lookups
|
|
69
|
-
self._rbasic_type =
|
|
70
|
-
self._value_type =
|
|
71
|
-
self._char_ptr_type =
|
|
73
|
+
self._rbasic_type = constants.type_struct('struct RBasic').pointer()
|
|
74
|
+
self._value_type = constants.type_struct('VALUE')
|
|
75
|
+
self._char_ptr_type = constants.type_struct('char').pointer()
|
|
72
76
|
|
|
73
77
|
# Cache flags field offset for fast memory access
|
|
74
78
|
# This is critical for LLDB performance where field lookup is expensive
|
|
75
79
|
try:
|
|
76
80
|
# Get a dummy RBasic to find the flags offset
|
|
77
|
-
rbasic_struct =
|
|
81
|
+
rbasic_struct = constants.type_struct('struct RBasic')
|
|
78
82
|
# In RBasic, 'flags' is the first field (offset 0)
|
|
79
83
|
# We need to find its offset programmatically for portability
|
|
80
84
|
fields = rbasic_struct.fields()
|
|
@@ -277,9 +281,9 @@ class RubyHeap:
|
|
|
277
281
|
return
|
|
278
282
|
|
|
279
283
|
# Cache types for pointer arithmetic and casting
|
|
280
|
-
rbasic_type =
|
|
284
|
+
rbasic_type = constants.type_struct('struct RBasic')
|
|
281
285
|
rbasic_ptr_type = rbasic_type.pointer()
|
|
282
|
-
char_ptr_type =
|
|
286
|
+
char_ptr_type = constants.type_struct('char').pointer()
|
|
283
287
|
|
|
284
288
|
for i in range(start_page, allocated_pages):
|
|
285
289
|
page = self._get_page(i)
|
|
@@ -400,7 +404,7 @@ class RubyHeap:
|
|
|
400
404
|
T_DATA = 0x0c
|
|
401
405
|
|
|
402
406
|
# Get RTypedData type for casting
|
|
403
|
-
rtypeddata_type =
|
|
407
|
+
rtypeddata_type = constants.type_struct('struct RTypedData').pointer()
|
|
404
408
|
|
|
405
409
|
try:
|
|
406
410
|
if progress:
|
|
@@ -431,9 +435,29 @@ class RubyHeap:
|
|
|
431
435
|
# Cast to RTypedData and check type
|
|
432
436
|
try:
|
|
433
437
|
typed_data = obj.cast(rtypeddata_type)
|
|
438
|
+
type_field = typed_data['type']
|
|
434
439
|
|
|
435
|
-
#
|
|
436
|
-
if
|
|
440
|
+
# Check if field access failed (returns None when type is incomplete)
|
|
441
|
+
if type_field is None:
|
|
442
|
+
# On first failure, print a helpful error message once
|
|
443
|
+
if not hasattr(self, '_incomplete_type_warning_shown'):
|
|
444
|
+
self._incomplete_type_warning_shown = True
|
|
445
|
+
print("\nError: struct RTypedData debug symbols are incomplete", file=sys.stderr)
|
|
446
|
+
print("Cannot access RTypedData fields with this Ruby version.", file=sys.stderr)
|
|
447
|
+
print("\nThis is a known issue with Ruby 3.4.x on macOS:", file=sys.stderr)
|
|
448
|
+
print(" • A dsymutil bug drops RTypedData from debug symbols", file=sys.stderr)
|
|
449
|
+
print(" • Caused by complex 'const T *const' type in the struct", file=sys.stderr)
|
|
450
|
+
print(" • Fixed in Ruby head (commit ce51ef30df)", file=sys.stderr)
|
|
451
|
+
print("\nWorkarounds:", file=sys.stderr)
|
|
452
|
+
print(" • Use Ruby head: ruby-install ruby-head -- CFLAGS=\"-g -O0\"", file=sys.stderr)
|
|
453
|
+
print(" • Or use GDB on Linux (works with Ruby 3.4.x)", file=sys.stderr)
|
|
454
|
+
print("\nSee: https://socketry.github.io/toolbox/guides/getting-started/", file=sys.stderr)
|
|
455
|
+
print(file=sys.stderr)
|
|
456
|
+
# Can't scan without complete type info
|
|
457
|
+
break
|
|
458
|
+
|
|
459
|
+
# Compare type pointers
|
|
460
|
+
if type_field == data_type:
|
|
437
461
|
# Return the VALUE, not the extracted data pointer
|
|
438
462
|
objects.append(obj)
|
|
439
463
|
if progress:
|
|
@@ -450,7 +474,7 @@ class RubyHeap:
|
|
|
450
474
|
return objects
|
|
451
475
|
|
|
452
476
|
|
|
453
|
-
class
|
|
477
|
+
class RubyHeapScanHandler:
|
|
454
478
|
"""Scan the Ruby heap for objects, optionally filtered by type.
|
|
455
479
|
|
|
456
480
|
Usage: rb-heap-scan [--type TYPE] [--limit N] [--from $heap]
|
|
@@ -480,23 +504,21 @@ class RubyHeapScanCommand(debugger.Command):
|
|
|
480
504
|
rb-heap-scan --from $heap # Continue from last scan
|
|
481
505
|
"""
|
|
482
506
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
print(" rb-heap-scan --type RUBY_T_STRING --limit 10 # First page")
|
|
499
|
-
print(" rb-heap-scan --type RUBY_T_STRING --from $heap # Next page")
|
|
507
|
+
USAGE = command.Usage(
|
|
508
|
+
summary="Scan the Ruby heap for objects, optionally filtered by type",
|
|
509
|
+
parameters=[],
|
|
510
|
+
options={
|
|
511
|
+
'type': (str, None, 'Filter by Ruby type (e.g., RUBY_T_STRING, RUBY_T_ARRAY, or 0x05)'),
|
|
512
|
+
'limit': (int, 10, 'Maximum objects to find'),
|
|
513
|
+
'from': (str, None, 'Start address for pagination (use $heap)')
|
|
514
|
+
},
|
|
515
|
+
flags=[],
|
|
516
|
+
examples=[
|
|
517
|
+
("rb-heap-scan --type RUBY_T_STRING", "Find up to 10 strings"),
|
|
518
|
+
("rb-heap-scan --type RUBY_T_ARRAY --limit 20", "Find first 20 arrays"),
|
|
519
|
+
("rb-heap-scan --from $heap", "Continue from last scan (pagination)")
|
|
520
|
+
]
|
|
521
|
+
)
|
|
500
522
|
|
|
501
523
|
def _parse_type(self, type_arg):
|
|
502
524
|
"""Parse a type argument and return the type value.
|
|
@@ -530,13 +552,9 @@ class RubyHeapScanCommand(debugger.Command):
|
|
|
530
552
|
|
|
531
553
|
return type_value
|
|
532
554
|
|
|
533
|
-
def invoke(self,
|
|
555
|
+
def invoke(self, arguments, terminal):
|
|
534
556
|
"""Execute the heap scan command."""
|
|
535
557
|
try:
|
|
536
|
-
# Parse arguments
|
|
537
|
-
import command
|
|
538
|
-
arguments = command.parse_arguments(arg if arg else "")
|
|
539
|
-
|
|
540
558
|
# Check if we're continuing from a previous scan
|
|
541
559
|
from_option = arguments.get_option('from')
|
|
542
560
|
if from_option is not None:
|
|
@@ -595,69 +613,61 @@ class RubyHeapScanCommand(debugger.Command):
|
|
|
595
613
|
print("(You may have reached the end of the heap)")
|
|
596
614
|
return
|
|
597
615
|
|
|
598
|
-
# Import format for terminal output
|
|
599
|
-
import format
|
|
600
|
-
terminal = format.create_terminal(from_tty)
|
|
601
|
-
|
|
602
|
-
# Import value module for interpretation
|
|
603
|
-
import value as value_module
|
|
604
|
-
|
|
605
616
|
print(f"Found {len(objects)} object(s):")
|
|
606
617
|
print()
|
|
607
618
|
|
|
608
619
|
for i, obj in enumerate(objects):
|
|
609
|
-
obj_int = int(obj)
|
|
610
|
-
|
|
611
620
|
# Set as convenience variable
|
|
621
|
+
obj_int = int(obj)
|
|
612
622
|
var_name = f"heap{i}"
|
|
613
623
|
debugger.set_convenience_variable(var_name, obj)
|
|
614
624
|
|
|
615
625
|
# Try to interpret and display the object
|
|
616
626
|
try:
|
|
617
|
-
interpreted =
|
|
627
|
+
interpreted = rvalue.interpret(obj)
|
|
618
628
|
|
|
619
|
-
|
|
629
|
+
terminal.print(
|
|
620
630
|
format.metadata, f" [{i}] ",
|
|
621
631
|
format.dim, f"${var_name} = ",
|
|
622
632
|
format.reset, interpreted
|
|
623
|
-
)
|
|
633
|
+
)
|
|
624
634
|
except Exception as e:
|
|
625
|
-
|
|
635
|
+
terminal.print(
|
|
626
636
|
format.metadata, f" [{i}] ",
|
|
627
637
|
format.dim, f"${var_name} = ",
|
|
628
638
|
format.error, f"<error: {e}>"
|
|
629
|
-
)
|
|
639
|
+
)
|
|
630
640
|
|
|
631
641
|
print()
|
|
632
|
-
|
|
642
|
+
terminal.print(
|
|
633
643
|
format.dim,
|
|
634
644
|
f"Objects saved in $heap0 through $heap{len(objects)-1}",
|
|
635
645
|
format.reset
|
|
636
|
-
)
|
|
646
|
+
)
|
|
637
647
|
|
|
638
648
|
# Save next address to $heap for pagination
|
|
639
649
|
if next_address is not None:
|
|
640
650
|
# Save the next address to continue from
|
|
641
|
-
void_ptr_type =
|
|
651
|
+
void_ptr_type = constants.type_struct('void').pointer()
|
|
642
652
|
debugger.set_convenience_variable('heap', debugger.create_value(next_address, void_ptr_type))
|
|
643
|
-
|
|
653
|
+
terminal.print(
|
|
644
654
|
format.dim,
|
|
645
655
|
f"Next scan address saved to $heap: 0x{next_address:016x}",
|
|
646
656
|
format.reset
|
|
647
|
-
)
|
|
648
|
-
|
|
657
|
+
)
|
|
658
|
+
terminal.print(
|
|
649
659
|
format.dim,
|
|
650
660
|
f"Run 'rb-heap-scan --type {type_option if type_option else '...'} --from $heap' for next page",
|
|
651
661
|
format.reset
|
|
652
|
-
)
|
|
662
|
+
)
|
|
653
663
|
else:
|
|
654
664
|
# Reached the end of the heap - unset $heap so next scan starts fresh
|
|
655
665
|
debugger.set_convenience_variable('heap', None)
|
|
656
|
-
|
|
666
|
+
terminal.print(
|
|
657
667
|
format.dim,
|
|
658
668
|
f"Reached end of heap (no more objects to scan)",
|
|
659
669
|
format.reset
|
|
660
|
-
)
|
|
670
|
+
)
|
|
661
671
|
|
|
662
672
|
except Exception as e:
|
|
663
673
|
print(f"Error: {e}")
|
|
@@ -666,4 +676,4 @@ class RubyHeapScanCommand(debugger.Command):
|
|
|
666
676
|
|
|
667
677
|
|
|
668
678
|
# Register commands
|
|
669
|
-
|
|
679
|
+
debugger.register("rb-heap-scan", RubyHeapScanHandler, usage=RubyHeapScanHandler.USAGE)
|
data/data/toolbox/init.py
CHANGED
|
@@ -24,8 +24,8 @@ failed_extensions = []
|
|
|
24
24
|
|
|
25
25
|
# Try to load each extension individually
|
|
26
26
|
extensions_to_load = [
|
|
27
|
-
('
|
|
28
|
-
('context', 'rb-context'),
|
|
27
|
+
('print', 'rb-print'),
|
|
28
|
+
('context', 'rb-context, rb-context-storage'),
|
|
29
29
|
('fiber', 'rb-fiber-scan-heap, rb-fiber-switch'),
|
|
30
30
|
('stack', 'rb-stack-trace'),
|
|
31
31
|
('heap', 'rb-heap-scan'),
|
|
@@ -33,8 +33,8 @@ extensions_to_load = [
|
|
|
33
33
|
|
|
34
34
|
for module_name, commands in extensions_to_load:
|
|
35
35
|
try:
|
|
36
|
-
if module_name == '
|
|
37
|
-
import
|
|
36
|
+
if module_name == 'print':
|
|
37
|
+
import print as print_module
|
|
38
38
|
elif module_name == 'context':
|
|
39
39
|
import context
|
|
40
40
|
elif module_name == 'fiber':
|
|
@@ -44,8 +44,12 @@ for module_name, commands in extensions_to_load:
|
|
|
44
44
|
elif module_name == 'heap':
|
|
45
45
|
import heap
|
|
46
46
|
loaded_extensions.append((module_name, commands))
|
|
47
|
-
except
|
|
47
|
+
except Exception as e:
|
|
48
|
+
# Catch all exceptions during module load
|
|
48
49
|
failed_extensions.append((module_name, str(e)))
|
|
50
|
+
import traceback
|
|
51
|
+
print(f"Failed to load {module_name}: {e}", file=sys.stderr)
|
|
52
|
+
traceback.print_exc(file=sys.stderr)
|
|
49
53
|
|
|
50
54
|
# Silently load - no status messages printed by default
|
|
51
55
|
# Users can run 'help' to see available commands
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Print command for Ruby values."""
|
|
2
|
+
|
|
3
|
+
import debugger
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# Import utilities
|
|
7
|
+
import command
|
|
8
|
+
import constants
|
|
9
|
+
import rvalue
|
|
10
|
+
import rstring
|
|
11
|
+
import rarray
|
|
12
|
+
import rhash
|
|
13
|
+
import rsymbol
|
|
14
|
+
import rstruct
|
|
15
|
+
import rfloat
|
|
16
|
+
import rbignum
|
|
17
|
+
import rbasic
|
|
18
|
+
import format
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RubyObjectPrinter:
|
|
22
|
+
"""Print Ruby objects with recursive descent into nested structures."""
|
|
23
|
+
|
|
24
|
+
USAGE = command.Usage(
|
|
25
|
+
summary="Print Ruby objects with recursive inspection",
|
|
26
|
+
parameters=[('value', 'VALUE or expression to print')],
|
|
27
|
+
options={
|
|
28
|
+
'depth': (int, 1, 'Maximum recursion depth for nested objects')
|
|
29
|
+
},
|
|
30
|
+
flags=[
|
|
31
|
+
('debug', 'Show internal structure and debug information')
|
|
32
|
+
],
|
|
33
|
+
examples=[
|
|
34
|
+
("rb-print $errinfo", "Print exception object"),
|
|
35
|
+
("rb-print $ec->storage --depth 3", "Print fiber storage with depth 3"),
|
|
36
|
+
("rb-print $ec->cfp->sp[-1] --debug", "Print top of stack with debug info")
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def invoke(self, arguments, terminal):
|
|
41
|
+
"""Execute the print command.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
arguments: Parsed Arguments object
|
|
45
|
+
terminal: Terminal formatter (already configured for TTY/non-TTY)
|
|
46
|
+
"""
|
|
47
|
+
# Get options
|
|
48
|
+
max_depth = arguments.get_option('depth', 1)
|
|
49
|
+
debug_mode = arguments.has_flag('debug')
|
|
50
|
+
|
|
51
|
+
# Validate depth
|
|
52
|
+
if max_depth < 1:
|
|
53
|
+
print("Error: --depth must be >= 1")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# Create printer
|
|
57
|
+
printer = format.Printer(terminal, max_depth, debug_mode)
|
|
58
|
+
|
|
59
|
+
# Process each expression
|
|
60
|
+
for expression in arguments.expressions:
|
|
61
|
+
try:
|
|
62
|
+
# Evaluate the expression
|
|
63
|
+
ruby_value = debugger.parse_and_eval(expression)
|
|
64
|
+
|
|
65
|
+
# Interpret the value and let it print itself recursively
|
|
66
|
+
ruby_object = rvalue.interpret(ruby_value)
|
|
67
|
+
ruby_object.print_recursive(printer, max_depth)
|
|
68
|
+
except debugger.Error as e:
|
|
69
|
+
print(f"Error evaluating expression '{expression}': {e}")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"Error processing '{expression}': {type(e).__name__}: {e}")
|
|
72
|
+
if debug_mode:
|
|
73
|
+
import traceback
|
|
74
|
+
traceback.print_exc(file=sys.stderr)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Register command using new interface
|
|
78
|
+
debugger.register("rb-print", RubyObjectPrinter, usage=RubyObjectPrinter.USAGE)
|
|
79
|
+
|
data/data/toolbox/rarray.py
CHANGED
|
@@ -77,12 +77,8 @@ class RArrayEmbedded(RArrayBase):
|
|
|
77
77
|
def print_to(self, terminal):
|
|
78
78
|
"""Print this array with formatting."""
|
|
79
79
|
addr = int(self.value)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
format.type, 'T_ARRAY',
|
|
83
|
-
format.metadata, f'@0x{addr:x} embedded length={len(self)}>',
|
|
84
|
-
format.reset
|
|
85
|
-
)
|
|
80
|
+
details = f"embedded length={len(self)}"
|
|
81
|
+
terminal.print_type_tag('T_ARRAY', addr, details)
|
|
86
82
|
|
|
87
83
|
class RArrayHeap(RArrayBase):
|
|
88
84
|
"""Heap array (larger arrays with separate memory allocation)."""
|
|
@@ -101,12 +97,8 @@ class RArrayHeap(RArrayBase):
|
|
|
101
97
|
def print_to(self, terminal):
|
|
102
98
|
"""Print this array with formatting."""
|
|
103
99
|
addr = int(self.value)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
format.type, 'T_ARRAY',
|
|
107
|
-
format.metadata, f'@0x{addr:x} heap length={len(self)}>',
|
|
108
|
-
format.reset
|
|
109
|
-
)
|
|
100
|
+
details = f"heap length={len(self)}"
|
|
101
|
+
terminal.print_type_tag('T_ARRAY', addr, details)
|
|
110
102
|
|
|
111
103
|
def RArray(value):
|
|
112
104
|
"""Factory function that returns the appropriate RArray variant.
|
data/data/toolbox/rbasic.py
CHANGED
|
@@ -29,39 +29,39 @@ def is_type(value, ruby_type_constant):
|
|
|
29
29
|
|
|
30
30
|
# Map of type constants to their names for display
|
|
31
31
|
TYPE_NAMES = {
|
|
32
|
-
'RUBY_T_NONE': '
|
|
33
|
-
'RUBY_T_OBJECT': '
|
|
34
|
-
'RUBY_T_CLASS': '
|
|
35
|
-
'RUBY_T_MODULE': '
|
|
36
|
-
'RUBY_T_FLOAT': '
|
|
37
|
-
'RUBY_T_STRING': '
|
|
38
|
-
'RUBY_T_REGEXP': '
|
|
39
|
-
'RUBY_T_ARRAY': '
|
|
40
|
-
'RUBY_T_HASH': '
|
|
41
|
-
'RUBY_T_STRUCT': '
|
|
42
|
-
'RUBY_T_BIGNUM': '
|
|
43
|
-
'RUBY_T_FILE': '
|
|
44
|
-
'RUBY_T_DATA': '
|
|
45
|
-
'RUBY_T_MATCH': '
|
|
46
|
-
'RUBY_T_COMPLEX': '
|
|
47
|
-
'RUBY_T_RATIONAL': '
|
|
48
|
-
'RUBY_T_NIL': '
|
|
49
|
-
'RUBY_T_TRUE': '
|
|
50
|
-
'RUBY_T_FALSE': '
|
|
51
|
-
'RUBY_T_SYMBOL': '
|
|
52
|
-
'RUBY_T_FIXNUM': '
|
|
53
|
-
'RUBY_T_UNDEF': '
|
|
54
|
-
'RUBY_T_IMEMO': '
|
|
55
|
-
'RUBY_T_NODE': '
|
|
56
|
-
'RUBY_T_ICLASS': '
|
|
57
|
-
'RUBY_T_ZOMBIE': '
|
|
32
|
+
'RUBY_T_NONE': 'T_NONE',
|
|
33
|
+
'RUBY_T_OBJECT': 'T_OBJECT',
|
|
34
|
+
'RUBY_T_CLASS': 'T_CLASS',
|
|
35
|
+
'RUBY_T_MODULE': 'T_MODULE',
|
|
36
|
+
'RUBY_T_FLOAT': 'T_FLOAT',
|
|
37
|
+
'RUBY_T_STRING': 'T_STRING',
|
|
38
|
+
'RUBY_T_REGEXP': 'T_REGEXP',
|
|
39
|
+
'RUBY_T_ARRAY': 'T_ARRAY',
|
|
40
|
+
'RUBY_T_HASH': 'T_HASH',
|
|
41
|
+
'RUBY_T_STRUCT': 'T_STRUCT',
|
|
42
|
+
'RUBY_T_BIGNUM': 'T_BIGNUM',
|
|
43
|
+
'RUBY_T_FILE': 'T_FILE',
|
|
44
|
+
'RUBY_T_DATA': 'T_DATA',
|
|
45
|
+
'RUBY_T_MATCH': 'T_MATCH',
|
|
46
|
+
'RUBY_T_COMPLEX': 'T_COMPLEX',
|
|
47
|
+
'RUBY_T_RATIONAL': 'T_RATIONAL',
|
|
48
|
+
'RUBY_T_NIL': 'T_NIL',
|
|
49
|
+
'RUBY_T_TRUE': 'T_TRUE',
|
|
50
|
+
'RUBY_T_FALSE': 'T_FALSE',
|
|
51
|
+
'RUBY_T_SYMBOL': 'T_SYMBOL',
|
|
52
|
+
'RUBY_T_FIXNUM': 'T_FIXNUM',
|
|
53
|
+
'RUBY_T_UNDEF': 'T_UNDEF',
|
|
54
|
+
'RUBY_T_IMEMO': 'T_IMEMO',
|
|
55
|
+
'RUBY_T_NODE': 'T_NODE',
|
|
56
|
+
'RUBY_T_ICLASS': 'T_ICLASS',
|
|
57
|
+
'RUBY_T_ZOMBIE': 'T_ZOMBIE',
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
def type_name(value):
|
|
61
61
|
"""Get the human-readable type name for a VALUE.
|
|
62
62
|
|
|
63
63
|
Returns:
|
|
64
|
-
String like '
|
|
64
|
+
String like 'T_STRING', 'T_ARRAY', 'T_HASH', etc., or 'Unknown(0x...)'
|
|
65
65
|
"""
|
|
66
66
|
type_flag = type_of(value)
|
|
67
67
|
|
|
@@ -85,18 +85,14 @@ class RBasic:
|
|
|
85
85
|
|
|
86
86
|
def __str__(self):
|
|
87
87
|
type_str = type_name(self.value)
|
|
88
|
-
return f"<{type_str}
|
|
88
|
+
return f"<{type_str}@0x{int(self.value):x}>"
|
|
89
89
|
|
|
90
90
|
def print_to(self, terminal):
|
|
91
|
-
"""
|
|
91
|
+
"""Print formatted basic object representation."""
|
|
92
92
|
type_str = type_name(self.value)
|
|
93
93
|
addr = int(self.value)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
format.type, type_str,
|
|
97
|
-
format.metadata, f':0x{addr:x}>',
|
|
98
|
-
format.reset
|
|
99
|
-
)
|
|
94
|
+
# Use print_type_tag for consistency with other types
|
|
95
|
+
terminal.print_type_tag(type_str, addr)
|
|
100
96
|
|
|
101
97
|
def print_recursive(self, printer, depth):
|
|
102
98
|
"""Print this basic object (no recursion)."""
|