Repository navigation
Expand file tree
/
Copy pathclean-daemon.ts
More file actions
127 lines (113 loc) · 3.72 KB
/
Copy pathclean-daemon.ts
File metadata and controls
127 lines (113 loc) · 3.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { AppError } from '@agent-device/kernel/errors';
import { resolveDaemonPaths } from '../src/daemon-resolution.ts';
import { isAgentDeviceDaemonProcess, stopDaemonProcess } from '../src/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) {
const termination = await stopDaemonProcess(
{ pid: daemonPid, startTime: info?.processStartTime ?? null },
{
mode: 'graceful',
termTimeoutMs: DAEMON_TERM_TIMEOUT_MS,
killTimeoutMs: DAEMON_KILL_TIMEOUT_MS,
},
);
if (termination.status !== 'exited') {
throw new AppError(
'COMMAND_FAILED',
'Daemon cleanup retained state because exit could not be confirmed.',
{
reason: 'daemon_exit_unconfirmed',
termination,
},
);
}
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;
}
}