Skip to content
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
2 changes: 1 addition & 1 deletion apps/headless/Sources/HeadlessCLI/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ private struct HostLauncher {
let count = withUnsafeMutableBytes(of: &byte) { buffer in
read(STDIN_FILENO, buffer.baseAddress, 1)
}
if count >= 0 || errno != EINTR { break }
if supervisedOwnerChannelClosed(readCount: count, errnoValue: errno) { break }
} else if status < 0, errno != EINTR {
break
}
Expand Down
10 changes: 9 additions & 1 deletion apps/headless/Sources/HeadlessProtocol/SupervisedHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import Darwin
import Glibc
#endif

/// Owner-pipe reads that mean the SDK/CLI is gone. EOF and unexpected data
/// close the host. `EINTR` / `EAGAIN` / `EWOULDBLOCK` must not, or a
/// spurious DispatchSource wakeup kills a live supervised session.
public func supervisedOwnerChannelClosed(readCount: Int, errnoValue: Int32) -> Bool {
if readCount >= 0 { return true }
return errnoValue != EINTR && errnoValue != EAGAIN && errnoValue != EWOULDBLOCK
}

public final class SupervisedHostOwnerMonitor: @unchecked Sendable {
private let source: DispatchSourceRead
private let lock = NSLock()
Expand All @@ -28,7 +36,7 @@ public final class SupervisedHostOwnerMonitor: @unchecked Sendable {
let count = withUnsafeMutableBytes(of: &byte) { buffer in
read(STDIN_FILENO, buffer.baseAddress, 1)
}
if count >= 0 || errno != EINTR {
if supervisedOwnerChannelClosed(readCount: count, errnoValue: errno) {
self.stop()
onOwnerExit()
}
Expand Down
28 changes: 28 additions & 0 deletions apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2766,6 +2766,33 @@ struct ProtocolTests {
)
}

static func supervisedOwnerChannelIgnoresRetryableReads() throws {
try expect(
supervisedOwnerChannelClosed(readCount: 0, errnoValue: 0),
"EOF on the owner pipe must stop the host"
)
try expect(
supervisedOwnerChannelClosed(readCount: 1, errnoValue: 0),
"unexpected owner-pipe data must stop the host"
)
try expect(
!supervisedOwnerChannelClosed(readCount: -1, errnoValue: EINTR),
"EINTR must not be treated as owner exit"
)
try expect(
!supervisedOwnerChannelClosed(readCount: -1, errnoValue: EAGAIN),
"EAGAIN must not be treated as owner exit"
)
try expect(
!supervisedOwnerChannelClosed(readCount: -1, errnoValue: EWOULDBLOCK),
"EWOULDBLOCK must not be treated as owner exit"
)
try expect(
supervisedOwnerChannelClosed(readCount: -1, errnoValue: EBADF),
"a broken owner descriptor must stop the host"
)
}

static func oversizedSocketRequestIsRejected() throws {
try LocalRuntime.preparePrivateDirectory()
let socketPath = LocalRuntime.directoryURL
Expand Down Expand Up @@ -4115,6 +4142,7 @@ struct ProtocolTests {
("host authentication orchestration", hostAuthenticationOrchestration),
("docs command reference matches help", docsCommandReferenceMatchesHelp),
("menu shortcuts have unique chords", menuShortcutsHaveUniqueChords),
("supervised owner channel ignores retryable reads", supervisedOwnerChannelIgnoresRetryableReads),
("artifact file upload boundaries", artifactUploadCommands),
]

Expand Down
5 changes: 3 additions & 2 deletions packages/headless-python/tests/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,9 @@ async def scenario() -> None:
HEADLESS_TEST_PID_FILE=str(pid_file),
),
)
# Cleanup has its own 100 ms budget after the single 300 ms startup budget.
assert time.monotonic() - started < 0.5
# Cleanup has its own 100 ms budget after the single 300 ms startup
# budget. Leave slack for slow macOS 3.14 CI runners.
assert time.monotonic() - started < 1.0

asyncio.run(scenario())
assert process_is_gone(int(pid_file.read_text()))
Expand Down