debug 1.0.0.beta4 → 1.0.0.beta5

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