Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 18 additions & 42 deletions apps/desktop/src/main/lib/host-service-coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,8 @@ export class HostServiceCoordinator extends EventEmitter {
clearTimeout(timeout);
if (!response.ok) return null;
const data = await response.json();
return data?.result?.data?.version ?? null;
const result = data?.result?.data;
return result?.json?.version ?? result?.version ?? null;
} catch {
return null;
}
Expand Down Expand Up @@ -401,44 +402,32 @@ export class HostServiceCoordinator extends EventEmitter {
this.emitStatus(organizationId, "starting", null);

const childEnv = await this.buildEnv(organizationId, port, secret, config);
// Gate on app.isPackaged — the authoritative "running from an installed
// bundle" signal. NODE_ENV is ambient (shell, wrappers, debug launches)
// and could silently flip detach off in a packaged app, which would
// re-introduce the exact Squirrel kill-chain this file exists to fix.
const isPackaged = app.isPackaged;

// In packaged builds, detach so the child survives app relaunch:
// auto-updater's quitAndInstall would otherwise take the host-service
// (and its PTYs) down with the old app's process group. Stdio must
// point at real fds — piped stdio would EPIPE once the parent exits.
// Unpackaged (dev) keeps pipes so logs flow to the Electron console;
// enableDevReload restarts instances on rebuild, so survival isn't
// needed.
const logFd = isPackaged
? openRotatingLogFd(
path.join(manifestDir(organizationId), "host-service.log"),
MAX_HOST_LOG_BYTES,
)
: -1;
const stdio: childProcess.StdioOptions = !isPackaged
? ["ignore", "pipe", "pipe"]
: logFd >= 0
? ["ignore", logFd, logFd]
: ["ignore", "ignore", "ignore"];
// Host-service owns v2 PTYs, so it must survive Electron restarts in
// every environment. This mirrors the terminal-host daemon: detach the
// child and back stdio with real files so parent teardown cannot close
// pipes and take the service down with the app.
const logFd = openRotatingLogFd(
path.join(manifestDir(organizationId), "host-service.log"),
MAX_HOST_LOG_BYTES,
);
const stdio: childProcess.StdioOptions =
logFd >= 0 ? ["ignore", logFd, logFd] : ["ignore", "ignore", "ignore"];

let child: childProcess.ChildProcess;
let scopeUnit: string | null = null;
try {
// On Linux, spawnPersistent wraps packaged-build spawns with
// FORK NOTE: spawnPersistent wraps Linux spawns with
// `systemd-run --user --scope` so the host-service survives
// Electron's systemd-logind app scope terminating on quit.
// In dev builds (`!isPackaged`) we keep plain pipes for log flow
// and spawnPersistent falls back to regular spawn automatically.
// On other platforms it falls back to regular childProcess.spawn.
// detached: true everywhere — host-service owns v2 PTYs and must
// survive Electron restarts in every environment (mirrors
// terminal-host daemon, see upstream PR #3732).
const spawned = spawnPersistent(
process.execPath,
[this.scriptPath],
{
detached: isPackaged,
detached: true,
stdio,
env: childEnv,
// Avoid a flashing CMD window on Windows for the detached child.
Expand Down Expand Up @@ -466,19 +455,6 @@ export class HostServiceCoordinator extends EventEmitter {

instance.pid = childPid;
instance.scopeUnit = scopeUnit;

if (!isPackaged) {
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);
Expand Down
Loading