Skip to content

refactor(desktop): host-service detach — rotation, perms, windowsHide, dev pipes#3616

Merged
Kitenite merged 6 commits into
mainfrom
fix/desktop-host-service-survives-update
Apr 21, 2026
Merged

refactor(desktop): host-service detach — rotation, perms, windowsHide, dev pipes#3616
Kitenite merged 6 commits into
mainfrom
fix/desktop-host-service-survives-update

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 21, 2026

Summary

Builds on #3620, which landed the core fix (spawn host-service with detached: true + file-backed stdio so it survives Squirrel's process-group SIGTERM on auto-update). This PR adds hardening and cleanup on top:

  • Gate on app.isPackaged, not NODE_ENV. Packaged builds detach and log to file; unpackaged (dev) keeps piped stdio so logs stream to the Electron console. NODE_ENV is ambient and could silently flip detach off in a packaged app — app.isPackaged is the authoritative signal.
  • Rotating log file. ~/.superset/host/{orgId}/host-service.log is truncated once it exceeds 5MB (mirrors MAX_DAEMON_LOG_BYTES in terminal-host/client.ts). Without rotation, a long-lived detached host-service's log grows unbounded.
  • Normalize log-file perms via chmodSync. openSync's mode arg only applies on create, so a pre-existing file rotated out-of-band keeps old perms. Warn on failure.
  • windowsHide: true on the spawn so the detached Node child doesn't flash a CMD window on Windows.
  • Keep dev-console log forwarding. Unpackaged runs still pipe stdout/stderr to the Electron console so bun dev and enableDevReload stay ergonomic.
  • Extract pure helpers (openRotatingLogFd, findFreePort, pollHealthCheck) to host-service-utils.ts to keep the coordinator focused on lifecycle.
  • Minor: clearTimeout now runs in finally in pollHealthCheck so the 2s abort timer is cleared on fetch failure too.

Test plan

  • Packaged build: update install → host-service survives, new app re-adopts via discoverAll(); PTYs stay alive.
  • Packaged build: ~/.superset/host/{orgId}/host-service.log is created at 0o600; verify truncation past 5MB.
  • Packaged build with NODE_ENV=development exported in shell: still detaches and writes to log file (proves app.isPackaged gate).
  • Dev (bun dev): host-service logs still stream to Electron console; enableDevReload restart on source edit still works.
  • Explicit "restart host service" from coordinator still tears down and respawns.

Summary by CodeRabbit

  • New Features

    • More reliable host startup with automatic free-port selection and robust health-check polling.
  • Bug Fixes

    • Per-organization rotating log files to prevent unbounded log growth.
    • Environment-aware process launching: dev shows live logs in the console; packaged builds run the host in background with file-backed logging and reduced console noise.

The host-service child was spawned without `detached: true`, so it
shared the Electron main's process group. When Squirrel relaunched
the app on auto-update, the old app's process group got killed —
taking the host-service and all its PTYs with it. The whole
manifest-adoption design in HOST_SERVICE_LIFECYCLE.md assumed the
child survives, but the spawn options contradicted that.

Mirror the v1 terminal-host daemon pattern (client.ts:1160-1221):
`detached: true` in prod, stdio pointed at a per-org rotating log
file at `~/.superset/host/{orgId}/host.log` (piped stdio would EPIPE
once the parent exits). Dev mode keeps pipes for live console logs
since enableDevReload restarts instances on bundle rebuild anyway.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors HostServiceCoordinator to use shared utilities for port selection and health polling, changes spawn behavior based on app.isPackaged (detached/stdio and logging), adds rotating log fd handling and best-effort fd cleanup, and introduces a new host-service-utils.ts with port, log, and health-poll helpers and related constants.

Changes

