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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f8bc49b89f83b1baa5270a63e16df4c63dfc3de61fa98e8c6da8fabbcde66e87
4
+ data.tar.gz: cd5b34b044e80e8aa2b278a67fa138cd5230829da83aa9710b04b8b77478b8ff
5
+ SHA512:
6
+ metadata.gz: 891326e75355c9a42cbed524b63345f7b47e6ee8ed29f5c08ee96a5c8a40cb495ad1913bfeb0043b2479f1e08ff4c763d93b4f8231a30f5b958a356eabc0ae46
7
+ data.tar.gz: c3edb1db9689d4b399c96a112876d8b8ba3f3b4754903d61e2fafb999d2913409509f4ee2a4964096a4ed5b30ccec0647deee5843a31d77b8700de0ef62ee9b9
checksums.yaml.gz.sig ADDED
Binary file
data/bake/ruby/gdb.rb ADDED
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "ruby/gdb"
7
+ require "fileutils"
8
+
9
+ # Install GDB extensions by adding source line to ~/.gdbinit
10
+ # @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
11
+ def install(gdbinit: nil)
12
+ gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
13
+ init_py_path = Ruby::GDB.init_script_path
14
+ source_line = "source #{init_py_path}"
15
+ marker_comment = "# Ruby GDB Extensions (ruby-gdb gem)"
16
+
17
+ puts "Installing Ruby GDB extensions..."
18
+ puts " Extensions: #{File.dirname(init_py_path)}"
19
+ puts " Config: #{gdbinit_path}"
20
+
21
+ # Read existing .gdbinit or create empty array
22
+ lines = File.exist?(gdbinit_path) ? File.readlines(gdbinit_path) : []
23
+
24
+ # Check if already installed (look for marker comment)
25
+ marker_index = lines.index{|line| line.strip == marker_comment}
26
+
27
+ if marker_index
28
+ # Already installed - update the source line in case path changed
29
+ source_index = marker_index + 1
30
+ if source_index < lines.size && lines[source_index].strip.start_with?("source")
31
+ old_source = lines[source_index].strip
32
+ if old_source == source_line
33
+ puts "\n✓ Already installed in #{gdbinit_path}"
34
+ puts " #{source_line}"
35
+ return
36
+ else
37
+ # Path changed - update it
38
+ lines[source_index] = "#{source_line}\n"
39
+ File.write(gdbinit_path, lines.join)
40
+ puts "\n✓ Updated installation in #{gdbinit_path}"
41
+ puts " Old: #{old_source}"
42
+ puts " New: #{source_line}"
43
+ return
44
+ end
45
+ end
46
+ end
47
+
48
+ # Not installed - add it
49
+ File.open(gdbinit_path, "a") do |f|
50
+ f.puts unless lines.last&.strip&.empty?
51
+ f.puts marker_comment
52
+ f.puts source_line
53
+ end
54
+
55
+ puts "\n✓ Installation complete!"
56
+ puts "\nAdded to #{gdbinit_path}:"
57
+ puts " #{source_line}"
58
+ puts "\nExtensions will load automatically when you start GDB."
59
+ end
60
+
61
+ # Uninstall GDB extensions by removing source line from ~/.gdbinit
62
+ # @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
63
+ def uninstall(gdbinit: nil)
64
+ gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
65
+ marker_comment = "# Ruby GDB Extensions (ruby-gdb gem)"
66
+
67
+ puts "Uninstalling Ruby GDB extensions..."
68
+
69
+ unless File.exist?(gdbinit_path)
70
+ puts "No ~/.gdbinit file found - nothing to uninstall."
71
+ return
72
+ end
73
+
74
+ lines = File.readlines(gdbinit_path)
75
+ marker_index = lines.index{|line| line.strip == marker_comment}
76
+
77
+ unless marker_index
78
+ puts "Extensions were not found in #{gdbinit_path}"
79
+ return
80
+ end
81
+
82
+ # Remove the marker comment and the source line after it
83
+ lines.delete_at(marker_index) # Remove comment
84
+ if marker_index < lines.size && lines[marker_index].strip.start_with?("source")
85
+ removed_line = lines.delete_at(marker_index).strip # Remove source line
86
+ puts " Removed: #{removed_line}"
87
+ end
88
+
89
+ # Clean up empty line before marker if it exists
90
+ if marker_index > 0 && lines[marker_index - 1].strip.empty?
91
+ lines.delete_at(marker_index - 1)
92
+ end
93
+
94
+ File.write(gdbinit_path, lines.join)
95
+ puts "✓ Removed Ruby GDB extensions from #{gdbinit_path}"
96
+ end
97
+
98
+ # Show installation information
99
+ # @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
100
+ def info(gdbinit: nil)
101
+ gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
102
+ init_py_path = Ruby::GDB.init_script_path
103
+ marker_comment = "# Ruby GDB Extensions (ruby-gdb gem)"
104
+
105
+ puts "Ruby GDB Extensions v#{Ruby::GDB::VERSION}"
106
+ puts "\nGem data directory: #{Ruby::GDB.data_path}"
107
+ puts "Init script: #{init_py_path}"
108
+
109
+ # Check installation status by looking for marker comment
110
+ installed = false
111
+ current_source = nil
112
+
113
+ if File.exist?(gdbinit_path)
114
+ lines = File.readlines(gdbinit_path)
115
+ marker_index = lines.index{|line| line.strip == marker_comment}
116
+ if marker_index
117
+ installed = true
118
+ source_index = marker_index + 1
119
+ if source_index < lines.size
120
+ current_source = lines[source_index].strip
121
+ end
122
+ end
123
+ end
124
+
125
+ puts "\nGDB config: #{gdbinit_path}"
126
+ if installed
127
+ puts "Status: ✓ Installed"
128
+ if current_source
129
+ puts " #{current_source}"
130
+ end
131
+ else
132
+ puts "Status: ✗ Not installed"
133
+ puts "\nRun: bake ruby:gdb:install"
134
+ end
135
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "toolbox/gdb"
7
+ require "fileutils"
8
+
9
+ # Install GDB extensions by adding source line to ~/.gdbinit
10
+ # @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
11
+ def install(gdbinit: nil)
12
+ gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
13
+ init_py_path = Toolbox::GDB.init_script_path
14
+ source_line = "source #{init_py_path}"
15
+ marker_comment = "# Ruby Toolbox GDB Extensions"
16
+
17
+ puts "Installing Ruby Toolbox GDB extensions..."
18
+ puts " Extensions: #{File.dirname(init_py_path)}"
19
+ puts " Config: #{gdbinit_path}"
20
+
21
+ # Read existing .gdbinit or create empty array
22
+ lines = File.exist?(gdbinit_path) ? File.readlines(gdbinit_path) : []
23
+
24
+ # Check if already installed (look for marker comment)
25
+ marker_index = lines.index{|line| line.strip == marker_comment}
26
+
27
+ if marker_index
28
+ # Already installed - update the source line in case path changed
29
+ source_index = marker_index + 1
30
+ if source_index < lines.size && lines[source_index].strip.start_with?("source")
31
+ old_source = lines[source_index].strip
32
+ if old_source == source_line
33
+ puts "\n✓ Already installed in #{gdbinit_path}"
34
+ puts " #{source_line}"
35
+ return
36
+ else
37
+ # Path changed - update it
38
+ lines[source_index] = "#{source_line}\n"
39
+ File.write(gdbinit_path, lines.join)
40
+ puts "\n✓ Updated installation in #{gdbinit_path}"
41
+ puts " Old: #{old_source}"
42
+ puts " New: #{source_line}"
43
+ return
44
+ end
45
+ end
46
+ end
47
+
48
+ # Not installed - add it
49
+ File.open(gdbinit_path, "a") do |f|
50
+ f.puts unless lines.last&.strip&.empty?
51
+ f.puts marker_comment
52
+ f.puts source_line
53
+ end
54
+
55
+ puts "\n✓ Installation complete!"
56
+ puts "\nAdded to #{gdbinit_path}:"
57
+ puts " #{source_line}"
58
+ puts "\nExtensions will load automatically when you start GDB."
59
+ end
60
+
61
+ # Uninstall GDB extensions by removing source line from ~/.gdbinit
62
+ # @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
63
+ def uninstall(gdbinit: nil)
64
+ gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
65
+ marker_comment = "# Ruby Toolbox GDB Extensions"
66
+
67
+ puts "Uninstalling Ruby Toolbox GDB extensions..."
68
+
69
+ unless File.exist?(gdbinit_path)
70
+ puts "No ~/.gdbinit file found - nothing to uninstall."
71
+ return
72
+ end
73
+
74
+ lines = File.readlines(gdbinit_path)
75
+ marker_index = lines.index{|line| line.strip == marker_comment}
76
+
77
+ unless marker_index
78
+ puts "Extensions were not found in #{gdbinit_path}"
79
+ return
80
+ end
81
+
82
+ # Remove the marker comment and the source line after it
83
+ lines.delete_at(marker_index) # Remove comment
84
+ if marker_index < lines.size && lines[marker_index].strip.start_with?("source")
85
+ removed_line = lines.delete_at(marker_index).strip # Remove source line
86
+ puts " Removed: #{removed_line}"
87
+ end
88
+
89
+ # Clean up empty line before marker if it exists
90
+ if marker_index > 0 && lines[marker_index - 1].strip.empty?
91
+ lines.delete_at(marker_index - 1)
92
+ end
93
+
94
+ File.write(gdbinit_path, lines.join)
95
+ puts "✓ Removed Ruby Toolbox GDB extensions from #{gdbinit_path}"
96
+ end
97
+
98
+ # Show installation information
99
+ # @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
100
+ def info(gdbinit: nil)
101
+ gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
102
+ init_py_path = Toolbox::GDB.init_script_path
103
+ marker_comment = "# Ruby Toolbox GDB Extensions"
104
+
105
+ puts "Ruby Toolbox GDB Extensions v#{Toolbox::VERSION}"
106
+ puts "\nToolbox directory: #{Toolbox::GDB.data_path}"
107
+ puts "Init script: #{init_py_path}"
108
+ puts "Note: Same init.py works for both GDB and LLDB"
109
+
110
+ # Check installation status by looking for marker comment
111
+ installed = false
112
+ current_source = nil
113
+
114
+ if File.exist?(gdbinit_path)
115
+ lines = File.readlines(gdbinit_path)
116
+ marker_index = lines.index{|line| line.strip == marker_comment}
117
+ if marker_index
118
+ installed = true
119
+ source_index = marker_index + 1
120
+ if source_index < lines.size
121
+ current_source = lines[source_index].strip
122
+ end
123
+ end
124
+ end
125
+
126
+ puts "\nGDB config: #{gdbinit_path}"
127
+ if installed
128
+ puts "Status: ✓ Installed"
129
+ if current_source
130
+ puts " #{current_source}"
131
+ end
132
+ else
133
+ puts "Status: ✗ Not installed"
134
+ puts "\nRun: bake toolbox:gdb:install"
135
+ end
136
+ end
137
+
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "toolbox/lldb"
7
+ require "fileutils"
8
+
9
+ # Install LLDB extensions by adding command to ~/.lldbinit
10
+ # @parameter lldbinit [String] Optional path to .lldbinit (defaults to ~/.lldbinit)
11
+ def install(lldbinit: nil)
12
+ lldbinit_path = lldbinit || File.join(Dir.home, ".lldbinit")
13
+ init_py_path = Toolbox::LLDB.init_script_path
14
+ command_line = "command script import #{init_py_path}"
15
+ marker_comment = "# Ruby Toolbox LLDB Extensions"
16
+
17
+ puts "Installing Ruby Toolbox LLDB extensions..."
18
+ puts " Extensions: #{File.dirname(init_py_path)}"
19
+ puts " Config: #{lldbinit_path}"
20
+
21
+ # Read existing .lldbinit or create empty array
22
+ lines = File.exist?(lldbinit_path) ? File.readlines(lldbinit_path) : []
23
+
24
+ # Check if already installed (look for marker comment)
25
+ marker_index = lines.index{|line| line.strip == marker_comment}
26
+
27
+ if marker_index
28
+ # Already installed - update the command line in case path changed
29
+ command_index = marker_index + 1
30
+ if command_index < lines.size && lines[command_index].strip.start_with?("command script import")
31
+ old_command = lines[command_index].strip
32
+ if old_command == command_line
33
+ puts "\n✓ Already installed in #{lldbinit_path}"
34
+ puts " #{command_line}"
35
+ return
36
+ else
37
+ # Path changed - update it
38
+ lines[command_index] = "#{command_line}\n"
39
+ File.write(lldbinit_path, lines.join)
40
+ puts "\n✓ Updated installation in #{lldbinit_path}"
41
+ puts " Old: #{old_command}"
42
+ puts " New: #{command_line}"
43
+ return
44
+ end
45
+ end
46
+ end
47
+
48
+ # Not installed - add it
49
+ File.open(lldbinit_path, "a") do |f|
50
+ f.puts unless lines.last&.strip&.empty?
51
+ f.puts marker_comment
52
+ f.puts command_line
53
+ end
54
+
55
+ puts "\n✓ Installation complete!"
56
+ puts "\nAdded to #{lldbinit_path}:"
57
+ puts " #{command_line}"
58
+ puts "\nExtensions will load automatically when you start LLDB."
59
+ end
60
+
61
+ # Uninstall LLDB extensions by removing command from ~/.lldbinit
62
+ # @parameter lldbinit [String] Optional path to .lldbinit (defaults to ~/.lldbinit)
63
+ def uninstall(lldbinit: nil)
64
+ lldbinit_path = lldbinit || File.join(Dir.home, ".lldbinit")
65
+ marker_comment = "# Ruby Toolbox LLDB Extensions"
66
+
67
+ puts "Uninstalling Ruby Toolbox LLDB extensions..."
68
+
69
+ unless File.exist?(lldbinit_path)
70
+ puts "No ~/.lldbinit file found - nothing to uninstall."
71
+ return
72
+ end
73
+
74
+ lines = File.readlines(lldbinit_path)
75
+ marker_index = lines.index{|line| line.strip == marker_comment}
76
+
77
+ unless marker_index
78
+ puts "Extensions were not found in #{lldbinit_path}"
79
+ return
80
+ end
81
+
82
+ # Remove the marker comment and the command line after it
83
+ lines.delete_at(marker_index) # Remove comment
84
+ if marker_index < lines.size && lines[marker_index].strip.start_with?("command script import")
85
+ removed_line = lines.delete_at(marker_index).strip # Remove command line
86
+ puts " Removed: #{removed_line}"
87
+ end
88
+
89
+ # Clean up empty line before marker if it exists
90
+ if marker_index > 0 && lines[marker_index - 1].strip.empty?
91
+ lines.delete_at(marker_index - 1)
92
+ end
93
+
94
+ File.write(lldbinit_path, lines.join)
95
+ puts "✓ Removed Ruby Toolbox LLDB extensions from #{lldbinit_path}"
96
+ end
97
+
98
+ # Show installation information
99
+ # @parameter lldbinit [String] Optional path to .lldbinit (defaults to ~/.lldbinit)
100
+ def info(lldbinit: nil)
101
+ lldbinit_path = lldbinit || File.join(Dir.home, ".lldbinit")
102
+ init_py_path = Toolbox::LLDB.init_script_path
103
+ marker_comment = "# Ruby Toolbox LLDB Extensions"
104
+
105
+ puts "Ruby Toolbox LLDB Extensions v#{Toolbox::VERSION}"
106
+ puts "\nToolbox directory: #{Toolbox::LLDB.data_path}"
107
+ puts "Init script: #{init_py_path}"
108
+ puts "Note: Same init.py works for both GDB and LLDB"
109
+
110
+ # Check installation status by looking for marker comment
111
+ installed = false
112
+ current_command = nil
113
+
114
+ if File.exist?(lldbinit_path)
115
+ lines = File.readlines(lldbinit_path)
116
+ marker_index = lines.index{|line| line.strip == marker_comment}
117
+ if marker_index
118
+ installed = true
119
+ command_index = marker_index + 1
120
+ if command_index < lines.size
121
+ current_command = lines[command_index].strip
122
+ end
123
+ end
124
+ end
125
+
126
+ puts "\nLLDB config: #{lldbinit_path}"
127
+ if installed
128
+ puts "Status: ✓ Installed"
129
+ if current_command
130
+ puts " #{current_command}"
131
+ end
132
+ else
133
+ puts "Status: ✗ Not installed"
134
+ puts "\nRun: bake toolbox:lldb:install"
135
+ end
136
+ end
137
+
@@ -0,0 +1,171 @@
1
+ # Fiber Debugging
2
+
3
+ This guide explains how to debug Ruby fibers using GDB, including inspecting fiber state, backtraces, and switching between fiber contexts.
4
+
5
+ ## Why Fiber Debugging is Critical
6
+
7
+ When debugging concurrent Ruby applications, fibers can be in various states - running, suspended, or terminated. Unlike traditional debugging where you see one call stack, fiber-based programs have multiple execution contexts simultaneously. Understanding what each fiber is doing and why it stopped is essential for diagnosing deadlocks, exceptions, and state corruption.
8
+
9
+ Use fiber debugging when you need:
10
+
11
+ - **Diagnose deadlocks**: See which fibers are waiting and on what
12
+ - **Find hidden exceptions**: Discover exceptions in suspended fibers that haven't propagated yet
13
+ - **Understand concurrency**: Visualize what all fibers are doing at a point in time
14
+ - **Debug async code**: Navigate between fiber contexts to trace execution flow
15
+
16
+ ## Scanning for Fibers
17
+
18
+ The first step in fiber debugging is finding all fibers in the heap.
19
+
20
+ ### Basic Scan
21
+
22
+ Scan the entire Ruby heap for fiber objects:
23
+
24
+ ~~~
25
+ (gdb) rb-fiber-scan-heap
26
+ Scanning heap for Fiber objects...
27
+ Checked 45000 objects, found 12 fiber(s)...
28
+
29
+ Found 12 fiber(s):
30
+
31
+ Fiber #0: <T_DATA@...> → <struct rb_fiber_struct@...>
32
+ Status: SUSPENDED
33
+ Stack: <void *@...>
34
+ VM Stack: <VALUE *@...>
35
+ CFP: <rb_control_frame_t@...>
36
+
37
+ Fiber #1: <T_DATA@...> → <struct rb_fiber_struct@...>
38
+ Status: SUSPENDED
39
+ ...
40
+ ~~~
41
+
42
+ ### Limiting Results
43
+
44
+ For large applications, limit the scan:
45
+
46
+ ~~~
47
+ (gdb) rb-fiber-scan-heap --limit 10 # Find first 10 fibers only
48
+ ~~~
49
+
50
+ ### Caching Results
51
+
52
+ Cache fiber addresses for faster subsequent access:
53
+
54
+ ~~~
55
+ (gdb) rb-fiber-scan-heap --cache # Save to fibers.json
56
+ (gdb) rb-fiber-scan-heap --cache my.json # Custom cache file
57
+ ~~~
58
+
59
+ Later, load from cache instantly:
60
+
61
+ ~~~
62
+ (gdb) rb-fiber-scan-heap --cache
63
+ Loaded 12 fiber(s) from fibers.json
64
+ ~~~
65
+
66
+ This is especially useful with core dumps where heap scanning is slow.
67
+
68
+ ## Inspecting Specific Fibers
69
+
70
+ After scanning, you can switch to a specific fiber's context or view all backtraces.
71
+
72
+ ### View All Fiber Backtraces
73
+
74
+ See Ruby-level call stacks for all fibers at once:
75
+
76
+ ~~~
77
+ (gdb) rb-fiber-scan-stack-trace-all
78
+ Found 12 fiber(s)
79
+
80
+ Fiber #0: <T_DATA@...> → <struct rb_fiber_struct@...>
81
+ Status: RESUMED
82
+ [No backtrace - fiber is running]
83
+
84
+ Fiber #1: <T_DATA@...> → <struct rb_fiber_struct@...>
85
+ Status: SUSPENDED
86
+ /app/lib/connection.rb:123:in `read'
87
+ /app/lib/connection.rb:89:in `receive'
88
+ /app/lib/server.rb:56:in `handle_client'
89
+ ...
90
+
91
+ Fiber #5: <T_DATA@...> → <struct rb_fiber_struct@...>
92
+ Status: SUSPENDED
93
+ Exception: IOError: Connection reset
94
+ /app/lib/connection.rb:45:in `write'
95
+ ...
96
+ ~~~
97
+
98
+ This gives you a complete overview of what every fiber is doing.
99
+
100
+ ## Switching Fiber Context
101
+
102
+ The most powerful feature: switch GDB's view to a fiber's stack (even in core dumps!).
103
+
104
+ ### Basic Usage
105
+
106
+ ~~~
107
+ (gdb) rb-fiber-scan-heap
108
+ (gdb) rb-fiber-scan-switch 5
109
+ Switching to Fiber #5: VALUE 0x...
110
+ Switched to Fiber: <T_DATA@...> → <struct rb_fiber_struct@...>
111
+ Status: SUSPENDED
112
+
113
+ Convenience variables set:
114
+ $fiber = Current fiber VALUE
115
+ $fiber_ptr = Current fiber pointer (struct rb_fiber_struct *)
116
+ $ec = Execution context (rb_execution_context_t *)
117
+ $errinfo = Exception being handled (VALUE)
118
+
119
+ Now try:
120
+ bt # Show C backtrace of fiber
121
+ rb-stack-trace # Show combined Ruby/C backtrace
122
+ info locals # Show local variables
123
+ ~~~
124
+
125
+ After switching, all standard GDB commands work with the fiber's context:
126
+
127
+ ~~~
128
+ (gdb) rb-stack-trace # Combined Ruby/C backtrace
129
+ (gdb) bt # C backtrace of fiber
130
+ #0 0x00007f8a1c567890 in fiber_setcontext
131
+ #1 0x00007f8a1c567900 in rb_fiber_yield
132
+ #2 0x00007f8a1c234567 in rb_io_wait_readable
133
+ ...
134
+
135
+ (gdb) frame 2
136
+ (gdb) info locals # Local C variables in that frame
137
+ ~~~
138
+
139
+ ### Switching by VALUE
140
+
141
+ You can also switch to a specific fiber by its VALUE or address:
142
+
143
+ ~~~
144
+ (gdb) rb-fiber-switch 0x7f8a1c800500
145
+ ~~~
146
+
147
+ ### Switching Back
148
+
149
+ Return to normal stack view:
150
+
151
+ ~~~
152
+ (gdb) rb-fiber-scan-switch off
153
+ ~~~
154
+
155
+ ## Analyzing Fiber State
156
+
157
+ After switching to a fiber with `rb-fiber-scan-switch`, you can use standard GDB commands to inspect the fiber's state:
158
+
159
+ ~~~
160
+ (gdb) rb-fiber-scan-switch 5
161
+ (gdb) bt # Show C backtrace
162
+ (gdb) frame <n> # Switch to specific frame
163
+ (gdb) info locals # Show local variables
164
+ (gdb) rb-print $errinfo # Print exception if present
165
+ ~~~
166
+
167
+ The fiber switch command sets up several convenience variables:
168
+ - `$fiber` - The fiber VALUE
169
+ - `$fiber_ptr` - Pointer to `struct rb_fiber_struct`
170
+ - `$ec` - The fiber's execution context
171
+ - `$errinfo` - Exception being handled (if any)