debug 1.2.2 → 1.3.1

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.
data/lib/debug/console.rb CHANGED
@@ -31,6 +31,7 @@ module DEBUGGER__
31
31
  end if SIGWINCH_SUPPORTED
32
32
 
33
33
  def readline_setup prompt
34
+ load_history_if_not_loaded
34
35
  commands = DEBUGGER__.commands
35
36
 
36
37
  Reline.completion_proc = -> given do
@@ -87,40 +88,106 @@ module DEBUGGER__
87
88
  Reline.readmultiline(prompt, true){ true }
88
89
  end
89
90
 
91
+ def history
92
+ Reline::HISTORY
93
+ end
94
+
90
95
  rescue LoadError
91
- begin
92
- require 'readline.so'
96
+ begin
97
+ require 'readline.so'
98
+
99
+ def readline_setup
100
+ load_history_if_not_loaded
101
+ commands = DEBUGGER__.commands
102
+
103
+ Readline.completion_proc = proc{|given|
104
+ buff = Readline.line_buffer
105
+ Readline.completion_append_character= ' '
106
+
107
+ if /\s/ =~ buff # second parameters
108
+ given = File.expand_path(given + 'a').sub(/a\z/, '')
109
+ files = Dir.glob(given + '*')
110
+ if files.size == 1 && File.directory?(files.first)
111
+ Readline.completion_append_character= '/'
112
+ end
113
+ files
114
+ else
115
+ commands.keys.grep(/\A#{given}/)
116
+ end
117
+ }
118
+ end
93
119
 
94
- def readline_setup
95
- Readline.completion_proc = proc{|given|
96
- buff = Readline.line_buffer
97
- Readline.completion_append_character= ' '
120
+ def readline prompt
121
+ readline_setup
122
+ Readline.readline(prompt, true)
123
+ end
98
124
 
99
- if /\s/ =~ buff # second parameters
100
- given = File.expand_path(given + 'a').sub(/a\z/, '')
101
- files = Dir.glob(given + '*')
102
- if files.size == 1 && File.directory?(files.first)
103
- Readline.completion_append_character= '/'
104
- end
105
- files
106
- else
107
- DEBUGGER__.commands.keys.grep(/\A#{given}/)
108
- end
109
- }
125
+ def history
126
+ Readline::HISTORY
127
+ end
128
+
129
+ rescue LoadError
130
+ def readline prompt
131
+ print prompt
132
+ gets
133
+ end
134
+
135
+ def history
136
+ nil
137
+ end
110
138
  end
139
+ end
111
140
 
112
- def readline prompt
113
- readline_setup
114
- Readline.readline(prompt, true)
141
+ def history_file
142
+ CONFIG[:history_file] || File.expand_path("~/.rdbg_history")
143
+ end
144
+
145
+ FH = "# Today's OMIKUJI: "
146
+
147
+ def read_history_file
148
+ if history && File.exists?(path = history_file)
149
+ f = (['', 'DAI-', 'CHU-', 'SHO-'].map{|e| e+'KICHI'}+['KYO']).sample
150
+ ["#{FH}#{f}".dup] + File.readlines(path)
151
+ else
152
+ []
115
153
  end
154
+ end
116
155
 
117
- rescue LoadError
118
- def readline prompt
119
- print prompt
120
- gets
156
+ def initialize
157
+ @init_history_lines = nil
158
+ end
159
+
160
+ def load_history_if_not_loaded
161
+ return if @init_history_lines
162
+
163
+ @init_history_lines = load_history
164
+ end
165
+
166
+ def deactivate
167
+ if history && @init_history_lines
168
+ added_records = history.to_a[@init_history_lines .. -1]
169
+ path = history_file
170
+ max = CONFIG[:save_history] || 10_000
171
+
172
+ if !added_records.empty? && !path.empty?
173
+ orig_records = read_history_file
174
+ open(history_file, 'w'){|f|
175
+ (orig_records + added_records).last(max).each{|line|
176
+ if !line.start_with?(FH) && !line.strip.empty?
177
+ f.puts line.strip
178
+ end
179
+ }
180
+ }
181
+ end
121
182
  end
122
183
  end
184
+
185
+ def load_history
186
+ read_history_file.count{|line|
187
+ line.strip!
188
+ history << line unless line.empty?
189
+ }
123
190
  end
124
- end
191
+ end # class Console
125
192
  end
126
193
 
data/lib/debug/local.rb CHANGED
@@ -26,8 +26,11 @@ module DEBUGGER__
26
26
 
27
27
  def deactivate
28
28
  if SESSION.intercept_trap_sigint?
29
- trap(:SIGINT, SESSION.intercepted_sigint_cmd)
29
+ prev = SESSION.intercept_trap_sigint_end
30
+ trap(:SIGINT, prev)
30
31
  end
32
+
33
+ @console.deactivate
31
34
  end
32
35
 
33
36
  def width
@@ -83,6 +86,24 @@ module DEBUGGER__
83
86
  trap(:INT, prev_handler)
84
87
  end
85
88
  end
89
+
90
+ def after_fork_parent
91
+ parent_pid = Process.pid
92
+
93
+ at_exit{
94
+ SESSION.intercept_trap_sigint_end
95
+ trap(:SIGINT, :IGNORE)
96
+
97
+ if Process.pid == parent_pid
98
+ # only check child process from its parent
99
+ begin
100
+ # wait for all child processes to keep terminal
101
+ loop{ Process.waitpid }
102
+ rescue Errno::ESRCH, Errno::ECHILD
103
+ end
104
+ end
105
+ }
106
+ end
86
107
  end
87
108
  end
88
109
 
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ return if defined?(::DEBUGGER__)
4
+
5
+ #
6
+ # put the following line in .bash_profile
7
+ # export RUBYOPT="-r .../debug/prelude $(RUBYOPT)"
8
+ #
9
+ module Kernel
10
+ def debugger(*a, up_level: 0, **kw)
11
+ begin
12
+ require_relative 'version'
13
+ cur_version = ::DEBUGGER__::VERSION
14
+ require_relative 'frame_info'
15
+
16
+ if !defined?(::DEBUGGER__::SO_VERSION) || ::DEBUGGER__::VERSION != ::DEBUGGER__::SO_VERSION
17
+ ::Object.send(:remove_const, :DEBUGGER__)
18
+ raise LoadError
19
+ end
20
+ require_relative 'session'
21
+ up_level += 1
22
+ rescue LoadError
23
+ $LOADED_FEATURES.delete_if{|e|
24
+ e.start_with?(__dir__) || e.end_with?('debug/debug.so')
25
+ }
26
+ require 'debug/session'
27
+ require 'debug/version'
28
+ ::DEBUGGER__.info "Can not activate debug #{cur_version} specified by debug/prelude.rb. Activate debug #{DEBUGGER__::VERSION} instead."
29
+ up_level += 1
30
+ end
31
+
32
+ ::DEBUGGER__::start no_sigint_hook: true, nonstop: true
33
+
34
+ begin
35
+ debugger(*a, up_level: up_level, **kw)
36
+ self
37
+ rescue ArgumentError # for 1.2.4 and earlier
38
+ debugger(*a, **kw)
39
+ self
40
+ end
41
+ end
42
+
43
+ alias bb debugger if ENV['RUBY_DEBUG_BB']
44
+ end
45
+
46
+ class Binding
47
+ alias break debugger
48
+ alias b debugger
49
+ end
data/lib/debug/server.rb CHANGED
@@ -15,6 +15,8 @@ module DEBUGGER__
15
15
  @q_ans = nil
16
16
  @unsent_messages = []
17
17
  @width = 80
18
+ @repl = true
19
+ @session = nil
18
20
  end
19
21
 
20
22
  class Terminate < StandardError
@@ -22,6 +24,7 @@ module DEBUGGER__
22
24
 
23
25
  def deactivate
24
26
  @reader_thread.raise Terminate
27
+ @reader_thread.join
25
28
  end
26
29
 
27
30
  def accept
@@ -36,9 +39,11 @@ module DEBUGGER__
36
39
  end
37
40
 
38
41
  def activate session, on_fork: false
42
+ @session = session
39
43
  @reader_thread = Thread.new do
40
44
  # An error on this thread should break the system.
41
45
  Thread.current.abort_on_exception = true
46
+ Thread.current.name = 'DEBUGGER__::Server::reader'
42
47
 
43
48
  accept do |server, already_connected: false|
44
49
  DEBUGGER__.warn "Connected."
@@ -52,7 +57,7 @@ module DEBUGGER__
52
57
  # flush unsent messages
53
58
  @unsent_messages.each{|m|
54
59
  @sock.puts m
55
- }
60
+ } if @repl
56
61
  @unsent_messages.clear
57
62
 
58
63
  @q_msg = Queue.new
@@ -68,6 +73,7 @@ module DEBUGGER__
68
73
  raise # should catch at outer scope
69
74
  rescue => e
70
75
  DEBUGGER__.warn "ReaderThreadError: #{e}"
76
+ pp e.backtrace
71
77
  ensure
72
78
  DEBUGGER__.warn "Disconnected."
73
79
  @sock = nil
@@ -103,23 +109,58 @@ module DEBUGGER__
103
109
 
104
110
  raise unless @sock.read(2) == "\r\n"
105
111
  self.extend(UI_DAP)
112
+ @repl = false
106
113
  dap_setup @sock.read($1.to_i)
114
+ when /^GET \/ HTTP\/1.1/
115
+ require_relative 'server_cdp'
116
+
117
+ self.extend(UI_CDP)
118
+ @repl = false
119
+ @web_sock = UI_CDP::WebSocket.new(@sock)
120
+ @web_sock.handshake
107
121
  else
108
122
  raise "Greeting message error: #{g}"
109
123
  end
110
124
  end
111
125
 
112
126
  def process
113
- while line = @sock.gets
127
+ while true
128
+ DEBUGGER__.info "sleep IO.select"
129
+ r = IO.select([@sock])
130
+ DEBUGGER__.info "wakeup IO.select"
131
+
132
+ line = @session.process_group.sync do
133
+ unless IO.select([@sock], nil, nil, 0)
134
+ DEBUGGER__.info "UI_Server can not read"
135
+ break :can_not_read
136
+ end
137
+ @sock.gets&.chomp.tap{|line|
138
+ DEBUGGER__.info "UI_Server received: #{line}"
139
+ }
140
+ end
141
+
142
+ next if line == :can_not_read
143
+
114
144
  case line
115
145
  when /\Apause/
116
146
  pause
117
- when /\Acommand ?(.+)/
118
- @q_msg << $1
119
- when /\Aanswer (.*)/
120
- @q_ans << $1
121
- when /\Awidth (.+)/
122
- @width = $1.to_i
147
+ when /\Acommand (\d+) (\d+) ?(.+)/
148
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
149
+
150
+ if $1.to_i == Process.pid
151
+ @width = $2.to_i
152
+ @q_msg << $3
153
+ else
154
+ raise "pid:#{Process.pid} but get #{line}"
155
+ end
156
+ when /\Aanswer (\d+) (.*)/
157
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
158
+
159
+ if $1.to_i == Process.pid
160
+ @q_ans << $2
161
+ else
162
+ raise "pid:#{Process.pid} but get #{line}"
163
+ end
123
164
  else
124
165
  STDERR.puts "unsupported: #{line}"
125
166
  exit!
@@ -135,6 +176,21 @@ module DEBUGGER__
135
176
  @width
136
177
  end
137
178
 
179
+ def sigurg_overridden? prev_handler
180
+ case prev_handler
181
+ when "SYSTEM_DEFAULT"
182
+ false
183
+ when Proc
184
+ if prev_handler.source_location[0] == __FILE__
185
+ false
186
+ else
187
+ true
188
+ end
189
+ else
190
+ true
191
+ end
192
+ end
193
+
138
194
  def setup_interrupt
139
195
  prev_handler = trap(:SIGURG) do
140
196
  # $stderr.puts "trapped SIGINT"
@@ -148,8 +204,8 @@ module DEBUGGER__
148
204
  end
149
205
  end
150
206
 
151
- if prev_handler != "SYSTEM_DEFAULT"
152
- DEBUGGER__.warn "SIGURG handler is overriddend by the debugger."
207
+ if sigurg_overridden?(prev_handler)
208
+ DEBUGGER__.warn "SIGURG handler is overridden by the debugger."
153
209
  end
154
210
  yield
155
211
  ensure
@@ -191,7 +247,7 @@ module DEBUGGER__
191
247
 
192
248
  def ask prompt
193
249
  sock do |s|
194
- s.puts "ask #{prompt}"
250
+ s.puts "ask #{Process.pid} #{prompt}"
195
251
  @q_ans.pop
196
252
  end
197
253
  end
@@ -220,13 +276,23 @@ module DEBUGGER__
220
276
 
221
277
  def readline prompt
222
278
  input = (sock do |s|
223
- s.puts "input"
279
+ if @repl
280
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
281
+ line = "input #{Process.pid}"
282
+ DEBUGGER__.info "send: #{line}"
283
+ s.puts line
284
+ end
224
285
  sleep 0.01 until @q_msg
286
+ @q_msg.pop.tap{|msg|
287
+ DEBUGGER__.info "readline: #{msg.inspect}"
288
+ }
289
+ end || 'continue')
225
290
 
226
- @q_msg.pop
227
- end || 'continue').strip
228
-
229
- input
291
+ if input.is_a?(String)
292
+ input.strip
293
+ else
294
+ input
295
+ end
230
296
  end
