debug 1.4.0 → 1.9.2

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +210 -6
  3. data/Gemfile +2 -0
  4. data/LICENSE.txt +0 -0
  5. data/README.md +161 -85
  6. data/Rakefile +33 -10
  7. data/TODO.md +8 -8
  8. data/debug.gemspec +9 -7
  9. data/exe/rdbg +23 -4
  10. data/ext/debug/debug.c +111 -21
  11. data/ext/debug/extconf.rb +23 -0
  12. data/ext/debug/iseq_collector.c +2 -0
  13. data/lib/debug/abbrev_command.rb +77 -0
  14. data/lib/debug/breakpoint.rb +102 -74
  15. data/lib/debug/client.rb +46 -12
  16. data/lib/debug/color.rb +0 -0
  17. data/lib/debug/config.rb +129 -36
  18. data/lib/debug/console.rb +46 -40
  19. data/lib/debug/dap_custom/traceInspector.rb +336 -0
  20. data/lib/debug/frame_info.rb +40 -25
  21. data/lib/debug/irb_integration.rb +37 -0
  22. data/lib/debug/local.rb +17 -11
  23. data/lib/debug/open.rb +0 -0
  24. data/lib/debug/open_nonstop.rb +0 -0
  25. data/lib/debug/prelude.rb +3 -2
  26. data/lib/debug/server.rb +126 -56
  27. data/lib/debug/server_cdp.rb +673 -248
  28. data/lib/debug/server_dap.rb +497 -261
  29. data/lib/debug/session.rb +899 -441
  30. data/lib/debug/source_repository.rb +122 -49
  31. data/lib/debug/start.rb +1 -1
  32. data/lib/debug/thread_client.rb +460 -155
  33. data/lib/debug/tracer.rb +10 -16
  34. data/lib/debug/version.rb +1 -1
  35. data/lib/debug.rb +7 -2
  36. data/misc/README.md.erb +106 -56
  37. metadata +14 -24
  38. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
  39. data/.github/ISSUE_TEMPLATE/custom.md +0 -10
  40. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
  41. data/.github/pull_request_template.md +0 -9
  42. data/.github/workflows/ruby.yml +0 -34
  43. data/.gitignore +0 -12
  44. data/bin/console +0 -14
  45. data/bin/gentest +0 -30
  46. data/bin/setup +0 -8
  47. data/lib/debug/bp.vim +0 -68
@@ -3,52 +3,51 @@
3
3
  require 'json'
4
4
  require 'irb/completion'
5
5
  require 'tmpdir'
6
- require 'json'
7
6
  require 'fileutils'
8
7
 
9
8
  module DEBUGGER__
10
9
  module UI_DAP
11
10
  SHOW_PROTOCOL = ENV['DEBUG_DAP_SHOW_PROTOCOL'] == '1' || ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
12
11
 
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
- }
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
+ DEBUGGER__.skip_all
22
+ FileUtils.rm_rf dir if tempdir
23
+ end
24
+
25
+ key = rand.to_s
26
+
19
27
  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|
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|
30
32
  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
- ]
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,
42
40
  })
43
- }
41
+ end
44
42
  end
45
43
 
46
- cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
44
+ cmds = ['code', "#{dir}/"]
47
45
  cmdline = cmds.join(' ')
48
- ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"
46
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/"
49
47
 
50
48
  STDERR.puts "Launching: #{cmdline}"
51
49
  env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
50
+ env['RUBY_DEBUG_AUTOATTACH'] = key
52
51
 
53
52
  unless system(env, *cmds)
54
53
  DEBUGGER__.warn <<~MESSAGE
@@ -71,9 +70,71 @@ module DEBUGGER__
71
70
  end
72
71
  end
73
72
 
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
90
+
91
+ nil
92
+ end
93
+ end
94
+
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
123
+ end
124
+
74
125
  def dap_setup bytes
75
126
  CONFIG.set_config no_color: true
76
127
  @seq = 0
128
+ @send_lock = Mutex.new
129
+
130
+ case self
131
+ when UI_UnixDomainServer
132
+ # If the user specified a mapping, respect it, otherwise, make sure that no mapping is used
133
+ UI_DAP.local_fs_map_set CONFIG[:local_fs_map] || true
134
+ when UI_TcpServer
135
+ # TODO: loopback address can be used to connect other FS env, like Docker containers
136
+ # UI_DAP.local_fs_set if @local_addr.ipv4_loopback? || @local_addr.ipv6_loopback?
137
+ end
77
138
 
78
139
  show_protocol :>, bytes
79
140
  req = JSON.load(bytes)
@@ -90,14 +151,13 @@ module DEBUGGER__
90
151
  {
91
152
  filter: 'any',
92
153
  label: 'rescue any exception',
93
- #supportsCondition: true,
154
+ supportsCondition: true,
94
155
  #conditionDescription: '',
95
156
  },
