Terminate subprocesses of macOS command line tool

2020-08-03 06:44发布

I'm writing a macOS command line tool in swift which executes shell commands:

let process = Process()
process.launchPath = "/bin/sleep"
process.arguments = ["100"]
process.launch()
process.waitUntilExit()

However, if an interrupt (CTRL-C) or a terminate signal gets sent to my program, these shell commands don't get terminated and just continue with their execution.

Is there a way to automatically terminate them if my program gets terminated unexpectedly?

1条回答
不美不萌又怎样
2楼-- · 2020-08-03 07:32

Here is what we did in order to react on interrupt (CTRL-C) when using two piped subprocesses.

Idea behind: Blocking waitUntilExit() call replaced with async terminationHandler. Infinite loop dispatchMain() used to serve dispatch events. On receiving Interrupt signal we calling interrupt() on subprocesses.

Example class which incapsulates subprocess launch and interrupt logic:

class AppTester: Builder {

   private var processes: [Process] = [] // Keeps references to launched processes.

   func test(completion: @escaping (Int32) -> Void) {

      let xcodebuildProcess = Process(executableName: "xcodebuild", arguments: ...)
      let xcprettyProcess = Process(executableName: "xcpretty", arguments: ...)

      // Organising pipe between processes. Like `xcodebuild ... | xcpretty` in shell
      let pipe = Pipe()
      xcodebuildProcess.standardOutput = pipe
      xcprettyProcess.standardInput = pipe

      // Assigning `terminationHandler` for needed subprocess.
      processes.append(xcodebuildProcess)
      xcodebuildProcess.terminationHandler = { process in
         completion(process.terminationStatus)
      }

      xcodebuildProcess.launch()
      xcprettyProcess.launch()
      // Note. We should not use blocking `waitUntilExit()` call.
   }

   func interrupt() {
      // Interrupting running processes (if any).
      processes.filter { $0.isRunning }.forEach { $0.interrupt() }
   }
}

Usage (i.e. main.swift):

let tester = AppTester(...)
tester.test(....) {
   if $0 == EXIT_SUCCESS {
      // Do some other work.
   } else {
      exit($0)
   }
}

// Making Interrupt signal listener.
let source = DispatchSource.makeSignalSource(signal: SIGINT)
source.setEventHandler {
   tester.interrupt() // Will interrupt running processes (if any).
   exit(SIGINT)
}
source.resume()
dispatchMain() // Starting dispatch loop. This function never returns.

Example output in shell:

...
▸ Running script 'Run Script: Verify Sources'
▸ Processing Framework-Info.plist
▸ Running script 'Run Script: Verify Sources'
▸ Linking AppTestability
^C** BUILD INTERRUPTED **
查看更多
登录 后发表回答