231
297
 
232
298
  def pause
@@ -240,10 +306,15 @@ module DEBUGGER__
240
306
  s.puts "quit"
241
307
  end
242
308
  end
309
+
310
+ def after_fork_parent
311
+ # do nothing
312
+ end
243
313
  end
244
314
 
245
315
  class UI_TcpServer < UI_ServerBase
246
316
  def initialize host: nil, port: nil
317
+ @addr = nil
247
318
  @host = host || CONFIG[:host] || '127.0.0.1'
248
319
  @port = port || begin
249
320
  port_str = CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
@@ -263,7 +334,24 @@ module DEBUGGER__
263
334
 
264
335
  begin
265
336
  Socket.tcp_server_sockets @host, @port do |socks|
266
- ::DEBUGGER__.warn "Debugger can attach via TCP/IP (#{socks.map{|e| e.local_address.inspect}})"
337
+ addr = socks[0].local_address.inspect_sockaddr # Change this part if `socks` are multiple.
338
+ rdbg = File.expand_path('../../exe/rdbg', __dir__)
339
+
340
+ DEBUGGER__.warn "Debugger can attach via TCP/IP (#{addr})"
341
+ DEBUGGER__.info <<~EOS
342
+ With rdbg, use the following command line:
343
+ #
344
+ # #{rdbg} --attach #{addr.split(':').join(' ')}
345
+ #
346
+ EOS
347
+
348
+ DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome'
349
+ With Chrome browser, type the following URL in the address-bar:
350
+
351
+ devtools://devtools/bundled/inspector.html?ws=#{addr}
352
+
353
+ EOS
354
+
267
355
  Socket.accept_loop(socks) do |sock, client|
