Skip to content
Closed
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
17 changes: 14 additions & 3 deletions JetlinkKit/Sources/jetlink-server/Serve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@
/// Runs `stop` on the main queue at SIGINT or SIGTERM, then exits 0 by
/// `exitNow`. Never returns.
func stopOnSignals(_ stop: @escaping @Sendable (String) -> Void) -> Never {
// The sources are registered before the main thread goes to dispatch: on
// Linux dispatchMain leaves it in sigsuspend with nothing blocked, so a
// held signal that comes before a source has taken over ends the process.
// Their own queue, since the main queue does not run until then.
let queue = DispatchQueue(label: "jetlink.signals")
let registered = DispatchGroup()
let sources = stopSignals.map { number, name in
#if os(macOS)
// Handled by the source alone; a shell that starts this in the
Expand All @@ -218,14 +224,19 @@
#endif
// Not on Linux, where the signals are held: ignoring one drops it if
// it is already pending, a SIGTERM that came during start-up.
let source = DispatchSource.makeSignalSource(signal: number, queue: .main)
let source = DispatchSource.makeSignalSource(signal: number, queue: queue)
registered.enter()
source.setRegistrationHandler { registered.leave() }
source.setEventHandler {
stop(name)
exitNow(0)
DispatchQueue.main.async {
stop(name)
exitNow(0)
}
}
source.resume()
return source
}
registered.wait()
withExtendedLifetime(sources) { dispatchMain() }
}

Expand Down
35 changes: 35 additions & 0 deletions JetlinkKit/Tests/JetlinkServerCommandTests/ProcessTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,40 @@
#expect(stopping.upperBound <= server.lowerBound)
#expect(server.upperBound <= statusPage.lowerBound)
}

#if os(Linux)
/// A SIGTERM during start-up waits for the handler, rather than ending
/// the process: Linux holds it until the signal sources are registered.
@Test func sigtermDuringStartUpStillStopsCleanly() throws {
let tmp = try TemporaryDirectory()
let page = try TCPListener(host: "127.0.0.1", port: 0).port
let log = try tmp.file("server.log", "")
let process = Process()
process.executableURL = try #require(Binary.url)
process.arguments = [
"--backend", "ort", "--device", "cpu", "--listen", "--host", "127.0.0.1", "--port", "0", "--cache", tmp.path, "--no-preload",
"--status-port", "\(page)",
]
let handle = try FileHandle(forWritingTo: log)
process.standardOutput = handle
process.standardError = handle
try process.run()
defer { if process.isRunning { process.terminate() } }

// At the serving line, before the web page and the handler are up.
let serving = eventually(timeout: 30) {
!process.isRunning || String(decoding: (try? Data(contentsOf: log)) ?? Data(), as: UTF8.self).contains("jetlink-server is serving")
}
guard serving, process.isRunning else {
Issue.record("never served: \(String(decoding: (try? Data(contentsOf: log)) ?? Data(), as: UTF8.self))")
return
}
kill(process.processIdentifier, SIGTERM)
process.waitUntilExit()
let text = String(decoding: try Data(contentsOf: log), as: UTF8.self)
#expect(process.terminationReason == .exit && process.terminationStatus == 0, "\(text)")
#expect(text.contains("stopping on SIGTERM") && text.contains("stopped the web page"), "\(text)")
}
#endif
}
#endif