debug 1.0.0.beta4 → 1.0.0.beta5

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