268
356
  @client_addr = client
269
357
  yield @sock_for_fork = sock
@@ -298,6 +386,64 @@ module DEBUGGER__
298
386
  super()
299
387
  end
300
388
 
389
+ def vscode_setup
390
+ require 'tmpdir'
391
+ require 'json'
392
+ require 'fileutils'
393
+
394
+ dir = Dir.mktmpdir("ruby-debug-vscode-")
395
+ at_exit{
396
+ FileUtils.rm_rf dir
397
+ }
398
+ Dir.chdir(dir) do
399
+ Dir.mkdir('.vscode')
400
+ open('README.rb', 'w'){|f|
401
+ f.puts <<~MSG
402
+ # Wait for starting the attaching to the Ruby process
403
+ # This file will be removed at the end of the debuggee process.
404
+ #
405
+ # Note that vscode-rdbg extension is needed. Please install if you don't have.
406
+ MSG
407
+ }
408
+ open('.vscode/launch.json', 'w'){|f|
409
+ f.puts JSON.pretty_generate({
410
+ version: '0.2.0',
411
+ configurations: [
412
+ {
413
+ type: "rdbg",
414
+ name: "Attach with rdbg",
415
+ request: "attach",
416
+ rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
417
+ debugPort: @sock_path,
418
+ autoAttach: true,
419
+ }
420
+ ]
421
+ })
422
+ }
423
+ end
424
+
425
+ cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
426
+ cmdline = cmds.join(' ')
427
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"
428
+
429
+ STDERR.puts "Launching: #{cmdline}"
430
+ env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
431
+
432
+ unless system(env, *cmds)
433
+ DEBUGGER__.warn <<~MESSAGE
434
+ Can not invoke the command.
435
+ Use the command-line on your terminal (with modification if you need).
436
+
437
+ #{cmdline}
438
+
439
+ If your application is running on a SSH remote host, please try:
440
+
441
+ #{ssh_cmdline}
442
+
443
+ MESSAGE
444
+ end
445
+ end
446
+
301
447
  def accept
