debug 1.2.2 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
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