debug 1.2.3 → 1.3.2

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,60 @@ 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
+ CONFIG.set_config no_color: true
120
+
121
+ @web_sock = UI_CDP::WebSocket.new(@sock)
122
+ @web_sock.handshake
107
123
  else
108
124
  raise "Greeting message error: #{g}"
109
125
  end
110
126
  end
111
127
 
112
128
  def process
113
- while line = @sock.gets
129
+ while true
130
+ DEBUGGER__.info "sleep IO.select"
131
+ r = IO.select([@sock])
132
+ DEBUGGER__.info "wakeup IO.select"
133
+
134
+ line = @session.process_group.sync do
135
+ unless IO.select([@sock], nil, nil, 0)
136
+ DEBUGGER__.info "UI_Server can not read"
137
+ break :can_not_read
138
+ end
139
+ @sock.gets&.chomp.tap{|line|
140
+ DEBUGGER__.info "UI_Server received: #{line}"
141
+ }
142
+ end
143
+
144
+ next if line == :can_not_read
145
+
114
146
  case line
115
147
  when /\Apause/
116
148
  pause
117
- when /\Acommand ?(.+)/
118
- @q_msg << $1
119
- when /\Aanswer (.*)/
120
- @q_ans << $1
121
- when /\Awidth (.+)/
122
- @width = $1.to_i
149
+ when /\Acommand (\d+) (\d+) ?(.+)/
150
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
151
+
152
+ if $1.to_i == Process.pid
153
+ @width = $2.to_i
154
+ @q_msg << $3
155
+ else
156
+ raise "pid:#{Process.pid} but get #{line}"
157
+ end
158
+ when /\Aanswer (\d+) (.*)/
159
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
160
+
161
+ if $1.to_i == Process.pid
162
+ @q_ans << $2
163
+ else
164
+ raise "pid:#{Process.pid} but get #{line}"
165
+ end
123
166
  else
124
167
  STDERR.puts "unsupported: #{line}"
125
168
  exit!
@@ -135,6 +178,21 @@ module DEBUGGER__
135
178
  @width
136
179
  end
137
180
 
181
+ def sigurg_overridden? prev_handler
182
+ case prev_handler
183
+ when "SYSTEM_DEFAULT"
184
+ false
185
+ when Proc
186
+ if prev_handler.source_location[0] == __FILE__
187
+ false
188
+ else
189
+ true
190
+ end
191
+ else
192
+ true
193
+ end
194
+ end
195
+
138
196
  def setup_interrupt
139
197
  prev_handler = trap(:SIGURG) do
140
198
  # $stderr.puts "trapped SIGINT"
@@ -148,8 +206,8 @@ module DEBUGGER__
148
206
  end
149
207
  end
150
208
 
151
- if prev_handler != "SYSTEM_DEFAULT"
152
- DEBUGGER__.warn "SIGURG handler is overriddend by the debugger."
209
+ if sigurg_overridden?(prev_handler)
210
+ DEBUGGER__.warn "SIGURG handler is overridden by the debugger."
153
211
  end
154
212
  yield
155
213
  ensure
@@ -191,7 +249,7 @@ module DEBUGGER__
191
249
 
192
250
  def ask prompt
193
251
  sock do |s|
194
- s.puts "ask #{prompt}"
252
+ s.puts "ask #{Process.pid} #{prompt}"
195
253
  @q_ans.pop
196
254
  end
197
255
  end
@@ -220,13 +278,23 @@ module DEBUGGER__
220
278
 
221
279
  def readline prompt
222
280
  input = (sock do |s|
223
- s.puts "input"
281
+ if @repl
282
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
283
+ line = "input #{Process.pid}"
284
+ DEBUGGER__.info "send: #{line}"
285
+ s.puts line
286
+ end
224
287
  sleep 0.01 until @q_msg
288
+ @q_msg.pop.tap{|msg|
289
+ DEBUGGER__.info "readline: #{msg.inspect}"
290
+ }
291
+ end || 'continue')
225
292
 
226
- @q_msg.pop
227
- end || 'continue').strip
228
-
229
- input
293
+ if input.is_a?(String)
294
+ input.strip
295
+ else
296
+ input
297
+ end
230
298
  end
231
299
 
232
300
  def pause
@@ -240,10 +308,15 @@ module DEBUGGER__
240
308
  s.puts "quit"
241
309
  end
242
310
  end
311
+
312
+ def after_fork_parent
313
+ # do nothing
314
+ end
243
315
  end
244
316
 