Cohort / File(s) Summary
Coordinator changes
apps/desktop/src/main/lib/host-service-coordinator.ts
Replaced local port/health helpers with imports from host-service-utils; gate spawn options on app.isPackaged (detached, windowsHide, stdio to pipes vs rotating log fd or ignore); forward child stdout/stderr in dev; updated timeout messaging to use HEALTH_POLL_TIMEOUT_MS; adjusted fd close to best-effort.
New host-service utilities
apps/desktop/src/main/lib/host-service-utils.ts
Added MAX_HOST_LOG_BYTES, HEALTH_POLL_TIMEOUT_MS, findFreePort(), openRotatingLogFd(...) (dir creation, truncate-over-limit, open+chmod, returns fd or -1), and pollHealthCheck(endpoint, secret, timeoutMs?) (fetch loop with 2s per-request abort, interval polling).

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    participant Coordinator
    end
    participant FS as "Filesystem"
    participant Child as "Host Service (child)"
    participant Health as "Child HTTP /health"
    Coordinator->>FS: openRotatingLogFd(path, MAX_HOST_LOG_BYTES)
    FS-->>Coordinator: fd or -1
    Coordinator->>Coordinator: findFreePort()
    Coordinator-->>Child: spawn(port, detached?, stdio->pipes|fd, windowsHide)
    alt non-packaged (dev)
        Child->>Coordinator: stdout/stderr chunks
        Coordinator-->>Coordinator: forward to Electron console
    else packaged
        Child->>FS: write to log fd (rotating)
    end
    Coordinator->>Health: pollHealthCheck(endpoint, secret, HEALTH_POLL_TIMEOUT_MS)
    loop until success or timeout
        Health-->>Coordinator: HTTP OK / not-OK
    end
    Coordinator->>Child: on timeout -> log failure using HEALTH_POLL_TIMEOUT_MS
    Coordinator->>FS: best-effort close(fd) when applicable
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I'm a rabbit in the build tree, hopping light and spry,
I found a free port and a log that wouldn't cry,
Spawned a child that hummed, then polled till it said "hi",
Rotating files, small bytes, permissions set just right —
Hooray for tidy starts beneath the moonlit sky 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main changes: refactoring host-service spawning with detach mode, rotating logs, permission hardening, windowsHide flag, and dev-mode piped stdio.
Description check ✅ Passed The PR description provides a detailed summary, explains the rationale for each change, includes a comprehensive test plan, and covers all major aspects of the refactor and lifecycle improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/desktop-host-service-survives-update

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 21, 2026

Greptile Summary

This PR fixes a critical bug where the v2 host-service child process was killed during Electron auto-updates because it shared the parent app's process group. The fix spawns the host-service with detached: true in production builds, making it a session/process-group leader (via setsid()) that survives the updater's quit-and-relaunch cycle. The discoverAll() manifest-adoption path on next launch then re-adopts the still-running process, preserving live PTY sessions across updates.

Key changes:

  • Production spawn: detached: true, stdio redirected to a per-org rotating log file at ~/.superset/host/{orgId}/host.log (real fds required — piped stdio would EPIPE on parent exit).
  • Dev spawn: unchanged — piped stdio with console forwarding; enableDevReload restarts on bundle rebuild, so session survival is not needed.
  • Log rotation: openRotatingLogFd truncates the log file in-place once it exceeds 5 MB; the parent fd is closed after spawn so the child holds the only live reference.
  • child.unref(): already present pre-PR; correctly tells Node.js the parent event loop should not be kept alive by this child.

Confidence Score: 4/5

Safe to merge — the fix is correct and well-reasoned; two non-blocking P2 suggestions remain.

Core logic is sound: detached spawn + real fds + unref() is the standard pattern for daemon-style child processes. The try/finally for fd cleanup is correct. Dev mode is unaffected. Only minor improvements remain around windowsHide for Windows UX and log rotation preserving history.

No files require special attention; both P2s are in host-service-coordinator.ts.

Important Files Changed

Filename Overview
apps/desktop/src/main/lib/host-service-coordinator.ts Adds detached: true in prod so the host-service survives auto-update relaunch; introduces per-org rotating log file for prod stdio; keeps piped stdio in dev. Two minor P2s: missing windowsHide: true and truncation-only log rotation.

Sequence Diagram

sequenceDiagram
    participant OldApp as Electron Main (old)
    participant HSC as HostServiceCoordinator
    participant HS as host-service (detached)
    participant Updater as Squirrel Updater
    participant NewApp as Electron Main (new)
    participant HSC2 as HostServiceCoordinator (new)

    OldApp->>HSC: spawn(orgId, config)
    HSC->>HS: "childProcess.spawn(detached=true, stdio to host.log)"
    Note over HS: "setsid() - new process group leader"
    HSC->>HSC: "close parent fd copy after spawn"
    HSC->>HS: child.unref()

    Updater->>OldApp: quitAndInstall()
    Note over OldApp: "Old process group killed"
    Note over HS: "Survives in own session - PTYs intact"

    NewApp->>HSC2: discoverAll()
    HSC2->>HSC2: listManifests()
    HSC2->>HS: "tryAdopt - health check"
    HS-->>HSC2: 200 OK
    HSC2->>HSC2: "instances.set with adopted pid and port"
    Note over HSC2: "Adopted pid=X logged"
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/lib/host-service-coordinator.ts
Line: 454-458

