Skip to content

Commit bf2cc1c

Browse files
authored
Merge pull request #1578 from spevans/pr_process_signals
2 parents e49dcf0 + 12c1638 commit bf2cc1c

File tree

3 files changed

+336
-24
lines changed

3 files changed

+336
-24
lines changed

Foundation/Process.swift

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,12 @@ open class Process: NSObject {
355355
} while ( (waitResult == -1) && (errno == EINTR) )
356356

357357
if WIFSIGNALED(exitCode) {
358-
process.terminationStatus = WTERMSIG(exitCode)
359-
process.terminationReason = .uncaughtSignal
358+
process._terminationStatus = WTERMSIG(exitCode)
359+
process._terminationReason = .uncaughtSignal
360360
} else {
361361
assert(WIFEXITED(exitCode))
362-
process.terminationStatus = WEXITSTATUS(exitCode)
363-
process.terminationReason = .exit
362+
process._terminationStatus = WEXITSTATUS(exitCode)
363+
process._terminationReason = .exit
364364
}
365365

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

375375
// Set the running flag to false
376376
process.isRunning = false
377-
process.processIdentifier = -1
378377

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

@@ -507,19 +506,59 @@ open class Process: NSObject {
507506
self.processIdentifier = pid
508507
}
509508

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() }
509+
open func interrupt() {
510+
precondition(hasStarted, "task not launched")
511+
kill(processIdentifier, SIGINT)
512+
}
513+
514+
open func terminate() {
515+
precondition(hasStarted, "task not launched")
516+
kill(processIdentifier, SIGTERM)
517+
}
518+
519+
// Every suspend() has to be balanced with a resume() so keep a count of both.
520+
private var suspendCount = 0
521+
522+
open func suspend() -> Bool {
523+
if kill(processIdentifier, SIGSTOP) == 0 {
524+
suspendCount += 1
525+
return true
526+
} else {
527+
return false
528+
}
529+
}
530+
531+
open func resume() -> Bool {
532+
var success = true
533+
if suspendCount == 1 {
534+
success = kill(processIdentifier, SIGCONT) == 0
535+
}
536+
if success {
537+
suspendCount -= 1
538+
}
539+
return success
540+
}
515541

516542
// status
517-
open private(set) var processIdentifier: Int32 = -1
543+
open private(set) var processIdentifier: Int32 = 0
518544
open private(set) var isRunning: Bool = false
519-
520-
open private(set) var terminationStatus: Int32 = 0
521-
open private(set) var terminationReason: TerminationReason = .exit
522-
545+
private var hasStarted: Bool { return processIdentifier > 0 }
546+
private var hasFinished: Bool { return !isRunning && processIdentifier > 0 }
547+
548+
private var _terminationStatus: Int32 = 0
549+
public var terminationStatus: Int32 {
550+
precondition(hasStarted, "task not launched")
551+
precondition(hasFinished, "task still running")
552+
return _terminationStatus
553+
}
554+
555+
private var _terminationReason: TerminationReason = .exit
556+
public var terminationReason: TerminationReason {
557+
precondition(hasStarted, "task not launched")
558+
precondition(hasFinished, "task still running")
559+
return _terminationReason
560+
}
561+
523562
/*
524563
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.
525564
*/

TestFoundation/TestProcess.swift

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class TestProcess : XCTestCase {
2929
("test_no_environment", test_no_environment),
3030
("test_custom_environment", test_custom_environment),
3131
("test_run", test_run),
32+
("test_preStartEndState", test_preStartEndState),
33+
("test_interrupt", test_interrupt),
34+
("test_terminate", test_terminate),
35+
("test_suspend_resume", test_suspend_resume),
3236
]
3337
#endif
3438
}
@@ -385,6 +389,135 @@ class TestProcess : XCTestCase {
385389
fm.changeCurrentDirectoryPath(cwd)
386390
}
387391

