Skip to content

Commit

Permalink
Merge pull request #1578 from spevans/pr_process_signals
Browse files Browse the repository at this point in the history
  • Loading branch information
swift-ci authored Jun 15, 2018
2 parents e49dcf0 + 12c1638 commit bf2cc1c
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 24 deletions.
69 changes: 54 additions & 15 deletions Foundation/Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,12 @@ open class Process: NSObject {
} while ( (waitResult == -1) && (errno == EINTR) )

if WIFSIGNALED(exitCode) {
process.terminationStatus = WTERMSIG(exitCode)
process.terminationReason = .uncaughtSignal
process._terminationStatus = WTERMSIG(exitCode)
process._terminationReason = .uncaughtSignal
} else {
assert(WIFEXITED(exitCode))
process.terminationStatus = WEXITSTATUS(exitCode)
process.terminationReason = .exit
process._terminationStatus = WEXITSTATUS(exitCode)
process._terminationReason = .exit
}

// If a termination handler has been set, invoke it on a background thread
Expand All @@ -374,7 +374,6 @@ open class Process: NSObject {

// Set the running flag to false
process.isRunning = false
process.processIdentifier = -1

// Invalidate the source and wake up the run loop, if it's available

Expand Down Expand Up @@ -507,19 +506,59 @@ 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() {
precondition(hasStarted, "task not launched")
kill(processIdentifier, SIGINT)
}

open func terminate() {
precondition(hasStarted, "task not launched")
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 {
if kill(processIdentifier, SIGSTOP) == 0 {
suspendCount += 1
return true
} else {
return false
}
}

open func resume() -> Bool {
var success = true
if suspendCount == 1 {
success = kill(processIdentifier, SIGCONT) == 0
}
if success {
suspendCount -= 1
}
return success
}

// status
open private(set) var processIdentifier: Int32 = -1
open private(set) var processIdentifier: Int32 = 0
open private(set) var isRunning: Bool = false

open private(set) var terminationStatus: Int32 = 0
open private(set) var terminationReason: TerminationReason = .exit

private var hasStarted: Bool { return processIdentifier > 0 }
private var hasFinished: Bool { return !isRunning && processIdentifier > 0 }

private var _terminationStatus: Int32 = 0
public var terminationStatus: Int32 {
precondition(hasStarted, "task not launched")
precondition(hasFinished, "task still running")
return _terminationStatus
}

private var _terminationReason: TerminationReason = .exit
public var terminationReason: TerminationReason {
precondition(hasStarted, "task not launched")
precondition(hasFinished, "task still running")
return _terminationReason
}

/*
A block to be invoked when the process underlying the Process terminates. Setting the block to nil is valid, and stops the previous block from being invoked, as long as it hasn't started in any way. The Process is passed as the argument to the block so the block does not have to capture, and thus retain, it. The block is copied when set. Only one termination handler block can be set at any time. The execution context in which the block is invoked is undefined. If the Process has already finished, the block is executed immediately/soon (not necessarily on the current thread). If a terminationHandler is set on an Process, the ProcessDidTerminateNotification notification is not posted for that process. Also note that -waitUntilExit won't wait until the terminationHandler has been fully executed. You cannot use this property in a concrete subclass of Process which hasn't been updated to include an implementation of the storage and use of it.
*/
Expand Down
216 changes: 216 additions & 0 deletions TestFoundation/TestProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class TestProcess : XCTestCase {
("test_no_environment", test_no_environment),
("test_custom_environment", test_custom_environment),
("test_run", test_run),
("test_preStartEndState", test_preStartEndState),
("test_interrupt", test_interrupt),
("test_terminate", test_terminate),
("test_suspend_resume", test_suspend_resume),
]
#endif
}
Expand Down Expand Up @@ -385,6 +389,135 @@ class TestProcess : XCTestCase {
fm.changeCurrentDirectoryPath(cwd)
}

func test_preStartEndState() {
let process = Process()
XCTAssertNil(process.executableURL)
XCTAssertNotNil(process.currentDirectoryURL)
XCTAssertNil(process.arguments)
XCTAssertNil(process.environment)
XCTAssertFalse(process.isRunning)
XCTAssertEqual(process.processIdentifier, 0)
XCTAssertEqual(process.qualityOfService, .default)

process.executableURL = URL(fileURLWithPath: "/bin/cat", isDirectory: false)
_ = try? process.run()
XCTAssertTrue(process.isRunning)
XCTAssertTrue(process.processIdentifier > 0)
process.terminate()
process.waitUntilExit()
XCTAssertFalse(process.isRunning)
XCTAssertTrue(process.processIdentifier > 0)
XCTAssertEqual(process.terminationReason, .uncaughtSignal)
XCTAssertEqual(process.terminationStatus, SIGTERM)
}

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 +527,89 @@ 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: { [weak self] in
if let strongSelf = self {
let newLine = UInt8(ascii: "\n")

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

if strongSelf.gotReady == false && line == "Ready" {
strongSelf.semaphore.signal()
strongSelf.gotReady = true;
}
else if strongSelf.gotReady == true {
if line == "Signal: SIGINT" {
strongSelf._sigIntCount += 1
strongSelf.semaphore.signal()
}
else if line == "Signal: SIGCONT" {
strongSelf._sigContCount += 1
strongSelf.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)
private func runTask(_ arguments: [String], environment: [String: String]? = nil, currentDirectoryPath: String? = nil) throws -> (String, String) {
let process = Process()
Expand Down
Loading

0 comments on commit bf2cc1c

Please sign in to comment.