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.
@@ -1,10 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'irb/completion'
5
+ require 'tmpdir'
6
+ require 'fileutils'
4
7
 
5
8
  module DEBUGGER__
6
9
  module UI_DAP
7
- SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
10
+ SHOW_PROTOCOL = ENV['DEBUG_DAP_SHOW_PROTOCOL'] == '1' || ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
11
+
12
+ def self.setup debug_port
13
+ if File.directory? '.vscode'
14
+ dir = Dir.pwd
15
+ else
16
+ dir = Dir.mktmpdir("ruby-debug-vscode-")
17
+ tempdir = true
18
+ end
19
+
20
+ at_exit do
21
+ CONFIG[:skip_path] = [//] # skip all
22
+ FileUtils.rm_rf dir if tempdir
23
+ end
24
+
25
+ key = rand.to_s
26
+
27
+ Dir.chdir(dir) do
28
+ Dir.mkdir('.vscode') if tempdir
29
+
30
+ # vscode-rdbg 0.0.9 or later is needed
31
+ open('.vscode/rdbg_autoattach.json', 'w') do |f|
32
+ f.puts JSON.pretty_generate({
33
+ type: "rdbg",
34
+ name: "Attach with rdbg",
35
+ request: "attach",
36
+ rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
37
+ debugPort: debug_port,
38
+ localfs: true,
39
+ autoAttach: key,
40
+ })
41
+ end
42
+ end
43
+
44
+ cmds = ['code', "#{dir}/"]
45
+ cmdline = cmds.join(' ')
46
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/"
47
+
48
+ STDERR.puts "Launching: #{cmdline}"
49
+ env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
50
+ env['RUBY_DEBUG_AUTOATTACH'] = key
51
+
52
+ unless system(env, *cmds)
53
+ DEBUGGER__.warn <<~MESSAGE
54
+ Can not invoke the command.
55
+ Use the command-line on your terminal (with modification if you need).
56
+
57
+ #{cmdline}
58
+
59
+ If your application is running on a SSH remote host, please try:
60
+
61
+ #{ssh_cmdline}
62
+
63
+ MESSAGE
64
+ end
65
+ end
8
66
 
9
67
  def show_protocol dir, msg
10
68
  if SHOW_PROTOCOL
@@ -12,9 +70,20 @@ module DEBUGGER__
12
70
  end
13
71
  end
14
72
 
73
+ @local_fs = false
74
+
75
+ def self.local_fs
76
+ @local_fs
77
+ end
78
+
79
+ def self.local_fs_set
80
+ @local_fs = true
81
+ end
82
+
15
83
  def dap_setup bytes
16
84
  CONFIG.set_config no_color: true
17
85
  @seq = 0
86
+ UI_DAP.local_fs_set if self.kind_of?(UI_UnixDomainServer)
18
87
 
19
88
  show_protocol :>, bytes
20
89
  req = JSON.load(bytes)
@@ -44,11 +113,12 @@ module DEBUGGER__
44
113
  ],
45
114
  supportsExceptionFilterOptions: true,
46
115
  supportsStepBack: true,
116
+ supportsEvaluateForHovers: true,
117
+ supportsCompletionsRequest: true,
47
118
 
48
119
  ## Will be supported
49
120
  # supportsExceptionOptions: true,
50
121
  # supportsHitConditionalBreakpoints:
51
- # supportsEvaluateForHovers:
52
122
  # supportsSetVariable: true,
53
123
  # supportSuspendDebuggee:
54
124
  # supportsLogPoints:
@@ -58,7 +128,6 @@ module DEBUGGER__
58
128
 
59
129
  ## Possible?
60
130
  # supportsRestartFrame:
61
- # supportsCompletionsRequest:
62
131
  # completionTriggerCharacters:
63
132
  # supportsModulesRequest:
64
133
  # additionalModuleColumns:
@@ -86,23 +155,23 @@ module DEBUGGER__
86
155
  def send **kw
87
156
  kw[:seq] = @seq += 1
88
157
  str = JSON.dump(kw)