Comment:
**Missing `windowsHide` for detached Windows processes**

On Windows, `detached: true` can cause a new console window to flash briefly (or remain visible) when the subprocess is created, since Electron's main process is a GUI process but the spawned Node process is a console subsystem process. Adding `windowsHide: true` suppresses this. Without it, users on Windows may see a transient CMD/terminal window on app launch or after auto-update.

```suggestion
		child = childProcess.spawn(process.execPath, [this.scriptPath], {
				detached: !isDev,
				stdio,
				env: childEnv,
				windowsHide: true,
			});
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/main/lib/host-service-coordinator.ts
Line: 65-83

Comment:
**Truncation-based rotation loses all previous logs**

The current rotation strategy overwrites the file with an empty string when it exceeds `MAX_HOST_LOG_BYTES`, meaning the entire log history (up to 5 MB) is discarded at once. A simple improvement is to rename the existing file to `host.log.1` before truncating, giving users a one-rotation window for debugging:

```typescript
if (size > maxBytes) {
    try {
        fs.renameSync(logPath, logPath + ".1");
    } catch {
        // Fall back to truncating in-place
        fs.writeFileSync(logPath, "", { mode: 0o600 });
    }
}
```

This is non-blocking to merge, but worth considering since post-crash diagnosis often relies on the rotated file.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(desktop): keep v2 host-service alive..." | Re-trigger Greptile

Comment on lines +454 to +458
child = childProcess.spawn(process.execPath, [this.scriptPath], {
detached: !isDev,
stdio,
env: childEnv,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing windowsHide for detached Windows processes

On Windows, detached: true can cause a new console window to flash briefly (or remain visible) when the subprocess is created, since Electron's main process is a GUI process but the spawned Node process is a console subsystem process. Adding windowsHide: true suppresses this. Without it, users on Windows may see a transient CMD/terminal window on app launch or after auto-update.

Suggested change
child = childProcess.spawn(process.execPath, [this.scriptPath], {
detached: !isDev,
stdio,
env: childEnv,
});
child = childProcess.spawn(process.execPath, [this.scriptPath], {
detached: !isDev,
stdio,
env: childEnv,
windowsHide: true,
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/lib/host-service-coordinator.ts
Line: 454-458

Comment:
**Missing `windowsHide` for detached Windows processes**

On Windows, `detached: true` can cause a new console window to flash briefly (or remain visible) when the subprocess is created, since Electron's main process is a GUI process but the spawned Node process is a console subsystem process. Adding `windowsHide: true` suppresses this. Without it, users on Windows may see a transient CMD/terminal window on app launch or after auto-update.

```suggestion
		child = childProcess.spawn(process.execPath, [this.scriptPath], {
				detached: !isDev,
				stdio,
				env: childEnv,
				windowsHide: true,
			});
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +65 to +83
function openRotatingLogFd(logPath: string, maxBytes: number): number {
try {
fs.mkdirSync(path.dirname(logPath), { recursive: true, mode: 0o700 });
if (fs.existsSync(logPath)) {
try {
const { size } = fs.statSync(logPath);
if (size > maxBytes) {
fs.writeFileSync(logPath, "", { mode: 0o600 });
}
} catch {
// Best-effort rotate
}
}
return fs.openSync(logPath, "a", 0o600);
} catch (error) {
console.warn(`[host-service] Failed to open log file ${logPath}: ${error}`);
return -1;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Truncation-based rotation loses all previous logs

The current rotation strategy overwrites the file with an empty string when it exceeds MAX_HOST_LOG_BYTES, meaning the entire log history (up to 5 MB) is discarded at once. A simple improvement is to rename the existing file to host.log.1 before truncating, giving users a one-rotation window for debugging:

if (size > maxBytes) {
    try {
        fs.renameSync(logPath, logPath + ".1");
    } catch {
        // Fall back to truncating in-place
        fs.writeFileSync(logPath, "", { mode: 0o600 });
    }
}

This is non-blocking to merge, but worth considering since post-crash diagnosis often relies on the rotated file.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/lib/host-service-coordinator.ts
Line: 65-83

Comment:
**Truncation-based rotation loses all previous logs**

The current rotation strategy overwrites the file with an empty string when it exceeds `MAX_HOST_LOG_BYTES`, meaning the entire log history (up to 5 MB) is discarded at once. A simple improvement is to rename the existing file to `host.log.1` before truncating, giving users a one-rotation window for debugging:

```typescript
if (size > maxBytes) {
    try {
        fs.renameSync(logPath, logPath + ".1");
    } catch {
        // Fall back to truncating in-place
        fs.writeFileSync(logPath, "", { mode: 0o600 });
    }
}
```

This is non-blocking to merge, but worth considering since post-crash diagnosis often relies on the rotated file.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/lib/host-service-coordinator.ts`:
- Around line 65-78: openRotatingLogFd currently only sets 0o600 when creating a
new file, leaving existing host.log with its old mode; to normalize permissions
like the daemon log pattern, after opening the file in openRotatingLogFd call
fs.chmodSync(logPath, 0o600) (wrapped in a try/catch to preserve best-effort
behavior) so both newly created and pre-existing log files end up with 0o600
before returning the fd; reference function name openRotatingLogFd and the
logPath variable when making this change.
- Around line 431-456: The code uses process.env.NODE_ENV (isDev) to decide
whether to detach the child process; change this to use Electron's packaging
flag instead: import Electron's app and replace the isDev check with a check on
app.isPackaged (or an inverted name like isPackaged) so the detach/stdio logic
is driven by app.isPackaged rather than NODE_ENV; update references around the
stdio/logFd/detached logic in host-service-coordinator.ts (the block that
computes isDev, logFd, stdio and spawns the child via childProcess.spawn) so
packaged apps detach regardless of NODE_ENV.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e1acd88-543c-4921-8ba2-a5ed33f50722

📥 Commits

Reviewing files that changed from the base of the PR and between ae6cf14 and f7c0d56.

📒 Files selected for processing (1)
  • apps/desktop/src/main/lib/host-service-coordinator.ts

Comment thread apps/desktop/src/main/lib/host-service-coordinator.ts Outdated
Comment thread apps/desktop/src/main/lib/host-service-coordinator.ts
- chmodSync log file after open: openSync's mode arg only applies on
  create, so a pre-existing file rotated out-of-band keeps old perms.
- windowsHide: true on the spawn so the detached Node child doesn't
  flash a CMD window on Windows.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/desktop/src/main/lib/host-service-coordinator.ts (1)

439-464: ⚠️ Potential issue | 🟠 Major

Gate detach on app.isPackaged, not NODE_ENV.

Per the earlier review on this hunk: the whole point of this PR is that the host-service must detach in the packaged/updateable app. Tying that to process.env.NODE_ENV means a packaged build launched with NODE_ENV=development (e.g., by a wrapper, CI smoke, or a user who exported it in their shell) silently falls back to piped stdio + non-detached, which reintroduces the exact auto-updater kill chain this PR fixes. app is already imported (line 9, used at 548).

🐛 Proposed fix
-		const childEnv = await this.buildEnv(organizationId, port, secret, config);
-		const isDev = process.env.NODE_ENV === "development";
+		const childEnv = await this.buildEnv(organizationId, port, secret, config);
+		const isDev = !app.isPackaged;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/host-service-coordinator.ts` around lines 439 -
464, Replace the current NODE_ENV-based gating with the Electron packaging flag
so detaching and log/stdio choices use app.isPackaged: change the isDev check
(currently "process.env.NODE_ENV === 'development'") to use app.isPackaged (or
invert as needed), then use that value when computing logFd, stdio, and the
detached option passed to childProcess.spawn (referencing symbols
isDev/logFd/stdio/childProcess.spawn/scriptPath/buildEnv). Ensure the code
detaches when app.isPackaged is true (and keeps pipes when not packaged); app is
already imported near the top, so use that import.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/desktop/src/main/lib/host-service-coordinator.ts`:
- Around line 439-464: Replace the current NODE_ENV-based gating with the
Electron packaging flag so detaching and log/stdio choices use app.isPackaged:
change the isDev check (currently "process.env.NODE_ENV === 'development'") to
use app.isPackaged (or invert as needed), then use that value when computing
logFd, stdio, and the detached option passed to childProcess.spawn (referencing
symbols isDev/logFd/stdio/childProcess.spawn/scriptPath/buildEnv). Ensure the
code detaches when app.isPackaged is true (and keeps pipes when not packaged);
app is already imported near the top, so use that import.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8d0cf3d3-5cbb-4913-ad3f-18fe4d8aa47a

📥 Commits

Reviewing files that changed from the base of the PR and between f7c0d56 and 0979a05.

📒 Files selected for processing (1)
  • apps/desktop/src/main/lib/host-service-coordinator.ts

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/main/lib/host-service-coordinator.ts">

<violation number="1" location="apps/desktop/src/main/lib/host-service-coordinator.ts:83">
P2: Do not silently swallow `chmodSync` failures here; log a warning so permission-hardening failures are observable.

(Based on your team's feedback about avoiding silent catch blocks that hide failures.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/desktop/src/main/lib/host-service-coordinator.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/desktop/src/main/lib/host-service-coordinator.ts (1)

368-397: ⚠️ Potential issue | 🟠 Major

Drive the detach decision off app.isPackaged, not NODE_ENV.

The whole point of this PR is for the packaged/auto-updating build to keep the host-service alive across relaunch. process.env.NODE_ENV is ambient and can be "development" in a packaged app (inherited shell env, debug launches, etc.) — in that case isDev is true, stdio stays piped, detached is false, and the child dies with the old process group on quitAndInstall, defeating manifest re-adoption. Electron's app.isPackaged is the authoritative signal for "running from an installed bundle."

🐛 Proposed fix
-		const childEnv = await this.buildEnv(organizationId, port, secret, config);
-		const isDev = process.env.NODE_ENV === "development";
+		const childEnv = await this.buildEnv(organizationId, port, secret, config);
+		const isDev = !app.isPackaged;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/host-service-coordinator.ts` around lines 368 -
397, The detach/stdio decision currently uses process.env.NODE_ENV (isDev) which
is unreliable; change it to use Electron's app.isPackaged so the child is
detached only for packaged apps. Replace the isDev definition with a boolean
based on app.isPackaged (e.g. const isPackaged = app.isPackaged) and flip the
logic where you decide logFd, stdio and the detached option in
childProcess.spawn (use detached: isPackaged and non-piped stdio when packaged,
keep pipes when not packaged). Ensure the module imports or accesses Electron's
app and update references to isDev accordingly (look for isDev, logFd, stdio,
and the childProcess.spawn call).
🧹 Nitpick comments (2)
apps/desktop/src/main/lib/host-service-utils.ts (2)

60-82: Minor: clearTimeout skipped when fetch rejects.

In the catch branch the 2s abort timer is never cleared, so each failed attempt keeps a pending timer alive until it fires (and aborts an already-settled controller). Harmless, but in a tight 200ms poll loop against a not-yet-listening endpoint (ECONNREFUSED resolves fast), you'll briefly stack ~10 timers. Move clearTimeout into a finally so it runs on both success and failure paths.

♻️ Suggested tweak
-		try {
-			const controller = new AbortController();
-			const timeout = setTimeout(() => controller.abort(), 2_000);
-			const res = await fetch(`${endpoint}/trpc/health.check`, {
-				signal: controller.signal,
-				headers: { Authorization: `Bearer ${secret}` },
-			});
-			clearTimeout(timeout);
-			if (res.ok) return true;
-		} catch {
-			// Not ready yet
-		}
+		const controller = new AbortController();
+		const timeout = setTimeout(() => controller.abort(), 2_000);
+		try {
+			const res = await fetch(`${endpoint}/trpc/health.check`, {
+				signal: controller.signal,
+				headers: { Authorization: `Bearer ${secret}` },
+			});
+			if (res.ok) return true;
+		} catch {
+			// Not ready yet
+		} finally {
+			clearTimeout(timeout);
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/host-service-utils.ts` around lines 60 - 82, The
pollHealthCheck function creates a 2s abort timer for each fetch but only calls
clearTimeout(timeout) on the success path, leaving timers alive when fetch
throws; fix by moving clearTimeout(timeout) into a finally block that runs after
the try/catch so the timeout is always cleared (refer to pollHealthCheck, the
AbortController instance named controller, the timeout variable, and the fetch
call to /trpc/health.check) ensuring clearTimeout(timeout) executes regardless
of success or failure.

19-28: Rotation drops all prior log history.

When host.log exceeds maxBytes you truncate in place, losing every line written before the rotation — exactly the history you'd want when debugging a host-service that just auto-updated. Consider a simple one-file rollover (host.loghost.log.1) before reopening. Keeping one previous file preserves the pre-rotation tail without unbounded growth.

♻️ Suggested rollover
-		if (fs.existsSync(logPath)) {
-			try {
-				const { size } = fs.statSync(logPath);
-				if (size > maxBytes) {
-					fs.writeFileSync(logPath, "", { mode: 0o600 });
-				}
-			} catch {
-				// Best-effort rotate
-			}
-		}
+		if (fs.existsSync(logPath)) {
+			try {
+				const { size } = fs.statSync(logPath);
+				if (size > maxBytes) {
+					const rotated = `${logPath}.1`;
+					try {
+						fs.rmSync(rotated, { force: true });
+					} catch {
+						// Best-effort
+					}
+					fs.renameSync(logPath, rotated);
+				}
+			} catch {
+				// Best-effort rotate
+			}
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/host-service-utils.ts` around lines 19 - 28,
Currently the code truncates logPath in place which drops all prior history;
instead implement a single-file rollover: if fs.existsSync(logPath) and size >
maxBytes, attempt to rename logPath to `${logPath}.1` (use fs.renameSync) — if
`${logPath}.1` already exists, unlink it first (fs.unlinkSync) or replace it
safely inside a try/catch — then create a new empty file at logPath with the
same permissions (fs.writeFileSync(logPath, "", { mode: 0o600 })) so the logger
can continue writing; fall back to the existing truncation behavior on any
rename/unlink error to preserve the current best-effort behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/desktop/src/main/lib/host-service-coordinator.ts`:
- Around line 368-397: The detach/stdio decision currently uses
process.env.NODE_ENV (isDev) which is unreliable; change it to use Electron's
app.isPackaged so the child is detached only for packaged apps. Replace the
isDev definition with a boolean based on app.isPackaged (e.g. const isPackaged =
app.isPackaged) and flip the logic where you decide logFd, stdio and the
detached option in childProcess.spawn (use detached: isPackaged and non-piped
stdio when packaged, keep pipes when not packaged). Ensure the module imports or
accesses Electron's app and update references to isDev accordingly (look for
isDev, logFd, stdio, and the childProcess.spawn call).

---

Nitpick comments:
In `@apps/desktop/src/main/lib/host-service-utils.ts`:
- Around line 60-82: The pollHealthCheck function creates a 2s abort timer for
each fetch but only calls clearTimeout(timeout) on the success path, leaving
timers alive when fetch throws; fix by moving clearTimeout(timeout) into a
finally block that runs after the try/catch so the timeout is always cleared
(refer to pollHealthCheck, the AbortController instance named controller, the
timeout variable, and the fetch call to /trpc/health.check) ensuring
clearTimeout(timeout) executes regardless of success or failure.
- Around line 19-28: Currently the code truncates logPath in place which drops
all prior history; instead implement a single-file rollover: if
fs.existsSync(logPath) and size > maxBytes, attempt to rename logPath to
`${logPath}.1` (use fs.renameSync) — if `${logPath}.1` already exists, unlink it
first (fs.unlinkSync) or replace it safely inside a try/catch — then create a
new empty file at logPath with the same permissions (fs.writeFileSync(logPath,
"", { mode: 0o600 })) so the logger can continue writing; fall back to the
existing truncation behavior on any rename/unlink error to preserve the current
best-effort behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c8da1fa-a77a-487f-a462-76d844dedf4d

📥 Commits

Reviewing files that changed from the base of the PR and between 0979a05 and f79f57d.

📒 Files selected for processing (2)
  • apps/desktop/src/main/lib/host-service-coordinator.ts
  • apps/desktop/src/main/lib/host-service-utils.ts

Reconcile with #3620 which landed a simpler version of the same fix on
main. Keep our implementation (dev/prod split, log rotation, chmod for
pre-existing files, windowsHide, utils extraction) and pick up the
`host-service/index.ts` change: drop `removeManifest` from the child's
SIGTERM handler — manifest lifecycle belongs to the coordinator.

Align log filename to `host-service.log` to match what landed on main.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/desktop/src/main/lib/host-service-coordinator.ts (1)

369-393: ⚠️ Potential issue | 🟠 Major

Drive the detach decision from app.isPackaged.

Line 369 uses ambient NODE_ENV to decide Line 392’s detached value. A packaged app launched with NODE_ENV=development would keep piped stdio and skip detaching, so the host-service would not survive the updater relaunch.

🐛 Proposed fix
-		const isDev = process.env.NODE_ENV === "development";
+		const isDev = !app.isPackaged;
#!/bin/bash
# Verify the packaged-app detach decision is not tied to ambient NODE_ENV.
rg -n -C4 'const isDev = process\.env\.NODE_ENV|const isDev = !app\.isPackaged|detached: !isDev' apps/desktop/src/main/lib/host-service-coordinator.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/host-service-coordinator.ts` around lines 369 -
393, Change the dev/packaged detection so detach is driven by Electron's
app.isPackaged: set isDev to process.env.NODE_ENV === "development" &&
!app.isPackaged (so a packaged app with NODE_ENV=development is not treated as
dev), and use app.isPackaged (or !app.isPackaged as appropriate) for the spawn
option instead of !isDev—i.e., replace detached: !isDev with detached:
app.isPackaged and keep stdio logic using the revised isDev; update references
to isDev and detached where they appear (the const isDev declaration and the
childProcess.spawn options).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/lib/host-service-coordinator.ts`:
- Around line 377-382: The current rotation call with openRotatingLogFd(...)
enforces MAX_HOST_LOG_BYTES only in the parent before spawning, but the detached
child inherits the fd and can grow the file unbounded; move log-rotation
responsibility into the spawned host service process so the process that owns
the FD enforces rotation for its lifetime. Modify the coordinator to pass a
filepath (path.join(manifestDir(organizationId), "host-service.log")) and
MAX_HOST_LOG_BYTES to the host-service entrypoint instead of pre-opening the FD
with openRotatingLogFd, update the host-service startup (the child process code
that runs as the service) to call openRotatingLogFd or equivalent rotation logic
on startup and keep the FD open and managed there, and remove/replace the
parent-side use of the returned logFd to avoid orphaned-unrotated files. Ensure
the unique symbols to change are openRotatingLogFd, MAX_HOST_LOG_BYTES,
manifestDir(organizationId), and the host-service process start routine so
rotation runs inside the spawned process.

---

Duplicate comments:
In `@apps/desktop/src/main/lib/host-service-coordinator.ts`:
- Around line 369-393: Change the dev/packaged detection so detach is driven by
Electron's app.isPackaged: set isDev to process.env.NODE_ENV === "development"
&& !app.isPackaged (so a packaged app with NODE_ENV=development is not treated
as dev), and use app.isPackaged (or !app.isPackaged as appropriate) for the
spawn option instead of !isDev—i.e., replace detached: !isDev with detached:
app.isPackaged and keep stdio logic using the revised isDev; update references
to isDev and detached where they appear (the const isDev declaration and the
childProcess.spawn options).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 703a37b7-ee5a-4add-b8e0-774175963294

📥 Commits

Reviewing files that changed from the base of the PR and between f79f57d and 3bd9c1b.

📒 Files selected for processing (2)
  • apps/desktop/src/main/lib/host-service-coordinator.ts
  • apps/desktop/src/main/lib/host-service-utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/main/lib/host-service-utils.ts

Comment on lines +377 to +382
const logFd = isDev
? -1
: openRotatingLogFd(
path.join(manifestDir(organizationId), "host-service.log"),
MAX_HOST_LOG_BYTES,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the log filename and whether rotation is only applied before spawn.
sed -n '16,42p' apps/desktop/src/main/lib/host-service-utils.ts
sed -n '377,405p' apps/desktop/src/main/lib/host-service-coordinator.ts
rg -n '"host\.log"|"host-service\.log"|MAX_HOST_LOG_BYTES|openRotatingLogFd' apps/desktop/src/main/lib packages/host-service --type ts

Repository: superset-sh/superset

Length of output: 2257


🏁 Script executed:

#!/bin/bash
# Search for test files and specifications related to host-service log filename
fd -e test.ts -e spec.ts -e test.tsx | xargs rg -l "host.*log|host-service.*log" 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for any references to the expected log filename in tests, fixtures, or docs
rg -n 'host\.log|"host-service\.log"' --type ts --type tsx apps/desktop packages/host-service -B 2 -A 2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Check if there's a test file for host-service-coordinator
find . -name '*host-service-coordinator*test*' -o -name '*host-service-coordinator*spec*' | head -10

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for any references to either filename (broader search, all file types)
rg -i 'host-service\.log|host\.log' apps/desktop packages/host-service

Repository: superset-sh/superset

Length of output: 296


🏁 Script executed:

#!/bin/bash
# Check for any PR, issue, or documentation comments about the log filename requirement
rg -n 'host.*service.*log|rotating.*log|MAX_HOST_LOG_BYTES' apps/desktop/src/main/lib/host-service-coordinator.ts -B 5 -A 5

Repository: superset-sh/superset

Length of output: 836


🏁 Script executed:

#!/bin/bash
# Look for any test or spec files with different naming patterns
find . -type f \( -name '*.test.ts' -o -name '*.spec.ts' -o -name '*.test.tsx' -o -name '*.spec.tsx' \) | xargs grep -l "host.*log" 2>/dev/null

Repository: superset-sh/superset

Length of output: 48


Address log rotation enforcement after child inherits the file descriptor.

openRotatingLogFd() only enforces MAX_HOST_LOG_BYTES before spawn; after the detached child inherits the fd, a surviving/adopted service can grow the log past 5MB indefinitely. Consider moving rotation into the host-service process that owns the fd for its lifetime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/host-service-coordinator.ts` around lines 377 -
382, The current rotation call with openRotatingLogFd(...) enforces
MAX_HOST_LOG_BYTES only in the parent before spawning, but the detached child
inherits the fd and can grow the file unbounded; move log-rotation
responsibility into the spawned host service process so the process that owns
the FD enforces rotation for its lifetime. Modify the coordinator to pass a
filepath (path.join(manifestDir(organizationId), "host-service.log")) and
MAX_HOST_LOG_BYTES to the host-service entrypoint instead of pre-opening the FD
with openRotatingLogFd, update the host-service startup (the child process code
that runs as the service) to call openRotatingLogFd or equivalent rotation logic
on startup and keep the FD open and managed there, and remove/replace the
parent-side use of the returned logFd to avoid orphaned-unrotated files. Ensure
the unique symbols to change are openRotatingLogFd, MAX_HOST_LOG_BYTES,
manifestDir(organizationId), and the host-service process start routine so
rotation runs inside the spawned process.

- Gate detach on app.isPackaged instead of NODE_ENV. NODE_ENV is ambient
  and could silently flip detach off in a packaged app (e.g. shell env),
  reintroducing the Squirrel kill-chain this PR exists to fix.
- Log a warning on chmodSync failure so permission issues are observable
  instead of being swallowed silently.
- Move clearTimeout into finally in pollHealthCheck so the 2s abort timer
  is also cleared when fetch rejects, not just on success.
@Kitenite Kitenite changed the title fix(desktop): keep v2 host-service alive across app updates refactor(desktop): host-service detach — rotation, perms, windowsHide, dev pipes Apr 21, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/desktop/src/main/lib/host-service-coordinator.ts (1)

382-386: ⚠️ Potential issue | 🟠 Major

Keep the log contract with the long-lived service.

openRotatingLogFd() enforces size only before spawn; after the detached child inherits the fd and survives app updates, the file can grow past MAX_HOST_LOG_BYTES until the service is restarted. Also, this writes host-service.log, while the PR objective/test plan calls out host.log.

Consider moving rotation/reopen ownership into the host-service process and aligning the filename with the documented path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/host-service-coordinator.ts` around lines 382 -
386, The coordinator currently opens a rotating fd via openRotatingLogFd (using
manifestDir(organizationId) and MAX_HOST_LOG_BYTES) and hands it to the detached
host service, which prevents ongoing rotation/reopen after the child outlives
the parent and also uses host-service.log instead of host.log; instead, stop
opening/rotating the file in host-service-coordinator.ts and change the
coordinator to pass a simple path/filename (use host.log) or an instruction flag
to the host service, then move rotation and reopen logic into the host-service
process so the long-lived service owns enforcement of MAX_HOST_LOG_BYTES, log
rotation and filename (refer to openRotatingLogFd, manifestDir,
MAX_HOST_LOG_BYTES, and the detached host service spawn logic).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/desktop/src/main/lib/host-service-coordinator.ts`:
- Around line 382-386: The coordinator currently opens a rotating fd via
openRotatingLogFd (using manifestDir(organizationId) and MAX_HOST_LOG_BYTES) and
hands it to the detached host service, which prevents ongoing rotation/reopen
after the child outlives the parent and also uses host-service.log instead of
host.log; instead, stop opening/rotating the file in host-service-coordinator.ts
and change the coordinator to pass a simple path/filename (use host.log) or an
instruction flag to the host service, then move rotation and reopen logic into
the host-service process so the long-lived service owns enforcement of
MAX_HOST_LOG_BYTES, log rotation and filename (refer to openRotatingLogFd,
manifestDir, MAX_HOST_LOG_BYTES, and the detached host service spawn logic).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d261b1f-a6c5-4028-bd31-d745d0ee29ba

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd9c1b and affabbc.

📒 Files selected for processing (2)
  • apps/desktop/src/main/lib/host-service-coordinator.ts
  • apps/desktop/src/main/lib/host-service-utils.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/main/lib/host-service-utils.ts

@Kitenite Kitenite merged commit ce065f3 into main Apr 21, 2026
6 of 7 checks passed
@Kitenite Kitenite deleted the fix/desktop-host-service-survives-update branch April 21, 2026 21:19
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant