debug 1.0.0.beta2 → 1.0.0.beta7

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.
@@ -0,0 +1,607 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module DEBUGGER__
6
+ module UI_DAP
7
+ SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
8
+
9
+ def dap_setup bytes
10
+ DEBUGGER__.set_config(no_color: true)
11
+ @seq = 0
12
+
13
+ $stderr.puts '[>]' + bytes if SHOW_PROTOCOL
14
+ req = JSON.load(bytes)
15
+
16
+ # capability
17
+ send_response(req,
18
+ ## Supported
19
+ supportsConfigurationDoneRequest: true,
20
+ supportsFunctionBreakpoints: true,
21
+ supportsConditionalBreakpoints: true,
22
+ supportTerminateDebuggee: true,
23
+ supportsTerminateRequest: true,
24
+ exceptionBreakpointFilters: [
25
+ {
26
+ filter: 'any',
27
+ label: 'rescue any exception',
28
+ #supportsCondition: true,
29
+ #conditionDescription: '',
30
+ },
31
+ {
32
+ filter: 'RuntimeError',
33
+ label: 'rescue RuntimeError',
34
+ default: true,
35
+ #supportsCondition: true,
36
+ #conditionDescription: '',
37
+ },
38
+ ],
39
+ supportsExceptionFilterOptions: true,
40
+
41
+ ## Will be supported
42
+ # supportsExceptionOptions: true,
43
+ # supportsHitConditionalBreakpoints:
44
+ # supportsEvaluateForHovers:
45
+ # supportsSetVariable: true,
46
+ # supportSuspendDebuggee:
47
+ # supportsLogPoints:
48
+ # supportsLoadedSourcesRequest:
49
+ # supportsDataBreakpoints:
50
+ # supportsBreakpointLocationsRequest:
51
+
52
+ ## Possible?
53
+ # supportsStepBack:
54
+ # supportsRestartFrame:
55
+ # supportsCompletionsRequest:
56
+ # completionTriggerCharacters:
57
+ # supportsModulesRequest:
58
+ # additionalModuleColumns:
59
+ # supportedChecksumAlgorithms:
60
+ # supportsRestartRequest:
61
+ # supportsValueFormattingOptions:
62
+ # supportsExceptionInfoRequest:
63
+ # supportsDelayedStackTraceLoading:
64
+ # supportsTerminateThreadsRequest:
65
+ # supportsSetExpression:
66
+ # supportsClipboardContext:
67
+
68
+ ## Never
69
+ # supportsGotoTargetsRequest:
70
+ # supportsStepInTargetsRequest:
71
+ # supportsReadMemoryRequest:
72
+ # supportsDisassembleRequest:
73
+ # supportsCancelRequest:
74
+ # supportsSteppingGranularity:
75
+ # supportsInstructionBreakpoints:
76
+ )
77
+ send_event 'initialized'
78
+ end
79
+
80
+ def send **kw
81
+ kw[:seq] = @seq += 1
82
+ str = JSON.dump(kw)
83
+ $stderr.puts "[<] #{str}" if SHOW_PROTOCOL
84
+ # STDERR.puts "[STDERR] [<] #{str}"
85
+ @sock.print header = "Content-Length: #{str.size}\r\n\r\n"
86
+ @sock.write str
87
+ end
88
+
89
+ def send_response req, success: true, **kw
90
+ if kw.empty?
91
+ send type: 'response',
92
+ command: req['command'],
93
+ request_seq: req['seq'],
94
+ success: success,
95
+ message: success ? 'Success' : 'Failed'
96
+ else
97
+ send type: 'response',
98
+ command: req['command'],
99
+ request_seq: req['seq'],
100
+ success: success,
101
+ message: success ? 'Success' : 'Failed',
102
+ body: kw
103
+ end
104
+ end
105
+
106
+ def send_event name, **kw
107
+ if kw.empty?
108
+ send type: 'event', event: name
109
+ else
110
+ send type: 'event', event: name, body: kw
111
+ end
112
+ end
113
+
114
+ def recv_request
115
+ case header = @sock.gets
116
+ when /Content-Length: (\d+)/
117
+ b = @sock.read(2)
118
+ raise b.inspect unless b == "\r\n"
119
+
120
+ l = @sock.read(s = $1.to_i)
121
+ $stderr.puts "[>] #{l}" if SHOW_PROTOCOL
122
+ JSON.load(l)
123
+ when nil
124
+ nil
125
+ else
126
+ raise "unrecognized line: #{l} (#{l.size} bytes)"
127
+ end
128
+ end
129
+
130
+ def process
131
+ while req = recv_request
132
+ raise "not a request: #{req.inpsect}" unless req['type'] == 'request'
133
+ args = req.dig('arguments')
134
+
135
+ case req['command']
136
+
137
+ ## boot/configuration
138
+ when 'launch'
139
+ send_response req
140
+ when 'setBreakpoints'
141
+ path = args.dig('source', 'path')
142
+ bp_args = args['breakpoints']
143
+ bps = []
144
+ bp_args.each{|bp|
145
+ line = bp['line']
146
+ if cond = bp['condition']
147
+ bps << SESSION.add_line_breakpoint(path, line, cond: cond)
148
+ else
149
+ bps << SESSION.add_line_breakpoint(path, line)
150
+ end
151
+ }
152
+ send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
153
+ when 'setFunctionBreakpoints'
154
+ send_response req
155
+ when 'setExceptionBreakpoints'
156
+ filters = args.dig('filterOptions').map{|bp_info|
157
+ case bp_info.dig('filterId')
158
+ when 'any'
159
+ bp = SESSION.add_catch_breakpoint 'Exception'
160
+ when 'RuntimeError'
161
+ bp = SESSION.add_catch_breakpoint 'RuntimeError'
162
+ else
163
+ bp = nil
164
+ end
165
+ {
166
+ verifiled: bp ? true : false,
167
+ message: bp.inspect,
168
+ }
169
+ }
170
+ send_response req, breakpoints: filters
171
+ when 'configurationDone'
172
+ send_response req
173
+ @q_msg << 'continue'
174
+ when 'attach'
175
+ send_response req
176
+ Process.kill(:SIGINT, Process.pid)
177
+ when 'disconnect'
178
+ send_response req
179
+ @q_msg << 'continue'
180
+
181
+ ## control
182
+ when 'continue'
183
+ @q_msg << 'c'
184
+ send_response req, allThreadsContinued: true
185
+ when 'next'
186
+ @q_msg << 'n'
187
+ send_response req
188
+ when 'stepIn'
189
+ @q_msg << 's'
190
+ send_response req
191
+ when 'stepOut'
192
+ @q_msg << 'fin'
193
+ send_response req
194
+ when 'terminate'
195
+ send_response req
196
+ exit
197
+ when 'pause'
198
+ send_response req
199
+ Process.kill(:SIGINT, Process.pid)
200
+
201
+ ## query
202
+ when 'threads'
203
+ send_response req, threads: SESSION.managed_thread_clients.map{|tc|
204
+ { id: tc.id,
205
+ name: tc.name,
206
+ }
207
+ }
208
+
209
+ when 'stackTrace',
210
+ 'scopes',
211
+ 'variables',
212
+ 'evaluate',
213
+ 'source'
214
+ @q_msg << req
215
+ else
216
+ raise "Unknown request: #{req.inspect}"
217
+ end
218
+ end
219
+ end
220
+
221
+ ## called by the SESSION thread
222
+
223
+ def readline
224
+ @q_msg.pop || 'kill!'
225
+ end
226
+
227
+ def sock skip: false
228
+ yield $stderr
229
+ end
230
+
231
+ def respond req, res
232
+ send_response(req, **res)
233
+ end
234
+
235
+ def puts result
236
+ # STDERR.puts "puts: #{result}"
237
+ # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
238
+ end
239
+
240
+ def event type, *args
241
+ case type
242
+ when :suspend_bp
243
+ _i, bp = *args
244
+ if bp.kind_of?(CatchBreakpoint)
245
+ reason = 'exception'
246
+ text = bp.description
247
+ else
248
+ reason = 'breakpoint'
249
+ text = bp ? bp.description : 'temporary bp'
250
+ end
251
+
252
+ send_event 'stopped', reason: reason,
253
+ description: text,
254
+ text: text,
255
+ threadId: 1,
256
+ allThreadsStopped: true
257
+ when :suspend_trap
258
+ send_event 'stopped', reason: 'pause',
259
+ threadId: 1,
260
+ allThreadsStopped: true
261
+ when :suspended
262
+ send_event 'stopped', reason: 'step',
263
+ threadId: 1,
264
+ allThreadsStopped: true
265
+ end
266
+ end
267
+ end
268
+
269
+ class Session
270
+ def find_tc id
271
+ @th_clients.each{|th, tc|
272
+ return tc if tc.id == id
273
+ }
274
+ return nil
275
+ end
276
+
277
+ def fail_response req, **kw
278
+ @ui.respond req, success: false, **kw
279
+ return :retry
280
+ end
281
+
282
+ def process_dap_request req
283
+ case req['command']
284
+ when 'stackTrace'
285
+ tid = req.dig('arguments', 'threadId')
286
+ if tc = find_tc(tid)
287
+ tc << [:dap, :backtrace, req]
288
+ else
289
+ fail_response req
290
+ end
291
+ when 'scopes'
292
+ frame_id = req.dig('arguments', 'frameId')
293
+ if @frame_map[frame_id]
294
+ tid, fid = @frame_map[frame_id]
295
+ if tc = find_tc(tid)
296
+ tc << [:dap, :scopes, req, fid]
297
+ else
298
+ fail_response req
299
+ end
300
+ else
301
+ fail_response req
302
+ end
303
+ when 'variables'
304
+ varid = req.dig('arguments', 'variablesReference')
305
+ if ref = @var_map[varid]
306
+ case ref[0]
307
+ when :globals
308
+ vars = global_variables.map do |name|
309
+ File.write('/tmp/x', "#{name}\n")
310
+ gv = 'Not implemented yet...'
311
+ {
312
+ name: name,
313
+ value: gv.inspect,
314
+ type: (gv.class.name || gv.class.to_s),
315
+ variablesReference: 0,
316
+ }
317
+ end
318
+
319
+ @ui.respond req, {
320
+ variables: vars,
321
+ }
322
+ return :retry
323
+
324
+ when :scope
325
+ frame_id = ref[1]
326
+ tid, fid = @frame_map[frame_id]
327
+
328
+ if tc = find_tc(tid)
329
+ tc << [:dap, :scope, req, fid]
330
+ else
331
+ fail_response req
332
+ end
333
+
334
+ when :variable
335
+ tid, vid = ref[1], ref[2]
336
+
337
+ if tc = find_tc(tid)
338
+ tc << [:dap, :variable, req, vid]
339
+ else
340
+ fail_response req
341
+ end
342
+ else
343
+ raise "Uknown type: #{ref.inspect}"
344
+ end
345
+ else
346
+ fail_response req
347
+ end
348
+ when 'evaluate'
349
+ frame_id = req.dig('arguments', 'frameId')
350
+ if @frame_map[frame_id]
351
+ tid, fid = @frame_map[frame_id]
352
+ expr = req.dig('arguments', 'expression')
353
+ if tc = find_tc(tid)
354
+ tc << [:dap, :evaluate, req, fid, expr]
355
+ else
356
+ fail_response req
357
+ end
358
+ else
359
+ fail_response req, result: "can't evaluate"
360
+ end
361
+ when 'source'
362
+ ref = req.dig('arguments', 'sourceReference')
363
+ if src = @src_map[ref]
364
+ @ui.respond req, content: src.join
365
+ else
366
+ fail_response req, message: 'not found...'
367
+ end
368
+
369
+ return :retry
370
+ else
371
+ raise "Unknown DAP request: #{req.inspect}"
372
+ end
373
+ end
374
+
375
+ def dap_event args
376
+ # puts({dap_event: args}.inspect)
377
+ type, req, result = args
378
+
379
+ case type
380
+ when :backtrace
381
+ result[:stackFrames].each.with_index{|fi, i|
382
+ fi[:id] = id = @frame_map.size + 1
383
+ @frame_map[id] = [req.dig('arguments', 'threadId'), i]
384
+ if fi[:source] && src = fi[:source][:sourceReference]
385
+ src_id = @src_map.size + 1
386
+ @src_map[src_id] = src
387
+ fi[:source][:sourceReference] = src_id
388
+ end
389
+ }
390
+ @ui.respond req, result
391
+ when :scopes
392
+ frame_id = req.dig('arguments', 'frameId')
393
+ local_scope = result[:scopes].first
394
+ local_scope[:variablesReference] = id = @var_map.size + 1
395
+
396
+ @var_map[id] = [:scope, frame_id]
397
+ @ui.respond req, result
398
+ when :scope
399
+ tid = result.delete :tid
400
+ register_vars result[:variables], tid
401
+ @ui.respond req, result
402
+ when :variable
403
+ tid = result.delete :tid
404
+ register_vars result[:variables], tid
405
+ @ui.respond req, result
406
+ when :evaluate
407
+ tid = result.delete :tid
408
+ register_var result, tid
409
+ @ui.respond req, result
410
+ else
411
+ raise "unsupported: #{args.inspect}"
412
+ end
413
+ end
414
+
415
+ def register_var v, tid
416
+ if (tl_vid = v[:variablesReference]) > 0
417
+ vid = @var_map.size + 1
418
+ @var_map[vid] = [:variable, tid, tl_vid]
419
+ v[:variablesReference] = vid
420
+ end
421
+ end
422
+
423
+ def register_vars vars, tid
424
+ raise tid.inspect unless tid.kind_of?(Integer)
425
+ vars.each{|v|
426
+ register_var v, tid
427
+ }
428
+ end
429
+ end
430
+
431
+ class ThreadClient
432
+ def process_dap args
433
+ # pp tc: self, args: args
434
+ type = args.shift
435
+ req = args.shift
436
+
437
+ case type
438
+ when :backtrace
439
+ event! :dap_result, :backtrace, req, {
440
+ stackFrames: @target_frames.map.with_index{|frame, i|
441
+ path = frame.realpath
442
+ ref = frame.file_lines unless File.exist?(path)
443
+
444
+ {
445
+ # id: ??? # filled by SESSION
446
+ name: frame.name,
447
+ line: frame.location.lineno,
448
+ column: 1,
449
+ source: {
450
+ name: File.basename(frame.path),
451
+ path: path,
452
+ sourceReference: ref,
453
+ },
454
+ }
455
+ }
456
+ }
457
+ when :scopes
458
+ fid = args.shift
459
+ frame = @target_frames[fid]
460
+ lnum = frame.binding ? frame.binding.local_variables.size : 0
461
+
462
+ event! :dap_result, :scopes, req, scopes: [{
463
+ name: 'Local variables',
464
+ presentationHint: 'locals',
465
+ # variablesReference: N, # filled by SESSION
466
+ namedVariables: lnum,
467
+ indexedVariables: 0,
468
+ expensive: false,
469
+ }, {
470
+ name: 'Global variables',
471
+ presentationHint: 'globals',
472
+ variablesReference: 1, # GLOBAL
473
+ namedVariables: global_variables.size,
474
+ indexedVariables: 0,
475
+ expensive: false,
476
+ }]
477
+ when :scope
478
+ fid = args.shift
479
+ frame = @target_frames[fid]
480
+ if b = frame.binding
481
+ vars = b.local_variables.map{|name|
482
+ v = b.local_variable_get(name)
483
+ variable(name, v)
484
+ }
485
+ vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
486
+ vars.unshift variable('%return', frame.return_value) if frame.has_return_value
487
+ vars.unshift variable('%self', b.receiver)
488
+ else
489
+ vars = [variable('%self', frame.self)]
490
+ vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
491
+ vars.push variable('%return', frame.return_value) if frame.has_return_value
492
+ end
493
+ event! :dap_result, :scope, req, variables: vars, tid: self.id
494
+
495
+ when :variable
496
+ vid = args.shift
497
+ obj = @var_map[vid]
498
+ if obj
499
+ case req.dig('arguments', 'filter')
500
+ when 'indexed'
501
+ start = req.dig('arguments', 'start') || 0
502
+ count = req.dig('arguments', 'count') || obj.size
503
+ vars = (start ... (start + count)).map{|i|
504
+ variable(i.to_s, obj[i])
505
+ }
506
+ else
507
+ vars = []
508
+
509
+ case obj
510
+ when Hash
511
+ vars = obj.map{|k, v|
512
+ variable(DEBUGGER__.short_inspect(k), v)
513
+ }
514
+ when Struct
515
+ vars = obj.members.map{|m|
516
+ variable(m, obj[m])
517
+ }
518
+ when String
519
+ vars = [
520
+ variable('#length', obj.length),
521
+ variable('#encoding', obj.encoding)
522
+ ]
523
+ when Class, Module
524
+ vars = obj.instance_variables.map{|iv|
525
+ variable(iv, obj.instance_variable_get(iv))
526
+ }
527
+ vars.unshift variable('%ancestors', obj.ancestors[1..])
528
+ when Range
529
+ vars = [
530
+ variable('#begin', obj.begin),
531
+ variable('#end', obj.end),
532
+ ]
533
+ end
534
+
535
+ vars += obj.instance_variables.map{|iv|
536
+ variable(iv, obj.instance_variable_get(iv))
537
+ }
538
+ vars.unshift variable('#class', obj.class)
539
+ end
540
+ end
541
+ event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
542
+
543
+ when :evaluate
544
+ fid, expr = args
545
+ frame = @target_frames[fid]
546
+
547
+ if frame && (b = frame.binding)
548
+ begin
549
+ result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
550
+ rescue Exception => e
551
+ result = e
552
+ end
553
+ else
554
+ result = 'can not evaluate on this frame...'
555
+ end
556
+ event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
557
+ else
558
+ raise "Unkown req: #{args.inspect}"
559
+ end
560
+ end
561
+
562
+ def evaluate_result r
563
+ v = variable nil, r
564
+ v.delete(:name)
565
+ v[:result] = DEBUGGER__.short_inspect(r)
566
+ v
567
+ end
568
+
569
+ def variable_ name, obj, indexedVariables: 0, namedVariables: 0, use_short: true
570
+ if indexedVariables > 0 || namedVariables > 0
571
+ vid = @var_map.size + 1
572
+ @var_map[vid] = obj
573
+ else
574
+ vid = 0
575
+ end
576
+
577
+ ivnum = obj.instance_variables.size
578
+
579
+ { name: name,
580
+ value: DEBUGGER__.short_inspect(obj, use_short),
581
+ type: obj.class.name || obj.class.to_s,
582
+ variablesReference: vid,
583
+ indexedVariables: indexedVariables,
584
+ namedVariables: namedVariables + ivnum,
585
+ }
586
+ end
587
+
588
+ def variable name, obj
589
+ case obj
590
+ when Array
591
+ variable_ name, obj, indexedVariables: obj.size
592
+ when Hash
593
+ variable_ name, obj, namedVariables: obj.size
594
+ when String
595
+ variable_ name, obj, use_short: false, namedVariables: 3 # #to_str, #length, #encoding
596
+ when Struct
597
+ variable_ name, obj, namedVariables: obj.size
598
+ when Class, Module
599
+ variable_ name, obj, namedVariables: 1 # %ancestors (#ancestors without self)
600
+ when Range
601
+ variable_ name, obj, namedVariables: 2 # #begin, #end
602
+ else
603
+ variable_ name, obj, namedVariables: 1 # #class
604
+ end
605
+ end
606
+ end
607
+ end