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