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
53 changes: 53 additions & 0 deletions apps/server/src/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,33 @@ import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import { TestClock } from "effect/testing";
import { vi } from "vitest";

import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap";
import { assertNone, assertSome } from "@effect/vitest/utils";

const openSyncInterceptor = vi.hoisted(() => ({ failPath: null as string | null }));

vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
...actual,
openSync: (...args: Parameters<typeof actual.openSync>) => {
const [filePath, flags] = args;
if (
typeof filePath === "string" &&
filePath === openSyncInterceptor.failPath &&
flags === "r"
) {
const error = new Error("no such device or address");
Object.assign(error, { code: "ENXIO" });
throw error;
}
return (actual.openSync as (...a: typeof args) => number)(...args);
},
};
});

const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });

it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
Expand Down Expand Up @@ -47,6 +70,36 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
}),
);

it.effect("falls back to reading the inherited fd when path duplication fails", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });

yield* fs.writeFileString(
filePath,
`${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({
mode: "desktop",
})}\n`,
);

// Open without acquireRelease: the direct-stream fallback uses autoClose: true,
// so the stream owns the fd lifecycle and closes it asynchronously on end.
// Attempting to also close it synchronously in a finalizer races with the
// stream's async close and produces an uncaught EBADF.
const fd = NFS.openSync(filePath, "r");

openSyncInterceptor.failPath = `/proc/self/fd/${fd}`;
try {
const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
assertSome(payload, {
mode: "desktop",
});
} finally {
openSyncInterceptor.failPath = null;
}
}),
);

it.effect("returns none when the fd is unavailable", () =>
Effect.gen(function* () {
const fd = NFS.openSync("/dev/null", "r");
Expand Down
54 changes: 41 additions & 13 deletions apps/server/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,26 @@ const makeBootstrapInputStream = (fd: number) =>
try: () => {
const fdPath = resolveFdPath(fd);
if (fdPath === undefined) {
const stream = new Net.Socket({
fd,
readable: true,
writable: false,
});
stream.setEncoding("utf8");
return stream;
return makeDirectBootstrapStream(fd);
}

const streamFd = NFS.openSync(fdPath, "r");
return NFS.createReadStream("", {
fd: streamFd,
encoding: "utf8",
autoClose: true,
});
let streamFd: number | undefined;
try {
streamFd = NFS.openSync(fdPath, "r");
return NFS.createReadStream("", {
fd: streamFd,
encoding: "utf8",
autoClose: true,
});
} catch (error) {
if (isBootstrapFdPathDuplicationError(error)) {
if (streamFd !== undefined) {
NFS.closeSync(streamFd);
}
return makeDirectBootstrapStream(fd);
}
throw error;
}
},
catch: (error) =>
new BootstrapError({
Expand All @@ -131,6 +136,29 @@ const makeBootstrapInputStream = (fd: number) =>
}),
});

const makeDirectBootstrapStream = (fd: number): Readable => {
try {
return NFS.createReadStream("", {
fd,
encoding: "utf8",
autoClose: true,
});
} catch {
const stream = new Net.Socket({
fd,
readable: true,
writable: false,
});
stream.setEncoding("utf8");
return stream;
}
};

const isBootstrapFdPathDuplicationError = Predicate.compose(
Predicate.hasProperty("code"),
(_) => _.code === "ENXIO" || _.code === "EINVAL" || _.code === "EPERM",
);

export function resolveFdPath(
fd: number,
platform: NodeJS.Platform = process.platform,
Expand Down