debug 1.3.3 → 1.5.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
- data/CONTRIBUTING.md +163 -6
- data/Gemfile +1 -0
- data/README.md +42 -15
- data/Rakefile +5 -5
- data/debug.gemspec +6 -4
- data/ext/debug/debug.c +88 -1
- data/ext/debug/extconf.rb +11 -0
- data/lib/debug/breakpoint.rb +75 -36
- data/lib/debug/client.rb +65 -18
- data/lib/debug/color.rb +29 -19
- data/lib/debug/config.rb +6 -3
- data/lib/debug/console.rb +50 -15
- data/lib/debug/frame_info.rb +14 -20
- data/lib/debug/local.rb +1 -1
- data/lib/debug/prelude.rb +2 -2
- data/lib/debug/server.rb +100 -94
- data/lib/debug/server_cdp.rb +878 -160
- data/lib/debug/server_dap.rb +277 -81
- data/lib/debug/session.rb +327 -154
- data/lib/debug/source_repository.rb +90 -52
- data/lib/debug/thread_client.rb +149 -78
- data/lib/debug/tracer.rb +4 -10
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +16 -8
- metadata +4 -12
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
- data/.github/ISSUE_TEMPLATE/custom.md +0 -10
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- data/.github/workflows/ruby.yml +0 -34
- data/.gitignore +0 -12
- data/bin/console +0 -14
- data/bin/gentest +0 -22
- data/bin/setup +0 -8
data/lib/debug/session.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
return if ENV['RUBY_DEBUG_ENABLE'] == '0'
|
4
|
+
|
3
5
|
# skip to load debugger for bundle exec
|
4
6
|
|
5
7
|
if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
|
6
8
|
trace_var(:$0) do |file|
|
7
9
|
trace_var(:$0, nil)
|
8
|
-
if /-r (#{__dir__}\S+)/ =~ ENV['RUBYOPT']
|
10
|
+
if /-r (#{Regexp.escape(__dir__)}\S+)/ =~ ENV['RUBYOPT']
|
9
11
|
lib = $1
|
10
12
|
$LOADED_FEATURES.delete_if{|path| path.start_with?(__dir__)}
|
11
13
|
ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH'] = file
|
@@ -29,7 +31,7 @@ $LOADED_FEATURES << 'debug.rb'
|
|
29
31
|
$LOADED_FEATURES << File.expand_path(File.join(__dir__, '..', 'debug.rb'))
|
30
32
|
require 'debug' # invalidate the $LOADED_FEATURE cache
|
31
33
|
|
32
|
-
require 'json' if ENV['
|
34
|
+
require 'json' if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
|
33
35
|
|
34
36
|
class RubyVM::InstructionSequence
|
35
37
|
def traceable_lines_norec lines
|
@@ -54,23 +56,22 @@ class RubyVM::InstructionSequence
|
|
54
56
|
|
55
57
|
def type
|
56
58
|
self.to_a[9]
|
57
|
-
end
|
59
|
+
end unless method_defined?(:type)
|
58
60
|
|
59
|
-
def
|
60
|
-
self.to_a
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
61
|
+
def parameters_symbols
|
62
|
+
ary = self.to_a
|
63
|
+
argc = ary[4][:arg_size]
|
64
|
+
locals = ary.to_a[10]
|
65
|
+
locals[0...argc]
|
66
|
+
end unless method_defined?(:parameters_symbols)
|
66
67
|
|
67
68
|
def last_line
|
68
69
|
self.to_a[4][:code_location][2]
|
69
|
-
end
|
70
|
+
end unless method_defined?(:last_line)
|
70
71
|
|
71
72
|
def first_line
|
72
73
|
self.to_a[4][:code_location][0]
|
73
|
-
end
|
74
|
+
end unless method_defined?(:first_line)
|
74
75
|
end
|
75
76
|
|
76
77
|
module DEBUGGER__
|
@@ -80,8 +81,10 @@ module DEBUGGER__
|
|
80
81
|
class Session
|
81
82
|
attr_reader :intercepted_sigint_cmd, :process_group
|
82
83
|
|
83
|
-
|
84
|
-
|
84
|
+
include Color
|
85
|
+
|
86
|
+
def initialize
|
87
|
+
@ui = nil
|
85
88
|
@sr = SourceRepository.new
|
86
89
|
@bps = {} # bp.key => bp
|
87
90
|
# [file, line] => LineBreakpoint
|
@@ -99,17 +102,17 @@ module DEBUGGER__
|
|
99
102
|
@preset_command = nil
|
100
103
|
@postmortem_hook = nil
|
101
104
|
@postmortem = false
|
102
|
-
@thread_stopper = nil
|
103
105
|
@intercept_trap_sigint = false
|
104
106
|
@intercepted_sigint_cmd = 'DEFAULT'
|
105
107
|
@process_group = ProcessGroup.new
|
106
|
-
@
|
108
|
+
@subsession_stack = []
|
107
109
|
|
108
|
-
@frame_map = {} # {id => [threadId, frame_depth]}
|
110
|
+
@frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
|
109
111
|
@var_map = {1 => [:globals], } # {id => ...} for DAP
|
110
112
|
@src_map = {} # {id => src}
|
111
113
|
|
112
|
-
@
|
114
|
+
@scr_id_map = {} # for CDP
|
115
|
+
@obj_map = {} # { object_id => ... } for CDP
|
113
116
|
|
114
117
|
@tp_thread_begin = nil
|
115
118
|
@tp_load_script = TracePoint.new(:script_compiled){|tp|
|
@@ -117,8 +120,7 @@ module DEBUGGER__
|
|
117
120
|
}
|
118
121
|
@tp_load_script.enable
|
119
122
|
|
120
|
-
|
121
|
-
|
123
|
+
@thread_stopper = thread_stopper
|
122
124
|
self.postmortem = CONFIG[:postmortem]
|
123
125
|
end
|
124
126
|
|
@@ -130,15 +132,13 @@ module DEBUGGER__
|
|
130
132
|
@bps.has_key? [file, line]
|
131
133
|
end
|
132
134
|
|
133
|
-
def activate on_fork: false
|
135
|
+
def activate ui = nil, on_fork: false
|
136
|
+
@ui = ui if ui
|
137
|
+
|
134
138
|
@tp_thread_begin&.disable
|
135
139
|
@tp_thread_begin = nil
|
136
140
|
|
137
|
-
|
138
|
-
@ui.activate self, on_fork: true
|
139
|
-
else
|
140
|
-
@ui.activate self, on_fork: false
|
141
|
-
end
|
141
|
+
@ui.activate self, on_fork: on_fork
|
142
142
|
|
143
143
|
q = Queue.new
|
144
144
|
@session_server = Thread.new do
|
@@ -147,15 +147,15 @@ module DEBUGGER__
|
|
147
147
|
|
148
148
|
# Thread management
|
149
149
|
setup_threads
|
150
|
-
thc =
|
151
|
-
thc.
|
150
|
+
thc = get_thread_client Thread.current
|
151
|
+
thc.mark_as_management
|
152
152
|
|
153
|
-
if @ui.respond_to?(:reader_thread) && thc =
|
154
|
-
thc.
|
153
|
+
if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
|
154
|
+
thc.mark_as_management
|
155
155
|
end
|
156
156
|
|
157
157
|
@tp_thread_begin = TracePoint.new(:thread_begin) do |tp|
|
158
|
-
|
158
|
+
get_thread_client
|
159
159
|
end
|
160
160
|
@tp_thread_begin.enable
|
161
161
|
|
@@ -168,12 +168,12 @@ module DEBUGGER__
|
|
168
168
|
end
|
169
169
|
|
170
170
|
def deactivate
|
171
|
-
|
172
|
-
@thread_stopper.disable
|
171
|
+
get_thread_client.deactivate
|
172
|
+
@thread_stopper.disable
|
173
173
|
@tp_load_script.disable
|
174
174
|
@tp_thread_begin.disable
|
175
|
-
@bps.
|
176
|
-
@th_clients.
|
175
|
+
@bps.each_value{|bp| bp.disable}
|
176
|
+
@th_clients.each_value{|thc| thc.close}
|
177
177
|
@tracers.values.each{|t| t.disable}
|
178
178
|
@q_evt.close
|
179
179
|
@ui&.deactivate
|
@@ -211,6 +211,7 @@ module DEBUGGER__
|
|
211
211
|
q << true
|
212
212
|
|
213
213
|
when :init
|
214
|
+
enter_subsession
|
214
215
|
wait_command_loop tc
|
215
216
|
|
216
217
|
when :load
|
@@ -233,12 +234,13 @@ module DEBUGGER__
|
|
233
234
|
case ev_args.first
|
234
235
|
when :breakpoint
|
235
236
|
bp, i = bp_index ev_args[1]
|
237
|
+
clean_bps unless bp
|
236
238
|
@ui.event :suspend_bp, i, bp, tc.id
|
237
239
|
when :trap
|
238
240
|
@ui.event :suspend_trap, sig = ev_args[1], tc.id
|
239
241
|
|
240
242
|
if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String))
|
241
|
-
@ui.puts "#{@intercepted_sigint_cmd.inspect} is
|
243
|
+
@ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler."
|
242
244
|
@ui.puts "`sigint` command execute it."
|
243
245
|
end
|
244
246
|
else
|
@@ -252,7 +254,7 @@ module DEBUGGER__
|
|
252
254
|
end
|
253
255
|
|
254
256
|
when :result
|
255
|
-
raise "[BUG] not in subsession"
|
257
|
+
raise "[BUG] not in subsession" if @subsession_stack.empty?
|
256
258
|
|
257
259
|
case ev_args.first
|
258
260
|
when :try_display
|
@@ -364,7 +366,7 @@ module DEBUGGER__
|
|
364
366
|
@ui.puts "(rdbg:#{@preset_command.source}) #{line}"
|
365
367
|
end
|
366
368
|
else
|
367
|
-
@ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['
|
369
|
+
@ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
|
368
370
|
line = @ui.readline prompt
|
369
371
|
end
|
370
372
|
|
@@ -418,10 +420,15 @@ module DEBUGGER__
|
|
418
420
|
# * `fin[ish]`
|
419
421
|
# * Finish this frame. Resume the program until the current frame is finished.
|
420
422
|
# * `fin[ish] <n>`
|
421
|
-
# * Finish frames
|
423
|
+
# * Finish `<n>`th frames.
|
422
424
|
when 'fin', 'finish'
|
423
425
|
cancel_auto_continue
|
424
426
|
check_postmortem
|
427
|
+
|
428
|
+
if arg&.to_i == 0
|
429
|
+
raise 'finish command with 0 does not make sense.'
|
430
|
+
end
|
431
|
+
|
425
432
|
step_command :finish, arg
|
426
433
|
|
427
434
|
# * `c[ontinue]`
|
@@ -447,7 +454,7 @@ module DEBUGGER__
|
|
447
454
|
leave_subsession nil
|
448
455
|
|
449
456
|
# * `kill`
|
450
|
-
# * Stop the debuggee process with `
|
457
|
+
# * Stop the debuggee process with `Kernel#exit!`.
|
451
458
|
when 'kill'
|
452
459
|
if ask 'Really kill?'
|
453
460
|
exit! (arg || 1).to_i
|
@@ -461,7 +468,7 @@ module DEBUGGER__
|
|
461
468
|
exit! (arg || 1).to_i
|
462
469
|
|
463
470
|
# * `sigint`
|
464
|
-
# * Execute SIGINT handler
|
471
|
+
# * Execute SIGINT handler registered by the debuggee.
|
465
472
|
# * Note that this command should be used just after stop by `SIGINT`.
|
466
473
|
when 'sigint'
|
467
474
|
begin
|
@@ -500,6 +507,8 @@ module DEBUGGER__
|
|
500
507
|
# * break and run `<command>` before stopping.
|
501
508
|
# * `b[reak] ... do: <command>`
|
502
509
|
# * break and run `<command>`, and continue.
|
510
|
+
# * `b[reak] ... path: <path>`
|
511
|
+
# * break if the path matches to `<path>`. `<path>` can be a regexp with `/regexp/`.
|
503
512
|
# * `b[reak] if: <expr>`
|
504
513
|
# * break if: `<expr>` is true at any lines.
|
505
514
|
# * Note that this feature is super slow.
|
@@ -526,7 +535,7 @@ module DEBUGGER__
|
|
526
535
|
require 'json'
|
527
536
|
|
528
537
|
h = Hash.new{|h, k| h[k] = []}
|
529
|
-
@bps.
|
538
|
+
@bps.each_value{|bp|
|
530
539
|
if LineBreakpoint === bp
|
531
540
|
h[bp.path] << {lnum: bp.line}
|
532
541
|
end
|
@@ -548,6 +557,14 @@ module DEBUGGER__
|
|
548
557
|
|
549
558
|
# * `catch <Error>`
|
550
559
|
# * Set breakpoint on raising `<Error>`.
|
560
|
+
# * `catch ... if: <expr>`
|
561
|
+
# * stops only if `<expr>` is true as well.
|
562
|
+
# * `catch ... pre: <command>`
|
563
|
+
# * runs `<command>` before stopping.
|
564
|
+
# * `catch ... do: <command>`
|
565
|
+
# * stops and run `<command>`, and continue.
|
566
|
+
# * `catch ... path: <path>`
|
567
|
+
# * stops if the exception is raised from a `<path>`. `<path>` can be a regexp with `/regexp/`.
|
551
568
|
when 'catch'
|
552
569
|
check_postmortem
|
553
570
|
|
@@ -562,11 +579,19 @@ module DEBUGGER__
|
|
562
579
|
# * `watch @ivar`
|
563
580
|
# * Stop the execution when the result of current scope's `@ivar` is changed.
|
564
581
|
# * Note that this feature is super slow.
|
582
|
+
# * `watch ... if: <expr>`
|
583
|
+
# * stops only if `<expr>` is true as well.
|
584
|
+
# * `watch ... pre: <command>`
|
585
|
+
# * runs `<command>` before stopping.
|
586
|
+
# * `watch ... do: <command>`
|
587
|
+
# * stops and run `<command>`, and continue.
|
588
|
+
# * `watch ... path: <path>`
|
589
|
+
# * stops if the path matches `<path>`. `<path>` can be a regexp with `/regexp/`.
|
565
590
|
when 'wat', 'watch'
|
566
591
|
check_postmortem
|
567
592
|
|
568
593
|
if arg && arg.match?(/\A@\w+/)
|
569
|
-
|
594
|
+
repl_add_watch_breakpoint(arg)
|
570
595
|
else
|
571
596
|
show_bps
|
572
597
|
return :retry
|
@@ -671,8 +696,8 @@ module DEBUGGER__
|
|
671
696
|
# * Show information about accessible constants except toplevel constants.
|
672
697
|
# * `i[nfo] g[lobal[s]]`
|
673
698
|
# * Show information about global variables
|
674
|
-
# * `i[nfo] ...
|
675
|
-
# * Filter the output with
|
699
|
+
# * `i[nfo] ... /regexp/`
|
700
|
+
# * Filter the output with `/regexp/`.
|
676
701
|
# * `i[nfo] th[read[s]]`
|
677
702
|
# * Show all threads (same as `th[read]`).
|
678
703
|
when 'i', 'info'
|
@@ -806,8 +831,8 @@ module DEBUGGER__
|
|
806
831
|
# * Add an exception tracer. It indicates raising exceptions.
|
807
832
|
# * `trace object <expr>`
|
808
833
|
# * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
|
809
|
-
# * `trace ...
|
810
|
-
# * Indicates only matched events to
|
834
|
+
# * `trace ... /regexp/`
|
835
|
+
# * Indicates only matched events to `/regexp/`.
|
811
836
|
# * `trace ... into: <file>`
|
812
837
|
# * Save trace information into: `<file>`.
|
813
838
|
# * `trace off <num>`
|
@@ -902,7 +927,7 @@ module DEBUGGER__
|
|
902
927
|
when nil, 'list', 'l'
|
903
928
|
thread_list
|
904
929
|
when /(\d+)/
|
905
|
-
|
930
|
+
switch_thread $1.to_i
|
906
931
|
else
|
907
932
|
@ui.puts "unknown thread command: #{arg}"
|
908
933
|
end
|
@@ -950,7 +975,7 @@ module DEBUGGER__
|
|
950
975
|
when 'open'
|
951
976
|
case arg&.downcase
|
952
977
|
when '', nil
|
953
|
-
|
978
|
+
repl_open
|
954
979
|
when 'vscode'
|
955
980
|
repl_open_vscode
|
956
981
|
when /\A(.+):(\d+)\z/
|
@@ -975,11 +1000,7 @@ module DEBUGGER__
|
|
975
1000
|
# * `h[elp] <command>`
|
976
1001
|
# * Show help for the given command.
|
977
1002
|
when 'h', 'help', '?'
|
978
|
-
|
979
|
-
show_help arg
|
980
|
-
else
|
981
|
-
@ui.puts DEBUGGER__.help
|
982
|
-
end
|
1003
|
+
show_help arg
|
983
1004
|
return :retry
|
984
1005
|
|
985
1006
|
### END
|
@@ -1016,8 +1037,8 @@ module DEBUGGER__
|
|
1016
1037
|
def repl_open_setup
|
1017
1038
|
@tp_thread_begin.disable
|
1018
1039
|
@ui.activate self
|
1019
|
-
if @ui.respond_to?(:reader_thread) && thc =
|
1020
|
-
thc.
|
1040
|
+
if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
|
1041
|
+
thc.mark_as_management
|
1021
1042
|
end
|
1022
1043
|
@tp_thread_begin.enable
|
1023
1044
|
end
|
@@ -1027,14 +1048,14 @@ module DEBUGGER__
|
|
1027
1048
|
repl_open_setup
|
1028
1049
|
end
|
1029
1050
|
|
1030
|
-
def
|
1031
|
-
DEBUGGER__.
|
1051
|
+
def repl_open
|
1052
|
+
DEBUGGER__.open nonstop: true
|
1032
1053
|
repl_open_setup
|
1033
1054
|
end
|
1034
1055
|
|
1035
1056
|
def repl_open_vscode
|
1036
1057
|
CONFIG[:open_frontend] = 'vscode'
|
1037
|
-
|
1058
|
+
repl_open
|
1038
1059
|
end
|
1039
1060
|
|
1040
1061
|
def step_command type, arg
|
@@ -1131,16 +1152,50 @@ module DEBUGGER__
|
|
1131
1152
|
end
|
1132
1153
|
end
|
1133
1154
|
|
1134
|
-
def show_help arg
|
1135
|
-
DEBUGGER__.
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1155
|
+
def show_help arg = nil
|
1156
|
+
instructions = (DEBUGGER__.commands.keys + DEBUGGER__.commands.values).uniq
|
1157
|
+
print_instructions = proc do |desc|
|
1158
|
+
desc.split("\n").each do |line|
|
1159
|
+
next if line.start_with?(" ") # workaround for step back
|
1160
|
+
formatted_line = line.gsub(/[\[\]\*]/, "").strip
|
1161
|
+
instructions.each do |inst|
|
1162
|
+
if formatted_line.start_with?("`#{inst}")
|
1163
|
+
desc.sub!(line, colorize(line, [:CYAN, :BOLD]))
|
1164
|
+
end
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
@ui.puts desc
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
print_category = proc do |cat|
|
1171
|
+
@ui.puts "\n"
|
1172
|
+
@ui.puts colorize("### #{cat}", [:GREEN, :BOLD])
|
1173
|
+
@ui.puts "\n"
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
DEBUGGER__.helps.each { |cat, cs|
|
1177
|
+
# categories
|
1178
|
+
if arg.nil?
|
1179
|
+
print_category.call(cat)
|
1180
|
+
else
|
1181
|
+
cs.each { |ws, _|
|
1182
|
+
if ws.include?(arg)
|
1183
|
+
print_category.call(cat)
|
1184
|
+
break
|
1185
|
+
end
|
1186
|
+
}
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
# instructions
|
1190
|
+
cs.each { |ws, desc|
|
1191
|
+
if arg.nil? || ws.include?(arg)
|
1192
|
+
print_instructions.call(desc.dup)
|
1193
|
+
return if arg
|
1140
1194
|
end
|
1141
1195
|
}
|
1142
1196
|
}
|
1143
|
-
|
1197
|
+
|
1198
|
+
@ui.puts "not found: #{arg}" if arg
|
1144
1199
|
end
|
1145
1200
|
|
1146
1201
|
def ask msg, default = 'Y'
|
@@ -1195,6 +1250,12 @@ module DEBUGGER__
|
|
1195
1250
|
}
|
1196
1251
|
end
|
1197
1252
|
|
1253
|
+
def clean_bps
|
1254
|
+
@bps.delete_if{|_k, bp|
|
1255
|
+
bp.deleted?
|
1256
|
+
}
|
1257
|
+
end
|
1258
|
+
|
1198
1259
|
def add_bp bp
|
1199
1260
|
# don't repeat commands that add breakpoints
|
1200
1261
|
@repl_prev_line = nil
|
@@ -1225,7 +1286,7 @@ module DEBUGGER__
|
|
1225
1286
|
end
|
1226
1287
|
end
|
1227
1288
|
|
1228
|
-
BREAK_KEYWORDS = %w(if: do: pre:).freeze
|
1289
|
+
BREAK_KEYWORDS = %w(if: do: pre: path:).freeze
|
1229
1290
|
|
1230
1291
|
def parse_break arg
|
1231
1292
|
mode = :sig
|
@@ -1238,13 +1299,20 @@ module DEBUGGER__
|
|
1238
1299
|
end
|
1239
1300
|
}
|
1240
1301
|
expr.default_proc = nil
|
1241
|
-
expr.transform_values{|v| v.join(' ')}
|
1302
|
+
expr = expr.transform_values{|v| v.join(' ')}
|
1303
|
+
|
1304
|
+
if (path = expr[:path]) && path =~ /\A\/(.*)\/\z/
|
1305
|
+
expr[:path] = Regexp.compile($1)
|
1306
|
+
end
|
1307
|
+
|
1308
|
+
expr
|
1242
1309
|
end
|
1243
1310
|
|
1244
1311
|
def repl_add_breakpoint arg
|
1245
1312
|
expr = parse_break arg.strip
|
1246
1313
|
cond = expr[:if]
|
1247
1314
|
cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
|
1315
|
+
path = expr[:path]
|
1248
1316
|
|
1249
1317
|
case expr[:sig]
|
1250
1318
|
when /\A(\d+)\z/
|
@@ -1252,10 +1320,10 @@ module DEBUGGER__
|
|
1252
1320
|
when /\A(.+)[:\s+](\d+)\z/
|
1253
1321
|
add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
|
1254
1322
|
when /\A(.+)([\.\#])(.+)\z/
|
1255
|
-
@tc << [:breakpoint, :method, $1, $2, $3, cond, cmd]
|
1323
|
+
@tc << [:breakpoint, :method, $1, $2, $3, cond, cmd, path]
|
1256
1324
|
return :noretry
|
1257
1325
|
when nil
|
1258
|
-
add_check_breakpoint cond
|
1326
|
+
add_check_breakpoint cond, path, cmd
|
1259
1327
|
else
|
1260
1328
|
@ui.puts "Unknown breakpoint format: #{arg}"
|
1261
1329
|
@ui.puts
|
@@ -1267,18 +1335,28 @@ module DEBUGGER__
|
|
1267
1335
|
expr = parse_break arg.strip
|
1268
1336
|
cond = expr[:if]
|
1269
1337
|
cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
|
1338
|
+
path = expr[:path]
|
1270
1339
|
|
1271
|
-
bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd)
|
1340
|
+
bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
|
1272
1341
|
add_bp bp
|
1273
1342
|
end
|
1274
1343
|
|
1344
|
+
def repl_add_watch_breakpoint arg
|
1345
|
+
expr = parse_break arg.strip
|
1346
|
+
cond = expr[:if]
|
1347
|
+
cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
|
1348
|
+
path = Regexp.compile(expr[:path]) if expr[:path]
|
1349
|
+
|
1350
|
+
@tc << [:breakpoint, :watch, expr[:sig], cond, cmd, path]
|
1351
|
+
end
|
1352
|
+
|
1275
1353
|
def add_catch_breakpoint pat
|
1276
1354
|
bp = CatchBreakpoint.new(pat)
|
1277
1355
|
add_bp bp
|
1278
1356
|
end
|
1279
1357
|
|
1280
|
-
def add_check_breakpoint
|
1281
|
-
bp = CheckBreakpoint.new(
|
1358
|
+
def add_check_breakpoint cond, path, command
|
1359
|
+
bp = CheckBreakpoint.new(cond: cond, path: path, command: command)
|
1282
1360
|
add_bp bp
|
1283
1361
|
end
|
1284
1362
|
|
@@ -1291,6 +1369,20 @@ module DEBUGGER__
|
|
1291
1369
|
@ui.puts e.message
|
1292
1370
|
end
|
1293
1371
|
|
1372
|
+
def clear_line_breakpoints path
|
1373
|
+
path = resolve_path(path)
|
1374
|
+
@bps.delete_if do |k, bp|
|
1375
|
+
if (Array === k) && DEBUGGER__.compare_path(k.first, path)
|
1376
|
+
bp.delete
|
1377
|
+
end
|
1378
|
+
end
|
1379
|
+
end
|
1380
|
+
|
1381
|
+
def add_iseq_breakpoint iseq, **kw
|
1382
|
+
bp = ISeqBreakpoint.new(iseq, [:line], **kw)
|
1383
|
+
add_bp bp
|
1384
|
+
end
|
1385
|
+
|
1294
1386
|
# tracers
|
1295
1387
|
|
1296
1388
|
def add_tracer tracer
|
@@ -1344,7 +1436,7 @@ module DEBUGGER__
|
|
1344
1436
|
thcs
|
1345
1437
|
end
|
1346
1438
|
|
1347
|
-
def
|
1439
|
+
def switch_thread n
|
1348
1440
|
thcs, _unmanaged_ths = update_thread_list
|
1349
1441
|
|
1350
1442
|
if tc = thcs[n]
|
@@ -1365,7 +1457,7 @@ module DEBUGGER__
|
|
1365
1457
|
if tc = prev_clients[th]
|
1366
1458
|
@th_clients[th] = tc
|
1367
1459
|
else
|
1368
|
-
|
1460
|
+
create_thread_client(th)
|
1369
1461
|
end
|
1370
1462
|
}
|
1371
1463
|
end
|
@@ -1374,17 +1466,17 @@ module DEBUGGER__
|
|
1374
1466
|
if @th_clients.has_key? th
|
1375
1467
|
# TODO: NG?
|
1376
1468
|
else
|
1377
|
-
|
1469
|
+
create_thread_client th
|
1378
1470
|
end
|
1379
1471
|
end
|
1380
1472
|
|
1381
|
-
private def
|
1473
|
+
private def create_thread_client th
|
1382
1474
|
# TODO: Ractor support
|
1383
1475
|
raise "Only session_server can create thread_client" unless Thread.current == @session_server
|
1384
1476
|
@th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
|
1385
1477
|
end
|
1386
1478
|
|
1387
|
-
private def ask_thread_client th
|
1479
|
+
private def ask_thread_client th
|
1388
1480
|
# TODO: Ractor support
|
1389
1481
|
q2 = Queue.new
|
1390
1482
|
# tc, output, ev, @internal_info, *ev_args = evt
|
@@ -1395,30 +1487,18 @@ module DEBUGGER__
|
|
1395
1487
|
end
|
1396
1488
|
|
1397
1489
|
# can be called by other threads
|
1398
|
-
def
|
1490
|
+
def get_thread_client th = Thread.current
|
1399
1491
|
if @th_clients.has_key? th
|
1400
1492
|
@th_clients[th]
|
1401
1493
|
else
|
1402
1494
|
if Thread.current == @session_server
|
1403
|
-
|
1495
|
+
create_thread_client th
|
1404
1496
|
else
|
1405
1497
|
ask_thread_client th
|
1406
1498
|
end
|
1407
1499
|
end
|
1408
1500
|
end
|
1409
1501
|
|
1410
|
-
private def thread_stopper
|
1411
|
-
@thread_stopper ||= TracePoint.new(:line) do
|
1412
|
-
# run on each thread
|
1413
|
-
tc = ThreadClient.current
|
1414
|
-
next if tc.management?
|
1415
|
-
next unless tc.running?
|
1416
|
-
next if tc == @tc
|
1417
|
-
|
1418
|
-
tc.on_pause
|
1419
|
-
end
|
1420
|
-
end
|
1421
|
-
|
1422
1502
|
private def running_thread_clients_count
|
1423
1503
|
@th_clients.count{|th, tc|
|
1424
1504
|
next if tc.management?
|
@@ -1435,15 +1515,27 @@ module DEBUGGER__
|
|
1435
1515
|
}.compact
|
1436
1516
|
end
|
1437
1517
|
|
1518
|
+
private def thread_stopper
|
1519
|
+
TracePoint.new(:line) do
|
1520
|
+
# run on each thread
|
1521
|
+
tc = ThreadClient.current
|
1522
|
+
next if tc.management?
|
1523
|
+
next unless tc.running?
|
1524
|
+
next if tc == @tc
|
1525
|
+
|
1526
|
+
tc.on_pause
|
1527
|
+
end
|
1528
|
+
end
|
1529
|
+
|
1438
1530
|
private def stop_all_threads
|
1439
1531
|
return if running_thread_clients_count == 0
|
1440
1532
|
|
1441
|
-
stopper = thread_stopper
|
1533
|
+
stopper = @thread_stopper
|
1442
1534
|
stopper.enable unless stopper.enabled?
|
1443
1535
|
end
|
1444
1536
|
|
1445
1537
|
private def restart_all_threads
|
1446
|
-
stopper = thread_stopper
|
1538
|
+
stopper = @thread_stopper
|
1447
1539
|
stopper.disable if stopper.enabled?
|
1448
1540
|
|
1449
1541
|
waiting_thread_clients.each{|tc|
|
@@ -1453,27 +1545,38 @@ module DEBUGGER__
|
|
1453
1545
|
end
|
1454
1546
|
|
1455
1547
|
private def enter_subsession
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1548
|
+
if !@subsession_stack.empty?
|
1549
|
+
DEBUGGER__.info "Enter subsession (nested #{@subsession_stack.size})"
|
1550
|
+
else
|
1551
|
+
DEBUGGER__.info "Enter subsession"
|
1552
|
+
stop_all_threads
|
1553
|
+
@process_group.lock
|
1554
|
+
end
|
1555
|
+
|
1556
|
+
@subsession_stack << true
|
1461
1557
|
end
|
1462
1558
|
|
1463
1559
|
private def leave_subsession type
|
1464
|
-
|
1465
|
-
@
|
1466
|
-
|
1560
|
+
raise '[BUG] leave_subsession: not entered' if @subsession_stack.empty?
|
1561
|
+
@subsession_stack.pop
|
1562
|
+
|
1563
|
+
if @subsession_stack.empty?
|
1564
|
+
DEBUGGER__.info "Leave subsession"
|
1565
|
+
@process_group.unlock
|
1566
|
+
restart_all_threads
|
1567
|
+
else
|
1568
|
+
DEBUGGER__.info "Leave subsession (nested #{@subsession_stack.size})"
|
1569
|
+
end
|
1570
|
+
|
1467
1571
|
@tc << type if type
|
1468
1572
|
@tc = nil
|
1469
|
-
@subsession = false
|
1470
1573
|
rescue Exception => e
|
1471
|
-
STDERR.puts [e, e.backtrace].
|
1574
|
+
STDERR.puts PP.pp([e, e.backtrace], ''.dup)
|
1472
1575
|
raise
|
1473
1576
|
end
|
1474
1577
|
|
1475
1578
|
def in_subsession?
|
1476
|
-
|
1579
|
+
!@subsession_stack.empty?
|
1477
1580
|
end
|
1478
1581
|
|
1479
1582
|
## event
|
@@ -1487,7 +1590,7 @@ module DEBUGGER__
|
|
1487
1590
|
end
|
1488
1591
|
|
1489
1592
|
pending_line_breakpoints.each do |_key, bp|
|
1490
|
-
if bp.path
|
1593
|
+
if DEBUGGER__.compare_path(bp.path, (iseq.absolute_path || iseq.path))
|
1491
1594
|
bp.try_activate
|
1492
1595
|
end
|
1493
1596
|
end
|
@@ -1550,7 +1653,7 @@ module DEBUGGER__
|
|
1550
1653
|
|
1551
1654
|
frames = exc.instance_variable_get(:@__debugger_postmortem_frames)
|
1552
1655
|
@postmortem = true
|
1553
|
-
ThreadClient.current.suspend :postmortem, postmortem_frames: frames
|
1656
|
+
ThreadClient.current.suspend :postmortem, postmortem_frames: frames, postmortem_exc: exc
|
1554
1657
|
ensure
|
1555
1658
|
@postmortem = false
|
1556
1659
|
end
|
@@ -1810,6 +1913,9 @@ module DEBUGGER__
|
|
1810
1913
|
puts "\nStop by #{args.first}"
|
1811
1914
|
end
|
1812
1915
|
end
|
1916
|
+
|
1917
|
+
def flush
|
1918
|
+
end
|
1813
1919
|
end
|
1814
1920
|
|
1815
1921
|
# manual configuration methods
|
@@ -1822,11 +1928,11 @@ module DEBUGGER__
|
|
1822
1928
|
::DEBUGGER__::SESSION.add_catch_breakpoint pat
|
1823
1929
|
end
|
1824
1930
|
|
1825
|
-
# String for
|
1931
|
+
# String for requiring location
|
1826
1932
|
# nil for -r
|
1827
1933
|
def self.require_location
|
1828
1934
|
locs = caller_locations
|
1829
|
-
dir_prefix = /#{__dir__}/
|
1935
|
+
dir_prefix = /#{Regexp.escape(__dir__)}/
|
1830
1936
|
|
1831
1937
|
locs.each do |loc|
|
1832
1938
|
case loc.absolute_path
|
@@ -1846,7 +1952,7 @@ module DEBUGGER__
|
|
1846
1952
|
|
1847
1953
|
unless defined? SESSION
|
1848
1954
|
require_relative 'local'
|
1849
|
-
initialize_session UI_LocalConsole.new
|
1955
|
+
initialize_session{ UI_LocalConsole.new }
|
1850
1956
|
end
|
1851
1957
|
|
1852
1958
|
setup_initial_suspend unless nonstop
|
@@ -1854,8 +1960,9 @@ module DEBUGGER__
|
|
1854
1960
|
|
1855
1961
|
def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
|
1856
1962
|
CONFIG.set_config(**kw)
|
1963
|
+
require_relative 'server'
|
1857
1964
|
|
1858
|
-
if port || CONFIG[:open_frontend] == 'chrome'
|
1965
|
+
if port || CONFIG[:open_frontend] == 'chrome' || (!::Addrinfo.respond_to?(:unix))
|
1859
1966
|
open_tcp host: host, port: (port || 0), nonstop: nonstop
|
1860
1967
|
else
|
1861
1968
|
open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop
|
@@ -1869,7 +1976,7 @@ module DEBUGGER__
|
|
1869
1976
|
if defined? SESSION
|
1870
1977
|
SESSION.reset_ui UI_TcpServer.new(host: host, port: port)
|
1871
1978
|
else
|
1872
|
-
initialize_session UI_TcpServer.new(host: host, port: port)
|
1979
|
+
initialize_session{ UI_TcpServer.new(host: host, port: port) }
|
1873
1980
|
end
|
1874
1981
|
|
1875
1982
|
setup_initial_suspend unless nonstop
|
@@ -1882,7 +1989,7 @@ module DEBUGGER__
|
|
1882
1989
|
if defined? SESSION
|
1883
1990
|
SESSION.reset_ui UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
|
1884
1991
|
else
|
1885
|
-
initialize_session UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
|
1992
|
+
initialize_session{ UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path) }
|
1886
1993
|
end
|
1887
1994
|
|
1888
1995
|
setup_initial_suspend unless nonstop
|
@@ -1909,9 +2016,10 @@ module DEBUGGER__
|
|
1909
2016
|
end
|
1910
2017
|
|
1911
2018
|
class << self
|
1912
|
-
define_method :initialize_session do |
|
2019
|
+
define_method :initialize_session do |&init_ui|
|
1913
2020
|
DEBUGGER__.info "Session start (pid: #{Process.pid})"
|
1914
|
-
::DEBUGGER__.const_set(:SESSION, Session.new
|
2021
|
+
::DEBUGGER__.const_set(:SESSION, Session.new)
|
2022
|
+
SESSION.activate init_ui.call
|
1915
2023
|
load_rc
|
1916
2024
|
end
|
1917
2025
|
end
|
@@ -1960,13 +2068,17 @@ module DEBUGGER__
|
|
1960
2068
|
METHOD_ADDED_TRACKER = self.create_method_added_tracker
|
1961
2069
|
|
1962
2070
|
SHORT_INSPECT_LENGTH = 40
|
1963
|
-
|
2071
|
+
|
2072
|
+
def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
|
1964
2073
|
str = obj.inspect
|
1965
|
-
|
1966
|
-
|
2074
|
+
|
2075
|
+
if short && str.length > max_length
|
2076
|
+
str[0...max_length] + '...'
|
1967
2077
|
else
|
1968
2078
|
str
|
1969
2079
|
end
|
2080
|
+
rescue Exception => e
|
2081
|
+
str = "<#inspect raises #{e.inspect}>"
|
1970
2082
|
end
|
1971
2083
|
|
1972
2084
|
def self.warn msg
|
@@ -2000,10 +2112,72 @@ module DEBUGGER__
|
|
2000
2112
|
end
|
2001
2113
|
end
|
2002
2114
|
|
2115
|
+
def self.step_in &b
|
2116
|
+
if defined?(SESSION) && SESSION.active?
|
2117
|
+
SESSION.add_iseq_breakpoint RubyVM::InstructionSequence.of(b), oneshot: true
|
2118
|
+
end
|
2119
|
+
|
2120
|
+
yield
|
2121
|
+
end
|
2122
|
+
|
2123
|
+
if File.identical?(__FILE__.upcase, __FILE__.downcase)
|
2124
|
+
# For case insensitive file system (like Windows)
|
2125
|
+
# Note that this check is not enough because case sensitive/insensitive is
|
2126
|
+
# depend on the file system. So this check is only roughly estimation.
|
2127
|
+
|
2128
|
+
def self.compare_path(a, b)
|
2129
|
+
a.downcase == b.downcase
|
2130
|
+
end
|
2131
|
+
else
|
2132
|
+
def self.compare_path(a, b)
|
2133
|
+
a == b
|
2134
|
+
end
|
2135
|
+
end
|
2136
|
+
|
2003
2137
|
module ForkInterceptor
|
2004
|
-
|
2005
|
-
|
2138
|
+
if Process.respond_to? :_fork
|
2139
|
+
def _fork
|
2140
|
+
return yield unless defined?(SESSION) && SESSION.active?
|
2141
|
+
|
2142
|
+
parent_hook, child_hook = __fork_setup_for_debugger
|
2143
|
+
|
2144
|
+
super.tap do |pid|
|
2145
|
+
if pid != 0
|
2146
|
+
# after fork: parent
|
2147
|
+
parent_hook.call pid
|
2148
|
+
else
|
2149
|
+
# after fork: child
|
2150
|
+
child_hook.call
|
2151
|
+
end
|
2152
|
+
end
|
2153
|
+
end
|
2154
|
+
else
|
2155
|
+
def fork(&given_block)
|
2156
|
+
return yield unless defined?(SESSION) && SESSION.active?
|
2157
|
+
parent_hook, child_hook = __fork_setup_for_debugger
|
2158
|
+
|
2159
|
+
if given_block
|
2160
|
+
new_block = proc {
|
2161
|
+
# after fork: child
|
2162
|
+
child_hook.call
|
2163
|
+
given_block.call
|
2164
|
+
}
|
2165
|
+
super(&new_block).tap{|pid| parent_hook.call(pid)}
|
2166
|
+
else
|
2167
|
+
super.tap do |pid|
|
2168
|
+
if pid
|
2169
|
+
# after fork: parent
|
2170
|
+
parent_hook.call pid
|
2171
|
+
else
|
2172
|
+
# after fork: child
|
2173
|
+
child_hook.call
|
2174
|
+
end
|
2175
|
+
end
|
2176
|
+
end
|
2177
|
+
end
|
2178
|
+
end
|
2006
2179
|
|
2180
|
+
private def __fork_setup_for_debugger
|
2007
2181
|
unless fork_mode = CONFIG[:fork_mode]
|
2008
2182
|
if CONFIG[:parent_on_fork]
|
2009
2183
|
fork_mode = :parent
|
@@ -2050,26 +2224,7 @@ module DEBUGGER__
|
|
2050
2224
|
}
|
2051
2225
|
end
|
2052
2226
|
|
2053
|
-
|
2054
|
-
new_block = proc {
|
2055
|
-
# after fork: child
|
2056
|
-
child_hook.call
|
2057
|
-
given_block.call
|
2058
|
-
}
|
2059
|
-
pid = super(&new_block)
|
2060
|
-
parent_hook.call(pid)
|
2061
|
-
pid
|
2062
|
-
else
|
2063
|
-
if pid = super
|
2064
|
-
# after fork: parent
|
2065
|
-
parent_hook.call pid
|
2066
|
-
else
|
2067
|
-
# after fork: child
|
2068
|
-
child_hook.call
|
2069
|
-
end
|
2070
|
-
|
2071
|
-
pid
|
2072
|
-
end
|
2227
|
+
return parent_hook, child_hook
|
2073
2228
|
end
|
2074
2229
|
end
|
2075
2230
|
|
@@ -2086,28 +2241,46 @@ module DEBUGGER__
|
|
2086
2241
|
end
|
2087
2242
|
end
|
2088
2243
|
|
2089
|
-
if
|
2244
|
+
if Process.respond_to? :_fork
|
2245
|
+
module ::Process
|
2246
|
+
class << self
|
2247
|
+
prepend ForkInterceptor
|
2248
|
+
end
|
2249
|
+
end
|
2250
|
+
|
2251
|
+
# trap
|
2090
2252
|
module ::Kernel
|
2091
|
-
prepend ForkInterceptor
|
2092
2253
|
prepend TrapInterceptor
|
2093
2254
|
end
|
2255
|
+
module ::Signal
|
2256
|
+
class << self
|
2257
|
+
prepend TrapInterceptor
|
2258
|
+
end
|
2259
|
+
end
|
2094
2260
|
else
|
2095
|
-
|
2096
|
-
|
2097
|
-
|
2261
|
+
if RUBY_VERSION >= '3.0.0'
|
2262
|
+
module ::Kernel
|
2263
|
+
prepend ForkInterceptor
|
2264
|
+
prepend TrapInterceptor
|
2265
|
+
end
|
2266
|
+
else
|
2267
|
+
class ::Object
|
2268
|
+
include ForkInterceptor
|
2269
|
+
include TrapInterceptor
|
2270
|
+
end
|
2098
2271
|
end
|
2099
|
-
end
|
2100
2272
|
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2273
|
+
module ::Kernel
|
2274
|
+
class << self
|
2275
|
+
prepend ForkInterceptor
|
2276
|
+
prepend TrapInterceptor
|
2277
|
+
end
|
2105
2278
|
end
|
2106
|
-
end
|
2107
2279
|
|
2108
|
-
|
2109
|
-
|
2110
|
-
|
2280
|
+
module ::Process
|
2281
|
+
class << self
|
2282
|
+
prepend ForkInterceptor
|
2283
|
+
end
|
2111
2284
|
end
|
2112
2285
|
end
|
2113
2286
|
|