302
448
  super # for fork
303
449
 
@@ -310,14 +456,29 @@ module DEBUGGER__
310
456
  end
311
457
 
312
458
  ::DEBUGGER__.warn "Debugger can attach via UNIX domain socket (#{@sock_path})"
313
- Socket.unix_server_loop @sock_path do |sock, client|
314
- @sock_for_fork = sock
315
- @client_addr = client
316
-
317
- yield sock
318
- ensure
319
- sock.close
320
- @sock_for_fork = nil
459
+ vscode_setup if CONFIG[:open_frontend] == 'vscode'
460
+
461
+ begin
462
+ Socket.unix_server_loop @sock_path do |sock, client|
463
+ @sock_for_fork = sock
464
+ @client_addr = client
465
+
466
+ yield sock
467
+ ensure
468
+ sock.close
469
+ @sock_for_fork = nil
470
+ end
471
+ rescue Errno::ECONNREFUSED => _e
472
+ ::DEBUGGER__.warn "#{_e.message} (socket path: #{@sock_path})"
473
+
474
+ if @sock_path.start_with? Config.unix_domain_socket_tmpdir
475
+ # try on homedir
476
+ @sock_path = Config.create_unix_domain_socket_name(unix_domain_socket_homedir)
477
+ ::DEBUGGER__.warn "retry with #{@sock_path}"
478
+ retry
479
+ else
480
+ raise
481
+ end
321
482
  end
322
483
  end
323
484
  end