158
+ @sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
89
159
  show_protocol '<', str
90
- @sock.write "Content-Length: #{str.size}\r\n\r\n#{str}"
91
160
  end
92
161
 
93
- def send_response req, success: true, **kw
162
+ def send_response req, success: true, message: nil, **kw
94
163
  if kw.empty?
95
164
  send type: 'response',
96
165
  command: req['command'],
97
166
  request_seq: req['seq'],
98
167
  success: success,
99
- message: success ? 'Success' : 'Failed'
168
+ message: message || (success ? 'Success' : 'Failed')
100
169
  else
101
170
  send type: 'response',
102
171
  command: req['command'],
103
172
  request_seq: req['seq'],
104
173
  success: success,
105
- message: success ? 'Success' : 'Failed',
174
+ message: message || (success ? 'Success' : 'Failed'),
106
175
  body: kw
107
176
  end
108
177
  end
@@ -119,29 +188,27 @@ module DEBUGGER__
119
188
  end
120
189
 
121
190
  def recv_request
122
- begin
123
- r = IO.select([@sock])
124
-
125
- @session.process_group.sync do
126
- raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
127
-
128
- case header = @sock.gets
129
- when /Content-Length: (\d+)/
130
- b = @sock.read(2)
131
- raise b.inspect unless b == "\r\n"
132
-
133
- l = @sock.read(s = $1.to_i)
134
- show_protocol :>, l
135
- JSON.load(l)
136
- when nil
137
- nil
138
- else
139
- raise "unrecognized line: #{l} (#{l.size} bytes)"
140
- end
191
+ r = IO.select([@sock])
192
+
193
+ @session.process_group.sync do
194
+ raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
195
+
196
+ case header = @sock.gets
197
+ when /Content-Length: (\d+)/
198
+ b = @sock.read(2)
199
+ raise b.inspect unless b == "\r\n"
200
+
201
+ l = @sock.read(s = $1.to_i)
202
+ show_protocol :>, l
203
+ JSON.load(l)
204
+ when nil
205
+ nil
206
+ else
207
+ raise "unrecognized line: #{l} (#{l.size} bytes)"
141
208
  end
142
- rescue RetryBecauseCantRead
143
- retry
144
209
  end
210
+ rescue RetryBecauseCantRead
211
+ retry
145
212
  end
146
213
 
147
214
  def process
@@ -155,15 +222,18 @@ module DEBUGGER__
155
222
  when 'launch'
156
223
  send_response req
157
224
  @is_attach = false
225
+ UI_DAP.local_fs_set if req.dig('arguments', 'localfs')
158
226
  when 'attach'
159
227
  send_response req
160
- Process.kill(:SIGURG, Process.pid)
228
+ Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
161
229
  @is_attach = true
230
+ UI_DAP.local_fs_set if req.dig('arguments', 'localfs')
162
231
  when 'setBreakpoints'
163
232
  path = args.dig('source', 'path')
164
- bp_args = args['breakpoints']
233
+ SESSION.clear_line_breakpoints path
234
+
165
235
  bps = []
