@@ -29,6 +29,9 @@ class TestProcess : XCTestCase {
29
29
( " test_no_environment " , test_no_environment) ,
30
30
( " test_custom_environment " , test_custom_environment) ,
31
31
( " test_run " , test_run) ,
32
+ ( " test_interrupt " , test_interrupt) ,
33
+ ( " test_terminate " , test_terminate) ,
34
+ ( " test_suspend_resume " , test_suspend_resume) ,
32
35
]
33
36
#endif
34
37
}
@@ -385,6 +388,113 @@ class TestProcess : XCTestCase {
385
388
fm. changeCurrentDirectoryPath ( cwd)
386
389
}
387
390
391
+ func test_interrupt( ) {
392
+ let helper = _SignalHelperRunner ( )
393
+ do {
394
+ try helper. start ( )
395
+ } catch {
396
+ XCTFail ( " Cant run xdgTestHelper: \( error) " )
397
+ return
398
+ }
399
+ if !helper. waitForReady ( ) {
400
+ XCTFail ( " Didnt receive Ready from sub-process " )
401
+ return
402
+ }
403
+
404
+ let now = DispatchTime . now ( ) . uptimeNanoseconds
405
+ let timeout = DispatchTime ( uptimeNanoseconds: now + 2_000_000_000 )
406
+
407
+ var count = 3
408
+ while count > 0 {
409
+ helper. process. interrupt ( )
410
+ guard helper. semaphore. wait ( timeout: timeout) == . success else {
411
+ helper. process. terminate ( )
412
+ XCTFail ( " Timedout waiting for signal " )
413
+ return
414
+ }
415
+
416
+ if helper. sigIntCount == 3 {
417
+ break
418
+ }
419
+ count -= 1
420
+ }
421
+ helper. process. terminate ( )
422
+ XCTAssertEqual ( helper. sigIntCount, 3 )
423
+ helper. process. waitUntilExit ( )
424
+ let terminationReason = helper. process. terminationReason
425
+ XCTAssertEqual ( terminationReason, Process . TerminationReason. exit)
426
+ let status = helper. process. terminationStatus
427
+ XCTAssertEqual ( status, 99 )
428
+ }
429
+
430
+ func test_terminate( ) {
431
+ let cat = URL ( fileURLWithPath: " /bin/cat " , isDirectory: false )
432
+ guard let process = try ? Process . run ( cat, arguments: [ ] ) else {
433
+ XCTFail ( " Cant run /bin/cat " )
434
+ return
435
+ }
436
+
437
+ process. terminate ( )
438
+ process. waitUntilExit ( )
439
+ let terminationReason = process. terminationReason
440
+ XCTAssertEqual ( terminationReason, Process . TerminationReason. uncaughtSignal)
441
+ XCTAssertEqual ( process. terminationStatus, SIGTERM)
442
+ }
443
+
444
+ func test_suspend_resume( ) {
445
+ let helper = _SignalHelperRunner ( )
446
+ do {
447
+ try helper. start ( )
448
+ } catch {
449
+ XCTFail ( " Cant run xdgTestHelper: \( error) " )
450
+ return
451
+ }
452
+ if !helper. waitForReady ( ) {
453
+ XCTFail ( " Didnt receive Ready from sub-process " )
454
+ return
455
+ }
456
+ let now = DispatchTime . now ( ) . uptimeNanoseconds
457
+ let timeout = DispatchTime ( uptimeNanoseconds: now + 2_000_000_000 )
458
+
459
+ func waitForSemaphore( ) -> Bool {
460
+ guard helper. semaphore. wait ( timeout: timeout) == . success else {
461
+ helper. process. terminate ( )
462
+ XCTFail ( " Timedout waiting for signal " )
463
+ return false
464
+ }
465
+ return true
466
+ }
467
+
468
+ XCTAssertTrue ( helper. process. isRunning)
469
+ XCTAssertTrue ( helper. process. suspend ( ) )
470
+ XCTAssertTrue ( helper. process. isRunning)
471
+ XCTAssertTrue ( helper. process. resume ( ) )
472
+ if waitForSemaphore ( ) == false { return }
473
+ XCTAssertEqual ( helper. sigContCount, 1 )
474
+
475
+ XCTAssertTrue ( helper. process. resume ( ) )
476
+ XCTAssertTrue ( helper. process. suspend ( ) )
477
+ XCTAssertTrue ( helper. process. resume ( ) )
478
+ XCTAssertEqual ( helper. sigContCount, 1 )
479
+
480
+ XCTAssertTrue ( helper. process. suspend ( ) )
481
+ XCTAssertTrue ( helper. process. suspend ( ) )
482
+ XCTAssertTrue ( helper. process. resume ( ) )
483
+ if waitForSemaphore ( ) == false { return }
484
+
485
+ helper. process. suspend ( )
486
+ helper. process. resume ( )
487
+ if waitForSemaphore ( ) == false { return }
488
+ XCTAssertEqual ( helper. sigContCount, 3 )
489
+
490
+ helper. process. terminate ( )
491
+ helper. process. waitUntilExit ( )
492
+ XCTAssertFalse ( helper. process. isRunning)
493
+ XCTAssertFalse ( helper. process. suspend ( ) )
494
+ XCTAssertTrue ( helper. process. resume ( ) )
495
+ XCTAssertTrue ( helper. process. resume ( ) )
496
+ }
497
+
388
498
#endif
389
499
}
390
500
@@ -394,6 +504,89 @@ private enum Error: Swift.Error {
394
504
case InvalidEnvironmentVariable( String )
395
505
}
396
506
507
+ // Run xdgTestHelper, wait for 'Ready' from the sub-process, then signal a semaphore.
508
+ // Read lines from a pipe and store in a queue.
509
+ class _SignalHelperRunner {
510
+ let process = Process ( )
511
+ let semaphore = DispatchSemaphore ( value: 0 )
512
+
513
+ private let outputPipe = Pipe ( )
514
+ private let sQueue = DispatchQueue ( label: " io queue " )
515
+ private let source : DispatchSourceRead
516
+
517
+ private var gotReady = false
518
+ private var bytesIn = Data ( )
519
+ private var _sigIntCount = 0
520
+ private var _sigContCount = 0
521
+ var sigIntCount : Int { return sQueue. sync { return _sigIntCount } }
522
+ var sigContCount : Int { return sQueue. sync { return _sigContCount } }
523
+
524
+
525
+ init ( ) {
526
+ process. executableURL = xdgTestHelperURL ( )
527
+ process. environment = ProcessInfo . processInfo. environment
528
+ process. arguments = [ " --signal-test " ]
529
+ process. standardOutput = outputPipe. fileHandleForWriting
530
+
531
+ source = DispatchSource . makeReadSource ( fileDescriptor: outputPipe. fileHandleForReading. fileDescriptor, queue: sQueue)
532
+ let workItem = DispatchWorkItem ( block: { [ weak self] in
533
+ if let strongSelf = self {
534
+ let newLine = UInt8 ( ascii: " \n " )
535
+
536
+ strongSelf. bytesIn. append ( strongSelf. outputPipe. fileHandleForReading. availableData)
537
+ if strongSelf. bytesIn. isEmpty {
538
+ return
539
+ }
540
+ // Split the incoming data into lines.
541
+ while let index = strongSelf. bytesIn. index ( of: newLine) {
542
+ if index >= strongSelf. bytesIn. startIndex {
543
+ // dont include the newline when converting to string
544
+ let line = String ( data: strongSelf. bytesIn [ strongSelf. bytesIn. startIndex..< index] , encoding: String . Encoding. utf8) ?? " "
545
+ strongSelf. bytesIn. removeSubrange ( strongSelf. bytesIn. startIndex... index)
546
+
547
+ if strongSelf. gotReady == false && line == " Ready " {
548
+ strongSelf. semaphore. signal ( )
549
+ strongSelf. gotReady = true ;
550
+ }
551
+ else if strongSelf. gotReady == true {
552
+ if line == " Signal: SIGINT " {
553
+ strongSelf. _sigIntCount += 1
554
+ strongSelf. semaphore. signal ( )
555
+ }
556
+ else if line == " Signal: SIGCONT " {
557
+ strongSelf. _sigContCount += 1
558
+ strongSelf. semaphore. signal ( )
559
+ }
560
+ }
561
+ }
562
+ }
563
+ }
564
+ } )
565
+ source. setEventHandler ( handler: workItem)
566
+ }
567
+
568
+ deinit {
569
+ source. cancel ( )
570
+ process. terminate ( )
571
+ process. waitUntilExit ( )
572
+ }
573
+
574
+ func start( ) throws {
575
+ source. resume ( )
576
+ try process. run ( )
577
+ }
578
+
579
+ func waitForReady( ) -> Bool {
580
+ let now = DispatchTime . now ( ) . uptimeNanoseconds
581
+ let timeout = DispatchTime ( uptimeNanoseconds: now + 2_000_000_000 )
582
+ guard semaphore. wait ( timeout: timeout) == . success else {
583
+ process. terminate ( )
584
+ return false
585
+ }
586
+ return true
587
+ }
588
+ }
589
+
397
590
#if !os(Android)
398
591
private func runTask( _ arguments: [ String ] , environment: [ String : String ] ? = nil , currentDirectoryPath: String ? = nil ) throws -> ( String , String ) {
399
592
let process = Process ( )
0 commit comments