debug 1.3.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -229,7 +286,7 @@ module DEBUGGER__
229
286
  rescue PostmortemError
230
287
  send_response req,
231
288
  success: false, message: 'postmortem mode',
232
- result: "'Next' is not supported while postrmotem mode"
289
+ result: "'Next' is not supported while postmortem mode"
233
290
  end
234
291
  when 'stepIn'
235
292
  begin
@@ -239,7 +296,7 @@ module DEBUGGER__
239
296
  rescue PostmortemError
240
297
  send_response req,
241
298
  success: false, message: 'postmortem mode',
242
- result: "'stepIn' is not supported while postrmotem mode"
299
+ result: "'stepIn' is not supported while postmortem mode"
243
300
  end
244
301
  when 'stepOut'
245
302
  begin
@@ -249,7 +306,7 @@ module DEBUGGER__
249
306
  rescue PostmortemError
250
307
  send_response req,
251
308
  success: false, message: 'postmortem mode',
252
- result: "'stepOut' is not supported while postrmotem mode"
309
+ result: "'stepOut' is not supported while postmortem mode"
253
310
  end
254
311
  when 'terminate'
255
312
  send_response req
@@ -276,7 +333,8 @@ module DEBUGGER__
276
333
  'scopes',
277
334
  'variables',
278
335
  'evaluate',
279
- 'source'
336
+ 'source',
337
+ 'completions'
280
338
  @q_msg << req
281
339
 
282
340
  else
@@ -382,7 +440,6 @@ module DEBUGGER__
382
440
  case ref[0]
383
441
  when :globals
384
442
  vars = global_variables.map do |name|
385
- File.write('/tmp/x', "#{name}\n")
386
443
  gv = 'Not implemented yet...'
387
444
  {
388
445
  name: name,
@@ -423,11 +480,13 @@ module DEBUGGER__
423
480
  end
424
481
  when 'evaluate'
425
482
  frame_id = req.dig('arguments', 'frameId')
483
+ context = req.dig('arguments', 'context')
484
+
426
485
  if @frame_map[frame_id]
427
486
  tid, fid = @frame_map[frame_id]
428
487
  expr = req.dig('arguments', 'expression')
429
488
  if tc = find_waiting_tc(tid)
430
- tc << [:dap, :evaluate, req, fid, expr]
489
+ tc << [:dap, :evaluate, req, fid, expr, context]
431
490
  else
432
491
  fail_response req
433
492
  end
@@ -442,6 +501,21 @@ module DEBUGGER__
442
501
  fail_response req, message: 'not found...'
443
502
  end
444
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
445
519
  else
446
520
  raise "Unknown DAP request: #{req.inspect}"
447
521
  end
@@ -479,8 +553,15 @@ module DEBUGGER__
479
553
  register_vars result[:variables], tid
480
554
  @ui.respond req, result
481
555
  when :evaluate
482
- tid = result.delete :tid
483
- 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
484
565
  @ui.respond req, result
485
566
  else
486
567
  raise "unsupported: #{args.inspect}"
@@ -565,8 +646,9 @@ module DEBUGGER__
565
646
  v = b.local_variable_get(name)
566
647
  variable(name, v)
567
648
  }
568
- vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
569
- 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
570
652
  vars.unshift variable('%self', b.receiver)
571
653
  elsif lvars = frame.local_variables
572
654
  vars = lvars.map{|var, val|
@@ -574,8 +656,9 @@ module DEBUGGER__
574
656
  }
575
657
  else
576
658
  vars = [variable('%self', frame.self)]
577
- vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
578
- 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
579
662
  end
580
663
  event! :dap_result, :scope, req, variables: vars, tid: self.id
581
664
 
@@ -596,7 +679,7 @@ module DEBUGGER__
596
679
  case obj
597
680
  when Hash
598
681
  vars = obj.map{|k, v|
599
- variable(DEBUGGER__.short_inspect(k), v)
682
+ variable(DEBUGGER__.safe_inspect(k), v,)
600
683
  }
601
684
  when Struct
602
685
  vars = obj.members.map{|m|
@@ -628,32 +711,127 @@ module DEBUGGER__
628
711
  event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
629
712
 
630
713
  when :evaluate
631
- fid, expr = args
714
+ fid, expr, context = args
632
715
  frame = @target_frames[fid]
716
+ message = nil
633
717
 
634
718
  if frame && (b = frame.binding)
635
- begin
636
- result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
637
- rescue Exception => e
638
- 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}"
639
768
  end
640
769
  else
641
- result = 'can not evaluate on this frame...'
770
+ result = 'Error: Can not evaluate on this frame'
771
+ end
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
642
781
  end
643
- event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
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
+
644
804
  else
645
805
  raise "Unknown req: #{args.inspect}"
646
806
  end
647
807
  end
648
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
+
649
826
  def evaluate_result r
650
827
  v = variable nil, r
651
- v.delete(:name)
652
- v[:result] = DEBUGGER__.short_inspect(r)
828
+ v.delete :name
829
+ v.delete :value
830
+ v[:result] = DEBUGGER__.safe_inspect(r)
653
831
  v
654
832
  end
655
833
 
656
- def variable_ name, obj, indexedVariables: 0, namedVariables: 0, use_short: true
834
+ def variable_ name, obj, indexedVariables: 0, namedVariables: 0
657
835
  if indexedVariables > 0 || namedVariables > 0
658
836
  vid = @var_map.size + 1
659
837
  @var_map[vid] = obj
@@ -664,7 +842,7 @@ module DEBUGGER__
664
842
  ivnum = obj.instance_variables.size
665
843
 
666
844
  { name: name,
667
- value: DEBUGGER__.short_inspect(obj, use_short),
845
+ value: DEBUGGER__.safe_inspect(obj),
668
846
  type: obj.class.name || obj.class.to_s,
669
847
  variablesReference: vid,
670
848
  indexedVariables: indexedVariables,
@@ -679,7 +857,7 @@ module DEBUGGER__
679
857
  when Hash
680
858
  variable_ name, obj, namedVariables: obj.size
681
859
  when String
682
- variable_ name, obj, use_short: false, namedVariables: 3 # #to_str, #length, #encoding
860
+ variable_ name, obj, namedVariables: 3 # #to_str, #length, #encoding
683
861
  when Struct
684
862
  variable_ name, obj, namedVariables: obj.size
685
863
  when Class, Module