debug 1.4.0 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
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