debug 1.3.1 → 1.4.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,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'irb/completion'
5
+ require 'tmpdir'
6
+ require 'json'
7
+ require 'fileutils'
4
8
 
5
9
  module DEBUGGER__
6
10
  module UI_DAP
7
- SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
11
+ SHOW_PROTOCOL = ENV['DEBUG_DAP_SHOW_PROTOCOL'] == '1' || ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
12
+
13
+ def self.setup sock_path
14
+ dir = Dir.mktmpdir("ruby-debug-vscode-")
15
+ at_exit{
16
+ CONFIG[:skip_path] = [//] # skip all
17
+ FileUtils.rm_rf dir
18
+ }
19
+ Dir.chdir(dir) do
20
+ Dir.mkdir('.vscode')
21
+ open('README.rb', 'w'){|f|
22
+ f.puts <<~MSG
23
+ # Wait for starting the attaching to the Ruby process
24
+ # This file will be removed at the end of the debuggee process.
25
+ #
26
+ # Note that vscode-rdbg extension is needed. Please install if you don't have.
27
+ MSG
28
+ }
29
+ open('.vscode/launch.json', 'w'){|f|
30
+ f.puts JSON.pretty_generate({
31
+ version: '0.2.0',
32
+ configurations: [
33
+ {
34
+ type: "rdbg",
35
+ name: "Attach with rdbg",
36
+ request: "attach",
37
+ rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
38
+ debugPort: sock_path,
39
+ autoAttach: true,
40
+ }
41
+ ]
42
+ })
43
+ }
44
+ end
45
+
46
+ cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
47
+ cmdline = cmds.join(' ')
48
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"
49
+
50
+ STDERR.puts "Launching: #{cmdline}"
51
+ env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
52
+
53
+ unless system(env, *cmds)
54
+ DEBUGGER__.warn <<~MESSAGE
55
+ Can not invoke the command.
56
+ Use the command-line on your terminal (with modification if you need).
57
+
58
+ #{cmdline}
59
+
60
+ If your application is running on a SSH remote host, please try:
61
+
62
+ #{ssh_cmdline}
63
+
64
+ MESSAGE
65
+ end
66
+ end
8
67
 
9
68
  def show_protocol dir, msg
10
69
  if SHOW_PROTOCOL
@@ -44,11 +103,12 @@ module DEBUGGER__
44
103
  ],
45
104
  supportsExceptionFilterOptions: true,
46
105
  supportsStepBack: true,
106
+ supportsEvaluateForHovers: true,
107
+ supportsCompletionsRequest: true,
47
108
 
48
109
  ## Will be supported
49
110
  # supportsExceptionOptions: true,
50
111
  # supportsHitConditionalBreakpoints:
51
- # supportsEvaluateForHovers:
52
112
  # supportsSetVariable: true,
53
113
  # supportSuspendDebuggee:
54
114
  # supportsLogPoints:
@@ -58,7 +118,6 @@ module DEBUGGER__
58
118
 
59
119
  ## Possible?
60
120
  # supportsRestartFrame:
61
- # supportsCompletionsRequest:
62
121
  # completionTriggerCharacters:
63
122
  # supportsModulesRequest:
64
123
  # additionalModuleColumns:
@@ -87,22 +146,22 @@ module DEBUGGER__
87
146
  kw[:seq] = @seq += 1
88
147
  str = JSON.dump(kw)
89
148
  show_protocol '<', str
90
- @sock.write "Content-Length: #{str.size}\r\n\r\n#{str}"
149
+ @sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
91
150
  end
92
151
 
93
- def send_response req, success: true, **kw
152
+ def send_response req, success: true, message: nil, **kw
94
153
  if kw.empty?
95
154
  send type: 'response',
96
155
  command: req['command'],
97
156
  request_seq: req['seq'],
98
157
  success: success,
99
- message: success ? 'Success' : 'Failed'
158
+ message: message || (success ? 'Success' : 'Failed')
100
159
  else
101
160
  send type: 'response',
102
161
  command: req['command'],
103
162
  request_seq: req['seq'],