392+
func test_preStartEndState() {
393+
let process = Process()
394+
XCTAssertNil(process.executableURL)
395+
XCTAssertNotNil(process.currentDirectoryURL)
396+
XCTAssertNil(process.arguments)
397+
XCTAssertNil(process.environment)
398+
XCTAssertFalse(process.isRunning)
399+
XCTAssertEqual(process.processIdentifier, 0)
400+
XCTAssertEqual(process.qualityOfService, .default)
401+
402+
process.executableURL = URL(fileURLWithPath: "/bin/cat", isDirectory: false)
403+
_ = try? process.run()
404+
XCTAssertTrue(process.isRunning)
405+
XCTAssertTrue(process.processIdentifier > 0)
406+
process.terminate()
407+
process.waitUntilExit()
408+
XCTAssertFalse(process.isRunning)
409+
XCTAssertTrue(process.processIdentifier > 0)
410+
XCTAssertEqual(process.terminationReason, .uncaughtSignal)
411+
XCTAssertEqual(process.terminationStatus, SIGTERM)
412+
}
413+
414+
func test_interrupt() {
415+
let helper = _SignalHelperRunner()
416+
do {
417+
try helper.start()
418+
} catch {
419+
XCTFail("Cant run xdgTestHelper: \(error)")
420+
return
421+
}
422+
if !helper.waitForReady() {
423+
XCTFail("Didnt receive Ready from sub-process")
424+
return
425+
}
426+
427+
let now = DispatchTime.now().uptimeNanoseconds
428+
let timeout = DispatchTime(uptimeNanoseconds: now + 2_000_000_000)
429+
430+
var count = 3
431+
while count > 0 {
432+
helper.process.interrupt()
433+
guard helper.semaphore.wait(timeout: timeout) == .success else {
434+
helper.process.terminate()
435+
XCTFail("Timedout waiting for signal")
436+
return
437+
}
438+
439+
if helper.sigIntCount == 3 {
440+
break
441+
}
442+
count -= 1
443+
}
444+
helper.process.terminate()
445+
XCTAssertEqual(helper.sigIntCount, 3)
446+
helper.process.waitUntilExit()
447+
let terminationReason = helper.process.terminationReason
448+
XCTAssertEqual(terminationReason, Process.TerminationReason.exit)
449+
let status = helper.process.terminationStatus
450+
XCTAssertEqual(status, 99)
451+
}
452+
453+
func test_terminate() {
454+
let cat = URL(fileURLWithPath: "/bin/cat", isDirectory: false)
455+
guard let process = try? Process.run(cat, arguments: []) else {
456+
XCTFail("Cant run /bin/cat")
457+
return
458+
}
459+
460+
process.terminate()
461+
process.waitUntilExit()
462+
let terminationReason = process.terminationReason
463+
XCTAssertEqual(terminationReason, Process.TerminationReason.uncaughtSignal)
464+
XCTAssertEqual(process.terminationStatus, SIGTERM)
465+
}
466+
467+
func test_suspend_resume() {
468+
let helper = _SignalHelperRunner()
469+
do {
470+
try helper.start()
471+
} catch {
472+
XCTFail("Cant run xdgTestHelper: \(error)")
473+
return
474+
}
475+
if !helper.waitForReady() {
476+
XCTFail("Didnt receive Ready from sub-process")
477+
return
478+
}
479+
let now = DispatchTime.now().uptimeNanoseconds
480+
let timeout = DispatchTime(uptimeNanoseconds: now + 2_000_000_000)
481+
482+
func waitForSemaphore() -> Bool {
483+
guard helper.semaphore.wait(timeout: timeout) == .success else {
484+
helper.process.terminate()
485+
XCTFail("Timedout waiting for signal")
486+
return false
487+
}
488+
return true
489+
}
490+
491+
XCTAssertTrue(helper.process.isRunning)
492+
XCTAssertTrue(helper.process.suspend())
493+
XCTAssertTrue(helper.process.isRunning)
494+
XCTAssertTrue(helper.process.resume())
495+
if waitForSemaphore() == false { return }
496+
XCTAssertEqual(helper.sigContCount, 1)
497+
498+
XCTAssertTrue(helper.process.resume())
499+
XCTAssertTrue(helper.process.suspend())
500+
XCTAssertTrue(helper.process.resume())
501+
XCTAssertEqual(helper.sigContCount, 1)
502+
503+
XCTAssertTrue(helper.process.suspend())
504+
XCTAssertTrue(helper.process.suspend())
505+
XCTAssertTrue(helper.process.resume())
506+
if waitForSemaphore() == false { return }
507+
508+
helper.process.suspend()
509+
helper.process.resume()
510+
if waitForSemaphore() == false { return }
511+
XCTAssertEqual(helper.sigContCount, 3)
512+
513+
helper.process.terminate()
514+
helper.process.waitUntilExit()
515+
XCTAssertFalse(helper.process.isRunning)
516+
XCTAssertFalse(helper.process.suspend())
517+
XCTAssertTrue(helper.process.resume())
518+
XCTAssertTrue(helper.process.resume())
519+
}
520+
388521
#endif
389522
}
390523

