debug 1.4.0 → 1.6.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
@@ -30,6 +30,18 @@ module DEBUGGER__
30
30
  prepend m
31
31
  end if SIGWINCH_SUPPORTED
32
32
 
33
+ def parse_input buff, commands
34
+ c, rest = get_command buff
35
+ case
36
+ when commands.keys.include?(c)
37
+ :command
38
+ when !rest && /\A\s*[a-z]*\z/ =~ c
39
+ nil
40
+ else
41
+ :ruby
42
+ end
43
+ end
44
+
33
45
  def readline_setup prompt
34
46
  load_history_if_not_loaded
35
47
  commands = DEBUGGER__.commands
@@ -38,7 +50,17 @@ module DEBUGGER__
38
50
  prev_output_modifier_proc = Reline.output_modifier_proc
39
51
  prev_prompt_proc = Reline.prompt_proc
40
52
 
41
- Reline.prompt_proc = nil
53
+ # prompt state
54
+ state = nil # :command, :ruby, nil (unknown)
55
+
56
+ Reline.prompt_proc = -> args, *kw do
57
+ case state = parse_input(args.first, commands)
58
+ when nil, :command
59
+ [prompt, prompt]
60
+ when :ruby
61
+ [prompt.sub('rdbg'){colorize('ruby', [:RED])}] * 2
62
+ end
63
+ end
42
64
 
43
65
  Reline.completion_proc = -> given do
44
66
  buff = Reline.line_buffer
@@ -52,17 +74,16 @@ module DEBUGGER__
52
74
  end
53
75
  files
54
76
  else
55
- commands.keys.grep(/\A#{given}/)
77
+ commands.keys.grep(/\A#{Regexp.escape(given)}/)
56
78
  end
57
79
  end
58
80
 
59
81
  Reline.output_modifier_proc = -> buff, **kw do
60
82
  c, rest = get_command buff
61
83
 
62
- case
63
- when commands.keys.include?(c = c.strip)
64
- # [:DIM, :CYAN, :BLUE, :CLEAR, :UNDERLINE, :REVERSE, :RED, :GREEN, :MAGENTA, :BOLD, :YELLOW]
65
- cmd = colorize(c.strip, [:CYAN, :UNDERLINE])
84
+ case state
85
+ when :command
86
+ cmd = colorize(c, [:CYAN, :UNDERLINE])
66
87
 
67
88
  if commands[c] == c
68
89
  rprompt = colorize(" # command", [:DIM])
@@ -70,14 +91,14 @@ module DEBUGGER__
70
91
  rprompt = colorize(" # #{commands[c]} command", [:DIM])
71
92
  end
72
93
 
73
- rest = (rest ? colorize_code(rest) : '') + rprompt
74
- cmd + rest
75
- when !rest && /\A\s*[a-z]*\z/ =~ c
94
+ rest = rest ? colorize_code(rest) : ''
95
+ cmd + rest + rprompt
96
+ when nil
76
97
  buff
77
- else
78
- colorize_code(buff.chomp) + colorize(" # ruby", [:DIM])
98
+ when :ruby
99
+ colorize_code(buff.chomp)
79
100
  end
80
- end
101
+ end unless CONFIG[:no_hint]
81
102
 
82
103
  yield
83
104
 
@@ -90,9 +111,9 @@ module DEBUGGER__
90
111
  private def get_command line
91
112
  case line.chomp
92
113
  when /\A(\s*[a-z]+)(\s.*)?\z$/
93
- return $1, $2
114
+ return $1.strip, $2
94
115
  else
95
- line.chomp
116
+ line.strip
96
117
  end
97
118
  end
98
119
 
@@ -153,7 +174,13 @@ module DEBUGGER__
153
174
  end
154
175
 
155
176
  def history_file
156
- CONFIG[:history_file] || File.expand_path("~/.rdbg_history")
177
+ history_file = CONFIG[:history_file]
178
+
179
+ if !history_file.empty?
180
+ File.expand_path(history_file)
181
+ else
182
+ history_file
183
+ end
157
184
  end
158
185
 
159
186
  FH = "# Today's OMIKUJI: "
@@ -181,7 +208,7 @@ module DEBUGGER__
181
208
  if history && @init_history_lines
182
209
  added_records = history.to_a[@init_history_lines .. -1]
183
210
  path = history_file
184
- max = CONFIG[:save_history] || 10_000
211
+ max = CONFIG[:save_history]
185
212
 
186
213
  if !added_records.empty? && !path.empty?
187
214
  orig_records = read_history_file
@@ -27,8 +27,8 @@ module DEBUGGER__
27
27
  location.absolute_path
28
28
  end
29
29
 
30
- def pretty_path
31
- return '#<none>' unless path = self.path
30
+ def self.pretty_path path
31
+ return '#<none>' unless path
32
32
  use_short_path = CONFIG[:use_short_path]
33
33
 
34
34
  case
@@ -45,15 +45,18 @@ module DEBUGGER__
45
45
  end
46
46
  end
47
47
 
48
+ def pretty_path
49
+ FrameInfo.pretty_path path
50
+ end
51
+
48
52
  def name
49
53
  # p frame_type: frame_type, self: self
50
54
  case frame_type
51
55
  when :block
52
- level, block_loc, _args = block_identifier
56
+ level, block_loc = block_identifier
53
57
  "block in #{block_loc}#{level}"
54
58
  when :method
55
- ci, _args = method_identifier
56
- "#{ci}"
59
+ method_identifier
57
60
  when :c
58
61
  c_identifier
59
62
  when :other
@@ -83,16 +86,13 @@ module DEBUGGER__
83
86
 
84
87
  def block_identifier
85
88
  return unless frame_type == :block
86
- args = parameters_info(iseq.argc)
87
89
  _, level, block_loc = location.label.match(BLOCK_LABL_REGEXP).to_a
88
- [level || "", block_loc, args]
90
+ [level || "", block_loc]
89
91
  end
90
92
 
91
93
  def method_identifier
92
94
  return unless frame_type == :method
93
- args = parameters_info(iseq.argc)
94
- ci = "#{klass_sig}#{callee}"
95
- [ci, args]
95
+ "#{klass_sig}#{callee}"
96
96
  end
97
97
 
98
98
  def c_identifier
@@ -106,7 +106,11 @@ module DEBUGGER__
106
106
  end
107
107
 
108
108
  def callee
109
- self._callee ||= self.binding&.eval('__callee__')
109
+ self._callee ||= begin
110
+ self.binding&.eval('__callee__')
111
+ rescue NameError # BasicObject
112
+ nil
113
+ end
110
114
  end
111
115
 
112
116
  def return_str
@@ -115,6 +119,11 @@ module DEBUGGER__
115
119
  end
116
120
  end
117
121
 
122
+ def matchable_location
123
+ # realpath can sometimes be nil so we can't use it here
124
+ "#{path}:#{location.lineno}"
125
+ end
126
+
118
127
  def location_str
119
128
  "#{pretty_path}:#{location.lineno}"
120
129
  end
@@ -123,7 +132,6 @@ module DEBUGGER__
123
132
  if b = self.dupped_binding
124
133
  b
125
134
  else
126
- b = TOPLEVEL_BINDING unless b = self.binding
127
135
  b = self.binding || TOPLEVEL_BINDING
128
136
  self.dupped_binding = b.dup
129
137
  end
@@ -139,9 +147,18 @@ module DEBUGGER__
139
147
  end
140
148
  end
141
149
 
142
- private
150
+ def parameters_info
151
+ vars = iseq.parameters_symbols
152
+ vars.map{|var|
153
+ begin
154
+ { name: var, value: DEBUGGER__.safe_inspect(local_variable_get(var), short: true) }
155
+ rescue NameError, TypeError
156
+ nil
157
+ end
158
+ }.compact
159
+ end
143
160
 
144
- def get_singleton_class obj
161
+ private def get_singleton_class obj
145
162
  obj.singleton_class # TODO: don't use it
146
163
  rescue TypeError
147
164
  nil
@@ -151,18 +168,7 @@ module DEBUGGER__
151
168
  local_variables[var]
152
169
  end
153
170
 
154
- def parameters_info(argc)
155
- vars = iseq.locals[0...argc]
156
- vars.map{|var|
157
- begin
158
- { name: var, value: DEBUGGER__.safe_inspect(local_variable_get(var), short: true) }
159
- rescue NameError, TypeError
160
- nil
161
- end
162
- }.compact
163
- end
164
-
165
- def klass_sig
171
+ private def klass_sig
166
172
  if self.class == get_singleton_class(self.self)
167
173
  "#{self.self}."
168
174
  else
data/lib/debug/local.rb CHANGED
@@ -98,7 +98,7 @@ module DEBUGGER__
98
98
  # only check child process from its parent
99
99
  begin
100
100
  # wait for all child processes to keep terminal
101
- loop{ Process.waitpid }
101
+ Process.waitpid
102
102
  rescue Errno::ESRCH, Errno::ECHILD
103
103
  end
104
104
  end
data/lib/debug/server.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'socket'
4
+ require 'etc'
4
5
  require_relative 'config'
5
6
  require_relative 'version'
6
7
 
@@ -19,8 +20,8 @@ module DEBUGGER__
19
20
  @session = nil
20
21
  end
21
22
 
22
- class Terminate < StandardError
23
- end
23
+ class Terminate < StandardError; end
24
+ class GreetingError < StandardError; end
24
25
 
25
26
  def deactivate
26
27
  @reader_thread.raise Terminate
@@ -47,10 +48,13 @@ module DEBUGGER__
47
48
 
48
49
  accept do |server, already_connected: false|
49
50
  DEBUGGER__.warn "Connected."
51
+ greeting_done = false
52
+ @need_pause_at_first = true
50
53
 
51
54
  @accept_m.synchronize{
52
55
  @sock = server
53
56
  greeting
57
+ greeting_done = true
54
58
 
55
59
  @accept_cv.signal
56
60
 
@@ -65,17 +69,21 @@ module DEBUGGER__
65
69
  } unless already_connected
66
70
 
67
71
  setup_interrupt do
68
- pause unless already_connected
72
+ pause if !already_connected && @need_pause_at_first
69
73
  process
70
74
  end
71
75
 
76
+ rescue GreetingError => e
77
+ DEBUGGER__.warn "GreetingError: #{e.message}"
78
+ next
72
79
  rescue Terminate
73
80
  raise # should catch at outer scope
74
81
  rescue => e
75
82
  DEBUGGER__.warn "ReaderThreadError: #{e}"
76
83
  pp e.backtrace
77
84
  ensure
78
- cleanup_reader
85
+ DEBUGGER__.warn "Disconnected."
86
+ cleanup_reader if greeting_done
79
87
  end # accept
80
88
 
81
89
  rescue Terminate
@@ -84,7 +92,7 @@ module DEBUGGER__
84
92
  end
85
93
 
86
94
  def cleanup_reader
87
- DEBUGGER__.warn "Disconnected."
95
+ @sock.close if @sock
88
96
  @sock = nil
89
97
  @q_msg.close
90
98
  @q_msg = nil
@@ -92,21 +100,53 @@ module DEBUGGER__
92
100
  @q_ans = nil
93
101
  end
94
102
 
103
+ def check_cookie c
104
+ cookie = CONFIG[:cookie]
105
+ if cookie && cookie != c
106
+ raise GreetingError, "Cookie mismatch (#{$2.inspect} was sent)"
107
+ end
108
+ end
109
+
110
+ def parse_option params
111
+ case params.strip
112
+ when /width:\s+(\d+)/
113
+ @width = $1.to_i
114
+ parse_option $~.post_match
115
+ when /cookie:\s+(\S+)/
116
+ check_cookie $1 if $1 != '-'
117
+ parse_option $~.post_match
118
+ when /nonstop: (true|false)/
119
+ @need_pause_at_first = false if $1 == 'true'
120
+ parse_option $~.post_match
121
+ when /(.+):(.+)/
122
+ raise GreetingError, "Unkown option: #{params}"
123
+ else
124
+ # OK
125
+ end
126
+ end
127
+
95
128
  def greeting
96
129
  case g = @sock.gets
97
- when /^version:\s+(.+)\s+width: (\d+) cookie:\s+(.*)$/
98
- v, w, c = $1, $2, $3
130
+ when /^info cookie:\s+(.*)$/
131
+ check_cookie $1
132
+ @sock.puts "PID: #{Process.pid}, $0: #{$0}"
133
+ @sock.puts "debug #{VERSION} on #{RUBY_DESCRIPTION}"
134
+ @sock.puts "uname: #{Etc.uname.inspect}"
135
+ @sock.close
136
+ raise GreetingError, "HEAD request"
137
+
138
+ when /^version:\s+(\S+)\s+(.+)$/
139
+ v, params = $1, $2
140
+
99
141
  # TODO: protocol version
100
142
  if v != VERSION
101
- raise "Incompatible version (#{VERSION} client:#{$1})"
102
- end
103
-
104
- cookie = CONFIG[:cookie]
105
- if cookie && cookie != c
106
- raise "Cookie mismatch (#{$2.inspect} was sent)"
143
+ raise GreetingError, "Incompatible version (server:#{VERSION} and client:#{$1})"
107
144
  end
145
+ parse_option(params)
108
146
 
109
- @width = w.to_i
147
+ puts "DEBUGGER (client): Connected. PID:#{Process.pid}, $0:#{$0}"
148
+ puts "DEBUGGER (client): Type `Ctrl-C` to enter the debug console." unless @need_pause_at_first
149
+ puts
110
150
 
111
151
  when /^Content-Length: (\d+)/
112
152
  require_relative 'server_dap'
@@ -114,18 +154,21 @@ module DEBUGGER__
114
154
  raise unless @sock.read(2) == "\r\n"
115
155
  self.extend(UI_DAP)
116
156
  @repl = false
157
+ @need_pause_at_first = false
117
158
  dap_setup @sock.read($1.to_i)
118
- when /^GET \/ HTTP\/1.1/
159
+
160
+ when /^GET \/.* HTTP\/1.1/
119
161
  require_relative 'server_cdp'
120
162
 
121
163
  self.extend(UI_CDP)
122
164
  @repl = false
165
+ @need_pause_at_first = false
123
166
  CONFIG.set_config no_color: true
124
167
 
125
168
  @ws_server = UI_CDP::WebSocketServer.new(@sock)
126
169
  @ws_server.handshake
127
170
  else
128
- raise "Greeting message error: #{g}"
171
+ raise GreetingError, "Unknown greeting message: #{g}"
129
172
  end
130
173
  end
131
174
 
@@ -145,6 +188,7 @@ module DEBUGGER__
145
188
  }
146
189
  end
147
190
 
191
+ return unless line
148
192
  next if line == :can_not_read
149
193
 
150
194
  case line
@@ -168,7 +212,7 @@ module DEBUGGER__
168
212
  raise "pid:#{Process.pid} but get #{line}"
169
213
  end
170
214
  else
171
- STDERR.puts "unsupported: #{line}"
215
+ STDERR.puts "unsupported: #{line.inspect}"
172
216
  exit!
173
217
  end
174
218
  end
@@ -290,7 +334,9 @@ module DEBUGGER__
290
334
  end
291
335
 
292
336
  def readline prompt
293
- input = (sock do |s|
337
+ input = (sock(skip: CONFIG[:skip_bp]) do |s|
338
+ next unless s
339
+
294
340
  if @repl
295
341
  raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
296
342
  line = "input #{Process.pid}"
@@ -325,18 +371,28 @@ module DEBUGGER__
325
371
  def after_fork_parent
326
372
  # do nothing
327
373
  end
374
+
375
+ def vscode_setup debug_port
376
+ require_relative 'server_dap'
377
+ UI_DAP.setup debug_port
378
+ end
328
379
  end
329
380
 
330
381
  class UI_TcpServer < UI_ServerBase
331
382
  def initialize host: nil, port: nil
332
- @addr = nil
333
- @host = host || CONFIG[:host] || '127.0.0.1'
334
- @port = port || begin
335
- port_str = CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
336
- if /\A\d+\z/ !~ port_str
337
- raise "Specify digits for port number"
338
- else
383
+ @local_addr = nil
384
+ @host = host || CONFIG[:host]
385
+ @port_save_file = nil
386
+ @port = begin
387
+ port_str = (port && port.to_s) || CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
388
+ case port_str
389
+ when /\A\d+\z/
339
390
  port_str.to_i
391
+ when /\A(\d+):(.+)\z/
392
+ @port_save_file = $2
393
+ $1.to_i
394
+ else
395
+ raise "Specify digits for port number"
340
396
  end
341
397
  end
342
398
 
@@ -346,12 +402,12 @@ module DEBUGGER__
346
402
  def chrome_setup
347
403
  require_relative 'server_cdp'
348
404
 
349
- unless @chrome_pid = UI_CDP.setup_chrome(@addr)
350
- DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome'
405
+ unless @chrome_pid = UI_CDP.setup_chrome(@local_addr.inspect_sockaddr)
406
+ DEBUGGER__.warn <<~EOS
351
407
  With Chrome browser, type the following URL in the address-bar:
352
-
353
- devtools://devtools/bundled/inspector.html?ws=#{@addr}
354
-
408
+
409
+ devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=#{@local_addr.inspect_sockaddr}/#{SecureRandom.uuid}
410
+
355
411
  EOS
356
412
  end
357
413
  end
@@ -362,18 +418,28 @@ module DEBUGGER__
362
418
 
363
419
  begin
364
420
  Socket.tcp_server_sockets @host, @port do |socks|
365
- @addr = socks[0].local_address.inspect_sockaddr # Change this part if `socks` are multiple.
421
+ @local_addr = socks.first.local_address # Change this part if `socks` are multiple.
366
422
  rdbg = File.expand_path('../../exe/rdbg', __dir__)
423
+ DEBUGGER__.warn "Debugger can attach via TCP/IP (#{@local_addr.inspect_sockaddr})"
424
+
425
+ if @port_save_file
426
+ File.write(@port_save_file, "#{socks[0].local_address.ip_port.to_s}\n")
427
+ DEBUGGER__.warn "Port is saved into #{@port_save_file}"
428
+ end
367
429
 
368
- DEBUGGER__.warn "Debugger can attach via TCP/IP (#{@addr})"
369
430
  DEBUGGER__.info <<~EOS
370
431
  With rdbg, use the following command line:
371
432
  #
372
- # #{rdbg} --attach #{@addr.split(':').join(' ')}
433
+ # #{rdbg} --attach #{@local_addr.ip_address} #{@local_addr.ip_port}
373
434
  #
374
435
  EOS
375
436
 
376
- chrome_setup if CONFIG[:open_frontend] == 'chrome'
437
+ case CONFIG[:open_frontend]
438
+ when 'chrome'
439
+ chrome_setup
440
+ when 'vscode'
441
+ vscode_setup @local_addr.inspect_sockaddr
442
+ end
377
443
 
378
444
  Socket.accept_loop(socks) do |sock, client|
379
445
  @client_addr = client
@@ -397,6 +463,10 @@ module DEBUGGER__
397
463
  end
398
464
  ensure
399
465
  @sock_for_fork = nil
466
+
467
+ if @port_save_file && File.exist?(@port_save_file)
468
+ File.unlink(@port_save_file)
469
+ end
400
470
  end
401
471
  end
402
472
 
@@ -409,11 +479,6 @@ module DEBUGGER__
409
479
  super()
410
480
  end
411
481
 
412
- def vscode_setup
413
- require_relative 'server_dap'
414
- UI_DAP.setup @sock_path
415
- end
416
-
417
482
  def accept
418
483
  super # for fork
419
484
 
@@ -426,7 +491,7 @@ module DEBUGGER__
426
491
  end
427
492
 
428
493
  ::DEBUGGER__.warn "Debugger can attach via UNIX domain socket (#{@sock_path})"
429
- vscode_setup if CONFIG[:open_frontend] == 'vscode'
494
+ vscode_setup @sock_path if CONFIG[:open_frontend] == 'vscode'
430
495
 
431
496
  begin
432
497
  Socket.unix_server_loop @sock_path do |sock, client|