104
163
  success: success,
105
- message: success ? 'Success' : 'Failed',
164
+ message: message || (success ? 'Success' : 'Failed'),
106
165
  body: kw
107
166
  end
108
167
  end
@@ -119,29 +178,27 @@ module DEBUGGER__
119
178
  end
120
179
 
121
180
  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
181
+ r = IO.select([@sock])
182
+
183
+ @session.process_group.sync do
184
+ raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
185
+
186
+ case header = @sock.gets
187
+ when /Content-Length: (\d+)/
188
+ b = @sock.read(2)
189
+ raise b.inspect unless b == "\r\n"
190
+
191
+ l = @sock.read(s = $1.to_i)
192
+ show_protocol :>, l
193
+ JSON.load(l)
194
+ when nil
195
+ nil
196
+ else
197
+ raise "unrecognized line: #{l} (#{l.size} bytes)"
141
198
  end
142
- rescue RetryBecauseCantRead
143
- retry
144
199
  end
200
+ rescue RetryBecauseCantRead
201
+ retry
145
202
  end
146
203
 
147
204
  def process
@@ -222,14 +279,35 @@ module DEBUGGER__
222
279
  @q_msg << 'c'
223
280
  send_response req, allThreadsContinued: true
224
281
  when 'next'
225
- @q_msg << 'n'
226
- send_response req
282
+ begin
283
+ @session.check_postmortem
284
+ @q_msg << 'n'
285
+ send_response req
286
+ rescue PostmortemError
287
+ send_response req,
288
+ success: false, message: 'postmortem mode',
289
+ result: "'Next' is not supported while postmortem mode"
290
+ end
227
291
  when 'stepIn'
228
- @q_msg << 's'
229
- send_response req
292
+ begin
293
+ @session.check_postmortem
294
+ @q_msg << 's'
295
+ send_response req
296
+ rescue PostmortemError
297
+ send_response req,
298
+ success: false, message: 'postmortem mode',
299
+ result: "'stepIn' is not supported while postmortem mode"
300
+ end
230
301
  when 'stepOut'
231
- @q_msg << 'fin'
232
- send_response req
302
+ begin
303
+ @session.check_postmortem
304
+ @q_msg << 'fin'
305
+ send_response req
306
+ rescue PostmortemError
307
+ send_response req,
308
+ success: false, message: 'postmortem mode',
309
+ result: "'stepOut' is not supported while postmortem mode"
310
+ end
233
311
  when 'terminate'
234
312
  send_response req
235
313
  exit
@@ -255,7 +333,8 @@ module DEBUGGER__
255
333
  'scopes',
256
334
  'variables',
257
335
  'evaluate',
258
- 'source'
336
+ 'source',
337
+ 'completions'
259
338
  @q_msg << req
260
339
 
261
340
  else
@@ -361,7 +440,6 @@ module DEBUGGER__
361
440
  case ref[0]
362
441
  when :globals
363
442
  vars = global_variables.map do |name|
364
- File.write('/tmp/x', "#{name}\n")
365
443
  gv = 'Not implemented yet...'
