Skip to content

Commit

Permalink
Process: Implement signal sending functions.
Browse files Browse the repository at this point in the history
- Implements suspend(), resume(), interrupt() and terminate().

- Add --signal-test to xdgTestHelper for testing signals.
  • Loading branch information
spevans committed Jun 12, 2018
1 parent 1afa1ef commit 05432c6
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 8 deletions.
43 changes: 38 additions & 5 deletions Foundation/Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,44 @@ open class Process: NSObject {
self.processIdentifier = pid
}

open func interrupt() { NSUnimplemented() } // Not always possible. Sends SIGINT.
open func terminate() { NSUnimplemented() }// Not always possible. Sends SIGTERM.

open func suspend() -> Bool { NSUnimplemented() }
open func resume() -> Bool { NSUnimplemented() }
open func interrupt() {
if isRunning && processIdentifier > 0 {
kill(processIdentifier, SIGINT)
}
}

open func terminate() {
if isRunning && processIdentifier > 0 {
kill(processIdentifier, SIGTERM)
}
}

// Every suspend() has to be balanced with a resume() so keep a count of both.
private var suspendCount = 0

open func suspend() -> Bool {
guard isRunning else {
return false
}

suspendCount += 1
if suspendCount == 1, processIdentifier > 0 {
kill(processIdentifier, SIGSTOP)
}
return true
}

open func resume() -> Bool {
guard isRunning else {
return true
}

suspendCount -= 1
if suspendCount == 0, processIdentifier > 0 {
kill(processIdentifier, SIGCONT)
}
return true
}

// status
open private(set) var processIdentifier: Int32 = -1
Expand Down
191 changes: 191 additions & 0 deletions TestFoundation/TestProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class TestProcess : XCTestCase {
("test_no_environment", test_no_environment),
("test_custom_environment", test_custom_environment),
("test_run", test_run),
("test_interrupt", test_interrupt),
("test_terminate", test_terminate),
("test_suspend_resume", test_suspend_resume),
]
#endif
}
Expand Down Expand Up @@ -385,6 +388,113 @@ class TestProcess : XCTestCase {
fm.changeCurrentDirectoryPath(cwd)
}

func test_interrupt() {
let helper = _SignalHelperRunner()
do {
try helper.start()
} catch {
XCTFail("Cant run xdgTestHelper: \(error)")
return
}
if !helper.waitForReady() {
XCTFail("Didnt receive Ready from sub-process")
return
}

let now = DispatchTime.now().uptimeNanoseconds
let timeout = DispatchTime(uptimeNanoseconds: now + 2_000_000_000)

var count = 3
while count > 0 {
helper.process.interrupt()
guard helper.semaphore.wait(timeout: timeout) == .success else {
helper.process.terminate()
XCTFail("Timedout waiting for signal")
return
}

if helper.sigIntCount == 3 {
break
}
count -= 1
}
helper.process.terminate()
XCTAssertEqual(helper.sigIntCount, 3)
helper.process.waitUntilExit()
let terminationReason = helper.process.terminationReason
XCTAssertEqual(terminationReason, Process.TerminationReason.exit)
let status = helper.process.terminationStatus
XCTAssertEqual(status, 99)
}

func test_terminate() {
let cat = URL(fileURLWithPath: "/bin/cat", isDirectory: false)
guard let process = try? Process.run(cat, arguments: []) else {
XCTFail("Cant run /bin/cat")
return
}

process.terminate()
process.waitUntilExit()
let terminationReason = process.terminationReason
XCTAssertEqual(terminationReason, Process.TerminationReason.uncaughtSignal)
XCTAssertEqual(process.terminationStatus, SIGTERM)
}

func test_suspend_resume() {
let helper = _SignalHelperRunner()
do {
try helper.start()
} catch {
XCTFail("Cant run xdgTestHelper: \(error)")
return
}
if !helper.waitForReady() {
XCTFail("Didnt receive Ready from sub-process")
return
}
let now = DispatchTime.now().uptimeNanoseconds
let timeout = DispatchTime(uptimeNanoseconds: now + 2_000_000_000)

func waitForSemaphore() -> Bool {
guard helper.semaphore.wait(timeout: timeout) == .success else {
helper.process.terminate()
XCTFail("Timedout waiting for signal")
return false
}
return true
}

XCTAssertTrue(helper.process.isRunning)
XCTAssertTrue(helper.process.suspend())
XCTAssertTrue(helper.process.isRunning)
XCTAssertTrue(helper.process.resume())
if waitForSemaphore() == false { return }
XCTAssertEqual(helper.sigContCount, 1)

XCTAssertTrue(helper.process.resume())
XCTAssertTrue(helper.process.suspend())
XCTAssertTrue(helper.process.resume())
XCTAssertEqual(helper.sigContCount, 1)

XCTAssertTrue(helper.process.suspend())
XCTAssertTrue(helper.process.suspend())
XCTAssertTrue(helper.process.resume())
if waitForSemaphore() == false { return }

helper.process.suspend()
helper.process.resume()
if waitForSemaphore() == false { return }
XCTAssertEqual(helper.sigContCount, 3)

helper.process.terminate()
helper.process.waitUntilExit()
XCTAssertFalse(helper.process.isRunning)
XCTAssertFalse(helper.process.suspend())
XCTAssertTrue(helper.process.resume())
XCTAssertTrue(helper.process.resume())
}

