From fbe5862df49382e2dcea1cd351180b088b4af6d2 Mon Sep 17 00:00:00 2001 From: Huangshuo Kuang <141250392+kkkhs@users.noreply.github.com> Date: Sat, 5 Sep 2026 03:17:37 +0000 Subject: [PATCH] fix(runner): stop local mount poll on geesefs exit --- .../runner/src/engines/sandbox_agent/mount.ts | 35 ++++++++++++++++--- .../tests/unit/sandbox-agent-mount.test.ts | 30 ++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/services/runner/src/engines/sandbox_agent/mount.ts b/services/runner/src/engines/sandbox_agent/mount.ts index 974775ea773..955c7816103 100644 --- a/services/runner/src/engines/sandbox_agent/mount.ts +++ b/services/runner/src/engines/sandbox_agent/mount.ts @@ -244,6 +244,32 @@ function credEnv(creds: MountCredentials): Record { return env; } +function waitForExitOrDelay(exited: Promise, ms: number): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(resolve, ms); + void exited.then(() => { + clearTimeout(timeout); + resolve(); + }); + }); +} + +export async function waitForLocalMount( + isAlive: () => Promise, + hasExited: () => boolean, + exited: Promise, + attempts = 30, + delayMs = 500, +): Promise { + for (let i = 0; i < attempts; i++) { + if (hasExited()) return false; + if (await isAlive()) return true; + if (hasExited()) return false; + await waitForExitOrDelay(exited, delayMs); + } + return false; +} + /** * True only when `cwd` is a mountpoint AND the FUSE backend still serves I/O. * @@ -358,10 +384,11 @@ export async function mountStorage( child.unref(); // Poll up to ~15s for the mountpoint to serve I/O; geesefs logs "successfully mounted" // within ~1s normally. Resolve as soon as it's alive; the caller re-verifies after. - for (let i = 0; i < 30; i++) { - if (await isMounted(cwd, () => {})) break; - await new Promise((r) => setTimeout(r, 500)); - } + await waitForLocalMount( + () => isMounted(cwd, () => {}), + () => processExited, + exited, + ); return { stop: async () => { const waitForExit = async (): Promise => { diff --git a/services/runner/tests/unit/sandbox-agent-mount.test.ts b/services/runner/tests/unit/sandbox-agent-mount.test.ts index 49bb094b72c..3c3548f4cc0 100644 --- a/services/runner/tests/unit/sandbox-agent-mount.test.ts +++ b/services/runner/tests/unit/sandbox-agent-mount.test.ts @@ -16,6 +16,7 @@ import { mountStorage, unmountStorage, mountpointFailureState, + waitForLocalMount, discoverTunnelEndpoint, mountStorageRemote, harnessSessionMounts, @@ -244,6 +245,35 @@ function notMountedThenAlive(): (cwd: string) => Promise { }; } +describe("waitForLocalMount", () => { + it("stops polling when geesefs exits before the mount is alive", async () => { + let probes = 0; + let exited = false; + let resolveExit!: () => void; + const exitPromise = new Promise((resolve) => { + resolveExit = () => { + exited = true; + resolve(); + }; + }); + + const ok = await waitForLocalMount( + async () => { + probes += 1; + resolveExit(); + return false; + }, + () => exited, + exitPromise, + 30, + 1_000, + ); + + assert.equal(ok, false); + assert.equal(probes, 1); + }); +}); + describe("mountStorage", () => { it("builds the geesefs command with creds in env, not argv", async () => { let seenArgs: string[] = [];