96
157
  {
97
158
  filter: 'RuntimeError',
98
159
  label: 'rescue RuntimeError',
99
- default: true,
100
- #supportsCondition: true,
160
+ supportsCondition: true,
101
161
  #conditionDescription: '',
102
162
  },
103
163
  ],
@@ -140,13 +200,26 @@ module DEBUGGER__
140
200
  # supportsInstructionBreakpoints:
141
201
  )
142
202
  send_event 'initialized'
203
+ puts <<~WELCOME
204
+ Ruby REPL: You can run any Ruby expression here.
205
+ Note that output to the STDOUT/ERR printed on the TERMINAL.
206
+ [experimental]
207
+ `,COMMAND` runs `COMMAND` debug command (ex: `,info`).
208
+ `,help` to list all debug commands.
209
+ WELCOME
143
210
  end
144
211
 
145
212
  def send **kw
146
- kw[:seq] = @seq += 1
147
- str = JSON.dump(kw)
148
- show_protocol '<', str
149
- @sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
213
+ if sock = @sock
214
+ kw[:seq] = @seq += 1
215
+ str = JSON.dump(kw)
216
+ @send_lock.synchronize do
217
+ sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
218
+ end
219
+ show_protocol '<', str
220
+ end
221
+ rescue Errno::EPIPE => e
222
+ $stderr.puts "#{e.inspect} rescued during sending message"
150
223
  end
151
224
 
152
225
  def send_response req, success: true, message: nil, **kw
@@ -178,17 +251,17 @@ module DEBUGGER__
178
251
  end
179
252
 
180
253
  def recv_request
181
- r = IO.select([@sock])
254
+ IO.select([@sock])
182
255
 
183
256
  @session.process_group.sync do
184
257
  raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
185
258
 
186
- case header = @sock.gets
259
+ case @sock.gets
187
260
  when /Content-Length: (\d+)/
188
261
  b = @sock.read(2)
189
262
  raise b.inspect unless b == "\r\n"
190
263
 
191
- l = @sock.read(s = $1.to_i)
264
+ l = @sock.read($1.to_i)
192
265
  show_protocol :>, l
193
266
  JSON.load(l)
194
267
  when nil
@@ -201,26 +274,81 @@ module DEBUGGER__
201
274
  retry
202
275
  end
203
276
 
277
+ def load_extensions req
278
+ if exts = req.dig('arguments', 'rdbgExtensions')
279
+ exts.each{|ext|
280
+ require_relative "dap_custom/#{File.basename(ext)}"
281
+ }
282
+ end
283
+
284
+ if scripts = req.dig('arguments', 'rdbgInitialScripts')
285
+ scripts.each do |script|
286
+ begin
287
+ eval(script)
288
+ rescue Exception => e
289
+ puts e.message
290
+ puts e.backtrace.inspect
291
+ end
292
+ end
293
+ end
294
+ end
295
+
204
296
  def process
205
297
  while req = recv_request
206
- raise "not a request: #{req.inpsect}" unless req['type'] == 'request'
207
- args = req.dig('arguments')
298
+ process_request(req)
299
+ end
300
+ ensure
301
+ send_event :terminated unless @sock.closed?
302
+ end
208
303
 
209
- case req['command']
304
+ def process_request req
305
+ raise "not a request: #{req.inspect}" unless req['type'] == 'request'
306
+ args = req.dig('arguments')
307
+
308
+ case req['command']
309
+
310
+ ## boot/configuration
311
+ when 'launch'
312
+ send_response req
313
+ # `launch` runs on debuggee on the same file system
314
+ UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap') || true
315
+ @nonstop = true
316
+
317
+ load_extensions req
318
+
319
+ when 'attach'
320
+ send_response req
321
+ UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap')
322
+
323
+ if req.dig('arguments', 'nonstop') == true
324
+ @nonstop = true
325
+ else
326
+ @nonstop = false
327
+ end
328
+
329
+ load_extensions req
330
+
331
+ when 'configurationDone'
332
+ send_response req
333
+
334
+ if @nonstop
335
+ @q_msg << 'continue'
336
+ else
337
+ if SESSION.in_subsession?
338
+ send_event 'stopped', reason: 'pause',
339
+ threadId: 1, # maybe ...
340
+ allThreadsStopped: true
341
+ end
342
+ end
343
+
344
+ when 'setBreakpoints'
345
+ req_path = args.dig('source', 'path')
346
+ path = UI_DAP.local_to_remote_path(req_path)
347
+ if path
348
+ SESSION.clear_line_breakpoints path
210
349
 
211
- ## boot/configuration
212
- when 'launch'
213
- send_response req
214
- @is_attach = false
215
- when 'attach'
216
- send_response req
217
- Process.kill(:SIGURG, Process.pid)
218
- @is_attach = true
219
- when 'setBreakpoints'
220
- path = args.dig('source', 'path')
221
- bp_args = args['breakpoints']
222
350
  bps = []
