Skip to content
Merged
Show file tree
Hide file tree
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
68 changes: 68 additions & 0 deletions packages/agent-cdp/src/__tests__/daemon-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getRequiredDaemonAction } from "../daemon-client.js";

describe("getRequiredDaemonAction", () => {
const currentVersion = "1.2.0";

it("reuses a live daemon with the same version", () => {
expect(
getRequiredDaemonAction(
{
pid: 123,
socketPath: "/tmp/daemon.sock",
startedAt: Date.now(),
version: currentVersion,
},
currentVersion,
true,
),
).toBe("reuse");
});

it("restarts a live daemon with a different version", () => {
expect(
getRequiredDaemonAction(
{
pid: 123,
socketPath: "/tmp/daemon.sock",
startedAt: Date.now(),
version: "1.1.0",
},
currentVersion,
true,
),
).toBe("restart");
});

it("restarts a live daemon when version metadata is missing", () => {
expect(
getRequiredDaemonAction(
{
pid: 123,
socketPath: "/tmp/daemon.sock",
startedAt: Date.now(),
},
currentVersion,
true,
),
).toBe("restart");
});

it("starts fresh when the recorded daemon is not alive", () => {
expect(
getRequiredDaemonAction(
{
pid: 123,
socketPath: "/tmp/daemon.sock",
startedAt: Date.now(),
version: currentVersion,
},
currentVersion,
false,
),
).toBe("start");
});

it("starts fresh when no daemon info exists", () => {
expect(getRequiredDaemonAction(null, currentVersion, false)).toBe("start");
});
});
11 changes: 3 additions & 8 deletions packages/agent-cdp/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readdirSync, readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { ensureDaemon, readDaemonInfo, sendCommand, stopDaemon } from "./daemon-client.js";
import { ensureDaemon, sendCommand, stopDaemon } from "./daemon-client.js";
import {
formatConsoleList,
formatConsoleMessage,
Expand Down Expand Up @@ -273,17 +273,12 @@ export async function main(): Promise<void> {
}

if (cmd === "stop") {
console.log(stopDaemon() ? "Daemon stopped" : "Daemon is not running");
console.log((await stopDaemon()) ? "Daemon stopped" : "Daemon is not running");
return;
}

if (cmd === "status") {
const info = readDaemonInfo();
if (!info) {
console.log("Daemon is not running");
process.exit(1);
}

await ensureDaemon();
const response = await sendCommand({ type: "status" });
if (!response.ok) {
throw new Error(response.error || "Failed to load daemon status");
Expand Down
68 changes: 59 additions & 9 deletions packages/agent-cdp/src/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from "node:path";
import { spawn } from "node:child_process";

import type { DaemonInfo, IpcCommand, IpcResponse } from "./types.js";
import { getPackageVersion } from "./version.js";

const STATE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "/tmp", ".agent-cdp");

Expand Down Expand Up @@ -33,19 +34,54 @@ function isDaemonAlive(info: DaemonInfo): boolean {
}
}

export async function ensureDaemon(): Promise<void> {
const info = readDaemonInfo();
if (info && isDaemonAlive(info)) {
return;
export function getRequiredDaemonAction(
info: DaemonInfo | null,
currentVersion: string,
daemonAlive = info ? isDaemonAlive(info) : false,
): "reuse" | "restart" | "start" {
if (!info || !daemonAlive) {
return "start";
}

if (info.version !== currentVersion) {
return "restart";
}

return "reuse";
}

function cleanupDaemonState(): void {
try {
fs.unlinkSync(getDaemonInfoPath());
} catch {}

try {
fs.unlinkSync(getSocketPath());
} catch {}
}

async function waitForDaemonExit(pid: number, timeoutMs = 5_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
process.kill(pid, 0);
await new Promise((resolve) => setTimeout(resolve, 100));
} catch {
return;
}
}

throw new Error("Daemon failed to stop within 5 seconds");
}

async function stopDaemonProcess(info: DaemonInfo): Promise<void> {
process.kill(info.pid, "SIGTERM");
await waitForDaemonExit(info.pid);
cleanupDaemonState();
}

async function startDaemon(): Promise<void> {
cleanupDaemonState();

const daemonScript = path.join(path.dirname(new URL(import.meta.url).pathname), "daemon.js");
const child = spawn(process.execPath, [daemonScript], {
Expand All @@ -66,19 +102,33 @@ export async function ensureDaemon(): Promise<void> {
throw new Error("Daemon failed to start within 5 seconds");
}

export function stopDaemon(): boolean {
export async function ensureDaemon(): Promise<void> {
const info = readDaemonInfo();
const action = getRequiredDaemonAction(info, getPackageVersion());
if (action === "reuse") {
return;
}

if (action === "restart" && info) {
await stopDaemonProcess(info);
} else {
cleanupDaemonState();
}

await startDaemon();
}

export async function stopDaemon(): Promise<boolean> {
const info = readDaemonInfo();
if (!info) {
return false;
}

try {
process.kill(info.pid, "SIGTERM");
try {
fs.unlinkSync(getDaemonInfoPath());
} catch {}
await stopDaemonProcess(info);
return true;
} catch {
cleanupDaemonState();
return false;
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-cdp/src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createTargetProviders } from "./providers.js";
import { SessionManager } from "./session-manager.js";
import { TraceRecorder } from "./trace.js";
import type { DaemonInfo, IpcCommand, IpcResponse, StatusInfo } from "./types.js";
import { getPackageVersion } from "./version.js";

const STATE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "/tmp", ".agent-cdp");

Expand Down Expand Up @@ -74,6 +75,7 @@ class Daemon {
pid: process.pid,
socketPath,
startedAt: this.startedAt,
version: getPackageVersion(),
buildMtime,
};

Expand Down
1 change: 1 addition & 0 deletions packages/agent-cdp/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export interface DaemonInfo {
pid: number;
socketPath: string;
startedAt: number;
version?: string;
buildMtime?: number;
}

Expand Down
18 changes: 18 additions & 0 deletions packages/agent-cdp/src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import fs from "node:fs";

let cachedPackageVersion: string | null = null;

export function getPackageVersion(): string {
if (cachedPackageVersion) {
return cachedPackageVersion;
}

const raw = fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8");
const parsed = JSON.parse(raw) as { version?: string };
if (!parsed.version) {
throw new Error("agent-cdp package version is missing");
}

cachedPackageVersion = parsed.version;
return cachedPackageVersion;
}
Loading