Skip to content

Commit 9464a1e

Browse files
committed
Process: Implement signal sending functions.
- Implements suspend(), resume(), interrupt() and terminate(). - Add --signal-test to xdgTestHelper for testing signals.
1 parent 1afa1ef commit 9464a1e

File tree

3 files changed

+283
-8
lines changed

3 files changed

+283
-8
lines changed

Foundation/Process.swift

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -507,11 +507,44 @@ open class Process: NSObject {
507507
self.processIdentifier = pid
508508
}
509509

510-
open func interrupt() { NSUnimplemented() } // Not always possible. Sends SIGINT.
511-
open func terminate() { NSUnimplemented() }// Not always possible. Sends SIGTERM.
512-
513-
open func suspend() -> Bool { NSUnimplemented() }
514-
open func resume() -> Bool { NSUnimplemented() }
510+
open func interrupt() {
511+
if isRunning && processIdentifier > 0 {
512+
kill(processIdentifier, SIGINT)
513+
}
514+
}
515+
516+
open func terminate() {
517+
if isRunning && processIdentifier > 0 {
518+
kill(processIdentifier, SIGTERM)
519+
}
520+
}
521+
522+
// Every suspend() has to be balanced with a resume() so keep a count of both.
523+
private var suspendCount = 0
524+
525+
open func suspend() -> Bool {
526+
guard isRunning else {
527+
return false
528+
}
529+
530+
suspendCount += 1
531+
if suspendCount == 1, processIdentifier > 0 {
532+
kill(processIdentifier, SIGSTOP)
533+
}
534+
return true
535+
}
536+
537+
open func resume() -> Bool {
538+
guard isRunning else {
539+
return true
540+
}
541+
542+
suspendCount -= 1
543+
if suspendCount == 0, processIdentifier > 0 {
544+
kill(processIdentifier, SIGCONT)
545+
}
546+
return true
547+
}
515548

516549
// status
517550
open private(set) var processIdentifier: Int32 = -1

TestFoundation/TestProcess.swift

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class TestProcess : XCTestCase {
2929
("test_no_environment", test_no_environment),
3030
("test_custom_environment", test_custom_environment),
3131
("test_run", test_run),
32+
("test_interrupt", test_interrupt),
33+
("test_terminate", test_terminate),
34+
("test_suspend_resume", test_suspend_resume),
3235
]
3336
#endif
3437
}
@@ -385,6 +388,113 @@ class TestProcess : XCTestCase {
385388
fm.changeCurrentDirectoryPath(cwd)
386389
}
387390

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+
388498
#endif
389499
}
390500

@@ -394,6 +504,87 @@ private enum Error: Swift.Error {
394504
case InvalidEnvironmentVariable(String)
395505
}
396506

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+
397588
#if !os(Android)
398589
internal func runTask(_ arguments: [String], environment: [String: String]? = nil, currentDirectoryPath: String? = nil) throws -> (String, String) {
399590
let process = Process()

TestFoundation/xdgTestHelper/main.swift

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// This source file is part of the Swift.org open source project
22
//
3-
// Copyright (c) 2017 Swift project authors
3+
// Copyright (c) 2017 - 2018 Swift project authors
44
// Licensed under Apache License v2.0 with Runtime Library Exception
55
//
66
// See http://swift.org/LICENSE.txt for license information
@@ -57,6 +57,55 @@ class XDGCheck {
5757
}
5858
}
5959

60+
61+
// Used by TestProcess: test_interrupt(), test_suspend_resume()
62+
func signalTest() {
63+
64+
var signalSet = sigset_t()
65+
sigemptyset(&signalSet)
66+
sigaddset(&signalSet, SIGTERM)
67+
sigaddset(&signalSet, SIGCONT)
68+
sigaddset(&signalSet, SIGINT)
69+
sigaddset(&signalSet, SIGALRM)
70+
guard sigprocmask(SIG_BLOCK, &signalSet, nil) == 0 else {
71+
fatalError("cant block signals")
72+
}
73+
alarm(3)
74+
75+
// On Linux, print() doesnt currently flush the output over the pipe so use
76+
// write() for now. On macOS, print() works fine.
77+
write(1, "Ready\n", 6)
78+
79+
while true {
80+
var receivedSignal: Int32 = 0
81+
let ret = sigwait(&signalSet, &receivedSignal)
82+
guard ret == 0 else {
83+
fatalError("sigwait() failed")
84+
}
85+
switch receivedSignal {
86+
case SIGINT:
87+
write(1, "Signal: SIGINT\n", 15)
88+
write(2, "Signal: SIGINT\n", 15)
89+
90+
case SIGCONT:
91+
write(1, "Signal: SIGCONT\n", 16)
92+
write(2, "Signal: SIGCONT\n", 16)
93+
94+
case SIGTERM:
95+
print("Terminated")
96+
exit(99)
97+
98+
case SIGALRM:
99+
print("Timedout")
100+
exit(127)
101+
102+
default:
103+
let msg = "Unexpected signal: \(receivedSignal)"
104+
fatalError(msg)
105+
}
106+
}
107+
}
108+
60109
// -----
61110

62111
#if !DEPLOYMENT_RUNTIME_OBJC
@@ -142,8 +191,10 @@ case "--nspathfor":
142191
let test = NSURLForPrintTest(method: method, identifier: identifier)
143192
test.run()
144193
#endif
145-
194+
195+
case "--signal-test":
196+
signalTest()
197+
146198
default:
147199
fatalError("These arguments are not recognized. Only run this from a unit test.")
148200
}
149-

0 commit comments

Comments
 (0)