Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Process: Implement signal sending functions. #1578

Merged
merged 2 commits into from
Jun 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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