authorgravatar for 29247443+rishi-chauhan@users.noreply.github.comRishi Raj Singh Chauhan <29247443+rishi-chauhan@users.noreply.github.com> 2026-03-29 00:11:26+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-28 18:41:26+00:00
log04298962be548a2d80433d0cc2c777a8b9de12a2
tree115cd31c5af3c6d2a087b57b06d7e172006197f4
parent61f983098be44316944e77e3bc81538a6a8fb0e9
signature Signed by PGP key B5690EEEBB952194

Fix server fallback to direct fd read on /proc/self/fd failure (#1488)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

2 files changed, 94 insertions(+), 13 deletions(-)

apps/server/src/bootstrap.test.ts+53
......@@ -8,10 +8,33 @@ import * as Duration from "effect/Duration";
88import * as Effect from "effect/Effect";
99import * as Fiber from "effect/Fiber";
1010import { TestClock } from "effect/testing";
11import { vi } from "vitest";
1112
1213import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap";
1314import { assertNone, assertSome } from "@effect/vitest/utils";
1415
16const openSyncInterceptor = vi.hoisted(() => ({ failPath: null as string | null }));
17
18vi.mock("node:fs", async (importOriginal) => {
19 const actual = await importOriginal<typeof import("node:fs")>();
20 return {
21 ...actual,
22 openSync: (...args: Parameters<typeof actual.openSync>) => {
23 const [filePath, flags] = args;
24 if (
25 typeof filePath === "string" &&
26 filePath === openSyncInterceptor.failPath &&
27 flags === "r"
28 ) {
29 const error = new Error("no such device or address");
30 Object.assign(error, { code: "ENXIO" });
31 throw error;
32 }
33 return (actual.openSync as (...a: typeof args) => number)(...args);
34 },
35 };
36});
37
1538const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
1639
1740it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
......@@ -47,6 +70,36 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
4770 }),
4871 );
4972
73 it.effect("falls back to reading the inherited fd when path duplication fails", () =>
74 Effect.gen(function* () {
75 const fs = yield* FileSystem.FileSystem;
76 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
77
78 yield* fs.writeFileString(
79 filePath,
80 `${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({
81 mode: "desktop",
82 })}\n`,
83 );
84
85 // Open without acquireRelease: the direct-stream fallback uses autoClose: true,
86 // so the stream owns the fd lifecycle and closes it asynchronously on end.
87 // Attempting to also close it synchronously in a finalizer races with the
88 // stream's async close and produces an uncaught EBADF.
89 const fd = NFS.openSync(filePath, "r");
90
91 openSyncInterceptor.failPath = `/proc/self/fd/${fd}`;
92 try {
93 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
94 assertSome(payload, {
95 mode: "desktop",
96 });
97 } finally {
98 openSyncInterceptor.failPath = null;
99 }
100 }),
101 );
102
50103 it.effect("returns none when the fd is unavailable", () =>
51104 Effect.gen(function* () {
52105 const fd = NFS.openSync("/dev/null", "r");
apps/server/src/bootstrap.ts+41-13
......@@ -108,21 +108,26 @@ const makeBootstrapInputStream = (fd: number) =>
108108 try: () => {
109109 const fdPath = resolveFdPath(fd);
110110 if (fdPath === undefined) {
111 const stream = new Net.Socket({
112 fd,
113 readable: true,
114 writable: false,
115 });
116 stream.setEncoding("utf8");
117 return stream;
111 return makeDirectBootstrapStream(fd);
118112 }
119113
120 const streamFd = NFS.openSync(fdPath, "r");
121 return NFS.createReadStream("", {
122 fd: streamFd,
123 encoding: "utf8",
124 autoClose: true,
125 });
114 let streamFd: number | undefined;
115 try {
116 streamFd = NFS.openSync(fdPath, "r");
117 return NFS.createReadStream("", {
118 fd: streamFd,
119 encoding: "utf8",
120 autoClose: true,
121 });
122 } catch (error) {
123 if (isBootstrapFdPathDuplicationError(error)) {
124 if (streamFd !== undefined) {
125 NFS.closeSync(streamFd);
126 }
127 return makeDirectBootstrapStream(fd);
128 }
129 throw error;
130 }
126131 },
127132 catch: (error) =>
128133 new BootstrapError({
......@@ -131,6 +136,29 @@ const makeBootstrapInputStream = (fd: number) =>
131136 }),
132137 });
133138
139const makeDirectBootstrapStream = (fd: number): Readable => {
140 try {
141 return NFS.createReadStream("", {
142 fd,
143 encoding: "utf8",
144 autoClose: true,
145 });
146 } catch {
147 const stream = new Net.Socket({
148 fd,
149 readable: true,
150 writable: false,
151 });
152 stream.setEncoding("utf8");
153 return stream;
154 }
155};
156
157const isBootstrapFdPathDuplicationError = Predicate.compose(
158 Predicate.hasProperty("code"),
159 (_) => _.code === "ENXIO" || _.code === "EINVAL" || _.code === "EPERM",
160);
161
134162export function resolveFdPath(
135163 fd: number,
136164 platform: NodeJS.Platform = process.platform,