In commit "util: extract ReadSpawnResult and report read() failures as SpawnError" (a761acaa0897d8e5326996d58315d3d50e0f387c)
Might want to handle short reads here and also short/interrupted writes. Claude suggests:
// Read the child's error report. Returns nullopt on success, or a SpawnError to
// throw on failure. Success is a clean EOF: read() returns 0 with nothing
// buffered because the child's write end was closed by a successful exec (via
// FD_CLOEXEC). A fully-read struct is the failure the child reported. A read()
// error, or an EOF partway through the struct (the child died mid-report), is
// surfaced as a SpawnErrorOp::READ failure rather than being mistaken for
// success. A single read() is not guaranteed to return all sizeof(SpawnError)
// bytes (the socket is SOCK_STREAM, which has no message boundaries) and may be
// interrupted by a signal, so loop until the whole struct is read.
std::optional<SpawnError> ReadSpawnResult(int fd)
{
SpawnError error{};
char* buf = reinterpret_cast<char*>(&error);
size_t remaining = sizeof(error);
while (remaining > 0) {
const ssize_t n = ::read(fd, buf, remaining);
if (n < 0) {
if (errno == EINTR) continue;
return SpawnError{.which = SpawnErrorOp::READ, .err = errno};
}
if (n == 0) {
if (remaining == sizeof(error)) return std::nullopt; // clean EOF: success
return SpawnError{.which = SpawnErrorOp::READ, .err = EPROTO}; // torn report
}
buf += n;
remaining -= static_cast<size_t>(n);
}
return error;
}
// Write the whole SpawnError to fd, retrying short writes and EINTR so the
// parent never sees a torn struct. Runs in the post-fork child, so it must stay
// async-signal-safe: it only calls write() and does not allocate or throw. This
// is best-effort -- if the write cannot complete there is nothing useful the
// child can do, so it stops and lets the caller _exit().
void WriteSpawnError(int fd, const SpawnError& error)
{
const char* buf = reinterpret_cast<const char*>(&error);
size_t remaining = sizeof(error);
while (remaining > 0) {
const ssize_t n = ::write(fd, buf, remaining);
if (n < 0) {
if (errno == EINTR) continue;
break;
}
buf += n;
remaining -= static_cast<size_t>(n);
}
if (remaining > 0) {
// The parent's read end is gone (e.g. the parent exited before the
// child could report), so the structured error can't be delivered.
// Leave a breadcrumb on stderr and exit. The exit code is irrelevant
// here since no live parent remains to wait on it.
ChildFail("SpawnProcess(child): failed and could not report error to parent\n");
}
}
Note this uses ChildFail from #311 (which will probably be merged first so should be available)