#endif
}

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

// Run xdgTestHelper, wait for 'Ready' from the sub-process, then signal a semaphore.
// Read lines from a pipe and store in a queue.
class _SignalHelperRunner {
let process = Process()
let semaphore = DispatchSemaphore(value: 0)

private let outputPipe = Pipe()
private let sQueue = DispatchQueue(label: "io queue")
private let source: DispatchSourceRead

private var gotReady = false
private var bytesIn = Data()
private var _sigIntCount = 0
private var _sigContCount = 0
var sigIntCount: Int { return sQueue.sync { return _sigIntCount } }
var sigContCount: Int { return sQueue.sync { return _sigContCount } }


init() {
process.executableURL = xdgTestHelperURL()
process.environment = ProcessInfo.processInfo.environment
process.arguments = ["--signal-test"]
process.standardOutput = outputPipe.fileHandleForWriting

source = DispatchSource.makeReadSource(fileDescriptor: outputPipe.fileHandleForReading.fileDescriptor, queue: sQueue)
let workItem = DispatchWorkItem(block: {
let newLine = UInt8(ascii: "\n")

self.bytesIn.append(self.outputPipe.fileHandleForReading.availableData)
if self.bytesIn.isEmpty {
return
}
// Split the incoming data into lines.
while let index = self.bytesIn.index(of: newLine) {
if index >= self.bytesIn.startIndex {
// dont include the newline when converting to string
let line = String(data: self.bytesIn[self.bytesIn.startIndex..<index], encoding: String.Encoding.utf8) ?? ""
self.bytesIn.removeSubrange(self.bytesIn.startIndex...index)

if self.gotReady == false && line == "Ready" {
self.semaphore.signal()
self.gotReady = true;
}
else if self.gotReady == true {
if line == "Signal: SIGINT" {
self._sigIntCount += 1
self.semaphore.signal()
}
else if line == "Signal: SIGCONT" {
self._sigContCount += 1
self.semaphore.signal()
}
}
}
}
})
source.setEventHandler(handler: workItem)
}

deinit {
source.cancel()
process.terminate()
process.waitUntilExit()
}

func start() throws {
source.resume()
try process.run()
}

func waitForReady() -> Bool {
let now = DispatchTime.now().uptimeNanoseconds
let timeout = DispatchTime(uptimeNanoseconds: now + 2_000_000_000)
guard semaphore.wait(timeout: timeout) == .success else {
process.terminate()
return false
}
return true
}
}

#if !os(Android)
internal func runTask(_ arguments: [String], environment: [String: String]? = nil, currentDirectoryPath: String? = nil) throws -> (String, String) {
let process = Process()
Expand Down
55 changes: 52 additions & 3 deletions TestFoundation/xdgTestHelper/main.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2017 Swift project authors
// Copyright (c) 2017 - 2018 Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -57,6 +57,53 @@ class XDGCheck {
}
}


// Used by TestProcess: test_interrupt(), test_suspend_resume()
func signalTest() {

var signalSet = sigset_t()
sigemptyset(&signalSet)
sigaddset(&signalSet, SIGTERM)
sigaddset(&signalSet, SIGCONT)
sigaddset(&signalSet, SIGINT)
sigaddset(&signalSet, SIGALRM)
guard sigprocmask(SIG_BLOCK, &signalSet, nil) == 0 else {
fatalError("cant block signals")
}
alarm(3)

// On Linux, print() doesnt currently flush the output over the pipe so use
// write() for now. On macOS, print() works fine.
write(1, "Ready\n", 6)

while true {
var receivedSignal: Int32 = 0
let ret = sigwait(&signalSet, &receivedSignal)
guard ret == 0 else {
fatalError("sigwait() failed")
}
switch receivedSignal {
case SIGINT:
write(1, "Signal: SIGINT\n", 15)

case SIGCONT:
write(1, "Signal: SIGCONT\n", 16)

case SIGTERM:
print("Terminated")
exit(99)

case SIGALRM:
print("Timedout")
exit(127)

default:
let msg = "Unexpected signal: \(receivedSignal)"
fatalError(msg)
}
}
}

// -----

#if !DEPLOYMENT_RUNTIME_OBJC
Expand Down Expand Up @@ -142,8 +189,10 @@ case "--nspathfor":
let test = NSURLForPrintTest(method: method, identifier: identifier)
test.run()
#endif


case "--signal-test":
signalTest()

default:
fatalError("These arguments are not recognized. Only run this from a unit test.")
}

0 comments on commit 05432c6

Please sign in to comment.