diff --git a/apps/desktop/src/backend/DesktopBackendManager.test.ts b/apps/desktop/src/backend/DesktopBackendManager.test.ts index 8cc1a9fc6433d..eaf7fc3084567 100644 --- a/apps/desktop/src/backend/DesktopBackendManager.test.ts +++ b/apps/desktop/src/backend/DesktopBackendManager.test.ts @@ -431,6 +431,30 @@ describe("DesktopBackendManager", () => { }), ); + it.effect("reports a SIGKILL exit without an exit code", () => + Effect.gen(function* () { + const exitCause = PlatformError.systemError({ + _tag: "Unknown", + module: "ChildProcess", + method: "exitCode", + cause: new Error("Process interrupted due to receipt of signal: 'SIGKILL'"), + }); + const layerSpawner = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => + Effect.succeed(makeProcess({ exitCode: Effect.fail(exitCause) })), + ), + ); + + const exit = yield* DesktopBackendManager.runBackendProcess({ + ...baseConfig, + desktopTelemetryStream: Stream.empty, + }).pipe(Effect.scoped, Effect.provide(Layer.merge(layerSpawner, layerHealthyHttpClient))); + + assert.deepEqual(exit, { code: Option.none(), reason: "killed by SIGKILL" }); + }), + ); + it.effect("reports output stream failures with process and stream context", () => Effect.gen(function* () { const outputCause = PlatformError.systemError({ diff --git a/apps/desktop/src/backend/DesktopBackendManager.ts b/apps/desktop/src/backend/DesktopBackendManager.ts index 8d2a3f1c4912f..5ad864f945d78 100644 --- a/apps/desktop/src/backend/DesktopBackendManager.ts +++ b/apps/desktop/src/backend/DesktopBackendManager.ts @@ -568,8 +568,22 @@ export const runBackendProcess = Effect.fn("runBackendProcess")(function* ( yield* probeReadiness().pipe(Effect.repeat({ while: (ready) => !ready }), Effect.forkScoped); const exit = yield* handle.exitCode.pipe( - Effect.mapError( - (cause) => + Effect.map((exitCode) => ({ + code: Option.some(exitCode), + reason: `code=${exitCode}`, + })), + Effect.catch((cause) => { + // Effect's Node spawner exposes signal exits only through this cause message. + const signal = + cause.cause instanceof Error + ? /^Process interrupted due to receipt of signal: '(SIG[A-Z0-9]+)'$/.exec( + cause.cause.message, + )?.[1] + : undefined; + if (signal !== undefined) { + return Effect.succeed({ code: Option.none(), reason: `killed by ${signal}` }); + } + return Effect.fail( new BackendProcessExitStatusError({ executablePath: options.executablePath, entryPath: options.entryPath, @@ -578,7 +592,8 @@ export const runBackendProcess = Effect.fn("runBackendProcess")(function* ( pid: Number(handle.pid), cause, }), - ), + ); + }), Effect.exit, ); yield* options.onExitObserved?.() ?? Effect.void; @@ -592,11 +607,7 @@ export const runBackendProcess = Effect.fn("runBackendProcess")(function* ( if (Exit.isFailure(exit)) { return yield* Effect.failCause(exit.cause); } - const exitCode = exit.value; - return { - code: Option.some(exitCode), - reason: `code=${exitCode}`, - } satisfies BackendProcessExit; + return exit.value; }); export const makeBackendInstance = Effect.fn("makeBackendInstance")(function* (