mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
9abcd7fe03
* refactor: move Apple platform family into package * fix: preserve Apple facade sync contracts * fix: complete Apple W4 rebase review fixes
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { resolveDaemonPaths } from '../src/daemon/config.ts';
|
|
import {
|
|
isAgentDeviceDaemonProcess,
|
|
stopProcessForTakeover,
|
|
} from '../src/daemon/daemon-process.ts';
|
|
|
|
const DAEMON_TERM_TIMEOUT_MS = 15_000;
|
|
const DAEMON_KILL_TIMEOUT_MS = 2_000;
|
|
const PRUNE_DEV_FLAG = '--prune-dev';
|
|
const PRUNE_DEV_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
|
|
type DaemonInfo = {
|
|
pid?: number;
|
|
processStartTime?: string;
|
|
};
|
|
|
|
const paths = resolveDaemonPaths(process.env.AGENT_DEVICE_STATE_DIR);
|
|
const info = readDaemonInfo(paths.infoPath);
|
|
const daemonPid = readPositivePid(info?.pid);
|
|
|
|
if (daemonPid !== null) {
|
|
await stopProcessForTakeover(daemonPid, {
|
|
termTimeoutMs: DAEMON_TERM_TIMEOUT_MS,
|
|
killTimeoutMs: DAEMON_KILL_TIMEOUT_MS,
|
|
expectedStartTime: info?.processStartTime,
|
|
});
|
|
const { cleanupRunnerLeasesForOwner } =
|
|
await import('@agent-device/platform-apple/runner/operations');
|
|
await cleanupRunnerLeasesForOwner({ pid: daemonPid, startTime: info?.processStartTime });
|
|
}
|
|
|
|
removeIfPresent(paths.infoPath);
|
|
removeIfPresent(paths.lockPath);
|
|
|
|
if (process.argv.includes(PRUNE_DEV_FLAG)) {
|
|
pruneStaleDevStateDirs();
|
|
}
|
|
|
|
function readDaemonInfo(infoPath: string): DaemonInfo | null {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(infoPath, 'utf8')) as DaemonInfo;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function removeIfPresent(filePath: string): void {
|
|
try {
|
|
fs.unlinkSync(filePath);
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Removes worktree-scoped state dirs under ~/.agent-device/dev/ that no live daemon
|
|
// owns and that have been idle past the retention threshold. Never touches the
|
|
// global ~/.agent-device root contents.
|
|
function pruneStaleDevStateDirs(): void {
|
|
const devRoot = path.join(os.homedir(), '.agent-device', 'dev');
|
|
const cutoffMs = Date.now() - PRUNE_DEV_MAX_AGE_MS;
|
|
for (const dirPath of listDevStateDirs(devRoot)) {
|
|
if (hasLiveDaemon(dirPath) || newestMtimeMs(dirPath) > cutoffMs) continue;
|
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
process.stdout.write(`Removed stale daemon state dir: ${dirPath}\n`);
|
|
}
|
|
}
|
|
|
|
function listDevStateDirs(devRoot: string): string[] {
|
|
let entries: fs.Dirent[];
|
|
try {
|
|
entries = fs.readdirSync(devRoot, { withFileTypes: true });
|
|
} catch {
|
|
return [];
|
|
}
|
|
return entries
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => path.join(devRoot, entry.name));
|
|
}
|
|
|
|
function hasLiveDaemon(stateDir: string): boolean {
|
|
const dirInfo = readDaemonInfo(path.join(stateDir, 'daemon.json'));
|
|
const pid = readPositivePid(dirInfo?.pid);
|
|
return pid !== null && isAgentDeviceDaemonProcess(pid, dirInfo?.processStartTime);
|
|
}
|
|
|
|
function readPositivePid(pid: number | undefined): number | null {
|
|
if (typeof pid !== 'number') return null;
|
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
}
|
|
|
|
function newestMtimeMs(dirPath: string): number {
|
|
let newest = statMtimeMs(dirPath);
|
|
let children: fs.Dirent[];
|
|
try {
|
|
children = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
} catch {
|
|
return newest;
|
|
}
|
|
for (const child of children) {
|
|
newest = Math.max(newest, statMtimeMs(path.join(dirPath, child.name)));
|
|
}
|
|
return newest;
|
|
}
|
|
|
|
function statMtimeMs(filePath: string): number {
|
|
try {
|
|
return fs.statSync(filePath).mtimeMs;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|