166
- bp_args.each{|bp|
236
+ args['breakpoints'].each{|bp|
167
237
  line = bp['line']
168
238
  if cond = bp['condition']
169
239
  bps << SESSION.add_line_breakpoint(path, line, cond: cond)
@@ -222,20 +292,41 @@ module DEBUGGER__
222
292
  @q_msg << 'c'
223
293
  send_response req, allThreadsContinued: true
224
294
  when 'next'
225
- @q_msg << 'n'
226
- send_response req
295
+ begin
296
+ @session.check_postmortem
297
+ @q_msg << 'n'
298
+ send_response req
299
+ rescue PostmortemError
300
+ send_response req,
301
+ success: false, message: 'postmortem mode',
302
+ result: "'Next' is not supported while postmortem mode"
303
+ end
227
304
  when 'stepIn'
228
- @q_msg << 's'
229
- send_response req
305
+ begin
306
+ @session.check_postmortem
307
+ @q_msg << 's'
308
+ send_response req
309
+ rescue PostmortemError
310
+ send_response req,
311
+ success: false, message: 'postmortem mode',
312
+ result: "'stepIn' is not supported while postmortem mode"
313
+ end
230
314
  when 'stepOut'
231
- @q_msg << 'fin'
232
- send_response req
315
+ begin
316
+ @session.check_postmortem
317
+ @q_msg << 'fin'
318
+ send_response req
319
+ rescue PostmortemError
320
+ send_response req,
321
+ success: false, message: 'postmortem mode',
322
+ result: "'stepOut' is not supported while postmortem mode"
323
+ end
233
324
  when 'terminate'
234
325
  send_response req
235
326
  exit
236
327
  when 'pause'
237
328
  send_response req
238
- Process.kill(:SIGURG, Process.pid)
329
+ Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
239
330
  when 'reverseContinue'
240
331
  send_response req,
241
332
  success: false, message: 'cancelled',
@@ -255,13 +346,16 @@ module DEBUGGER__
255
346
  'scopes',
256
347
  'variables',
257
348
  'evaluate',
258
- 'source'
349
+ 'source',
350
+ 'completions'
259
351
  @q_msg << req
260
352
 
261
353
  else
262
354
  raise "Unknown request: #{req.inspect}"
263
355
  end
264
356
  end
357
+ ensure
358
+ send_event :terminated unless @sock.closed?
265
359
  end
266
360
 
267
361
  ## called by the SESSION thread
@@ -361,7 +455,6 @@ module DEBUGGER__
361
455
  case ref[0]
362
456
  when :globals
363
457
  vars = global_variables.map do |name|
364
- File.write('/tmp/x', "#{name}\n")
365
458
  gv = 'Not implemented yet...'
366
459
  {
367
460
  name: name,
@@ -402,11 +495,13 @@ module DEBUGGER__
402
495
  end
403
496
  when 'evaluate'
404
497
  frame_id = req.dig('arguments', 'frameId')
498
+ context = req.dig('arguments', 'context')
499
+
405
500
  if @frame_map[frame_id]
406
501
  tid, fid = @frame_map[frame_id]
407
502
  expr = req.dig('arguments', 'expression')
408
503
  if tc = find_waiting_tc(tid)
409
- tc << [:dap, :evaluate, req, fid, expr]
504
+ tc << [:dap, :evaluate, req, fid, expr, context]
410
505
  else
411
506
  fail_response req
412
507
  end
@@ -416,11 +511,26 @@ module DEBUGGER__
416
511
  when 'source'
417
512
  ref = req.dig('arguments', 'sourceReference')
418
513
  if src = @src_map[ref]
419
- @ui.respond req, content: src.join
514
+ @ui.respond req, content: src.join("\n")
420
515
  else
421
516
  fail_response req, message: 'not found...'
422
517
  end
423
518
  return :retry
519
+
520
+ when 'completions'
521
+ frame_id = req.dig('arguments', 'frameId')
522
+ tid, fid = @frame_map[frame_id]
523
+
524
+ if tc = find_waiting_tc(tid)
525
+ text = req.dig('arguments', 'text')
526
+ line = req.dig('arguments', 'line')
527
+ if col = req.dig('arguments', 'column')
528
+ text = text.split(/\n/)[line.to_i - 1][0...(col.to_i - 1)]
529
+ end
530
+ tc << [:dap, :completions, req, fid, text]
531
+ else
532
+ fail_response req
533
+ end
424
534
  else
425
535
  raise "Unknown DAP request: #{req.inspect}"
426
536
  end
@@ -458,8 +568,15 @@ module DEBUGGER__
458
568
  register_vars result[:variables], tid
459
569
  @ui.respond req, result
460
570
  when :evaluate
461
- tid = result.delete :tid
462
- register_var result, tid
571
+ message = result.delete :message
572
+ if message
573
+ @ui.respond req, success: false, message: message
574
+ else
575
+ tid = result.delete :tid
576
+ register_var result, tid
577
+ @ui.respond req, result
578
+ end
579
+ when :completions
463
580
  @ui.respond req, result
464
581
  else
465
582
  raise "unsupported: #{args.inspect}"
@@ -491,9 +608,13 @@ module DEBUGGER__
491
608
  case type
492
609
  when :backtrace
493
610
  event! :dap_result, :backtrace, req, {
494
- stackFrames: @target_frames.map.{|frame|
611
+ stackFrames: @target_frames.map{|frame|
495
612
  path = frame.realpath || frame.path
496
- ref = frame.file_lines unless path && File.exist?(path)
613
+ source_name = path ? File.basename(path) : frame.location.to_s
614
+
615
+ if !UI_DAP.local_fs || !(path && File.exist?(path))
616
+ ref = frame.file_lines
617
+ end
497
618
 
498
619
  {
499
620
  # id: ??? # filled by SESSION
@@ -501,7 +622,7 @@ module DEBUGGER__
501
622
  line: frame.location.lineno,
502
623
  column: 1,
503
624
  source: {
504
- name: File.basename(frame.path),
625
+ name: source_name,
505
626
  path: path,
506
627
  sourceReference: ref,
507
628
  },
@@ -510,7 +631,7 @@ module DEBUGGER__
510
631
  }
511
632
  when :scopes
512
633
  fid = args.shift
513
- frame = @target_frames[fid]
634
+ frame = get_frame(fid)
514
635
 
515
636
  lnum =
516
637
  if frame.binding
@@ -538,26 +659,12 @@ module DEBUGGER__
538
659
  }]
539
660
  when :scope
540
661
  fid = args.shift
541
- frame = @target_frames[fid]
542
- if b = frame.binding
543
- vars = b.local_variables.map{|name|
544
- v = b.local_variable_get(name)
545
- variable(name, v)
546
- }
547
- vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
548
- vars.unshift variable('%return', frame.return_value) if frame.has_return_value
549
- vars.unshift variable('%self', b.receiver)
550
- elsif lvars = frame.local_variables
551
- vars = lvars.map{|var, val|
552
- variable(var, val)
553
- }
554
- else
555
- vars = [variable('%self', frame.self)]
556
- vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
557
- vars.push variable('%return', frame.return_value) if frame.has_return_value
662
+ frame = get_frame(fid)
663
+ vars = collect_locals(frame).map do |var, val|
664
+ variable(var, val)
558
665
  end
559
- event! :dap_result, :scope, req, variables: vars, tid: self.id
560
666
 
667
+ event! :dap_result, :scope, req, variables: vars, tid: self.id
561
668
  when :variable
562
669
  vid = args.shift
563
670
  obj = @var_map[vid]
@@ -575,7 +682,7 @@ module DEBUGGER__
575
682
  case obj
576
683
  when Hash
577
684
  vars = obj.map{|k, v|
578
- variable(DEBUGGER__.short_inspect(k), v)
685
+ variable(DEBUGGER__.safe_inspect(k), v,)
579
686
  }
580
687
  when Struct
581
688
  vars = obj.members.map{|m|
@@ -607,32 +714,121 @@ module DEBUGGER__
607
714
  event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
608
715
 
609
716
  when :evaluate
610
- fid, expr = args
611
- frame = @target_frames[fid]
717
+ fid, expr, context = args
718
+ frame = get_frame(fid)
719
+ message = nil
612
720
 
613
- if frame && (b = frame.binding)
614
- begin
615
- result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
616
- rescue Exception => e
617
- result = e
721
+ if frame && (b = frame.eval_binding)
722
+ special_local_variables frame do |name, var|
723
+ b.local_variable_set(name, var) if /\%/ !~ name
724
+ end
725
+
726
+ case context
727
+ when 'repl', 'watch'
728
+ begin
729
+ result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
730
+ rescue Exception => e
731
+ result = e
732
+ end
733
+
734
+ when 'hover'
735
+ case expr
736
+ when /\A\@\S/
737
+ begin
738
+ result = b.receiver.instance_variable_get(expr)
739
+ rescue NameError
740
+ message = "Error: Not defined instance variable: #{expr.inspect}"
741
+ end
742
+ when /\A\$\S/
743
+ global_variables.each{|gvar|
744
+ if gvar.to_s == expr
745
+ result = eval(gvar.to_s)
746
+ break false
747
+ end
748
+ } and (message = "Error: Not defined global variable: #{expr.inspect}")
749
+ when /(\A((::[A-Z]|[A-Z])\w*)+)/
750
+ unless result = search_const(b, $1)
751
+ message = "Error: Not defined constants: #{expr.inspect}"
752
+ end
753
+ else
754
+ begin
755
+ result = b.local_variable_get(expr)
756
+ rescue NameError
757
+ # try to check method
758
+ if b.receiver.respond_to? expr, include_all: true
759
+ result = b.receiver.method(expr)
760
+ else
761
+ message = "Error: Can not evaluate: #{expr.inspect}"
762
+ end
763
+ end
764
+ end
765
+ else
766
+ message = "Error: unknown context: #{context}"
618
767
  end
619
768
  else
620
- result = 'can not evaluate on this frame...'
769
+ result = 'Error: Can not evaluate on this frame'
770
+ end
771
+
772
+ event! :dap_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
773
+
774
+ when :completions
775
+ fid, text = args
776
+ frame = get_frame(fid)
777
+
778
+ if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last
779
+ words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
621
780
  end
622
- event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
781
+
782
+ event! :dap_result, :completions, req, targets: (words || []).map{|phrase|
783
+ if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
784
+ w = $1
785
+ else
786
+ w = phrase
787
+ end
788
+
789
+ begin
790
+ v = b.local_variable_get(w)
791
+ phrase += " (variable:#{DEBUGGER__.safe_inspect(v)})"
792
+ rescue NameError
793
+ end
794
+
795
+ {
796
+ label: phrase,
797
+ text: w,
798
+ }
799
+ }
800
+
623
801
  else
624
802
  raise "Unknown req: #{args.inspect}"
625
803
  end
626
804
  end
627
805
 
806
+ def search_const b, expr
807
+ cs = expr.delete_prefix('::').split('::')
808
+ [Object, *b.eval('Module.nesting')].reverse_each{|mod|
809
+ if cs.all?{|c|
810
+ if mod.const_defined?(c)
811
+ mod = mod.const_get(c)
812
+ else
813
+ false
814
+ end
815
+ }
816
+ # if-body
817
+ return mod
818
+ end
819
+ }
820
+ false
821
+ end
822
+
628
823
  def evaluate_result r
629
824
  v = variable nil, r
630
- v.delete(:name)
631
- v[:result] = DEBUGGER__.short_inspect(r)
825
+ v.delete :name
826
+ v.delete :value
827
+ v[:result] = DEBUGGER__.safe_inspect(r)
632
828
  v
633
829
  end
634
830
 
635
- def variable_ name, obj, indexedVariables: 0, namedVariables: 0, use_short: true
831
+ def variable_ name, obj, indexedVariables: 0, namedVariables: 0
636
832
  if indexedVariables > 0 || namedVariables > 0
637
833
  vid = @var_map.size + 1
638
834
  @var_map[vid] = obj
@@ -643,7 +839,7 @@ module DEBUGGER__
643
839
  ivnum = obj.instance_variables.size
644
840
 
645
841
  { name: name,
646
- value: DEBUGGER__.short_inspect(obj, use_short),
842
+ value: DEBUGGER__.safe_inspect(obj),
647
843
  type: obj.class.name || obj.class.to_s,
648
844
  variablesReference: vid,
649
845
  indexedVariables: indexedVariables,
@@ -658,7 +854,7 @@ module DEBUGGER__
658
854
  when Hash
659
855
  variable_ name, obj, namedVariables: obj.size
660
856
  when String
661
- variable_ name, obj, use_short: false, namedVariables: 3 # #to_str, #length, #encoding
857
+ variable_ name, obj, namedVariables: 3 # #to_str, #length, #encoding
662
858
  when Struct
663
859
  variable_ name, obj, namedVariables: obj.size
664
860
  when Class, Module