Skip to content

refactor: unify serve + daemon — single process owns the stack - #991

Merged
namastex888 merged 1 commit into
devfrom
refactor/unify-serve-daemon
Apr 2, 2026
Merged

namastex888 merged 1 commit into
devfrom
refactor/unify-serve-daemon

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

  • Unifies genie daemon and genie serve under a single process that owns pgserve, scheduler, and all services
  • genie serve --headless replaces genie daemon start as the headless service mode (no TUI)
  • Adds service PID registry (service-registry.ts) for tracking child processes and orphan cleanup
  • Enhanced shutdown handler with proper ordering, 10s force-kill timeout, and pgserve lockfile cleanup

Changes

  • src/lib/db.ts: Remove GENIE_APP check; only GENIE_IS_DAEMON spawns pgserve. autoStartDaemon() now launches genie serve --headless and checks both serve.pid and scheduler.pid
  • src/term-commands/serve.ts: Add --headless flag, service registry integration, enhanced shutdown with force-kill timeout
  • src/term-commands/daemon.ts: daemon start redirects to genie serve --headless with deprecation notice. stop/status/logs check both PID files. systemd unit updated
  • src/lib/service-registry.ts: New in-memory PID registry with register/unregister/reap/killAll
  • Tests: Updated daemon.test.ts for new systemd unit, added service-registry.test.ts

Test plan

  • bun run build passes
  • All 1796 tests pass (0 failures)
  • genie serve --headless starts services without TUI
  • genie daemon start redirects to serve --headless
  • genie daemon stop/status work with serve.pid
  • Service registry tracks PIDs and kills on shutdown

- Remove GENIE_APP check from db.ts: only GENIE_IS_DAEMON spawns pgserve
- autoStartDaemon() now launches `genie serve --headless` instead of `genie daemon start`
- Add --headless flag to `genie serve` (services only, no TUI)
- `genie daemon start` redirects to `genie serve --headless` with deprecation notice
- `genie daemon stop/status/logs` check both serve.pid and scheduler.pid
- systemd unit now points to `genie serve start --headless --foreground`
- Add service-registry.ts for PID tracking and orphan cleanup
- Enhanced shutdown: proper ordering, 10s force-kill timeout, pgserve lockfile cleanup
@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2f48016b-5b7c-4c20-85e3-cf63e3a88823

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/unify-serve-daemon

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.

@namastex888
namastex888 merged commit 69e1cee into dev Apr 2, 2026
6 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request unifies the daemon and serve processes, transitioning genie daemon to an alias for genie serve --headless. It introduces a new service-registry module to track and manage the lifecycle of child processes like pgserve and the scheduler. The review identifies several critical issues in the shutdown implementation: the main process incorrectly registers its own PID, which would cause it to attempt to kill itself during cleanup; the force-kill timeout is rendered ineffective by an immediate process.exit() call; and there is redundant cleanup logic for the database port file that is already handled elsewhere.

Comment on lines +58 to +102
export function killAllServices(): void {
for (const [_name, entry] of registry) {
try {
process.kill(entry.pid, 'SIGTERM');
} catch {
// Already dead — clean up
registry.delete(_name);
}
}

// Brief grace period for SIGTERM to take effect
const deadline = Date.now() + 3000;
const checkInterval = 200;

const stillAlive = () => {
for (const [, entry] of registry) {
try {
process.kill(entry.pid, 0);
return true;
} catch {
// dead
}
}
return false;
};

// Synchronous busy-wait for up to 3s (shutdown context, blocking is acceptable)
while (Date.now() < deadline && stillAlive()) {
const waitUntil = Date.now() + checkInterval;
while (Date.now() < waitUntil) {
// busy wait
}
}

// SIGKILL any survivors
for (const [name, entry] of registry) {
try {
process.kill(entry.pid, 0); // check alive
process.kill(entry.pid, 'SIGKILL');
} catch {
// Already dead
}
registry.delete(name);
}
}

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.

critical

The killAllServices function has a critical flaw: if the current process's PID is registered (as is currently done in serve.ts), it will attempt to SIGKILL itself. This terminates the process immediately, preventing any subsequent cleanup logic (such as removing PID files or closing other handles) from executing. Additionally, the presence of process.pid in the registry causes the stillAlive check to always return true, forcing the full 3-second busy-wait even if all other services have exited. You should explicitly skip process.pid during the kill and liveness check phases.

