diff --git a/Sources/LiveAudioServer/LiveAudioServerApp.swift b/Sources/LiveAudioServer/LiveAudioServerApp.swift index 2e930d6..44a555a 100644 --- a/Sources/LiveAudioServer/LiveAudioServerApp.swift +++ b/Sources/LiveAudioServer/LiveAudioServerApp.swift @@ -94,6 +94,10 @@ func printUsage() { stay live. --no-fifo-reopen Disable the FIFO re-open behaviour above; the reader will silence-fill after EOF instead. + --exit-with-parent Exit if the parent process dies (even on a + crash). Useful when launched as a helper by a + host app that needs this server reaped instead + of orphaned holding its HTTP port. --silence-dither Inject inaudible TPDF dither (±1 LSB) once a run of digitally-silent samples crosses --silence-dither-ms. Prevents downstream @@ -219,6 +223,25 @@ struct LiveAudioServerApp { sigTerm.resume() _ = sigInt; _ = sigTerm + // Parent-death watchdog (--exit-with-parent). Polls getppid(); when the + // launching app dies (quit or crash) this process is reparented to + // launchd (pid 1), so getppid() changes — at which point we shut down + // gracefully, freeing the HTTP port instead of orphaning it. + var parentWatchdog: DispatchSourceTimer? + if config.exitWithParent { + let originalParent = getppid() + let timer = DispatchSource.makeTimerSource(queue: .global()) + timer.schedule(deadline: .now() + 0.5, repeating: 0.5) + timer.setEventHandler { + if getppid() != originalParent { + runGracefulShutdown("Parent process exit") + } + } + timer.resume() + parentWatchdog = timer + } + _ = parentWatchdog + RunLoop.main.run() } } diff --git a/Sources/LiveAudioServerCore/CLIParse.swift b/Sources/LiveAudioServerCore/CLIParse.swift index 0cf58c3..7670406 100644 --- a/Sources/LiveAudioServerCore/CLIParse.swift +++ b/Sources/LiveAudioServerCore/CLIParse.swift @@ -226,6 +226,8 @@ public func parseCLI(_ args: [String]) -> CLIParseResult { config.httpAuthRealm = r case "--keep-alive": config.keepAliveOnInputEnd = true + case "--exit-with-parent": + config.exitWithParent = true case "--no-fifo-reopen": config.reopenStdinFIFO = false case "--silence-dither": diff --git a/Sources/LiveAudioServerCore/Config.swift b/Sources/LiveAudioServerCore/Config.swift index 06685bb..47053a0 100644 --- a/Sources/LiveAudioServerCore/Config.swift +++ b/Sources/LiveAudioServerCore/Config.swift @@ -62,6 +62,11 @@ public struct ServerConfig { public var aacBitrate: Int = 128_000 // bps (AudioToolbox uses bps) public var verbose: Bool = false public var stdinChunkFrames: Int = 4096 // PCM frames read per stdin iteration + /// When set, the CLI exits if its parent process dies (even on a crash where + /// the parent can't run cleanup). Lets a host app guarantee this helper is + /// reaped rather than orphaned holding its HTTP port. CLI-only; see + /// `LiveAudioServerApp`. + public var exitWithParent: Bool = false public var keepAliveOnInputEnd: Bool = false /// When the input stream is a FIFO/named pipe and `keepAliveOnInputEnd` is /// on, re-`open()` the same path on EOF so a new producer can attach.