Repository navigation
Expand file tree
/
Copy pathdaemon.ts
More file actions
144 lines (139 loc) · 6.14 KB
/
Copy pathdaemon.ts
File metadata and controls
144 lines (139 loc) · 6.14 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import {
readPluginInventory,
type PluginInventoryEntry,
} from '@agent-device/contracts/plugin-inventory';
import { resolveDaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution';
import type { DaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution';
import {
isUnusableRecord,
readDaemonStatus,
readDaemonStopIdentity,
readStopCleanupOwner,
type DaemonStatus,
type DaemonStopIdentity,
} from '@agent-device/daemon-contracts/daemon-registration';
import type { DaemonStopResult } from '@agent-device/daemon-contracts/daemon-stop';
import { AppError } from '@agent-device/kernel/errors';
import { shellQuoteIfNeeded } from '@agent-device/kernel/device-shell';
import { writeCommandOutput } from './shared.ts';
import type { ClientCommandHandler } from './router-types.ts';
import { createDaemonOwnerCleanup } from '../../platform-runtime-daemon-owner-cleanup.ts';
import { stopRecordedDaemon } from '../../daemon-client/daemon-client-stop.ts';
export const daemonCommand: ClientCommandHandler = async ({ positionals, flags, host }) => {
const daemonOwnerCleanup = createDaemonOwnerCleanup(host);
const subcommand = positionals[0];
if ((subcommand !== 'stop' && subcommand !== 'status') || positionals.length !== 1) {
throw new AppError('INVALID_ARGS', 'daemon accepts only: stop, status');
}
const paths = resolveDaemonPaths(flags.stateDir, {
cwd: host.cwd,
env: host.env,
});
if (subcommand === 'status') {
const status = readDaemonStatus(paths);
const shown = { ...status, plugins: readPluginInventory(status.plugins) };
await writeCommandOutput(flags, shown, () => renderDaemonStatus(shown));
return true;
}
// One read decides every route below, so routing, retirement, and a `--clean` walk judge the same
// record rather than three snapshots of a file another process may be rewriting.
const record = readDaemonStopIdentity(paths.infoPath);
// Cleanup may walk only a detached daemon's process tree; a host may own unrelated work.
if (flags.clean === true && record.ownership !== 'absent' && record.ownership !== 'detached') {
throw daemonCleanRefusedForUnprovenOwner(paths, record.ownership);
}
const result = await stopRecordedDaemon(record, { paths, host });
const cleanupOwner = readStopCleanupOwner(record);
const shouldClean = flags.clean === true && cleanupOwner !== null && result.stopped;
if (shouldClean) {
await daemonOwnerCleanup.cleanup({
pid: cleanupOwner.pid,
startTime: cleanupOwner.processStartTime,
});
}
const data = { ...result, clean: shouldClean };
await writeCommandOutput(flags, data, () => renderDaemonStop(data));
return true;
};
/** Refuses process-tree cleanup unless the record proves detached ownership. */
function daemonCleanRefusedForUnprovenOwner(
paths: DaemonPaths,
record: DaemonStopIdentity['ownership'],
): AppError {
return new AppError(
'COMMAND_FAILED',
'This daemon record does not prove the process is a daemon of its own, so its retained-runner cleanup cannot be run from here.',
{
reason: 'daemon-clean-not-detached',
stateDir: paths.baseDir,
ownership: record,
hint: daemonCleanRefusalHint(paths, record),
},
);
}
/**
* What to do next, which depends on whether a plain `daemon stop` has a route left for this record.
* A readable record naming a host or an unfamiliar owner does: that stop asks the running process to
* leave and never signals it. A record that cannot be read has none — the same stop refuses it for the
* same reason — so its hint names the record repair instead of sending the operator to a second refusal.
*/
function daemonCleanRefusalHint(
paths: DaemonPaths,
record: DaemonStopIdentity['ownership'],
): string {
if (record === 'unreadable') {
return `Nothing was signaled. Check what owns that process, then repair the record: stop it by its own means and remove ${shellQuoteIfNeeded(`${paths.baseDir}/daemon.json`)} so a daemon can register again.`;
}
if (record === 'embedded') {
return 'Stop it without --clean, which drains it through its own close, or let the host that started it close it.';
}
return 'Stop it without --clean, which asks the running process to leave and reports what it answers.';
}
function renderDaemonStop(
result: Pick<DaemonStopResult, 'stopped' | 'mode' | 'warnings'> & {
clean: boolean;
},
): string {
const headline = result.stopped ? `Daemon stopped (${result.mode}).` : 'No running daemon found.';
return [headline, result.clean ? 'Retained runner cleanup completed.' : null, ...result.warnings]
.filter((line): line is string => Boolean(line))
.join('\n');
}
function renderDaemonStatus(
status: Omit<DaemonStatus, 'plugins'> & { plugins: readonly PluginInventoryEntry[] },
): string {
if (isUnusableRecord(status)) {
return `The daemon record for ${status.stateDir} is ${status.state === 'invalid' ? 'not a valid record' : 'unreadable'}, so whether a daemon runs there is unknown.`;
}
if (status.state === 'stopped') return `No running daemon for ${status.stateDir}.`;
const details = [
`pid ${status.pid}`,
// An embedded pid belongs to a host process, and an unrecognized owner says the record states an
// owner this build cannot read; either way, what a stop may do to that pid changes.
status.ownership === 'embedded'
? 'host-owned'
: status.ownership === 'unknown'
? 'owner-not-recognized'
: null,
status.version,
status.transport,
].filter(Boolean);
return [
`Daemon running (${details.join(', ')}) for ${status.stateDir}.`,
...(status.plugins.length === 0
? ['No providers registered.']
: [
'Providers:',
...status.plugins.map((plugin) => {
const origin = plugin.package
? `${plugin.package}${plugin.version ? `@${plugin.version}` : ''}`
: plugin.source;
const outcome =
plugin.state === 'active'
? 'active'
: `failed: ${plugin.error?.message ?? 'unknown'}`;
return ` ${plugin.provider ?? '(unknown provider)'} [${plugin.source}] ${origin} - ${outcome}`;
}),
]),
].join('\n');
}