export function killAllServices(): void {
  for (const [name, entry] of registry) {
    if (entry.pid === process.pid) continue;
    try {
      process.kill(entry.pid, 'SIGTERM');
    } catch {
      // Already dead — clean up
      registry.delete(name);
    }
  }

  // Brief grace period for SIGTERM to take effect
  const deadline = Date.now() + 3000;
  const checkInterval = 200;

  const stillAlive = () => {
    for (const [, entry] of registry) {
      if (entry.pid === process.pid) continue;
      try {
        process.kill(entry.pid, 0);
        return true;
      } catch {
        // dead
      }
    }
    return false;
  };

  // Synchronous busy-wait for up to 3s (shutdown context, blocking is acceptable)
  while (Date.now() < deadline && stillAlive()) {
    const waitUntil = Date.now() + checkInterval;
    while (Date.now() < waitUntil) {
      // busy wait
    }
  }

  // SIGKILL any survivors
  for (const [name, entry] of registry) {
    if (entry.pid === process.pid) {
      registry.delete(name);
      continue;
    }
    try {
      process.kill(entry.pid, 0); // check alive
      process.kill(entry.pid, 'SIGKILL');
    } catch {
      // Already dead
    }
    registry.delete(name);
  }
}
References
  1. It is acceptable to use hardcoded numeric limits (magic numbers) in non-critical fallback logic, especially when they serve as intentional caps to prevent performance issues like excessive I/O.

const { registerService } = await import('../lib/service-registry.js');
// pgserve child PID is internal to db.ts — register our own process
// as the owner; db.ts exit handler cleans up pgserve child
registerService('pgserve-owner', process.pid);

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.

high

Registering process.pid in the service registry is incorrect and dangerous. The registry is intended to track child processes for cleanup. Registering the main process PID will cause killAllServices to attempt to kill the current process, which can abort the shutdown sequence prematurely and leave stale PID files or other resources. Since pgserve is already managed by db.ts via its own exit handlers, this registration is redundant and should be removed. The same issue applies to the registration on line 484.

Comment on lines +563 to 570
process.on('SIGTERM', () => {
forceKillShutdown();
process.exit(143);
});
process.on('SIGINT', () => {
shutdown();
forceKillShutdown();
process.exit(130);
});

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.

high

The signal handlers call process.exit() immediately after forceKillShutdown(). Since forceKillShutdown schedules a 10-second timeout but returns immediately, the process will exit right away, rendering the 10-second force-kill safety net useless. To implement a proper graceful shutdown with a timeout, the signal handler should be async, await the graceful shutdown process, and only then exit. However, given that shutdown() is currently synchronous, the process will exit as soon as shutdown() finishes anyway, making the 10s timeout redundant in its current form.

Comment on lines +527 to +534
try {
const lockfilePath = join(genieHome(), 'pgserve.port');
if (existsSync(lockfilePath)) {
unlinkSync(lockfilePath);
}
} catch {
// Best effort
}

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.

medium

This manual cleanup of the pgserve.port lockfile is redundant. The db.ts module already manages this lockfile via its own shutdown() and process exit handlers (see src/lib/db.ts line 389). Duplicating this logic here is unnecessary and could lead to race conditions.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 604b26f908

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +431 to +433
// pgserve child PID is internal to db.ts — register our own process
// as the owner; db.ts exit handler cleans up pgserve child
registerService('pgserve-owner', process.pid);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid registering serve PID as managed child service

startForeground() registers process.pid as both pgserve-owner and scheduler, but killAllServices() later sends SIGTERM/SIGKILL to every registered PID. That means the owner process is treated as a child and can be killed mid-shutdown, skipping the remaining cleanup steps (e.g., PID/lockfile removal) whenever this path executes. Register only real child PIDs (or explicitly skip process.pid in the registry kill path) to avoid self-termination during shutdown.

Useful? React with 👍 / 👎.

Comment on lines +242 to +245
if (!pid) {
pid = readPid();
source = 'scheduler';
}

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 Badge Fall back to scheduler PID when serve PID is stale

The fallback to scheduler.pid only runs when serve.pid is missing, not when it exists but points to a dead process. In that stale-serve case, daemon stop returns after cleanup and never attempts to stop a live legacy scheduler process, which breaks the stated backward-compat behavior and can leave the scheduler running orphaned. Resolve the primary PID candidate based on liveness before deciding whether to fall back.

Useful? React with 👍 / 👎.

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