toolbox 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/bake/ruby/gdb.rb +135 -0
- data/bake/toolbox/gdb.rb +137 -0
- data/bake/toolbox/lldb.rb +137 -0
- data/context/fiber-debugging.md +171 -0
- data/context/getting-started.md +178 -0
- data/context/heap-debugging.md +351 -0
- data/context/index.yaml +28 -0
- data/context/object-inspection.md +208 -0
- data/context/stack-inspection.md +188 -0
- data/data/toolbox/command.py +254 -0
- data/data/toolbox/constants.py +200 -0
- data/data/toolbox/context.py +295 -0
- data/data/toolbox/debugger/__init__.py +99 -0
- data/data/toolbox/debugger/gdb_backend.py +595 -0
- data/data/toolbox/debugger/lldb_backend.py +885 -0
- data/data/toolbox/fiber.py +885 -0
- data/data/toolbox/format.py +200 -0
- data/data/toolbox/heap.py +669 -0
- data/data/toolbox/init.py +85 -0
- data/data/toolbox/object.py +84 -0
- data/data/toolbox/rarray.py +124 -0
- data/data/toolbox/rbasic.py +103 -0
- data/data/toolbox/rbignum.py +52 -0
- data/data/toolbox/rclass.py +136 -0
- data/data/toolbox/readme.md +214 -0
- data/data/toolbox/rexception.py +150 -0
- data/data/toolbox/rfloat.py +98 -0
- data/data/toolbox/rhash.py +159 -0
- data/data/toolbox/rstring.py +234 -0
- data/data/toolbox/rstruct.py +157 -0
- data/data/toolbox/rsymbol.py +302 -0
- data/data/toolbox/stack.py +630 -0
- data/data/toolbox/value.py +183 -0
- data/lib/toolbox/gdb.rb +21 -0
- data/lib/toolbox/lldb.rb +21 -0
- data/lib/toolbox/version.rb +7 -1
- data/lib/toolbox.rb +9 -24
- data/license.md +21 -0
- data/readme.md +64 -0
- data/releases.md +9 -0
- data.tar.gz.sig +2 -0
- metadata +95 -165
- metadata.gz.sig +0 -0
- data/Rakefile +0 -61
- data/lib/dirs.rb +0 -9
- data/lib/toolbox/config.rb +0 -211
- data/lib/toolbox/default_controller.rb +0 -393
- data/lib/toolbox/helpers.rb +0 -11
- data/lib/toolbox/rendering.rb +0 -413
- data/lib/toolbox/searching.rb +0 -85
- data/lib/toolbox/session_params.rb +0 -63
- data/lib/toolbox/sorting.rb +0 -74
- data/locale/de/LC_MESSAGES/toolbox.mo +0 -0
- data/public/images/add.png +0 -0
- data/public/images/arrow_down.gif +0 -0
- data/public/images/arrow_up.gif +0 -0
- data/public/images/close.png +0 -0
- data/public/images/edit.gif +0 -0
- data/public/images/email.png +0 -0
- data/public/images/page.png +0 -0
- data/public/images/page_acrobat.png +0 -0
- data/public/images/page_add.png +0 -0
- data/public/images/page_copy.png +0 -0
- data/public/images/page_delete.png +0 -0
- data/public/images/page_edit.png +0 -0
- data/public/images/page_excel.png +0 -0
- data/public/images/page_list.png +0 -0
- data/public/images/page_save.png +0 -0
- data/public/images/page_word.png +0 -0
- data/public/images/remove.png +0 -0
- data/public/images/show.gif +0 -0
- data/public/images/spinner.gif +0 -0
- data/public/javascripts/popup.js +0 -498
- data/public/javascripts/toolbox.js +0 -18
- data/public/stylesheets/context_menu.css +0 -168
- data/public/stylesheets/popup.css +0 -30
- data/public/stylesheets/toolbox.css +0 -107
- data/view/toolbox/_collection.html.erb +0 -24
- data/view/toolbox/_collection_header.html.erb +0 -7
- data/view/toolbox/_context_menu.html.erb +0 -17
- data/view/toolbox/_dialogs.html.erb +0 -6
- data/view/toolbox/_form.html.erb +0 -30
- data/view/toolbox/_form_collection_row.html.erb +0 -18
- data/view/toolbox/_form_fieldset.html.erb +0 -30
- data/view/toolbox/_form_fieldset_row.html.erb +0 -19
- data/view/toolbox/_list.html.erb +0 -25
- data/view/toolbox/_list_row.html.erb +0 -10
- data/view/toolbox/_menu.html.erb +0 -7
- data/view/toolbox/_search_field.html.erb +0 -8
- data/view/toolbox/_show.html.erb +0 -12
- data/view/toolbox/_show_collection_row.html.erb +0 -6
- data/view/toolbox/_show_fieldset.html.erb +0 -21
- data/view/toolbox/edit.html.erb +0 -5
- data/view/toolbox/index.html.erb +0 -3
- data/view/toolbox/new.html.erb +0 -9
- data/view/toolbox/show.html.erb +0 -39
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 694d8c20e3efb7fad4c3ce738ab9784308fca74971e7198e627458349be2f316
|
|
4
|
+
data.tar.gz: 41e1fa7e7352ade3a28fdc90ec0db233f937dfef0ddb1a1ac819c052c9edb1f4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 65ebb449eadbcd6c92ba560aac7fd3216e9e50c2fdc2864eb2daf2c0169191d678c7cda7956e3a3ee359eab926bc08c13ff0d8fd1605509b999cb69516d8031a
|
|
7
|
+
data.tar.gz: '0806493639960770d85ff768e2ad985657b46b7db46327acdfb6248e44cf1cb6b39586273975ce7c7f2e5128307cfc0a667cc13e846d29023615e3b7f9c79068'
|
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
|
data/bake/toolbox/gdb.rb
ADDED
|
@@ -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-object-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)
|