debug 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,7 +18,7 @@ module DEBUGGER__
18
18
  end
19
19
 
20
20
  at_exit do
21
- CONFIG[:skip_path] = [//] # skip all
21
+ CONFIG.skip_all
22
22
  FileUtils.rm_rf dir if tempdir
23
23
  end
24
24
 
@@ -70,20 +70,69 @@ module DEBUGGER__
70
70
  end
71
71
  end
72
72
 
73
- @local_fs = false
73
+ # true: all localfs
74
+ # Array: part of localfs
75
+ # nil: no localfs
76
+ @local_fs_map = nil
77
+
78
+ def self.remote_to_local_path path
79
+ case @local_fs_map
80
+ when nil
81
+ nil
82
+ when true
83
+ path
84
+ else # Array
85
+ @local_fs_map.each do |(remote_path_prefix, local_path_prefix)|
86
+ if path.start_with? remote_path_prefix
87
+ return path.sub(remote_path_prefix){ local_path_prefix }
88
+ end
89
+ end
74
90
 
75
- def self.local_fs
76
- @local_fs
91
+ nil
92
+ end
77
93
  end
78
94
 
79
- def self.local_fs_set
80
- @local_fs = true
95
+ def self.local_to_remote_path path
96
+ case @local_fs_map
97
+ when nil
98
+ nil
99
+ when true
100
+ path
101
+ else # Array
102
+ @local_fs_map.each do |(remote_path_prefix, local_path_prefix)|
103
+ if path.start_with? local_path_prefix
104
+ return path.sub(local_path_prefix){ remote_path_prefix }
105
+ end
106
+ end
107
+
108
+ nil
109
+ end
110
+ end
111
+
112
+ def self.local_fs_map_set map
113
+ return if @local_fs_map # already setup
114
+
115
+ case map
116
+ when String
117
+ @local_fs_map = map.split(',').map{|e| e.split(':').map{|path| path.delete_suffix('/') + '/'}}
118
+ when true
119
+ @local_fs_map = map
120
+ when nil
121
+ @local_fs_map = CONFIG[:local_fs_map]
122
+ end
81
123
  end
82
124
 
83
125
  def dap_setup bytes
84
126
  CONFIG.set_config no_color: true
85
127
  @seq = 0
86
- UI_DAP.local_fs_set if self.kind_of?(UI_UnixDomainServer)
128
+
129
+ case self
130
+ when UI_UnixDomainServer
131
+ UI_DAP.local_fs_map_set true
132
+ when UI_TcpServer
133
+ # TODO: loopback address can be used to connect other FS env, like Docker containers
134
+ # UI_DAP.local_fs_set if @local_addr.ipv4_loopback? || @local_addr.ipv6_loopback?
135
+ end
87
136
 
88
137
  show_protocol :>, bytes
89
138
  req = JSON.load(bytes)
@@ -100,14 +149,13 @@ module DEBUGGER__
100
149
  {
101
150
  filter: 'any',
102
151
  label: 'rescue any exception',
103
- #supportsCondition: true,
152
+ supportsCondition: true,
104
153
  #conditionDescription: '',
105
154
  },
106
155
  {
107
156
  filter: 'RuntimeError',
108
157
  label: 'rescue RuntimeError',
109
- default: true,
110
- #supportsCondition: true,
158
+ supportsCondition: true,
111
159
  #conditionDescription: '',
112
160
  },
113
161
  ],
@@ -153,10 +201,12 @@ module DEBUGGER__
153
201
  end
154
202
 
155
203
  def send **kw
156
- kw[:seq] = @seq += 1
157
- str = JSON.dump(kw)
158
- @sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
159
- show_protocol '<', str
204
+ if sock = @sock
205
+ kw[:seq] = @seq += 1
206
+ str = JSON.dump(kw)
207
+ sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
208
+ show_protocol '<', str
209
+ end
160
210
  end
161
211
 
162
212
  def send_response req, success: true, message: nil, **kw
@@ -221,70 +271,97 @@ module DEBUGGER__
221
271
  ## boot/configuration
222
272
  when 'launch'
223
273
  send_response req
224
- @is_attach = false
225
- UI_DAP.local_fs_set if req.dig('arguments', 'localfs')
274
+ UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap')
275
+ @is_launch = true
276
+
226
277
  when 'attach'
227
278
  send_response req
228
- Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
229
- @is_attach = true
230
- UI_DAP.local_fs_set if req.dig('arguments', 'localfs')
231
- when 'setBreakpoints'
232
- path = args.dig('source', 'path')
233
- SESSION.clear_line_breakpoints path
234
-
235
- bps = []
236
- args['breakpoints'].each{|bp|
237
- line = bp['line']
238
- if cond = bp['condition']
239
- bps << SESSION.add_line_breakpoint(path, line, cond: cond)
240
- else
241
- bps << SESSION.add_line_breakpoint(path, line)
279
+ UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap')
280
+ @is_launch = false
281
+
282
+ when 'configurationDone'
283
+ send_response req
284
+
285
+ if @is_launch
286
+ @q_msg << 'continue'
287
+ else
288
+ if SESSION.in_subsession?
289
+ send_event 'stopped', reason: 'pause',
290
+ threadId: 1, # maybe ...
291
+ allThreadsStopped: true
242
292
  end
243
- }
244
- send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
293
+ end
294
+
295
+ when 'setBreakpoints'
296
+ req_path = args.dig('source', 'path')
297
+ path = UI_DAP.local_to_remote_path(req_path)
298
+
299
+ if path
300
+ SESSION.clear_line_breakpoints path
301
+
302
+ bps = []
303
+ args['breakpoints'].each{|bp|
304
+ line = bp['line']
305
+ if cond = bp['condition']
306
+ bps << SESSION.add_line_breakpoint(path, line, cond: cond)
307
+ else
308
+ bps << SESSION.add_line_breakpoint(path, line)
309
+ end
310
+ }
311
+ send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
312
+ else
313
+ send_response req, success: false, message: "#{req_path} is not available"
314
+ end
315
+
245
316
  when 'setFunctionBreakpoints'
246
317
  send_response req
318
+
247
319
  when 'setExceptionBreakpoints'