245
317
  class UI_TcpServer < UI_ServerBase
246
318
  def initialize host: nil, port: nil
319
+ @addr = nil
247
320
  @host = host || CONFIG[:host] || '127.0.0.1'
248
321
  @port = port || begin
249
322
  port_str = CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
@@ -263,7 +336,24 @@ module DEBUGGER__
263
336
 
264
337
  begin
265
338
  Socket.tcp_server_sockets @host, @port do |socks|
266
- ::DEBUGGER__.warn "Debugger can attach via TCP/IP (#{socks.map{|e| e.local_address.inspect}})"
339
+ addr = socks[0].local_address.inspect_sockaddr # Change this part if `socks` are multiple.
340
+ rdbg = File.expand_path('../../exe/rdbg', __dir__)
341
+
342
+ DEBUGGER__.warn "Debugger can attach via TCP/IP (#{addr})"
343
+ DEBUGGER__.info <<~EOS
344
+ With rdbg, use the following command line:
345
+ #
346
+ # #{rdbg} --attach #{addr.split(':').join(' ')}
347
+ #
348
+ EOS
349
+
350
+ DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome'
351
+ With Chrome browser, type the following URL in the address-bar:
352
+
353
+ devtools://devtools/bundled/inspector.html?ws=#{addr}
354
+
355
+ EOS
356
+
267
357
  Socket.accept_loop(socks) do |sock, client|
268
358
  @client_addr = client
269
359
  yield @sock_for_fork = sock
@@ -298,6 +388,64 @@ module DEBUGGER__
298
388
  super()
299
389
  end
300
390
 
391
+ def vscode_setup
392
+ require 'tmpdir'
393
+ require 'json'
394
+ require 'fileutils'
395
+
396
+ dir = Dir.mktmpdir("ruby-debug-vscode-")
397
+ at_exit{
398
+ FileUtils.rm_rf dir
399
+ }
400
+ Dir.chdir(dir) do
401
+ Dir.mkdir('.vscode')
402
+ open('README.rb', 'w'){|f|
403
+ f.puts <<~MSG
404
+ # Wait for starting the attaching to the Ruby process
405
+ # This file will be removed at the end of the debuggee process.
406
+ #
407
+ # Note that vscode-rdbg extension is needed. Please install if you don't have.
408
+ MSG
409
+ }
410
+ open('.vscode/launch.json', 'w'){|f|
411
+ f.puts JSON.pretty_generate({
412
+ version: '0.2.0',
413
+ configurations: [
414
+ {
415
+ type: "rdbg",
416
+ name: "Attach with rdbg",
417
+ request: "attach",
418
+ rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
419
+ debugPort: @sock_path,
420
+ autoAttach: true,
421
+ }
422
+ ]
423
+ })
424
+ }
425
+ end
426
+
427
+ cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
428
+ cmdline = cmds.join(' ')
429
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"
430
+
431
+ STDERR.puts "Launching: #{cmdline}"
432
+ env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
433
+
434
+ unless system(env, *cmds)
435
+ DEBUGGER__.warn <<~MESSAGE
436
+ Can not invoke the command.
437
+ Use the command-line on your terminal (with modification if you need).
438
+
439
+ #{cmdline}
440
+
441
+ If your application is running on a SSH remote host, please try:
442
+
443
+ #{ssh_cmdline}
444
+
445
+ MESSAGE
446
+ end
447
+ end
448
+
301
449
  def accept
302
450
  super # for fork
303
451
 
@@ -310,14 +458,29 @@ module DEBUGGER__
310
458
  end
311
459
 
312
460
  ::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
461
+ vscode_setup if CONFIG[:open_frontend] == 'vscode'
462
+
463
+ begin
464
+ Socket.unix_server_loop @sock_path do |sock, client|
465
+ @sock_for_fork = sock
466
+ @client_addr = client
467
+
468
+ yield sock
469
+ ensure
470
+ sock.close
471
+ @sock_for_fork = nil
472
+ end
473
+ rescue Errno::ECONNREFUSED => _e
474
+ ::DEBUGGER__.warn "#{_e.message} (socket path: #{@sock_path})"
475
+
476
+ if @sock_path.start_with? Config.unix_domain_socket_tmpdir
477
+ # try on homedir
478
+ @sock_path = Config.create_unix_domain_socket_name(unix_domain_socket_homedir)
479
+ ::DEBUGGER__.warn "retry with #{@sock_path}"
480
+ retry
481
+ else
482
+ raise
483
+ end
321
484
  end
322
485
  end
323
486
  end