diff --git a/apps/desktop/src/main/host-service/index.ts b/apps/desktop/src/main/host-service/index.ts index d4224a576fa..0a757b2b55a 100644 --- a/apps/desktop/src/main/host-service/index.ts +++ b/apps/desktop/src/main/host-service/index.ts @@ -18,7 +18,7 @@ import { resolveTerminalBaseEnv, } from "@superset/host-service/terminal-env"; import { connectRelay } from "@superset/host-service/tunnel"; -import { removeManifest, writeManifest } from "main/lib/host-service-manifest"; +import { writeManifest } from "main/lib/host-service-manifest"; import { env } from "./env"; async function main(): Promise { @@ -81,10 +81,8 @@ async function main(): Promise { ); injectWebSocket(server); + // Manifest lifecycle belongs to the coordinator, not the child. const shutdown = () => { - if (env.ORGANIZATION_ID) { - removeManifest(env.ORGANIZATION_ID); - } server.close(); process.exit(0); }; diff --git a/apps/desktop/src/main/lib/host-service-coordinator.ts b/apps/desktop/src/main/lib/host-service-coordinator.ts index a39c1898dde..7ad475f02df 100644 --- a/apps/desktop/src/main/lib/host-service-coordinator.ts +++ b/apps/desktop/src/main/lib/host-service-coordinator.ts @@ -55,6 +55,18 @@ const HEALTH_POLL_INTERVAL = 200; const HEALTH_POLL_TIMEOUT = 10_000; const ADOPTED_LIVENESS_INTERVAL = 5_000; +function openLogFile(organizationId: string): number { + try { + const dir = manifestDir(organizationId); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + } + return fs.openSync(path.join(dir, "host-service.log"), "a", 0o600); + } catch { + return -1; + } +} + async function findFreePort(): Promise { return new Promise((resolve, reject) => { const server = createServer(); @@ -402,10 +414,25 @@ export class HostServiceCoordinator extends EventEmitter { this.emitStatus(organizationId, "starting", null); const env = await this.buildEnv(organizationId, port, secret, config); - const child = childProcess.spawn(process.execPath, [this.scriptPath], { - stdio: ["ignore", "pipe", "pipe"], - env, - }); + + // Detached + file-backed stdio so the child outlives the parent's process + // group (Squirrel.Mac SIGTERMs it during updates) and doesn't depend on + // parent-held stdout/stderr pipes. + const logFd = openLogFile(organizationId); + let child: childProcess.ChildProcess; + try { + child = childProcess.spawn(process.execPath, [this.scriptPath], { + detached: true, + stdio: logFd >= 0 ? ["ignore", logFd, logFd] : "ignore", + env, + }); + } finally { + if (logFd >= 0) { + try { + fs.closeSync(logFd); + } catch {} + } + } const childPid = child.pid; if (!childPid) { @@ -415,14 +442,6 @@ export class HostServiceCoordinator extends EventEmitter { instance.pid = childPid; - child.stdout?.on("data", (data: Buffer) => { - console.log(`[host-service:${organizationId}] ${data.toString().trim()}`); - }); - child.stderr?.on("data", (data: Buffer) => { - console.error( - `[host-service:${organizationId}] ${data.toString().trim()}`, - ); - }); child.on("exit", (code) => { console.log(`[host-service:${organizationId}] exited with code ${code}`); const current = this.instances.get(organizationId);