248
- process_filter = ->(filter_id) {
249
- case filter_id
250
- when 'any'
251
- bp = SESSION.add_catch_breakpoint 'Exception'
252
- when 'RuntimeError'
253
- bp = SESSION.add_catch_breakpoint 'RuntimeError'
254
- else
255
- bp = nil
256
- end
257
- {
258
- verified: bp ? true : false,
259
- message: bp.inspect,
320
+ process_filter = ->(filter_id, cond = nil) {
321
+ bp =
322
+ case filter_id
323
+ when 'any'
324
+ SESSION.add_catch_breakpoint 'Exception', cond: cond
325
+ when 'RuntimeError'
326
+ SESSION.add_catch_breakpoint 'RuntimeError', cond: cond
327
+ else
328
+ nil
329
+ end
330
+ {
331
+ verified: !bp.nil?,
332
+ message: bp.inspect,
333
+ }
260
334
  }
261
- }
262
335
 
263
- filters = args.fetch('filters').map {|filter_id|
264
- process_filter.call(filter_id)
265
- }
336
+ SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError'
337
+
338
+ filters = args.fetch('filters').map {|filter_id|
339
+ process_filter.call(filter_id)
340
+ }
266
341
 
267
- filters += args.fetch('filterOptions', {}).map{|bp_info|
268
- process_filter.call(bp_info.dig('filterId'))
342
+ filters += args.fetch('filterOptions', {}).map{|bp_info|
343
+ process_filter.call(bp_info['filterId'], bp_info['condition'])
269
344
  }
270
345
 
271
346
  send_response req, breakpoints: filters
272
- when 'configurationDone'
273
- send_response req
274
- if defined?(@is_attach) && @is_attach
275
- @q_msg << 'p'
276
- send_event 'stopped', reason: 'pause',
277
- threadId: 1,
278
- allThreadsStopped: true
279
- else
280
- @q_msg << 'continue'
281
- end
347
+
282
348
  when 'disconnect'
283
- if args.fetch("terminateDebuggee", false)
284
- @q_msg << 'kill!'
349
+ terminate = args.fetch("terminateDebuggee", false)
350
+
351
+ if SESSION.in_subsession?
352
+ if terminate
353
+ @q_msg << 'kill!'
354
+ else
355
+ @q_msg << 'continue'
356
+ end
285
357
  else
286
- @q_msg << 'continue'
358
+ if terminate
359
+ @q_msg << 'kill!'
360
+ pause
361
+ end
287
362
  end
363
+
364
+ SESSION.clear_all_breakpoints
288
365
  send_response req
289
366
 
290
367
  ## control
@@ -360,14 +437,6 @@ module DEBUGGER__
360
437
 
361
438
  ## called by the SESSION thread
362
439
 
363
- def readline prompt
364
- @q_msg.pop || 'kill!'
365
- end
366
-
367
- def sock skip: false
368
- yield $stderr
369
- end
370
-
371
440
  def respond req, res
372
441
  send_response(req, **res)
373
442
  end
@@ -379,6 +448,16 @@ module DEBUGGER__
379
448
 
380
449
  def event type, *args
381
450
  case type
451
+ when :load
452
+ file_path, reloaded = *args
453
+
454
+ if file_path
455
+ send_event 'loadedSource',
456
+ reason: (reloaded ? :changed : :new),
457
+ source: {
458
+ path: file_path,
459
+ }
460
+ end
382
461
  when :suspend_bp
383
462
  _i, bp, tid = *args
384
463
  if bp.kind_of?(CatchBreakpoint)
@@ -425,15 +504,16 @@ module DEBUGGER__
425
504
  case req['command']
426
505
  when 'stepBack'
427
506
  if @tc.recorder&.can_step_back?
428
- @tc << [:step, :back]
507
+ request_tc [:step, :back]
429
508
  else
430
509
  fail_response req, message: 'cancelled'
431
510
  end
432
511
 
433
512
  when 'stackTrace'
434
513
  tid = req.dig('arguments', 'threadId')
514
+
435
515
  if tc = find_waiting_tc(tid)
436
- tc << [:dap, :backtrace, req]
516
+ request_tc [:dap, :backtrace, req]
437
517
  else
438
518
  fail_response req
439
519
  end
@@ -442,7 +522,7 @@ module DEBUGGER__
442
522
  if @frame_map[frame_id]
443
523
  tid, fid = @frame_map[frame_id]
444
524
  if tc = find_waiting_tc(tid)
445
- tc << [:dap, :scopes, req, fid]
525
+ request_tc [:dap, :scopes, req, fid]
446
526
  else
447
527
  fail_response req
448
528
  end
@@ -474,7 +554,7 @@ module DEBUGGER__
474
554
  tid, fid = @frame_map[frame_id]
475
555
 
476
556
  if tc = find_waiting_tc(tid)
477
- tc << [:dap, :scope, req, fid]
557
+ request_tc [:dap, :scope, req, fid]
478
558
  else
479
559
  fail_response req
480
560
  end
@@ -483,7 +563,7 @@ module DEBUGGER__
483
563
  tid, vid = ref[1], ref[2]
484
564
 
485
565
  if tc = find_waiting_tc(tid)
486
- tc << [:dap, :variable, req, vid]
566
+ request_tc [:dap, :variable, req, vid]
487
567
  else
488
568
  fail_response req
489
569
  end
@@ -501,7 +581,7 @@ module DEBUGGER__
501
581
  tid, fid = @frame_map[frame_id]
502
582
  expr = req.dig('arguments', 'expression')
503
583
  if tc = find_waiting_tc(tid)
504
- tc << [:dap, :evaluate, req, fid, expr, context]
584
+ request_tc [:dap, :evaluate, req, fid, expr, context]
505
585
  else
506
586
  fail_response req
507
587
  end
@@ -527,7 +607,7 @@ module DEBUGGER__
527
607
  if col = req.dig('arguments', 'column')
528
608
  text = text.split(/\n/)[line.to_i - 1][0...(col.to_i - 1)]
529
609
  end
530
- tc << [:dap, :completions, req, fid, text]
610
+ request_tc [:dap, :completions, req, fid, text]
531
611
  else
532
612
  fail_response req
533
613
  end
@@ -542,13 +622,18 @@ module DEBUGGER__
542
622
 
543
623
  case type
544
624
  when :backtrace
545
- result[:stackFrames].each.with_index{|fi, i|
625
+ result[:stackFrames].each{|fi|
626
+ frame_depth = fi[:id]
546
627
  fi[:id] = id = @frame_map.size + 1
547
- @frame_map[id] = [req.dig('arguments', 'threadId'), i]
548
- if fi[:source] && src = fi[:source][:sourceReference]
549
- src_id = @src_map.size + 1
550
- @src_map[src_id] = src
551
- fi[:source][:sourceReference] = src_id
628
+ @frame_map[id] = [req.dig('arguments', 'threadId'), frame_depth]
629
+ if fi[:source]
630
+ if src = fi[:source][:sourceReference]
631
+ src_id = @src_map.size + 1
632
+ @src_map[src_id] = src
633
+ fi[:source][:sourceReference] = src_id
634
+ else
635
+ fi[:source][:sourceReference] = 0
636
+ end
552
637
  end
553
638
  }
554
639
  @ui.respond req, result
@@ -600,6 +685,11 @@ module DEBUGGER__
600
685
  end
601
686
 
602
687
  class ThreadClient
688
+ def value_inspect obj
689
+ # TODO: max length should be configuarable?
690
+ DEBUGGER__.safe_inspect obj, short: true, max_length: 4 * 1024
691
+ end
692
+
603
693
  def process_dap args
604
694
  # pp tc: self, args: args
605
695
  type = args.shift
@@ -607,27 +697,38 @@ module DEBUGGER__
607
697
 
608
698
  case type
609
699
  when :backtrace
610
- event! :dap_result, :backtrace, req, {
611
- stackFrames: @target_frames.map{|frame|
612
- path = frame.realpath || frame.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
700
+ start_frame = req.dig('arguments', 'startFrame') || 0
701
+ levels = req.dig('arguments', 'levels') || 1_000
702
+ frames = []
703
+ @target_frames.each_with_index do |frame, i|
704
+ next if i < start_frame
705
+ break if (levels -= 1) < 0
706
+
707
+ path = frame.realpath || frame.path
708
+ source_name = path ? File.basename(path) : frame.location.to_s
709
+
710
+ if (path && File.exist?(path)) && (local_path = UI_DAP.remote_to_local_path(path))
711
+ # ok
712
+ else
713
+ ref = frame.file_lines
714
+ end
618
715
 
619
- {
620
- # id: ??? # filled by SESSION
621
- name: frame.name,
622
- line: frame.location.lineno,
623
- column: 1,
624
- source: {
625
- name: source_name,
626
- path: path,
627
- sourceReference: ref,
628
- },
629
- }
716
+ frames << {
717
+ id: i, # id is refilled by SESSION
718
+ name: frame.name,
719
+ line: frame.location.lineno,
720
+ column: 1,
721
+ source: {
722
+ name: source_name,
723
+ path: (local_path || path),
724
+ sourceReference: ref,
725
+ },
630
726
  }
727
+ end
728
+
729
+ event! :dap_result, :backtrace, req, {
730
+ stackFrames: frames,
731
+ totalFrames: @target_frames.size,
631
732
  }
632
733
  when :scopes
633
734
  fid = args.shift
@@ -682,7 +783,7 @@ module DEBUGGER__
682
783
  case obj
683
784
  when Hash
684
785
  vars = obj.map{|k, v|
685
- variable(DEBUGGER__.safe_inspect(k), v,)
786
+ variable(value_inspect(k), v,)
686
787
  }
687
788
  when Struct
688
789
  vars = obj.members.map{|m|
@@ -705,10 +806,10 @@ module DEBUGGER__
705
806
  ]
706
807
  end
707
808
 
708
- vars += obj.instance_variables.map{|iv|
709
- variable(iv, obj.instance_variable_get(iv))
809
+ vars += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
810
+ variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
710
811
  }
711
- vars.unshift variable('#class', obj.class)
812
+ vars.unshift variable('#class', M_CLASS.bind_call(obj))
712
813
  end
713
814
  end
714
815
  event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
@@ -735,7 +836,7 @@ module DEBUGGER__
735
836
  case expr
736
837
  when /\A\@\S/
737
838
  begin
738
- result = b.receiver.instance_variable_get(expr)
839
+ result = M_INSTANCE_VARIABLE_GET.bind_call(b.receiver, expr)
739
840
  rescue NameError
740
841
  message = "Error: Not defined instance variable: #{expr.inspect}"
741
842
  end
@@ -746,6 +847,8 @@ module DEBUGGER__
746
847
  break false
747
848
  end
748
849
  } and (message = "Error: Not defined global variable: #{expr.inspect}")
850
+ when /\Aself$/
851
+ result = b.receiver
749
852
  when /(\A((::[A-Z]|[A-Z])\w*)+)/
750
853
  unless result = search_const(b, $1)
751
854
  message = "Error: Not defined constants: #{expr.inspect}"
@@ -755,8 +858,8 @@ module DEBUGGER__
755
858
  result = b.local_variable_get(expr)
756
859
  rescue NameError
757
860
  # try to check method
758
- if b.receiver.respond_to? expr, include_all: true
759
- result = b.receiver.method(expr)
861
+ if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
862
+ result = M_METHOD.bind_call(b.receiver, expr)
760
863
  else
761
864
  message = "Error: Can not evaluate: #{expr.inspect}"
762
865
  end
@@ -780,6 +883,8 @@ module DEBUGGER__
780
883
  end
781
884
 
782
885
  event! :dap_result, :completions, req, targets: (words || []).map{|phrase|
886
+ detail = nil
887
+
783
888
  if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
784
889
  w = $1
785
890
  else
@@ -788,13 +893,14 @@ module DEBUGGER__
788
893
 
789
894
  begin
790
895
  v = b.local_variable_get(w)
791
- phrase += " (variable:#{DEBUGGER__.safe_inspect(v)})"
896
+ detail ="(variable: #{value_inspect(v)})"
792
897
  rescue NameError
793
898
  end
794
899
 
795
900
  {
796
901
  label: phrase,
797
902
  text: w,
903
+ detail: detail,
798
904
  }
799
905
  }
800
906
 
@@ -824,7 +930,7 @@ module DEBUGGER__
824
930
  v = variable nil, r
825
931
  v.delete :name
826
932
  v.delete :value
827
- v[:result] = DEBUGGER__.safe_inspect(r)
933
+ v[:result] = value_inspect(r)
828
934
  v
829
935
  end
830
936
 
@@ -836,11 +942,11 @@ module DEBUGGER__
836
942
  vid = 0
837
943
  end
838
944
 
839
- ivnum = obj.instance_variables.size
945
+ ivnum = M_INSTANCE_VARIABLES.bind_call(obj).size
840
946
 
841
947
  { name: name,
842
- value: DEBUGGER__.safe_inspect(obj),
843
- type: obj.class.name || obj.class.to_s,
948
+ value: value_inspect(obj),
949
+ type: (klass = M_CLASS.bind_call(obj)).name || klass.to_s,
844
950
  variablesReference: vid,
845
951
  indexedVariables: indexedVariables,
846
952
  namedVariables: namedVariables + ivnum,