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