366
444
  {
367
445
  name: name,
@@ -402,11 +480,13 @@ module DEBUGGER__
402
480
  end
403
481
  when 'evaluate'
404
482
  frame_id = req.dig('arguments', 'frameId')
483
+ context = req.dig('arguments', 'context')
484
+
405
485
  if @frame_map[frame_id]
406
486
  tid, fid = @frame_map[frame_id]
407
487
  expr = req.dig('arguments', 'expression')
408
488
  if tc = find_waiting_tc(tid)
409
- tc << [:dap, :evaluate, req, fid, expr]
489
+ tc << [:dap, :evaluate, req, fid, expr, context]
410
490
  else
411
491
  fail_response req
412
492
  end
@@ -421,6 +501,21 @@ module DEBUGGER__
421
501
  fail_response req, message: 'not found...'
422
502
  end
423
503
  return :retry
504
+
505
+ when 'completions'
506
+ frame_id = req.dig('arguments', 'frameId')
507
+ tid, fid = @frame_map[frame_id]
508
+
509
+ if tc = find_waiting_tc(tid)
510
+ text = req.dig('arguments', 'text')
511
+ line = req.dig('arguments', 'line')
512
+ if col = req.dig('arguments', 'column')
513
+ text = text.split(/\n/)[line.to_i - 1][0...(col.to_i - 1)]
514
+ end
515
+ tc << [:dap, :completions, req, fid, text]
516
+ else
517
+ fail_response req
518
+ end
424
519
  else
425
520
  raise "Unknown DAP request: #{req.inspect}"
426
521
  end
@@ -458,8 +553,15 @@ module DEBUGGER__
458
553
  register_vars result[:variables], tid
459
554
  @ui.respond req, result
460
555
  when :evaluate
461
- tid = result.delete :tid
462
- register_var result, tid
556
+ message = result.delete :message
557
+ if message
558
+ @ui.respond req, success: false, message: message
559
+ else
560
+ tid = result.delete :tid
561
+ register_var result, tid
562
+ @ui.respond req, result
563
+ end
564
+ when :completions
463
565
  @ui.respond req, result
464
566
  else
465
567
  raise "unsupported: #{args.inspect}"
@@ -491,7 +593,7 @@ module DEBUGGER__
491
593
  case type
492
594
  when :backtrace
493
595
  event! :dap_result, :backtrace, req, {
494
- stackFrames: @target_frames.map.with_index{|frame, i|
596
+ stackFrames: @target_frames.map{|frame|
495
597
  path = frame.realpath || frame.path
496
598
  ref = frame.file_lines unless path && File.exist?(path)
497
599
 
@@ -544,8 +646,9 @@ module DEBUGGER__
544
646
  v = b.local_variable_get(name)
545
647
  variable(name, v)
546
648
  }
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
649
+ special_local_variables frame do |name, val|
650
+ vars.unshift variable(name, val)
651
+ end
549
652
  vars.unshift variable('%self', b.receiver)
550
653
  elsif lvars = frame.local_variables
551
654
  vars = lvars.map{|var, val|
@@ -553,8 +656,9 @@ module DEBUGGER__
553
656
  }
554
657
  else
555
658
  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
659
+ special_local_variables frame do |name, val|
660
+ vars.push variable(name, val)
661
+ end
558
662
  end
559
663
  event! :dap_result, :scope, req, variables: vars, tid: self.id
560
664
 
@@ -575,7 +679,7 @@ module DEBUGGER__
575
679
  case obj
576
680
  when Hash
577
681
  vars = obj.map{|k, v|
578
- variable(DEBUGGER__.short_inspect(k), v)
682
+ variable(DEBUGGER__.safe_inspect(k), v,)
579
683
  }
580
684
  when Struct
581
685
  vars = obj.members.map{|m|
@@ -607,32 +711,127 @@ module DEBUGGER__
607
711
  event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
608
712
 
609
713
  when :evaluate
610
- fid, expr = args
714
+ fid, expr, context = args
611
715
  frame = @target_frames[fid]
716
+ message = nil
612
717
 
613
718
  if frame && (b = frame.binding)
614
- begin
615
- result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
616
- rescue Exception => e
617
- result = e
719
+ b = b.dup
720
+ special_local_variables current_frame do |name, var|
721
+ b.local_variable_set(name, var) if /\%/ !~ name
722
+ end
723
+
724
+ case context
725
+ when 'repl', 'watch'
726
+ begin
727
+ result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
728
+ rescue Exception => e
729
+ result = e
730
+ end
731
+
732
+ when 'hover'
733
+ case expr
734
+ when /\A\@\S/
735
+ begin
736
+ (r = b.receiver).instance_variable_defined?(expr) or raise(NameError)
737
+ result = r.instance_variable_get(expr)
738
+ rescue NameError
739
+ message = "Error: Not defined instance variable: #{expr.inspect}"
740
+ end
741
+ when /\A\$\S/
742
+ global_variables.each{|gvar|
743
+ if gvar.to_s == expr
744
+ result = eval(gvar.to_s)
745
+ break false
746
+ end
747
+ } and (message = "Error: Not defined global variable: #{expr.inspect}")
748
+ when /\A[A-Z]/
749
+ unless result = search_const(b, expr)
750
+ message = "Error: Not defined constants: #{expr.inspect}"
751
+ end
752
+ else
753
+ begin
754
+ # try to check local variables
755
+ b.local_variable_defined?(expr) or raise NameError
756
+ result = b.local_variable_get(expr)
757
+ rescue NameError
758
+ # try to check method
759
+ if b.receiver.respond_to? expr, include_all: true
760
+ result = b.receiver.method(expr)
761
+ else
762
+ message = "Error: Can not evaluate: #{expr.inspect}"
763
+ end
764
+ end
765
+ end
766
+ else
767
+ message = "Error: unknown context: #{context}"
618
768
  end
619
769
  else
620
- result = 'can not evaluate on this frame...'
770
+ result = 'Error: Can not evaluate on this frame'
621
771
  end
622
- event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
772
+
773
+ event! :dap_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
774
+
775
+ when :completions
776
+ fid, text = args
777
+ frame = @target_frames[fid]
778
+
779
+ if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last
780
+ words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
781
+ end
782
+
783
+ event! :dap_result, :completions, req, targets: (words || []).map{|phrase|
784
+ if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
785
+ w = $1
786
+ else
787
+ w = phrase
788
+ end
789
+
790
+ begin
791
+ if b&.local_variable_defined?(w)
792
+ v = b.local_variable_get(w)
793
+ phrase += " (variable:#{DEBUGGER__.safe_inspect(v)})"
794
+ end
795
+ rescue NameError
796
+ end
797
+
798
+ {
799
+ label: phrase,
800
+ text: w,
801
+ }
802
+ }
803
+
623
804
  else
624
805
  raise "Unknown req: #{args.inspect}"
625
806
  end
626
807
  end
627
808
 
809
+ def search_const b, expr
810
+ cs = expr.split('::')
811
+ [Object, *b.eval('Module.nesting')].reverse_each{|mod|
812
+ if cs.all?{|c|
813
+ if mod.const_defined?(c)
814
+ mod = mod.const_get(c)
815
+ else
816
+ false
817
+ end
818
+ }
819
+ # if-body
820
+ return mod
821
+ end
822
+ }
823
+ false
824
+ end
825
+
628
826
  def evaluate_result r
629
827
  v = variable nil, r
630
- v.delete(:name)
631
- v[:result] = DEBUGGER__.short_inspect(r)
828
+ v.delete :name
829
+ v.delete :value
830
+ v[:result] = DEBUGGER__.safe_inspect(r)
632
831
  v
633
832
  end
634
833
 
635
- def variable_ name, obj, indexedVariables: 0, namedVariables: 0, use_short: true
834
+ def variable_ name, obj, indexedVariables: 0, namedVariables: 0
636
835
  if indexedVariables > 0 || namedVariables > 0
637
836
  vid = @var_map.size + 1
638
837
  @var_map[vid] = obj
@@ -643,7 +842,7 @@ module DEBUGGER__
643
842
  ivnum = obj.instance_variables.size
644
843
 
645
844
  { name: name,
646
- value: DEBUGGER__.short_inspect(obj, use_short),
845
+ value: DEBUGGER__.safe_inspect(obj),
647
846
  type: obj.class.name || obj.class.to_s,
648
847
  variablesReference: vid,
649
848
  indexedVariables: indexedVariables,
@@ -658,7 +857,7 @@ module DEBUGGER__
658
857
  when Hash
659
858
  variable_ name, obj, namedVariables: obj.size
660
859
  when String
661
- variable_ name, obj, use_short: false, namedVariables: 3 # #to_str, #length, #encoding
860
+ variable_ name, obj, namedVariables: 3 # #to_str, #length, #encoding
662
861
  when Struct
663
862
  variable_ name, obj, namedVariables: obj.size
664
863
  when Class, Module