223
- bp_args.each{|bp|
351
+ args['breakpoints'].each{|bp|
224
352
  line = bp['line']
225
353
  if cond = bp['condition']
226
354
  bps << SESSION.add_line_breakpoint(path, line, cond: cond)
@@ -229,114 +357,138 @@ module DEBUGGER__
229
357
  end
230
358
  }
231
359
  send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
232
- when 'setFunctionBreakpoints'
233
- send_response req
234
- when 'setExceptionBreakpoints'
235
- process_filter = ->(filter_id) {
360
+ else
361
+ send_response req, success: false, message: "#{req_path} is not available"
362
+ end
363
+
364
+ when 'setFunctionBreakpoints'
365
+ send_response req
366
+
367
+ when 'setExceptionBreakpoints'
368
+ process_filter = ->(filter_id, cond = nil) {
369
+ bp =
236
370
  case filter_id
237
371
  when 'any'
238
- bp = SESSION.add_catch_breakpoint 'Exception'
372
+ SESSION.add_catch_breakpoint 'Exception', cond: cond
239
373
  when 'RuntimeError'
240
- bp = SESSION.add_catch_breakpoint 'RuntimeError'
374
+ SESSION.add_catch_breakpoint 'RuntimeError', cond: cond
241
375
  else
242
- bp = nil
376
+ nil
243
377
  end
244
378
  {
245
- verified: bp ? true : false,
379
+ verified: !bp.nil?,
246
380
  message: bp.inspect,
247
381
  }
248
382
  }
249
383
 
384
+ SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError'
385
+
250
386
  filters = args.fetch('filters').map {|filter_id|
251
387
  process_filter.call(filter_id)
252
388
  }
253
389
 
254
390
  filters += args.fetch('filterOptions', {}).map{|bp_info|
255
- process_filter.call(bp_info.dig('filterId'))
256
- }
391
+ process_filter.call(bp_info['filterId'], bp_info['condition'])
392
+ }
257
393
 
258
- send_response req, breakpoints: filters
259
- when 'configurationDone'
260
- send_response req
261
- if defined?(@is_attach) && @is_attach
262
- @q_msg << 'p'
263
- send_event 'stopped', reason: 'pause',
264
- threadId: 1,
265
- allThreadsStopped: true
394
+ send_response req, breakpoints: filters
395
+
396
+ when 'disconnect'
397
+ terminate = args.fetch("terminateDebuggee", false)
398
+
399
+ SESSION.clear_all_breakpoints
400
+ send_response req
401
+
402
+ if SESSION.in_subsession?
403
+ if terminate
404
+ @q_msg << 'kill!'
266
405
  else
267
406
  @q_msg << 'continue'
268
407
  end
269
- when 'disconnect'
270
- if args.fetch("terminateDebuggee", false)
408
+ else
409
+ if terminate
271
410
  @q_msg << 'kill!'
272
- else
273
- @q_msg << 'continue'
411
+ pause
274
412
  end
275
- send_response req
413
+ end
276
414
 
277
- ## control
278
- when 'continue'
279
- @q_msg << 'c'
280
- send_response req, allThreadsContinued: true
281
- when 'next'
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
291
- when 'stepIn'
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
301
- when 'stepOut'
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
311
- when 'terminate'
415
+ ## control
416
+ when 'continue'
417
+ @q_msg << 'c'
418
+ send_response req, allThreadsContinued: true
419
+ when 'next'
420
+ begin
421
+ @session.check_postmortem
422
+ @q_msg << 'n'
312
423
  send_response req
313
- exit
314
- when 'pause'
424
+ rescue PostmortemError
425
+ send_response req,
426
+ success: false, message: 'postmortem mode',
427
+ result: "'Next' is not supported while postmortem mode"
428
+ end
429
+ when 'stepIn'
430
+ begin
431
+ @session.check_postmortem
432
+ @q_msg << 's'
315
433
  send_response req
316
- Process.kill(:SIGURG, Process.pid)
317
- when 'reverseContinue'
434
+ rescue PostmortemError
318
435
  send_response req,
319
- success: false, message: 'cancelled',
320
- result: "Reverse Continue is not supported. Only \"Step back\" is supported."
321
- when 'stepBack'
322
- @q_msg << req
436
+ success: false, message: 'postmortem mode',
437
+ result: "'stepIn' is not supported while postmortem mode"
438
+ end
439
+ when 'stepOut'
440
+ begin
441
+ @session.check_postmortem
442
+ @q_msg << 'fin'
443
+ send_response req
444
+ rescue PostmortemError
445
+ send_response req,
446
+ success: false, message: 'postmortem mode',
447
+ result: "'stepOut' is not supported while postmortem mode"
448
+ end
449
+ when 'terminate'
450
+ send_response req
451
+ exit
452
+ when 'pause'
453
+ send_response req
454
+ Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
455
+ when 'reverseContinue'
456
+ send_response req,
457
+ success: false, message: 'cancelled',
458
+ result: "Reverse Continue is not supported. Only \"Step back\" is supported."
459
+ when 'stepBack'
460
+ @q_msg << req
323
461
 
324
- ## query
325
- when 'threads'
326
- send_response req, threads: SESSION.managed_thread_clients.map{|tc|
327
- { id: tc.id,
328
- name: tc.name,
329
- }
462
+ ## query
463
+ when 'threads'
464
+ send_response req, threads: SESSION.managed_thread_clients.map{|tc|
465
+ { id: tc.id,
466
+ name: tc.name,
330
467
  }
468
+ }
331
469
 
332
- when 'stackTrace',
333
- 'scopes',
334
- 'variables',
335
- 'evaluate',
336
- 'source',
337
- 'completions'
470
+ when 'evaluate'
471
+ expr = req.dig('arguments', 'expression')
472
+ if /\A\s*,(.+)\z/ =~ expr
473
+ dbg_expr = $1.strip
474
+ dbg_expr.split(';;') { |cmd| @q_msg << cmd }
475
+
476
+ send_response req,
477
+ result: "(rdbg:command) #{dbg_expr}",
478
+ variablesReference: 0
479
+ else
338
480
  @q_msg << req
481
+ end
482
+ when 'stackTrace',
483
+ 'scopes',
484
+ 'variables',
485
+ 'source',
486
+ 'completions'
487
+ @q_msg << req
339
488
 
489
+ else
490
+ if respond_to? mid = "custom_dap_request_#{req['command']}"
491
+ __send__ mid, req
340
492
  else
341
493
  raise "Unknown request: #{req.inspect}"
342
494
  end
@@ -345,25 +497,31 @@ module DEBUGGER__
345
497
 
346
498
  ## called by the SESSION thread
347
499
 
348
- def readline prompt
349
- @q_msg.pop || 'kill!'
350
- end
351
-
352
- def sock skip: false
353
- yield $stderr
354
- end
355
-
356
500
  def respond req, res
357
501
  send_response(req, **res)
358
502
  end
359
503
 
360
504
  def puts result
361
505
  # STDERR.puts "puts: #{result}"
362
- # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
506
+ send_event 'output', category: 'console', output: "#{result&.chomp}\n"
507
+ end
508
+
509
+ def ignore_output_on_suspend?
510
+ true
363
511
  end
364
512
 
365
513
  def event type, *args
366
514
  case type
515
+ when :load
516
+ file_path, reloaded = *args
517
+
518
+ if file_path
519
+ send_event 'loadedSource',
520
+ reason: (reloaded ? :changed : :new),
521
+ source: {
522
+ path: file_path,
523
+ }
524
+ end
367
525
  when :suspend_bp
368
526
  _i, bp, tid = *args
369
527
  if bp.kind_of?(CatchBreakpoint)
@@ -394,6 +552,8 @@ module DEBUGGER__
394
552
  end
395
553
 
396
554
  class Session
555
+ include GlobalVariablesHelper
556
+
397
557
  def find_waiting_tc id
398
558
  @th_clients.each{|th, tc|
399
559
  return tc if tc.id == id && tc.waiting?
@@ -410,15 +570,16 @@ module DEBUGGER__
410
570
  case req['command']
411
571
  when 'stepBack'
412
572
  if @tc.recorder&.can_step_back?
413
- @tc << [:step, :back]
573
+ request_tc [:step, :back]
414
574
  else
415
575
  fail_response req, message: 'cancelled'
416
576
  end
417
577
 
418
578
  when 'stackTrace'
419
579
  tid = req.dig('arguments', 'threadId')
420
- if tc = find_waiting_tc(tid)
421
- tc << [:dap, :backtrace, req]
580
+
581
+ if find_waiting_tc(tid)
582
+ request_tc [:dap, :backtrace, req]
422
583
  else
423
584
  fail_response req
424
585
  end
@@ -426,8 +587,8 @@ module DEBUGGER__
426
587
  frame_id = req.dig('arguments', 'frameId')
427
588
  if @frame_map[frame_id]
428
589
  tid, fid = @frame_map[frame_id]
429
- if tc = find_waiting_tc(tid)
430
- tc << [:dap, :scopes, req, fid]
590
+ if find_waiting_tc(tid)
591
+ request_tc [:dap, :scopes, req, fid]
431
592
  else
432
593
  fail_response req
433
594
  end
@@ -439,8 +600,12 @@ module DEBUGGER__
439
600
  if ref = @var_map[varid]
440
601
  case ref[0]
441
602
  when :globals
442
- vars = global_variables.map do |name|
443
- gv = 'Not implemented yet...'
603
+ vars = safe_global_variables.sort.map do |name|
604
+ begin
605
+ gv = eval(name.to_s)
606
+ rescue Exception => e
607
+ gv = e.inspect
608
+ end
444
609
  {
445
610
  name: name,
446
611
  value: gv.inspect,
@@ -458,8 +623,8 @@ module DEBUGGER__
458
623
  frame_id = ref[1]
459
624
  tid, fid = @frame_map[frame_id]
460
625
 
461
- if tc = find_waiting_tc(tid)
462
- tc << [:dap, :scope, req, fid]
626
+ if find_waiting_tc(tid)
627
+ request_tc [:dap, :scope, req, fid]
463
628
  else
464
629
  fail_response req
465
630
  end
@@ -467,8 +632,8 @@ module DEBUGGER__
467
632
  when :variable
468
633
  tid, vid = ref[1], ref[2]
469
634
 
470
- if tc = find_waiting_tc(tid)
471
- tc << [:dap, :variable, req, vid]
635
+ if find_waiting_tc(tid)
636
+ request_tc [:dap, :variable, req, vid]
472
637
  else
473
638
  fail_response req
474
639
  end
@@ -485,8 +650,10 @@ module DEBUGGER__
485
650
  if @frame_map[frame_id]
486
651
  tid, fid = @frame_map[frame_id]
487
652
  expr = req.dig('arguments', 'expression')
488
- if tc = find_waiting_tc(tid)
489
- tc << [:dap, :evaluate, req, fid, expr, context]
653
+
654
+ if find_waiting_tc(tid)
655
+ restart_all_threads
656
+ request_tc [:dap, :evaluate, req, fid, expr, context]
490
657
  else
491
658
  fail_response req
492
659
  end
@@ -496,7 +663,7 @@ module DEBUGGER__
496
663
  when 'source'
497
664
  ref = req.dig('arguments', 'sourceReference')
498
665
  if src = @src_map[ref]
499
- @ui.respond req, content: src.join
666
+ @ui.respond req, content: src.join("\n")
500
667
  else
501
668
  fail_response req, message: 'not found...'
502
669
  end
@@ -506,34 +673,43 @@ module DEBUGGER__
506
673
  frame_id = req.dig('arguments', 'frameId')
507
674
  tid, fid = @frame_map[frame_id]
508
675
 
509
- if tc = find_waiting_tc(tid)
676
+ if find_waiting_tc(tid)
510
677
  text = req.dig('arguments', 'text')
511
678
  line = req.dig('arguments', 'line')
512
679
  if col = req.dig('arguments', 'column')
513
680
  text = text.split(/\n/)[line.to_i - 1][0...(col.to_i - 1)]
514
681
  end
515
- tc << [:dap, :completions, req, fid, text]
682
+ request_tc [:dap, :completions, req, fid, text]
516
683
  else
517
684
  fail_response req
518
685
  end
519
686
  else
520
- raise "Unknown DAP request: #{req.inspect}"
687
+ if respond_to? mid = "custom_dap_request_#{req['command']}"
688
+ __send__ mid, req
689
+ else
690
+ raise "Unknown request: #{req.inspect}"
691
+ end
521
692
  end
522
693
  end
523
694
 
524
- def dap_event args
695
+ def process_protocol_result args
525
696
  # puts({dap_event: args}.inspect)
526
697
  type, req, result = args
527
698
 
528
699
  case type
529
700
  when :backtrace
530
- result[:stackFrames].each.with_index{|fi, i|
701
+ result[:stackFrames].each{|fi|
702
+ frame_depth = fi[:id]
531
703
  fi[:id] = id = @frame_map.size + 1
532
- @frame_map[id] = [req.dig('arguments', 'threadId'), i]
533
- if fi[:source] && src = fi[:source][:sourceReference]
534
- src_id = @src_map.size + 1
535
- @src_map[src_id] = src
536
- fi[:source][:sourceReference] = src_id
704
+ @frame_map[id] = [req.dig('arguments', 'threadId'), frame_depth]
705
+ if fi[:source]
706
+ if src = fi[:source][:sourceReference]
707
+ src_id = @src_map.size + 1
708
+ @src_map[src_id] = src
709
+ fi[:source][:sourceReference] = src_id
710
+ else
711
+ fi[:source][:sourceReference] = 0
712
+ end
537
713
  end
538
714
  }
539
715
  @ui.respond req, result
@@ -553,6 +729,7 @@ module DEBUGGER__
553
729
  register_vars result[:variables], tid
554
730
  @ui.respond req, result
555
731
  when :evaluate
732
+ stop_all_threads
556
733
  message = result.delete :message
557
734
  if message
558
735
  @ui.respond req, success: false, message: message
@@ -564,7 +741,11 @@ module DEBUGGER__
564
741
  when :completions
565
742
  @ui.respond req, result
566
743
  else
567
- raise "unsupported: #{args.inspect}"
744
+ if respond_to? mid = "custom_dap_request_event_#{type}"
745
+ __send__ mid, req, result
746
+ else
747
+ raise "unsupported: #{args.inspect}"
748
+ end
568
749
  end
569
750
  end
570
751
 
@@ -584,7 +765,37 @@ module DEBUGGER__
584
765
  end
585
766
  end
586
767
 
768
+ class NaiveString
769
+ attr_reader :str
770
+ def initialize str
771
+ @str = str
772
+ end
773
+ end
774
+
587
775
  class ThreadClient
776
+ MAX_LENGTH = 180
777
+
778
+ def value_inspect obj, short: true
779
+ # TODO: max length should be configuarable?
780
+ str = DEBUGGER__.safe_inspect obj, short: short, max_length: MAX_LENGTH
781
+
782
+ if str.encoding == Encoding::UTF_8
783
+ str.scrub
784
+ else
785
+ str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
786
+ end
787
+ end
788
+
789
+ def dap_eval b, expr, _context, prompt: '(repl_eval)'
790
+ begin
791
+ tp_allow_reentry do
792
+ b.eval(expr.to_s, prompt)
793
+ end
794
+ rescue Exception => e
795
+ e
796
+ end
797
+ end
798
+
588
799
  def process_dap args
589
800
  # pp tc: self, args: args
590
801
  type = args.shift
@@ -592,27 +803,43 @@ module DEBUGGER__
592
803
 
593
804
  case type
594
805
  when :backtrace
595
- event! :dap_result, :backtrace, req, {
596
- stackFrames: @target_frames.map{|frame|
597
- path = frame.realpath || frame.path
598
- ref = frame.file_lines unless path && File.exist?(path)
806
+ start_frame = req.dig('arguments', 'startFrame') || 0
807
+ levels = req.dig('arguments', 'levels') || 1_000
808
+ frames = []
809
+ @target_frames.each_with_index do |frame, i|
810
+ next if i < start_frame
811
+
812
+ path = frame.realpath || frame.path
813
+ next if skip_path?(path) && !SESSION.stop_stepping?(path, frame.location.lineno)
814
+ break if (levels -= 1) < 0
815
+ source_name = path ? File.basename(path) : frame.location.to_s
816
+
817
+ if (path && File.exist?(path)) && (local_path = UI_DAP.remote_to_local_path(path))
818
+ # ok
819
+ else
820
+ ref = frame.file_lines
821
+ end
599
822
 
600
- {
601
- # id: ??? # filled by SESSION
602
- name: frame.name,
603
- line: frame.location.lineno,
604
- column: 1,
605
- source: {
606
- name: File.basename(frame.path),
607
- path: path,
608
- sourceReference: ref,
609
- },
610
- }
823
+ frames << {
824
+ id: i, # id is refilled by SESSION
825
+ name: frame.name,
826
+ line: frame.location.lineno,
827
+ column: 1,
828
+ source: {
829
+ name: source_name,
830
+ path: (local_path || path),
831
+ sourceReference: ref,
832
+ },
611
833
  }
834
+ end
835
+
836
+ event! :protocol_result, :backtrace, req, {
837
+ stackFrames: frames,
838
+ totalFrames: @target_frames.size,
612
839
  }
613
840
  when :scopes
614
841
  fid = args.shift
615
- frame = @target_frames[fid]
842
+ frame = get_frame(fid)
616
843
 
617
844
  lnum =
618
845
  if frame.binding
@@ -623,7 +850,7 @@ module DEBUGGER__
623
850
  0
624
851
  end
625
852
 
626
- event! :dap_result, :scopes, req, scopes: [{
853
+ event! :protocol_result, :scopes, req, scopes: [{
627
854
  name: 'Local variables',
628
855
  presentationHint: 'locals',
629
856
  # variablesReference: N, # filled by SESSION
@@ -634,34 +861,18 @@ module DEBUGGER__
634
861
  name: 'Global variables',
635
862
  presentationHint: 'globals',
636
863
  variablesReference: 1, # GLOBAL
637
- namedVariables: global_variables.size,
864
+ namedVariables: safe_global_variables.size,
638
865
  indexedVariables: 0,
639
866
  expensive: false,
640
867
  }]
641
868
  when :scope
642
869
  fid = args.shift
643
- frame = @target_frames[fid]
644
- if b = frame.binding
645
- vars = b.local_variables.map{|name|
646
- v = b.local_variable_get(name)
647
- variable(name, v)
648
- }
649
- special_local_variables frame do |name, val|
650
- vars.unshift variable(name, val)
651
- end
652
- vars.unshift variable('%self', b.receiver)
653
- elsif lvars = frame.local_variables
654
- vars = lvars.map{|var, val|
655
- variable(var, val)
656
- }
657
- else
658
- vars = [variable('%self', frame.self)]
659
- special_local_variables frame do |name, val|
660
- vars.push variable(name, val)
661
- end
870
+ frame = get_frame(fid)
871
+ vars = collect_locals(frame).map do |var, val|
872
+ variable(var, val)
662
873
  end
663
- event! :dap_result, :scope, req, variables: vars, tid: self.id
664
874
 
875
+ event! :protocol_result, :scope, req, variables: vars, tid: self.id
665
876
  when :variable
666
877
  vid = args.shift
667
878
  obj = @var_map[vid]
@@ -679,7 +890,7 @@ module DEBUGGER__
679
890
  case obj
680
891
  when Hash
681
892
  vars = obj.map{|k, v|
682
- variable(DEBUGGER__.safe_inspect(k), v,)
893
+ variable(value_inspect(k), v,)
683
894
  }
684
895
  when Struct
685
896
  vars = obj.members.map{|m|
@@ -688,13 +899,12 @@ module DEBUGGER__
688
899
  when String
689
900
  vars = [
690
901
  variable('#length', obj.length),
691
- variable('#encoding', obj.encoding)
902
+ variable('#encoding', obj.encoding),
692
903
  ]
904
+ printed_str = value_inspect(obj)
905
+ vars << variable('#dump', NaiveString.new(obj)) if printed_str.end_with?('...')
693
906
  when Class, Module
694
- vars = obj.instance_variables.map{|iv|
695
- variable(iv, obj.instance_variable_get(iv))
696
- }
697
- vars.unshift variable('%ancestors', obj.ancestors[1..])
907
+ vars << variable('%ancestors', obj.ancestors[1..])
698
908
  when Range
699
909
  vars = [
700
910
  variable('#begin', obj.begin),
@@ -702,62 +912,57 @@ module DEBUGGER__
702
912
  ]
703
913
  end
704
914
 
705
- vars += obj.instance_variables.map{|iv|
706
- variable(iv, obj.instance_variable_get(iv))
707
- }
708
- vars.unshift variable('#class', obj.class)
915
+ unless NaiveString === obj
916
+ vars += M_INSTANCE_VARIABLES.bind_call(obj).sort.map{|iv|
917
+ variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
918
+ }
919
+ vars.unshift variable('#class', M_CLASS.bind_call(obj))
920
+ end
709
921
  end
710
922
  end
711
- event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
923
+ event! :protocol_result, :variable, req, variables: (vars || []), tid: self.id
712
924
 
713
925
  when :evaluate
714
926
  fid, expr, context = args
715
- frame = @target_frames[fid]
927
+ frame = get_frame(fid)
716
928
  message = nil
717
929
 
718
- if frame && (b = frame.binding)
719
- b = b.dup
720
- special_local_variables current_frame do |name, var|
930
+ if frame && (b = frame.eval_binding)
931
+ special_local_variables frame do |name, var|
721
932
  b.local_variable_set(name, var) if /\%/ !~ name
722
933
  end
723
934
 
724
935
  case context
725
936
  when 'repl', 'watch'
726
- begin
727
- result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
728
- rescue Exception => e
729
- result = e
730
- end
731
-
937
+ result = dap_eval b, expr, context, prompt: '(DEBUG CONSOLE)'
732
938
  when 'hover'
733
939
  case expr
734
940
  when /\A\@\S/
735
941
  begin
736
- (r = b.receiver).instance_variable_defined?(expr) or raise(NameError)
737
- result = r.instance_variable_get(expr)
942
+ result = M_INSTANCE_VARIABLE_GET.bind_call(b.receiver, expr)
738
943
  rescue NameError
739
944
  message = "Error: Not defined instance variable: #{expr.inspect}"
740
945
  end
741
946
  when /\A\$\S/
742
- global_variables.each{|gvar|
947
+ safe_global_variables.each{|gvar|
743
948
  if gvar.to_s == expr
744
949
  result = eval(gvar.to_s)
745
950
  break false
746
951
  end
747
952
  } and (message = "Error: Not defined global variable: #{expr.inspect}")
748
- when /\A[A-Z]/
749
- unless result = search_const(b, expr)
953
+ when /\Aself$/
954
+ result = b.receiver
955
+ when /(\A((::[A-Z]|[A-Z])\w*)+)/
956
+ unless result = search_const(b, $1)
750
957
  message = "Error: Not defined constants: #{expr.inspect}"
751
958
  end
752
959
  else
753
960
  begin
754
- # try to check local variables
755
- b.local_variable_defined?(expr) or raise NameError
756
961
  result = b.local_variable_get(expr)
757
962
  rescue NameError
758
963
  # try to check method
759
- if b.receiver.respond_to? expr, include_all: true
760
- result = b.receiver.method(expr)
964
+ if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
965
+ result = M_METHOD.bind_call(b.receiver, expr)
761
966
  else
762
967
  message = "Error: Can not evaluate: #{expr.inspect}"
763
968
  end
@@ -770,17 +975,19 @@ module DEBUGGER__
770
975
  result = 'Error: Can not evaluate on this frame'
771
976
  end
772
977
 
773
- event! :dap_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
978
+ event! :protocol_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
774
979
 
775
980
  when :completions
776
981
  fid, text = args
777
- frame = @target_frames[fid]
982
+ frame = get_frame(fid)
778
983
 
779
984
  if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last
780
985
  words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
781
986
  end
782
987
 
783
- event! :dap_result, :completions, req, targets: (words || []).map{|phrase|
988
+ event! :protocol_result, :completions, req, targets: (words || []).map{|phrase|
989
+ detail = nil
990
+
784
991
  if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
785
992
  w = $1
786
993
  else
@@ -788,30 +995,37 @@ module DEBUGGER__
788
995
  end
789
996
 
790
997
  begin
791
- if b&.local_variable_defined?(w)
792
- v = b.local_variable_get(w)
793
- phrase += " (variable:#{DEBUGGER__.safe_inspect(v)})"
794
- end
998
+ v = b.local_variable_get(w)
999
+ detail ="(variable: #{value_inspect(v)})"
795
1000
  rescue NameError
796
1001
  end
797
1002
 
798
1003
  {
799
1004
  label: phrase,
800
1005
  text: w,
1006
+ detail: detail,
801
1007
  }
802
1008
  }
803
1009
 
804
1010
  else
805
- raise "Unknown req: #{args.inspect}"
1011
+ if respond_to? mid = "custom_dap_request_#{type}"
1012
+ __send__ mid, req
1013
+ else
1014
+ raise "Unknown request: #{args.inspect}"
1015
+ end
806
1016
  end
807
1017
  end
808
1018
 
809
1019
  def search_const b, expr
810
- cs = expr.split('::')
811
- [Object, *b.eval('Module.nesting')].reverse_each{|mod|
1020
+ cs = expr.delete_prefix('::').split('::')
1021
+ [Object, *b.eval('::Module.nesting')].reverse_each{|mod|
812
1022
  if cs.all?{|c|
813
1023
  if mod.const_defined?(c)
814
- mod = mod.const_get(c)
1024
+ begin
1025
+ mod = mod.const_get(c)
1026
+ rescue Exception
1027
+ false
1028
+ end
815
1029
  else
816
1030
  false
817
1031
  end
@@ -824,11 +1038,17 @@ module DEBUGGER__
824
1038
  end
825
1039
 
826
1040
  def evaluate_result r
827
- v = variable nil, r
828
- v.delete :name
829
- v.delete :value
830
- v[:result] = DEBUGGER__.safe_inspect(r)
831
- v
1041
+ variable nil, r
1042
+ end
1043
+
1044
+ def type_name obj
1045
+ klass = M_CLASS.bind_call(obj)
1046
+
1047
+ begin
1048
+ M_NAME.bind_call(klass) || klass.to_s
1049
+ rescue Exception => e
1050
+ "<Error: #{e.message} (#{e.backtrace.first}>"
1051
+ end
832
1052
  end
833
1053
 
834
1054
  def variable_ name, obj, indexedVariables: 0, namedVariables: 0
@@ -839,15 +1059,31 @@ module DEBUGGER__
839
1059
  vid = 0
840
1060
  end
841
1061
 
842
- ivnum = obj.instance_variables.size
1062
+ namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size
843
1063
 
844
- { name: name,
845
- value: DEBUGGER__.safe_inspect(obj),
846
- type: obj.class.name || obj.class.to_s,
847
- variablesReference: vid,
848
- indexedVariables: indexedVariables,
849
- namedVariables: namedVariables + ivnum,
850
- }
1064
+ if NaiveString === obj
1065
+ str = obj.str.dump
1066
+ vid = indexedVariables = namedVariables = 0
1067
+ else
1068
+ str = value_inspect(obj)
1069
+ end
1070
+
1071
+ if name
1072
+ { name: name,
1073
+ value: str,
1074
+ type: type_name(obj),
1075
+ variablesReference: vid,
1076
+ indexedVariables: indexedVariables,
1077
+ namedVariables: namedVariables,
1078
+ }
1079
+ else
1080
+ { result: str,
1081
+ type: type_name(obj),
1082
+ variablesReference: vid,
1083
+ indexedVariables: indexedVariables,
1084
+ namedVariables: namedVariables,
1085
+ }
1086
+ end
851
1087
  end
852
1088
 
853
1089
  def variable name, obj
@@ -857,7 +1093,7 @@ module DEBUGGER__
857
1093
  when Hash
858
1094
  variable_ name, obj, namedVariables: obj.size
859
1095
  when String
860
- variable_ name, obj, namedVariables: 3 # #to_str, #length, #encoding
1096
+ variable_ name, obj, namedVariables: 3 # #length, #encoding, #to_str
861
1097
  when Struct
862
1098
  variable_ name, obj, namedVariables: obj.size
863
1099
  when Class, Module