debug 1.7.1 → 1.8.0
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.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +2 -2
- data/README.md +6 -3
- data/Rakefile +8 -3
- data/lib/debug/breakpoint.rb +6 -8
- data/lib/debug/config.rb +24 -2
- data/lib/debug/dap_custom/traceInspector.rb +336 -0
- data/lib/debug/frame_info.rb +9 -0
- data/lib/debug/server.rb +5 -6
- data/lib/debug/server_cdp.rb +69 -70
- data/lib/debug/server_dap.rb +224 -178
- data/lib/debug/session.rb +73 -43
- data/lib/debug/source_repository.rb +2 -2
- data/lib/debug/thread_client.rb +33 -11
- data/lib/debug/tracer.rb +4 -5
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +1 -1
- metadata +3 -2
data/lib/debug/server_dap.rb
CHANGED
@@ -125,6 +125,7 @@ module DEBUGGER__
|
|
125
125
|
def dap_setup bytes
|
126
126
|
CONFIG.set_config no_color: true
|
127
127
|
@seq = 0
|
128
|
+
@send_lock = Mutex.new
|
128
129
|
|
129
130
|
case self
|
130
131
|
when UI_UnixDomainServer
|
@@ -212,9 +213,13 @@ module DEBUGGER__
|
|
212
213
|
if sock = @sock
|
213
214
|
kw[:seq] = @seq += 1
|
214
215
|
str = JSON.dump(kw)
|
215
|
-
|
216
|
+
@send_lock.synchronize do
|
217
|
+
sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
|
218
|
+
end
|
216
219
|
show_protocol '<', str
|
217
220
|
end
|
221
|
+
rescue Errno::EPIPE => e
|
222
|
+
$stderr.puts "#{e.inspect} rescued during sending message"
|
218
223
|
end
|
219
224
|
|
220
225
|
def send_response req, success: true, message: nil, **kw
|
@@ -246,17 +251,17 @@ module DEBUGGER__
|
|
246
251
|
end
|
247
252
|
|
248
253
|
def recv_request
|
249
|
-
|
254
|
+
IO.select([@sock])
|
250
255
|
|
251
256
|
@session.process_group.sync do
|
252
257
|
raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
|
253
258
|
|
254
|
-
case
|
259
|
+
case @sock.gets
|
255
260
|
when /Content-Length: (\d+)/
|
256
261
|
b = @sock.read(2)
|
257
262
|
raise b.inspect unless b == "\r\n"
|
258
263
|
|
259
|
-
l = @sock.read(
|
264
|
+
l = @sock.read($1.to_i)
|
260
265
|
show_protocol :>, l
|
261
266
|
JSON.load(l)
|
262
267
|
when nil
|
@@ -269,198 +274,225 @@ module DEBUGGER__
|
|
269
274
|
retry
|
270
275
|
end
|
271
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
|
+
|
272
296
|
def process
|
273
297
|
while req = recv_request
|
274
|
-
|
275
|
-
|
298
|
+
process_request(req)
|
299
|
+
end
|
300
|
+
ensure
|
301
|
+
send_event :terminated unless @sock.closed?
|
302
|
+
end
|
276
303
|
|
277
|
-
|
304
|
+
def process_request req
|
305
|
+
raise "not a request: #{req.inspect}" unless req['type'] == 'request'
|
306
|
+
args = req.dig('arguments')
|
278
307
|
|
279
|
-
|
280
|
-
when 'launch'
|
281
|
-
send_response req
|
282
|
-
# `launch` runs on debuggee on the same file system
|
283
|
-
UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap') || true
|
284
|
-
@nonstop = true
|
308
|
+
case req['command']
|
285
309
|
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|
289
316
|
|
290
|
-
|
291
|
-
@nonstop = true
|
292
|
-
else
|
293
|
-
@nonstop = false
|
294
|
-
end
|
317
|
+
load_extensions req
|
295
318
|
|
296
|
-
|
297
|
-
|
319
|
+
when 'attach'
|
320
|
+
send_response req
|
321
|
+
UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap')
|
298
322
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
threadId: 1, # maybe ...
|
305
|
-
allThreadsStopped: true
|
306
|
-
end
|
307
|
-
end
|
323
|
+
if req.dig('arguments', 'nonstop') == true
|
324
|
+
@nonstop = true
|
325
|
+
else
|
326
|
+
@nonstop = false
|
327
|
+
end
|
308
328
|
|
309
|
-
|
310
|
-
req_path = args.dig('source', 'path')
|
311
|
-
path = UI_DAP.local_to_remote_path(req_path)
|
329
|
+
load_extensions req
|
312
330
|
|
313
|
-
|
314
|
-
|
331
|
+
when 'configurationDone'
|
332
|
+
send_response req
|
315
333
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
end
|
324
|
-
}
|
325
|
-
send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
|
326
|
-
else
|
327
|
-
send_response req, success: false, message: "#{req_path} is not available"
|
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
|
328
341
|
end
|
342
|
+
end
|
329
343
|
|
330
|
-
|
331
|
-
|
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
|
349
|
+
|
350
|
+
bps = []
|
351
|
+
args['breakpoints'].each{|bp|
|
352
|
+
line = bp['line']
|
353
|
+
if cond = bp['condition']
|
354
|
+
bps << SESSION.add_line_breakpoint(path, line, cond: cond)
|
355
|
+
else
|
356
|
+
bps << SESSION.add_line_breakpoint(path, line)
|
357
|
+
end
|
358
|
+
}
|
359
|
+
send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
|
360
|
+
else
|
361
|
+
send_response req, success: false, message: "#{req_path} is not available"
|
362
|
+
end
|
332
363
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
364
|
+
when 'setFunctionBreakpoints'
|
365
|
+
send_response req
|
366
|
+
|
367
|
+
when 'setExceptionBreakpoints'
|
368
|
+
process_filter = ->(filter_id, cond = nil) {
|
369
|
+
bp =
|
370
|
+
case filter_id
|
371
|
+
when 'any'
|
372
|
+
SESSION.add_catch_breakpoint 'Exception', cond: cond
|
373
|
+
when 'RuntimeError'
|
374
|
+
SESSION.add_catch_breakpoint 'RuntimeError', cond: cond
|
375
|
+
else
|
376
|
+
nil
|
377
|
+
end
|
378
|
+
{
|
379
|
+
verified: !bp.nil?,
|
380
|
+
message: bp.inspect,
|
348
381
|
}
|
382
|
+
}
|
349
383
|
|
350
|
-
|
351
|
-
|
352
|
-
filters = args.fetch('filters').map {|filter_id|
|
353
|
-
process_filter.call(filter_id)
|
354
|
-
}
|
384
|
+
SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError'
|
355
385
|
|
356
|
-
|
357
|
-
process_filter.call(
|
386
|
+
filters = args.fetch('filters').map {|filter_id|
|
387
|
+
process_filter.call(filter_id)
|
358
388
|
}
|
359
389
|
|
360
|
-
|
390
|
+
filters += args.fetch('filterOptions', {}).map{|bp_info|
|
391
|
+
process_filter.call(bp_info['filterId'], bp_info['condition'])
|
392
|
+
}
|
361
393
|
|
362
|
-
|
363
|
-
terminate = args.fetch("terminateDebuggee", false)
|
394
|
+
send_response req, breakpoints: filters
|
364
395
|
|
365
|
-
|
366
|
-
|
396
|
+
when 'disconnect'
|
397
|
+
terminate = args.fetch("terminateDebuggee", false)
|
367
398
|
|
368
|
-
|
369
|
-
|
370
|
-
@q_msg << 'kill!'
|
371
|
-
else
|
372
|
-
@q_msg << 'continue'
|
373
|
-
end
|
374
|
-
else
|
375
|
-
if terminate
|
376
|
-
@q_msg << 'kill!'
|
377
|
-
pause
|
378
|
-
end
|
379
|
-
end
|
399
|
+
SESSION.clear_all_breakpoints
|
400
|
+
send_response req
|
380
401
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
begin
|
387
|
-
@session.check_postmortem
|
388
|
-
@q_msg << 'n'
|
389
|
-
send_response req
|
390
|
-
rescue PostmortemError
|
391
|
-
send_response req,
|
392
|
-
success: false, message: 'postmortem mode',
|
393
|
-
result: "'Next' is not supported while postmortem mode"
|
394
|
-
end
|
395
|
-
when 'stepIn'
|
396
|
-
begin
|
397
|
-
@session.check_postmortem
|
398
|
-
@q_msg << 's'
|
399
|
-
send_response req
|
400
|
-
rescue PostmortemError
|
401
|
-
send_response req,
|
402
|
-
success: false, message: 'postmortem mode',
|
403
|
-
result: "'stepIn' is not supported while postmortem mode"
|
402
|
+
if SESSION.in_subsession?
|
403
|
+
if terminate
|
404
|
+
@q_msg << 'kill!'
|
405
|
+
else
|
406
|
+
@q_msg << 'continue'
|
404
407
|
end
|
405
|
-
|
406
|
-
|
407
|
-
@
|
408
|
-
|
409
|
-
send_response req
|
410
|
-
rescue PostmortemError
|
411
|
-
send_response req,
|
412
|
-
success: false, message: 'postmortem mode',
|
413
|
-
result: "'stepOut' is not supported while postmortem mode"
|
408
|
+
else
|
409
|
+
if terminate
|
410
|
+
@q_msg << 'kill!'
|
411
|
+
pause
|
414
412
|
end
|
415
|
-
|
413
|
+
end
|
414
|
+
|
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'
|
416
423
|
send_response req
|
417
|
-
|
418
|
-
|
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'
|
419
433
|
send_response req
|
420
|
-
|
421
|
-
when 'reverseContinue'
|
434
|
+
rescue PostmortemError
|
422
435
|
send_response req,
|
423
|
-
success: false, message: '
|
424
|
-
result: "
|
425
|
-
|
426
|
-
|
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
|
427
461
|
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
}
|
462
|
+
## query
|
463
|
+
when 'threads'
|
464
|
+
send_response req, threads: SESSION.managed_thread_clients.map{|tc|
|
465
|
+
{ id: tc.id,
|
466
|
+
name: tc.name,
|
434
467
|
}
|
468
|
+
}
|
435
469
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
end
|
447
|
-
when 'stackTrace',
|
448
|
-
'scopes',
|
449
|
-
'variables',
|
450
|
-
'source',
|
451
|
-
'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
|
452
480
|
@q_msg << req
|
481
|
+
end
|
482
|
+
when 'stackTrace',
|
483
|
+
'scopes',
|
484
|
+
'variables',
|
485
|
+
'source',
|
486
|
+
'completions'
|
487
|
+
@q_msg << req
|
453
488
|
|
489
|
+
else
|
490
|
+
if respond_to? mid = "custom_dap_request_#{req['command']}"
|
491
|
+
__send__ mid, req
|
454
492
|
else
|
455
|
-
|
456
|
-
send mid, req
|
457
|
-
else
|
458
|
-
raise "Unknown request: #{req.inspect}"
|
459
|
-
end
|
493
|
+
raise "Unknown request: #{req.inspect}"
|
460
494
|
end
|
461
495
|
end
|
462
|
-
ensure
|
463
|
-
send_event :terminated unless @sock.closed?
|
464
496
|
end
|
465
497
|
|
466
498
|
## called by the SESSION thread
|
@@ -546,7 +578,7 @@ module DEBUGGER__
|
|
546
578
|
when 'stackTrace'
|
547
579
|
tid = req.dig('arguments', 'threadId')
|
548
580
|
|
549
|
-
if
|
581
|
+
if find_waiting_tc(tid)
|
550
582
|
request_tc [:dap, :backtrace, req]
|
551
583
|
else
|
552
584
|
fail_response req
|
@@ -555,7 +587,7 @@ module DEBUGGER__
|
|
555
587
|
frame_id = req.dig('arguments', 'frameId')
|
556
588
|
if @frame_map[frame_id]
|
557
589
|
tid, fid = @frame_map[frame_id]
|
558
|
-
if
|
590
|
+
if find_waiting_tc(tid)
|
559
591
|
request_tc [:dap, :scopes, req, fid]
|
560
592
|
else
|
561
593
|
fail_response req
|
@@ -591,7 +623,7 @@ module DEBUGGER__
|
|
591
623
|
frame_id = ref[1]
|
592
624
|
tid, fid = @frame_map[frame_id]
|
593
625
|
|
594
|
-
if
|
626
|
+
if find_waiting_tc(tid)
|
595
627
|
request_tc [:dap, :scope, req, fid]
|
596
628
|
else
|
597
629
|
fail_response req
|
@@ -600,7 +632,7 @@ module DEBUGGER__
|
|
600
632
|
when :variable
|
601
633
|
tid, vid = ref[1], ref[2]
|
602
634
|
|
603
|
-
if
|
635
|
+
if find_waiting_tc(tid)
|
604
636
|
request_tc [:dap, :variable, req, vid]
|
605
637
|
else
|
606
638
|
fail_response req
|
@@ -619,7 +651,8 @@ module DEBUGGER__
|
|
619
651
|
tid, fid = @frame_map[frame_id]
|
620
652
|
expr = req.dig('arguments', 'expression')
|
621
653
|
|
622
|
-
if
|
654
|
+
if find_waiting_tc(tid)
|
655
|
+
restart_all_threads
|
623
656
|
request_tc [:dap, :evaluate, req, fid, expr, context]
|
624
657
|
else
|
625
658
|
fail_response req
|
@@ -640,7 +673,7 @@ module DEBUGGER__
|
|
640
673
|
frame_id = req.dig('arguments', 'frameId')
|
641
674
|
tid, fid = @frame_map[frame_id]
|
642
675
|
|
643
|
-
if
|
676
|
+
if find_waiting_tc(tid)
|
644
677
|
text = req.dig('arguments', 'text')
|
645
678
|
line = req.dig('arguments', 'line')
|
646
679
|
if col = req.dig('arguments', 'column')
|
@@ -651,11 +684,15 @@ module DEBUGGER__
|
|
651
684
|
fail_response req
|
652
685
|
end
|
653
686
|
else
|
654
|
-
|
687
|
+
if respond_to? mid = "custom_dap_request_#{req['command']}"
|
688
|
+
__send__ mid, req
|
689
|
+
else
|
690
|
+
raise "Unknown request: #{req.inspect}"
|
691
|
+
end
|
655
692
|
end
|
656
693
|
end
|
657
694
|
|
658
|
-
def
|
695
|
+
def process_protocol_result args
|
659
696
|
# puts({dap_event: args}.inspect)
|
660
697
|
type, req, result = args
|
661
698
|
|
@@ -692,6 +729,7 @@ module DEBUGGER__
|
|
692
729
|
register_vars result[:variables], tid
|
693
730
|
@ui.respond req, result
|
694
731
|
when :evaluate
|
732
|
+
stop_all_threads
|
695
733
|
message = result.delete :message
|
696
734
|
if message
|
697
735
|
@ui.respond req, success: false, message: message
|
@@ -703,7 +741,11 @@ module DEBUGGER__
|
|
703
741
|
when :completions
|
704
742
|
@ui.respond req, result
|
705
743
|
else
|
706
|
-
|
744
|
+
if respond_to? mid = "custom_dap_request_event_#{type}"
|
745
|
+
__send__ mid, req, result
|
746
|
+
else
|
747
|
+
raise "unsupported: #{args.inspect}"
|
748
|
+
end
|
707
749
|
end
|
708
750
|
end
|
709
751
|
|
@@ -789,7 +831,7 @@ module DEBUGGER__
|
|
789
831
|
}
|
790
832
|
end
|
791
833
|
|
792
|
-
event! :
|
834
|
+
event! :protocol_result, :backtrace, req, {
|
793
835
|
stackFrames: frames,
|
794
836
|
totalFrames: @target_frames.size,
|
795
837
|
}
|
@@ -806,7 +848,7 @@ module DEBUGGER__
|
|
806
848
|
0
|
807
849
|
end
|
808
850
|
|
809
|
-
event! :
|
851
|
+
event! :protocol_result, :scopes, req, scopes: [{
|
810
852
|
name: 'Local variables',
|
811
853
|
presentationHint: 'locals',
|
812
854
|
# variablesReference: N, # filled by SESSION
|
@@ -828,7 +870,7 @@ module DEBUGGER__
|
|
828
870
|
variable(var, val)
|
829
871
|
end
|
830
872
|
|
831
|
-
event! :
|
873
|
+
event! :protocol_result, :scope, req, variables: vars, tid: self.id
|
832
874
|
when :variable
|
833
875
|
vid = args.shift
|
834
876
|
obj = @var_map[vid]
|
@@ -854,7 +896,7 @@ module DEBUGGER__
|
|
854
896
|
}
|
855
897
|
when String
|
856
898
|
vars = [
|
857
|
-
variable('#
|
899
|
+
variable('#length', obj.length),
|
858
900
|
variable('#encoding', obj.encoding),
|
859
901
|
]
|
860
902
|
printed_str = value_inspect(obj)
|
@@ -876,7 +918,7 @@ module DEBUGGER__
|
|
876
918
|
end
|
877
919
|
end
|
878
920
|
end
|
879
|
-
event! :
|
921
|
+
event! :protocol_result, :variable, req, variables: (vars || []), tid: self.id
|
880
922
|
|
881
923
|
when :evaluate
|
882
924
|
fid, expr, context = args
|
@@ -931,7 +973,7 @@ module DEBUGGER__
|
|
931
973
|
result = 'Error: Can not evaluate on this frame'
|
932
974
|
end
|
933
975
|
|
934
|
-
event! :
|
976
|
+
event! :protocol_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
|
935
977
|
|
936
978
|
when :completions
|
937
979
|
fid, text = args
|
@@ -941,7 +983,7 @@ module DEBUGGER__
|
|
941
983
|
words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
|
942
984
|
end
|
943
985
|
|
944
|
-
event! :
|
986
|
+
event! :protocol_result, :completions, req, targets: (words || []).map{|phrase|
|
945
987
|
detail = nil
|
946
988
|
|
947
989
|
if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
|
@@ -964,7 +1006,11 @@ module DEBUGGER__
|
|
964
1006
|
}
|
965
1007
|
|
966
1008
|
else
|
967
|
-
|
1009
|
+
if respond_to? mid = "custom_dap_request_#{type}"
|
1010
|
+
__send__ mid, req
|
1011
|
+
else
|
1012
|
+
raise "Unknown request: #{args.inspect}"
|
1013
|
+
end
|
968
1014
|
end
|
969
1015
|
end
|
970
1016
|
|
@@ -997,7 +1043,7 @@ module DEBUGGER__
|
|
997
1043
|
klass = M_CLASS.bind_call(obj)
|
998
1044
|
|
999
1045
|
begin
|
1000
|
-
klass
|
1046
|
+
M_NAME.bind_call(klass) || klass.to_s
|
1001
1047
|
rescue Exception => e
|
1002
1048
|
"<Error: #{e.message} (#{e.backtrace.first}>"
|
1003
1049
|
end
|