@@ -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,87 @@ 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: {
533
+ let newLine = UInt8 ( ascii: " \n " )
534
+
535
+ self . bytesIn. append ( self . outputPipe. fileHandleForReading. availableData)
536
+ if self . bytesIn. isEmpty {
537
+ return
538
+ }
539
+ // Split the incoming data into lines.
540
+ while let index = self . bytesIn. index ( of: newLine) {
541
+ if index >= self . bytesIn. startIndex {
542
+ // dont include the newline when converting to string
543
+ let line = String ( data: self . bytesIn [ self . bytesIn. startIndex..< index] , encoding: String . Encoding. utf8) ?? " "
544
+ self . bytesIn. removeSubrange ( self . bytesIn. startIndex... index)
545
+
546
+ if self . gotReady == false && line == " Ready " {
547
+ self . semaphore. signal ( )
548
+ self . gotReady = true ;
549
+ }
550
+ else if self . gotReady == true {
551
+ if line == " Signal: SIGINT " {
552
+ self . _sigIntCount += 1
553
+ self . semaphore. signal ( )
554
+ }
555
+ else if line == " Signal: SIGCONT " {
556
+ self . _sigContCount += 1
557
+ self . semaphore. signal ( )
558
+ }
559
+ }
560
+ }
561
+ }
562
+ } )
563
+ source. setEventHandler ( handler: workItem)
564
+ }
565
+
566
+ deinit {
567
+ source. cancel ( )
568
+ process. terminate ( )
569
+ process. waitUntilExit ( )
570
+ }
571
+
572
+ func start( ) throws {
573
+ source. resume ( )
574
+ try process. run ( )
575
+ }
576
+
577
+ func waitForReady( ) -> Bool {
578
+ let now = DispatchTime . now ( ) . uptimeNanoseconds
579
+ let timeout = DispatchTime ( uptimeNanoseconds: now + 2_000_000_000 )
580
+ guard semaphore. wait ( timeout: timeout) == . success else {
581
+ process. terminate ( )
582
+ return false
583
+ }
584
+ return true
585
+ }
586
+ }
587
+
397
588
#if !os(Android)
398
589
private func runTask( _ arguments: [ String ] , environment: [ String : String ] ? = nil , currentDirectoryPath: String ? = nil ) throws -> ( String , String ) {
399
590
let process = Process ( )
0 commit comments