From ee86e0f83587d17d44f3fbb320694d1559317a4e Mon Sep 17 00:00:00 2001 From: unlockdigital Date: Tue, 27 Oct 2026 15:27:34 -0500 Subject: [PATCH] fix(desktop): tolerate disk-full console output --- apps/desktop/src/app/stdioErrors.test.ts | 43 ++++++++++++++++++++++++ apps/desktop/src/app/stdioErrors.ts | 12 +++++++ apps/desktop/src/main.ts | 5 ++- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/app/stdioErrors.test.ts create mode 100644 apps/desktop/src/app/stdioErrors.ts diff --git a/apps/desktop/src/app/stdioErrors.test.ts b/apps/desktop/src/app/stdioErrors.test.ts new file mode 100644 index 0000000000000..2872c06abac4c --- /dev/null +++ b/apps/desktop/src/app/stdioErrors.test.ts @@ -0,0 +1,43 @@ +// @effect-diagnostics nodeBuiltinImport:off +import * as NodeChildProcess from "node:child_process"; +import * as NodeProcess from "node:process"; +import { assert, it } from "vite-plus/test"; + +import { handleStdioError } from "./stdioErrors.ts"; + +it.each([ + ["stdout", "EPIPE"], + ["stderr", "EPIPE"], + ["stdout", "ENOSPC"], + ["stderr", "ENOSPC"], + ["stdout", "EDQUOT"], + ["stderr", "EDQUOT"], +])("keeps the application alive when %s emits %s", (streamName, code) => { + const result = NodeChildProcess.spawnSync( + NodeProcess.execPath, + [ + "--input-type=module", + "--eval", + ` + import { handleStdioError } from ${JSON.stringify(new URL("./stdioErrors.ts", import.meta.url).href)}; + const stream = process[${JSON.stringify(streamName)}]; + const other = process[${JSON.stringify(streamName === "stdout" ? "stderr" : "stdout")}]; + stream.on("error", handleStdioError); + stream._write = (_chunk, _encoding, callback) => { + callback(Object.assign(new Error("diagnostic output unavailable"), { code: ${JSON.stringify(code)} })); + }; + stream.once("error", () => other.write("application continued\\n")); + stream.write("diagnostic record\\n"); + `, + ], + { encoding: "utf8" }, + ); + + assert.equal(result.status, 0, result.stderr); + assert.include(result.stdout + result.stderr, "application continued"); +}); + +it("preserves unexpected failures from standard output", () => { + const error = Object.assign(new Error("unexpected output failure"), { code: "EIO" }); + assert.throws(() => handleStdioError(error), error); +}); diff --git a/apps/desktop/src/app/stdioErrors.ts b/apps/desktop/src/app/stdioErrors.ts new file mode 100644 index 0000000000000..37f41c445fc9f --- /dev/null +++ b/apps/desktop/src/app/stdioErrors.ts @@ -0,0 +1,12 @@ +/** Console output failures must stay separate from application storage failures. */ +export function handleStdioError(error: unknown): void { + if ( + typeof error === "object" && + error !== null && + "code" in error && + (error.code === "EPIPE" || error.code === "ENOSPC" || error.code === "EDQUOT") + ) { + return; + } + throw error; +} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 3056ce1ea2637..60229639b3dcf 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1,8 +1,7 @@ import * as MacPermissions from "./permissions/MacPermissions.ts"; +import { handleStdioError } from "./app/stdioErrors.ts"; for (const stream of [process.stdout, process.stderr]) { - stream.on("error", (err: NodeJS.ErrnoException) => { - if (err.code !== "EPIPE") throw err; - }); + stream.on("error", handleStdioError); } import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";