@@ -394,6 +527,89 @@ private enum Error: Swift.Error {
394527
case InvalidEnvironmentVariable(String)
395528
}
396529

530+
// Run xdgTestHelper, wait for 'Ready' from the sub-process, then signal a semaphore.
531+
// Read lines from a pipe and store in a queue.
532+
class _SignalHelperRunner {
533+
let process = Process()
534+
let semaphore = DispatchSemaphore(value: 0)
535+
536+
private let outputPipe = Pipe()
537+
private let sQueue = DispatchQueue(label: "io queue")
538+
private let source: DispatchSourceRead
539+
540+
private var gotReady = false
541+
private var bytesIn = Data()
542+
private var _sigIntCount = 0
543+
private var _sigContCount = 0
544+
var sigIntCount: Int { return sQueue.sync { return _sigIntCount } }
545+
var sigContCount: Int { return sQueue.sync { return _sigContCount } }
546+
547+
548+
init() {
549+
process.executableURL = xdgTestHelperURL()
550+
process.environment = ProcessInfo.processInfo.environment
551+
process.arguments = ["--signal-test"]
552+
process.standardOutput = outputPipe.fileHandleForWriting
553+
554+
source = DispatchSource.makeReadSource(fileDescriptor: outputPipe.fileHandleForReading.fileDescriptor, queue: sQueue)
555+
let workItem = DispatchWorkItem(block: { [weak self] in
556+
if let strongSelf = self {
557+
let newLine = UInt8(ascii: "\n")
558+
559+
strongSelf.bytesIn.append(strongSelf.outputPipe.fileHandleForReading.availableData)
560+
if strongSelf.bytesIn.isEmpty {
561+
return
562+
}
563+
// Split the incoming data into lines.
564+
while let index = strongSelf.bytesIn.index(of: newLine) {
565+
if index >= strongSelf.bytesIn.startIndex {
566+
// dont include the newline when converting to string
567+
let line = String(data: strongSelf.bytesIn[strongSelf.bytesIn.startIndex..<index], encoding: String.Encoding.utf8) ?? ""
568+
strongSelf.bytesIn.removeSubrange(strongSelf.bytesIn.startIndex...index)
569+
570+
if strongSelf.gotReady == false && line == "Ready" {
571+
strongSelf.semaphore.signal()
572+
strongSelf.gotReady = true;
573+
}
574+
else if strongSelf.gotReady == true {
575+
if line == "Signal: SIGINT" {
576+
strongSelf._sigIntCount += 1
577+
strongSelf.semaphore.signal()
578+
}
579+
else if line == "Signal: SIGCONT" {
580+
strongSelf._sigContCount += 1
581+
strongSelf.semaphore.signal()
582+
}
583+
}
584+
}
585+
}
586+
}
587+
})
588+
source.setEventHandler(handler: workItem)
589+
}
590+
591+
deinit {
592+
source.cancel()
593+
process.terminate()
594+
process.waitUntilExit()
595+
}
596+
597+
func start() throws {
598+
source.resume()
599+
try process.run()
600+
}
601+
602+
func waitForReady() -> Bool {
603+
let now = DispatchTime.now().uptimeNanoseconds
604+
let timeout = DispatchTime(uptimeNanoseconds: now + 2_000_000_000)
605+
guard semaphore.wait(timeout: timeout) == .success else {
606+
process.terminate()
607+
return false
608+
}
609+
return true
610+
}
611+
}
612+
397613
#if !os(Android)
398614
private func runTask(_ arguments: [String], environment: [String: String]? = nil, currentDirectoryPath: String? = nil) throws -> (String, String) {
399615
let process = Process()

0 commit comments

Comments
 (0)