Skip to content

Commit 128dac9

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

File tree

3 files changed

+297
-14
lines changed

3 files changed

+297
-14
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: 193 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,89 @@ 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: { [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+
397590
#if !os(Android)
398591
private func runTask(_ arguments: [String], environment: [String: String]? = nil, currentDirectoryPath: String? = nil) throws -> (String, String) {
399592
let process = Process()

TestFoundation/xdgTestHelper/main.swift

Lines changed: 66 additions & 9 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
@@ -55,15 +55,72 @@ class XDGCheck {
5555
}
5656
}
5757

58-
if let arg = ProcessInfo.processInfo.arguments.last {
59-
if arg == "--xdgcheck" {
60-
XDGCheck.run()
61-
}
62-
if arg == "--getcwd" {
63-
print(FileManager.default.currentDirectoryPath)
58+
// Used by TestProcess: test_interrupt(), test_suspend_resume()
59+
func signalTest() {
60+
61+
var signalSet = sigset_t()
62+
sigemptyset(&signalSet)
63+
sigaddset(&signalSet, SIGTERM)
64+
sigaddset(&signalSet, SIGCONT)
65+
sigaddset(&signalSet, SIGINT)
66+
sigaddset(&signalSet, SIGALRM)
67+
guard sigprocmask(SIG_BLOCK, &signalSet, nil) == 0 else {
68+
fatalError("Cant block signals")
6469
}
65-
if arg == "--echo-PWD" {
66-
print(ProcessInfo.processInfo.environment["PWD"] ?? "")
70+
// Timeout
71+
alarm(3)
72+
73+
// On Linux, print() doesnt currently flush the output over the pipe so use
74+
// write() for now. On macOS, print() works fine.
75+
write(1, "Ready\n", 6)
76+
77+
while true {
78+
var receivedSignal: Int32 = 0
79+
let ret = sigwait(&signalSet, &receivedSignal)
80+
guard ret == 0 else {
81+
fatalError("sigwait() failed")
82+
}
83+
switch receivedSignal {
84+
case SIGINT:
85+
write(1, "Signal: SIGINT\n", 15)
86+
87+
case SIGCONT:
88+
write(1, "Signal: SIGCONT\n", 16)
89+
90+
case SIGTERM:
91+
print("Terminated")
92+
exit(99)
93+
94+
case SIGALRM:
95+
print("Timedout")
96+
exit(127)
97+
98+
default:
99+
let msg = "Unexpected signal: \(receivedSignal)"
100+
fatalError(msg)
101+
}
67102
}
68103
}
69104

105+
var arguments = ProcessInfo.processInfo.arguments.dropFirst().makeIterator()
106+
107+
guard let arg = arguments.next() else {
108+
fatalError("The unit test must specify the correct number of flags and arguments.")
109+
}
110+
111+
switch arg {
112+
case "--xdgcheck":
113+
XDGCheck.run()
114+
115+
case "--getcwd":
116+
print(FileManager.default.currentDirectoryPath)
117+
118+
case "--echo-PWD":
119+
print(ProcessInfo.processInfo.environment["PWD"] ?? "")
120+
121+
case "--signal-test":
122+
signalTest()
123+
124+
default:
125+
fatalError("These arguments are not recognized. Only run this from a unit test.")
126+
}

0 commit comments

Comments
 (0)