tmux-ide 2.9.0-beta.19 → 2.9.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +1979 -783
- package/package.json +4 -2
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +32 -0
- package/packages/contracts/src/daemon-events.ts +12 -12
- package/packages/contracts/src/daemon-wire.ts +30 -0
- package/packages/daemon/dist/command-center/agent-status-watch.js +9 -2
- package/packages/daemon/dist/command-center/diagnostics.js +65 -0
- package/packages/daemon/dist/command-center/log-stream.js +9 -0
- package/packages/daemon/dist/command-center/resources/fleet-preview-route.js +22 -6
- package/packages/daemon/dist/command-center/server.js +7 -0
- package/packages/daemon/dist/doctor.js +62 -0
- package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +400 -0
- package/packages/daemon/dist/lib/app-config.js +4 -2
- package/packages/daemon/dist/lib/canonical-daemon.js +1 -0
- package/packages/daemon/dist/lib/daemon-embed.js +24 -0
- package/packages/daemon/dist/lib/daemon-provenance.js +575 -0
- package/packages/daemon/dist/lib/headless-daemon.js +1 -0
- package/packages/daemon/dist/lib/log-sanitize.js +199 -0
- package/packages/daemon/dist/lib/log.js +123 -13
- package/packages/daemon/dist/lib/soak-diagnostics.js +124 -0
- package/packages/daemon/dist/lib/soak-verdict.js +472 -0
- package/packages/daemon/dist/lib/terminal-host-color.js +33 -0
- package/packages/daemon/dist/lib/tmux-external-interaction-observer.js +187 -39
- package/packages/daemon/dist/lib/tmux-interaction-retention.js +21 -0
- package/packages/daemon/dist/lib/workspace-promotion.js +55 -37
- package/packages/daemon/dist/terminal/mirror/session-channel.js +2 -1
- package/packages/daemon/dist/tui/mirror/automatic-contrast.js +161 -0
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +9 -3
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +7 -5
- package/packages/daemon/dist/tui/mirror/resize-transaction.js +48 -20
- package/packages/daemon/dist/tui/mirror/runtime/application-appearance-owner.js +40 -6
- package/packages/daemon/dist/tui/mirror/runtime/application-fleet-preview.js +4 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +63 -42
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +138 -67
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-palette-owner.js +44 -28
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +49 -11
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +140 -2
- package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +3 -1
- package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +2 -1
- package/packages/daemon/dist/tui/mirror/theme.js +4 -31
- package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +15 -8
- package/packages/daemon/dist/tui/team/wait-receipts.js +112 -33
- package/packages/daemon/src/command-center/agent-status-watch.ts +8 -2
- package/packages/daemon/src/command-center/diagnostics.ts +75 -0
- package/packages/daemon/src/command-center/log-stream.ts +8 -0
- package/packages/daemon/src/command-center/resources/fleet-preview-route.ts +27 -5
- package/packages/daemon/src/command-center/server.ts +8 -0
- package/packages/daemon/src/doctor.ts +70 -0
- package/packages/daemon/src/lib/app-config.ts +11 -3
- package/packages/daemon/src/lib/canonical-daemon.ts +1 -0
- package/packages/daemon/src/lib/daemon-embed.ts +30 -0
- package/packages/daemon/src/lib/daemon-provenance.ts +769 -0
- package/packages/daemon/src/lib/fleet-preview-model.ts +1 -0
- package/packages/daemon/src/lib/headless-daemon.ts +1 -0
- package/packages/daemon/src/lib/log-sanitize.ts +248 -0
- package/packages/daemon/src/lib/log.ts +165 -14
- package/packages/daemon/src/lib/soak-diagnostics.ts +183 -0
- package/packages/daemon/src/lib/soak-verdict.ts +785 -0
- package/packages/daemon/src/lib/terminal-host-color.ts +43 -0
- package/packages/daemon/src/lib/tmux-external-interaction-observer.ts +233 -37
- package/packages/daemon/src/lib/tmux-interaction-retention.ts +24 -0
- package/packages/daemon/src/lib/workspace-promotion.ts +74 -45
- package/packages/daemon/src/terminal/mirror/session-channel.ts +2 -1
- package/packages/daemon/src/tui/mirror/automatic-contrast.ts +180 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +26 -5
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -8
- package/packages/daemon/src/tui/mirror/resize-transaction.ts +46 -22
- package/packages/daemon/src/tui/mirror/runtime/application-appearance-owner.ts +45 -6
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-preview.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-switcher.tsx +2 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +99 -75
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +13 -3
- package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +125 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +2 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +199 -157
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +148 -69
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-palette-owner.ts +55 -28
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +59 -17
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +151 -3
- package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +3 -1
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +2 -1
- package/packages/daemon/src/tui/mirror/theme.ts +4 -39
- package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +21 -8
- package/packages/daemon/src/tui/team/wait-receipts.ts +115 -40
- package/packages/daemon-client/src/terminal-fast-lane.test.ts +18 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +7 -2
- package/packages/tmux-bridge/src/index.ts +2 -0
- package/packages/tmux-bridge/src/runner.test.ts +41 -0
- package/packages/tmux-bridge/src/runner.ts +41 -2
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon identity and log provenance.
|
|
3
|
+
*
|
|
4
|
+
* Two halves:
|
|
5
|
+
*
|
|
6
|
+
* 1. `captureDaemonProvenance` — run by the daemon at startup. It records how
|
|
7
|
+
* the process was launched (headless vs embedded, which supervisor), and
|
|
8
|
+
* where its stdout/stderr ACTUALLY go (file path + dev/ino, tty, pipe,
|
|
9
|
+
* socket, /dev/null). The result is stamped into the daemon record so a
|
|
10
|
+
* later triage derives the live log destination from the record instead of
|
|
11
|
+
* guessing file names. It never throws: anything it cannot determine
|
|
12
|
+
* degrades to `unknown` plus a warning.
|
|
13
|
+
*
|
|
14
|
+
* 2. `buildDaemonProvenanceReport` (pure) + `collectDaemonProvenanceReport`
|
|
15
|
+
* (io) — the credential-free triage report behind `tmux-ide daemon info`
|
|
16
|
+
* and the doctor row. Discovered log files are labeled `current` only when
|
|
17
|
+
* they are the live destination; everything else is `historical` (with the
|
|
18
|
+
* pid they belong to when recorded) or `unattributed`. Files are only ever
|
|
19
|
+
* read, never modified.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import {
|
|
24
|
+
closeSync,
|
|
25
|
+
fstatSync,
|
|
26
|
+
lstatSync,
|
|
27
|
+
openSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
readlinkSync,
|
|
30
|
+
readSync,
|
|
31
|
+
realpathSync,
|
|
32
|
+
statSync,
|
|
33
|
+
} from "node:fs";
|
|
34
|
+
import { join, resolve } from "node:path";
|
|
35
|
+
import type {
|
|
36
|
+
CanonicalDaemonInfo,
|
|
37
|
+
DaemonLogStream,
|
|
38
|
+
DaemonProvenance,
|
|
39
|
+
DaemonSupervisorKind,
|
|
40
|
+
} from "@tmux-ide/contracts";
|
|
41
|
+
import {
|
|
42
|
+
getCanonicalDaemonInfoPath,
|
|
43
|
+
inspectCanonicalDaemonInfo,
|
|
44
|
+
type CanonicalDaemonInfoState,
|
|
45
|
+
} from "./canonical-daemon.ts";
|
|
46
|
+
import { redactText } from "./log-sanitize.ts";
|
|
47
|
+
import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
|
|
48
|
+
|
|
49
|
+
export type DaemonLauncher = DaemonProvenance["launcher"];
|
|
50
|
+
export type PidLiveness = "alive" | "dead" | "unknown";
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Startup capture
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export interface SupervisorFacts {
|
|
57
|
+
readonly launcher: DaemonLauncher;
|
|
58
|
+
readonly platform: NodeJS.Platform;
|
|
59
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
60
|
+
readonly parentPid: number;
|
|
61
|
+
readonly supervisionId?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* PURE — classify the supervising process from launch facts. Environment
|
|
66
|
+
* evidence wins over the reservation id hint; an explicit reservation without
|
|
67
|
+
* environment evidence still counts as that supervisor because the operator
|
|
68
|
+
* asserted it when reserving the namespace.
|
|
69
|
+
*/
|
|
70
|
+
export function classifySupervisor(facts: SupervisorFacts): {
|
|
71
|
+
kind: DaemonSupervisorKind;
|
|
72
|
+
evidence: string;
|
|
73
|
+
} {
|
|
74
|
+
if (facts.launcher === "embedded")
|
|
75
|
+
return { kind: "embedded", evidence: "startEmbeddedDaemon host" };
|
|
76
|
+
const { env, platform, parentPid, supervisionId } = facts;
|
|
77
|
+
if (env.INVOCATION_ID || env.JOURNAL_STREAM || env.SYSTEMD_EXEC_PID) {
|
|
78
|
+
const markers = ["INVOCATION_ID", "JOURNAL_STREAM", "SYSTEMD_EXEC_PID"].filter((k) => env[k]);
|
|
79
|
+
return { kind: "systemd", evidence: `env ${markers.join(",")}` };
|
|
80
|
+
}
|
|
81
|
+
if (platform === "darwin") {
|
|
82
|
+
const xpc = env.XPC_SERVICE_NAME;
|
|
83
|
+
if (xpc && xpc !== "0" && !xpc.startsWith("application.")) {
|
|
84
|
+
return { kind: "launchd", evidence: `XPC_SERVICE_NAME=${xpc}` };
|
|
85
|
+
}
|
|
86
|
+
if (parentPid === 1 && supervisionId) return { kind: "launchd", evidence: "parent pid 1" };
|
|
87
|
+
}
|
|
88
|
+
if (supervisionId) {
|
|
89
|
+
if (/\.(service|socket|timer)$/u.test(supervisionId)) {
|
|
90
|
+
return { kind: "systemd", evidence: `reservation ${supervisionId}` };
|
|
91
|
+
}
|
|
92
|
+
if (platform === "darwin") return { kind: "launchd", evidence: `reservation ${supervisionId}` };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
kind: "manual",
|
|
96
|
+
evidence: parentPid === 1 ? "detached (parent pid 1)" : "foreground or detached shell",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface StreamStat {
|
|
101
|
+
readonly isFile: boolean;
|
|
102
|
+
readonly isFIFO: boolean;
|
|
103
|
+
readonly isSocket: boolean;
|
|
104
|
+
readonly isCharacterDevice: boolean;
|
|
105
|
+
readonly dev: number;
|
|
106
|
+
readonly ino: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** PURE — describe one stdio stream from its fstat and (optional) resolved path. */
|
|
110
|
+
export function describeStream(
|
|
111
|
+
stat: StreamStat | null,
|
|
112
|
+
path: string | undefined,
|
|
113
|
+
isTTY: boolean,
|
|
114
|
+
): DaemonLogStream {
|
|
115
|
+
if (!stat) return { kind: "unknown", detail: "fstat failed" };
|
|
116
|
+
if (isTTY) return { kind: "tty", ...(path ? { path } : {}) };
|
|
117
|
+
if (stat.isFile) {
|
|
118
|
+
return {
|
|
119
|
+
kind: "file",
|
|
120
|
+
...(path ? { path } : {}),
|
|
121
|
+
dev: Number(stat.dev),
|
|
122
|
+
ino: Number(stat.ino),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (stat.isFIFO) return { kind: "pipe", ...(path ? { path } : {}) };
|
|
126
|
+
if (stat.isSocket) return { kind: "socket", ...(path ? { path } : {}) };
|
|
127
|
+
if (stat.isCharacterDevice) {
|
|
128
|
+
if (!path || path === "/dev/null") return { kind: "null", path: "/dev/null" };
|
|
129
|
+
return { kind: "unknown", path, detail: "character device" };
|
|
130
|
+
}
|
|
131
|
+
return { kind: "unknown", detail: "unrecognized stream type" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type FdPathResolver = (
|
|
135
|
+
fd: number,
|
|
136
|
+
pid: number,
|
|
137
|
+
platform: NodeJS.Platform,
|
|
138
|
+
) => string | undefined;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve where a file descriptor points. Linux answers via /proc; macOS has
|
|
142
|
+
* no /proc, so `lsof` (present on every stock install) is consulted once per
|
|
143
|
+
* fd with a short timeout. Returns undefined rather than throwing.
|
|
144
|
+
*/
|
|
145
|
+
export const resolveFdPath: FdPathResolver = (fd, pid, platform) => {
|
|
146
|
+
if (platform === "linux") {
|
|
147
|
+
try {
|
|
148
|
+
return readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
149
|
+
} catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (platform === "darwin") {
|
|
154
|
+
try {
|
|
155
|
+
const out = execFileSync("lsof", ["-a", "-p", String(pid), "-d", String(fd), "-F", "fn"], {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
158
|
+
timeout: 2_000,
|
|
159
|
+
});
|
|
160
|
+
const lines = out.split("\n");
|
|
161
|
+
const index = lines.indexOf(`f${fd}`);
|
|
162
|
+
if (index === -1) return undefined;
|
|
163
|
+
const name = lines.slice(index + 1).find((line) => line.startsWith("n"));
|
|
164
|
+
return name ? name.slice(1) : undefined;
|
|
165
|
+
} catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export interface CaptureDaemonProvenanceOptions {
|
|
173
|
+
readonly launcher: DaemonLauncher;
|
|
174
|
+
readonly supervisionId?: string;
|
|
175
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
176
|
+
readonly platform?: NodeJS.Platform;
|
|
177
|
+
readonly parentPid?: number;
|
|
178
|
+
readonly pid?: number;
|
|
179
|
+
readonly fdPath?: FdPathResolver;
|
|
180
|
+
readonly fstat?: (fd: number) => StreamStat;
|
|
181
|
+
readonly isTTY?: (fd: number) => boolean;
|
|
182
|
+
/** Identity of /dev/null, compared against character-device streams. */
|
|
183
|
+
readonly nullDevice?: () => { dev: number; ino: number } | null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function defaultNullDevice(): { dev: number; ino: number } | null {
|
|
187
|
+
try {
|
|
188
|
+
const stat = statSync("/dev/null");
|
|
189
|
+
return { dev: Number(stat.dev), ino: Number(stat.ino) };
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function defaultFstat(fd: number): StreamStat {
|
|
196
|
+
const stat = fstatSync(fd);
|
|
197
|
+
return {
|
|
198
|
+
isFile: stat.isFile(),
|
|
199
|
+
isFIFO: stat.isFIFO(),
|
|
200
|
+
isSocket: stat.isSocket(),
|
|
201
|
+
isCharacterDevice: stat.isCharacterDevice(),
|
|
202
|
+
dev: stat.dev,
|
|
203
|
+
ino: stat.ino,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function defaultIsTTY(fd: number): boolean {
|
|
208
|
+
return fd === 1
|
|
209
|
+
? process.stdout.isTTY === true
|
|
210
|
+
: fd === 2
|
|
211
|
+
? process.stderr.isTTY === true
|
|
212
|
+
: false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Capture launch + log-destination provenance for the current process.
|
|
217
|
+
* Never throws; partial knowledge is reported with warnings so startup can
|
|
218
|
+
* continue and the report can say what it could not determine.
|
|
219
|
+
*/
|
|
220
|
+
export function captureDaemonProvenance(options: CaptureDaemonProvenanceOptions): DaemonProvenance {
|
|
221
|
+
const warnings: string[] = [];
|
|
222
|
+
const env = options.env ?? process.env;
|
|
223
|
+
const platform = options.platform ?? process.platform;
|
|
224
|
+
const parentPid = options.parentPid ?? process.ppid;
|
|
225
|
+
const pid = options.pid ?? process.pid;
|
|
226
|
+
const fdPath = options.fdPath ?? resolveFdPath;
|
|
227
|
+
const fstat = options.fstat ?? defaultFstat;
|
|
228
|
+
const isTTY = options.isTTY ?? defaultIsTTY;
|
|
229
|
+
const nullDevice = options.nullDevice ?? defaultNullDevice;
|
|
230
|
+
const supervisor = classifySupervisor({
|
|
231
|
+
launcher: options.launcher,
|
|
232
|
+
platform,
|
|
233
|
+
env,
|
|
234
|
+
parentPid,
|
|
235
|
+
...(options.supervisionId ? { supervisionId: options.supervisionId } : {}),
|
|
236
|
+
});
|
|
237
|
+
const capture = (fd: number, name: string): DaemonLogStream => {
|
|
238
|
+
let stat: StreamStat;
|
|
239
|
+
try {
|
|
240
|
+
stat = fstat(fd);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
warnings.push(`${name}: fstat failed (${errorMessage(error)})`);
|
|
243
|
+
return { kind: "unknown", detail: "fstat failed" };
|
|
244
|
+
}
|
|
245
|
+
let path: string | undefined;
|
|
246
|
+
let tty = false;
|
|
247
|
+
try {
|
|
248
|
+
tty = isTTY(fd);
|
|
249
|
+
} catch {
|
|
250
|
+
// treat as non-tty
|
|
251
|
+
}
|
|
252
|
+
if (tty) return describeStream(stat, undefined, true);
|
|
253
|
+
if (stat.isCharacterDevice) {
|
|
254
|
+
// Cheap and exact: /dev/null is identified by inode, no path lookup.
|
|
255
|
+
const devNull = nullDevice();
|
|
256
|
+
if (devNull && devNull.dev === Number(stat.dev) && devNull.ino === Number(stat.ino)) {
|
|
257
|
+
return { kind: "null", path: "/dev/null" };
|
|
258
|
+
}
|
|
259
|
+
return { kind: "unknown", detail: "character device" };
|
|
260
|
+
}
|
|
261
|
+
if (stat.isFile) {
|
|
262
|
+
// The only case that needs a path lookup (lsof on macOS, /proc on Linux).
|
|
263
|
+
try {
|
|
264
|
+
path = fdPath(fd, pid, platform);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
warnings.push(`${name}: path resolution failed (${errorMessage(error)})`);
|
|
267
|
+
}
|
|
268
|
+
if (!path) warnings.push(`${name}: regular file but its path could not be resolved`);
|
|
269
|
+
}
|
|
270
|
+
return describeStream(stat, path, false);
|
|
271
|
+
};
|
|
272
|
+
const stdout = capture(1, "stdout");
|
|
273
|
+
const stderr = capture(2, "stderr");
|
|
274
|
+
return {
|
|
275
|
+
launcher: options.launcher,
|
|
276
|
+
supervisor: supervisor.kind,
|
|
277
|
+
parentPid,
|
|
278
|
+
stdout,
|
|
279
|
+
stderr,
|
|
280
|
+
...(warnings.length ? { warnings: warnings.slice(0, 8).map((w) => w.slice(0, 256)) } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function errorMessage(error: unknown): string {
|
|
285
|
+
return redactText(error instanceof Error ? error.message : String(error)).slice(0, 160);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Report (pure core)
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
export interface LogFileObservation {
|
|
293
|
+
readonly path: string;
|
|
294
|
+
readonly bytes: number;
|
|
295
|
+
readonly lastWriteAt: string;
|
|
296
|
+
readonly dev: number;
|
|
297
|
+
readonly ino: number;
|
|
298
|
+
/** Last daemon pid the file mentions, when recorded. */
|
|
299
|
+
readonly pid?: number;
|
|
300
|
+
readonly instanceId?: string;
|
|
301
|
+
readonly productVersion?: string;
|
|
302
|
+
readonly readError?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export type LogFileStatus = "current" | "historical" | "unattributed";
|
|
306
|
+
|
|
307
|
+
export interface ProvenanceLogFile extends LogFileObservation {
|
|
308
|
+
readonly status: LogFileStatus;
|
|
309
|
+
readonly reason: string;
|
|
310
|
+
readonly pidLiveness?: PidLiveness;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface ProvenanceDaemon {
|
|
314
|
+
readonly instanceId: string;
|
|
315
|
+
readonly productVersion: string;
|
|
316
|
+
readonly protocolVersion: number;
|
|
317
|
+
readonly pid: number;
|
|
318
|
+
readonly port: number;
|
|
319
|
+
readonly bindHostname: string;
|
|
320
|
+
readonly startedAt: string;
|
|
321
|
+
readonly supervisionId: string | null;
|
|
322
|
+
readonly liveness: PidLiveness;
|
|
323
|
+
readonly launcher: DaemonLauncher | null;
|
|
324
|
+
readonly supervisor: DaemonSupervisorKind | null;
|
|
325
|
+
readonly parentPid: number | null;
|
|
326
|
+
/** False for records written by a daemon that predates provenance stamping. */
|
|
327
|
+
readonly provenanceRecorded: boolean;
|
|
328
|
+
readonly logs: {
|
|
329
|
+
readonly stdout: DaemonLogStream | null;
|
|
330
|
+
readonly stderr: DaemonLogStream | null;
|
|
331
|
+
};
|
|
332
|
+
/** One-line human description of where the live daemon writes its logs. */
|
|
333
|
+
readonly logDestination: string;
|
|
334
|
+
readonly provenanceWarnings: readonly string[];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export type ProvenanceStatus = "running" | "stale-record" | "not-running" | "record-invalid";
|
|
338
|
+
|
|
339
|
+
export interface DaemonProvenanceReport {
|
|
340
|
+
readonly generatedAt: string;
|
|
341
|
+
readonly status: ProvenanceStatus;
|
|
342
|
+
readonly namespace: {
|
|
343
|
+
readonly mode: string;
|
|
344
|
+
readonly stateHome: string;
|
|
345
|
+
readonly daemonInfoDir: string;
|
|
346
|
+
readonly logsDir: string;
|
|
347
|
+
readonly recordPath: string;
|
|
348
|
+
};
|
|
349
|
+
readonly record:
|
|
350
|
+
| { readonly status: "valid" }
|
|
351
|
+
| { readonly status: "missing" }
|
|
352
|
+
| { readonly status: "reserved"; readonly supervisionId: string; readonly reservedAt: string }
|
|
353
|
+
| { readonly status: "invalid"; readonly reason: string; readonly detail: string };
|
|
354
|
+
readonly daemon: ProvenanceDaemon | null;
|
|
355
|
+
readonly logFiles: readonly ProvenanceLogFile[];
|
|
356
|
+
readonly warnings: readonly string[];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export interface BuildDaemonProvenanceReportInput {
|
|
360
|
+
readonly namespace: DaemonProvenanceReport["namespace"];
|
|
361
|
+
readonly recordState: CanonicalDaemonInfoState;
|
|
362
|
+
readonly files: readonly LogFileObservation[];
|
|
363
|
+
readonly pidLiveness: (pid: number) => PidLiveness;
|
|
364
|
+
readonly now?: () => Date;
|
|
365
|
+
readonly warnings?: readonly string[];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** PURE — human description of a stream destination. */
|
|
369
|
+
export function describeLogDestination(
|
|
370
|
+
stream: DaemonLogStream | null,
|
|
371
|
+
supervisor: DaemonSupervisorKind | null,
|
|
372
|
+
): string {
|
|
373
|
+
if (!stream) return "unknown (record predates provenance stamping)";
|
|
374
|
+
switch (stream.kind) {
|
|
375
|
+
case "file":
|
|
376
|
+
return stream.path
|
|
377
|
+
? `file ${stream.path}`
|
|
378
|
+
: `file (unresolved path, dev ${stream.dev} ino ${stream.ino})`;
|
|
379
|
+
case "tty":
|
|
380
|
+
return stream.path ? `terminal ${stream.path}` : "terminal";
|
|
381
|
+
case "pipe":
|
|
382
|
+
return supervisor === "embedded"
|
|
383
|
+
? "pipe to the embedding host"
|
|
384
|
+
: "pipe to the parent process";
|
|
385
|
+
case "socket":
|
|
386
|
+
return supervisor === "systemd" ? "systemd journal (socket; use journalctl)" : "socket";
|
|
387
|
+
case "null":
|
|
388
|
+
return "discarded (/dev/null)";
|
|
389
|
+
default:
|
|
390
|
+
return stream.detail ? `unknown (${stream.detail})` : "unknown";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function sameFile(stream: DaemonLogStream | null, file: LogFileObservation): boolean {
|
|
395
|
+
if (!stream || stream.kind !== "file") return false;
|
|
396
|
+
if (stream.dev !== undefined && stream.ino !== undefined) {
|
|
397
|
+
return stream.dev === file.dev && stream.ino === file.ino;
|
|
398
|
+
}
|
|
399
|
+
return stream.path !== undefined && stream.path === file.path;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function stripCredentials(info: CanonicalDaemonInfo): Omit<CanonicalDaemonInfo, "authToken"> {
|
|
403
|
+
// Explicit copy so a future record field named like a credential still has
|
|
404
|
+
// to be added here deliberately.
|
|
405
|
+
const rest: Record<string, unknown> = { ...info };
|
|
406
|
+
delete rest.authToken;
|
|
407
|
+
return rest as Omit<CanonicalDaemonInfo, "authToken">;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function classifyFile(
|
|
411
|
+
file: LogFileObservation,
|
|
412
|
+
daemon: ProvenanceDaemon | null,
|
|
413
|
+
pidLiveness: (pid: number) => PidLiveness,
|
|
414
|
+
): ProvenanceLogFile {
|
|
415
|
+
const liveness = file.pid !== undefined ? pidLiveness(file.pid) : undefined;
|
|
416
|
+
const withLiveness = (status: LogFileStatus, reason: string): ProvenanceLogFile => ({
|
|
417
|
+
...file,
|
|
418
|
+
status,
|
|
419
|
+
reason,
|
|
420
|
+
...(liveness ? { pidLiveness: liveness } : {}),
|
|
421
|
+
});
|
|
422
|
+
if (daemon && daemon.liveness !== "dead") {
|
|
423
|
+
if (sameFile(daemon.logs.stdout, file) || sameFile(daemon.logs.stderr, file)) {
|
|
424
|
+
return withLiveness("current", `live destination of daemon pid ${daemon.pid}`);
|
|
425
|
+
}
|
|
426
|
+
if (file.pid !== undefined && file.pid !== daemon.pid) {
|
|
427
|
+
return withLiveness(
|
|
428
|
+
"historical",
|
|
429
|
+
liveness === "dead"
|
|
430
|
+
? `written by pid ${file.pid}, which is no longer running; the current daemon is pid ${daemon.pid}`
|
|
431
|
+
: `written by pid ${file.pid}, not the current daemon pid ${daemon.pid}`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (file.pid === daemon.pid) {
|
|
435
|
+
return withLiveness(
|
|
436
|
+
"historical",
|
|
437
|
+
`mentions the current pid ${daemon.pid} but is not its live log destination`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (Date.parse(file.lastWriteAt) < Date.parse(daemon.startedAt)) {
|
|
441
|
+
return withLiveness(
|
|
442
|
+
"historical",
|
|
443
|
+
`last written before the current daemon started (${daemon.startedAt})`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return withLiveness(
|
|
447
|
+
"unattributed",
|
|
448
|
+
"no daemon identity recorded in the file and it is not the live destination",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if (file.pid !== undefined) {
|
|
452
|
+
return withLiveness(
|
|
453
|
+
"historical",
|
|
454
|
+
liveness === "dead"
|
|
455
|
+
? `written by pid ${file.pid}, which is no longer running`
|
|
456
|
+
: `written by pid ${file.pid}; no live canonical daemon record`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return withLiveness("unattributed", "no daemon identity recorded in the file");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** PURE — assemble the credential-free report from observed facts. */
|
|
463
|
+
export function buildDaemonProvenanceReport(
|
|
464
|
+
input: BuildDaemonProvenanceReportInput,
|
|
465
|
+
): DaemonProvenanceReport {
|
|
466
|
+
const warnings = [...(input.warnings ?? [])];
|
|
467
|
+
const generatedAt = (input.now ?? (() => new Date()))().toISOString();
|
|
468
|
+
const { recordState } = input;
|
|
469
|
+
let daemon: ProvenanceDaemon | null = null;
|
|
470
|
+
let status: ProvenanceStatus;
|
|
471
|
+
let record: DaemonProvenanceReport["record"];
|
|
472
|
+
if (recordState.status === "missing") {
|
|
473
|
+
status = "not-running";
|
|
474
|
+
record = { status: "missing" };
|
|
475
|
+
} else if (recordState.status === "reserved") {
|
|
476
|
+
// A supervisor reservation without a published owner: no daemon, and the
|
|
477
|
+
// namespace is held for that supervisor.
|
|
478
|
+
status = "not-running";
|
|
479
|
+
record = {
|
|
480
|
+
status: "reserved",
|
|
481
|
+
supervisionId: recordState.reservation.supervisionId,
|
|
482
|
+
reservedAt: recordState.reservation.reservedAt,
|
|
483
|
+
};
|
|
484
|
+
} else if (recordState.status === "invalid") {
|
|
485
|
+
status = "record-invalid";
|
|
486
|
+
record = {
|
|
487
|
+
status: "invalid",
|
|
488
|
+
reason: recordState.reason,
|
|
489
|
+
detail: redactText(recordState.detail),
|
|
490
|
+
};
|
|
491
|
+
} else {
|
|
492
|
+
record = { status: "valid" };
|
|
493
|
+
const info = stripCredentials(recordState.info);
|
|
494
|
+
const liveness = input.pidLiveness(info.pid);
|
|
495
|
+
const provenance = info.provenance ?? null;
|
|
496
|
+
status = liveness === "dead" ? "stale-record" : "running";
|
|
497
|
+
daemon = {
|
|
498
|
+
instanceId: info.instanceId,
|
|
499
|
+
productVersion: info.productVersion,
|
|
500
|
+
protocolVersion: info.protocolVersion,
|
|
501
|
+
pid: info.pid,
|
|
502
|
+
port: info.port,
|
|
503
|
+
bindHostname: info.bindHostname,
|
|
504
|
+
startedAt: info.startedAt,
|
|
505
|
+
supervisionId: info.supervisionId ?? null,
|
|
506
|
+
liveness,
|
|
507
|
+
launcher: provenance?.launcher ?? null,
|
|
508
|
+
supervisor: provenance?.supervisor ?? null,
|
|
509
|
+
parentPid: provenance?.parentPid ?? null,
|
|
510
|
+
provenanceRecorded: provenance !== null,
|
|
511
|
+
logs: { stdout: provenance?.stdout ?? null, stderr: provenance?.stderr ?? null },
|
|
512
|
+
logDestination: describeLogDestination(
|
|
513
|
+
provenance?.stdout ?? null,
|
|
514
|
+
provenance?.supervisor ?? null,
|
|
515
|
+
),
|
|
516
|
+
provenanceWarnings: provenance?.warnings ?? [],
|
|
517
|
+
};
|
|
518
|
+
if (!provenance)
|
|
519
|
+
warnings.push("daemon record predates provenance stamping; log destination unknown");
|
|
520
|
+
if (liveness === "dead")
|
|
521
|
+
warnings.push(`daemon record names pid ${info.pid}, which is not running (stale record)`);
|
|
522
|
+
if (liveness === "unknown")
|
|
523
|
+
warnings.push(`liveness of pid ${info.pid} could not be determined`);
|
|
524
|
+
}
|
|
525
|
+
const logFiles = input.files
|
|
526
|
+
.map((file) => classifyFile(file, daemon, input.pidLiveness))
|
|
527
|
+
.sort((a, b) => Date.parse(b.lastWriteAt) - Date.parse(a.lastWriteAt));
|
|
528
|
+
return { generatedAt, status, namespace: input.namespace, record, daemon, logFiles, warnings };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** PURE — human rendering of the report for the non-JSON CLI path. */
|
|
532
|
+
export function formatDaemonProvenanceReport(report: DaemonProvenanceReport): string[] {
|
|
533
|
+
const lines: string[] = [];
|
|
534
|
+
const d = report.daemon;
|
|
535
|
+
switch (report.status) {
|
|
536
|
+
case "running":
|
|
537
|
+
lines.push(`Canonical daemon: running (pid ${d!.pid}, v${d!.productVersion})`);
|
|
538
|
+
break;
|
|
539
|
+
case "stale-record":
|
|
540
|
+
lines.push(
|
|
541
|
+
`Canonical daemon: not running (stale record names pid ${d!.pid}, v${d!.productVersion})`,
|
|
542
|
+
);
|
|
543
|
+
break;
|
|
544
|
+
case "not-running":
|
|
545
|
+
lines.push(
|
|
546
|
+
report.record.status === "reserved"
|
|
547
|
+
? `Canonical daemon: not running (namespace reserved for supervisor ${report.record.supervisionId})`
|
|
548
|
+
: "Canonical daemon: not running (no record)",
|
|
549
|
+
);
|
|
550
|
+
break;
|
|
551
|
+
case "record-invalid":
|
|
552
|
+
lines.push(
|
|
553
|
+
`Canonical daemon: record invalid (${report.record.status === "invalid" ? report.record.reason : "unknown"})`,
|
|
554
|
+
);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
lines.push(` record: ${report.namespace.recordPath}`);
|
|
558
|
+
if (d) {
|
|
559
|
+
lines.push(` instance: ${d.instanceId}`);
|
|
560
|
+
lines.push(
|
|
561
|
+
` started: ${d.startedAt} protocol ${d.protocolVersion} ${d.bindHostname}:${d.port}`,
|
|
562
|
+
);
|
|
563
|
+
lines.push(
|
|
564
|
+
` supervisor: ${d.supervisor ?? "unknown"}${d.supervisionId ? ` (${d.supervisionId})` : ""}` +
|
|
565
|
+
`${d.launcher ? `, launcher ${d.launcher}` : ""}${d.parentPid !== null ? `, parent pid ${d.parentPid}` : ""}`,
|
|
566
|
+
);
|
|
567
|
+
lines.push(` logs: stdout → ${d.logDestination}`);
|
|
568
|
+
if (d.logs.stderr && JSON.stringify(d.logs.stderr) !== JSON.stringify(d.logs.stdout)) {
|
|
569
|
+
lines.push(` stderr → ${describeLogDestination(d.logs.stderr, d.supervisor)}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (report.logFiles.length) {
|
|
573
|
+
lines.push("Log files:");
|
|
574
|
+
for (const file of report.logFiles) {
|
|
575
|
+
const who =
|
|
576
|
+
file.pid !== undefined
|
|
577
|
+
? ` pid ${file.pid}${file.pidLiveness ? ` (${file.pidLiveness})` : ""}`
|
|
578
|
+
: "";
|
|
579
|
+
lines.push(
|
|
580
|
+
` [${file.status}] ${file.path} — ${file.bytes} bytes, last write ${file.lastWriteAt}${who}`,
|
|
581
|
+
);
|
|
582
|
+
lines.push(` ${file.reason}`);
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
lines.push("Log files: none discovered");
|
|
586
|
+
}
|
|
587
|
+
for (const warning of report.warnings) lines.push(`warning: ${warning}`);
|
|
588
|
+
return lines;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// Observation (io)
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
const LOG_FILE_NAME = /\.(log|out|err)$/iu;
|
|
596
|
+
const MAX_DISCOVERED_FILES = 64;
|
|
597
|
+
const READ_WINDOW_BYTES = 64 * 1024;
|
|
598
|
+
const PID_MARKERS = [/\(pid (\d{1,10})\)/gu, /"pid":\s*(\d{1,10})/gu];
|
|
599
|
+
const INSTANCE_MARKER = /"instanceId":\s*"([0-9a-f-]{36})"/giu;
|
|
600
|
+
const VERSION_MARKER = /"(?:version|productVersion)":\s*"([^"]{1,64})"/gu;
|
|
601
|
+
|
|
602
|
+
function lastMatch(pattern: RegExp, text: string): string | undefined {
|
|
603
|
+
let found: string | undefined;
|
|
604
|
+
pattern.lastIndex = 0;
|
|
605
|
+
for (const match of text.matchAll(pattern)) found = match[1];
|
|
606
|
+
return found;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** PURE — extract the last daemon identity markers from a bounded text window. */
|
|
610
|
+
export function extractLogIdentity(text: string): {
|
|
611
|
+
pid?: number;
|
|
612
|
+
instanceId?: string;
|
|
613
|
+
productVersion?: string;
|
|
614
|
+
} {
|
|
615
|
+
const out: { pid?: number; instanceId?: string; productVersion?: string } = {};
|
|
616
|
+
let lastPidIndex = -1;
|
|
617
|
+
for (const pattern of PID_MARKERS) {
|
|
618
|
+
pattern.lastIndex = 0;
|
|
619
|
+
for (const match of text.matchAll(pattern)) {
|
|
620
|
+
if (match.index !== undefined && match.index > lastPidIndex) {
|
|
621
|
+
lastPidIndex = match.index;
|
|
622
|
+
out.pid = Number(match[1]);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const instanceId = lastMatch(INSTANCE_MARKER, text);
|
|
627
|
+
if (instanceId) out.instanceId = instanceId.toLowerCase();
|
|
628
|
+
const version = lastMatch(VERSION_MARKER, text);
|
|
629
|
+
if (version) out.productVersion = version;
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function readWindow(path: string, size: number): string {
|
|
634
|
+
const fd = openSync(path, "r");
|
|
635
|
+
try {
|
|
636
|
+
const head = Buffer.alloc(Math.min(size, READ_WINDOW_BYTES));
|
|
637
|
+
const headRead = readSync(fd, head, 0, head.length, 0);
|
|
638
|
+
if (size <= READ_WINDOW_BYTES) return head.subarray(0, headRead).toString("utf8");
|
|
639
|
+
const tail = Buffer.alloc(READ_WINDOW_BYTES);
|
|
640
|
+
const tailRead = readSync(fd, tail, 0, tail.length, size - READ_WINDOW_BYTES);
|
|
641
|
+
return `${head.subarray(0, headRead).toString("utf8")}\n${tail.subarray(0, tailRead).toString("utf8")}`;
|
|
642
|
+
} finally {
|
|
643
|
+
closeSync(fd);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Observe one candidate log file read-only; read errors degrade to `readError`. */
|
|
648
|
+
export function observeLogFile(path: string): LogFileObservation | null {
|
|
649
|
+
let stat;
|
|
650
|
+
try {
|
|
651
|
+
stat = lstatSync(path);
|
|
652
|
+
} catch {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
if (!stat.isFile()) return null;
|
|
656
|
+
const base: LogFileObservation = {
|
|
657
|
+
path,
|
|
658
|
+
bytes: stat.size,
|
|
659
|
+
lastWriteAt: stat.mtime.toISOString(),
|
|
660
|
+
dev: Number(stat.dev),
|
|
661
|
+
ino: Number(stat.ino),
|
|
662
|
+
};
|
|
663
|
+
try {
|
|
664
|
+
const identity = extractLogIdentity(readWindow(path, stat.size));
|
|
665
|
+
return { ...base, ...identity };
|
|
666
|
+
} catch (error) {
|
|
667
|
+
return { ...base, readError: errorMessage(error) };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export interface DiscoverLogFilesOptions {
|
|
672
|
+
readonly directories: readonly string[];
|
|
673
|
+
/** Explicit paths (e.g. the record's stdout path) included even outside the directories. */
|
|
674
|
+
readonly extraPaths?: readonly string[];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Discover candidate log files non-recursively; never throws. */
|
|
678
|
+
export function discoverLogFiles(options: DiscoverLogFilesOptions): {
|
|
679
|
+
files: LogFileObservation[];
|
|
680
|
+
warnings: string[];
|
|
681
|
+
} {
|
|
682
|
+
const seen = new Set<string>();
|
|
683
|
+
const files: LogFileObservation[] = [];
|
|
684
|
+
const warnings: string[] = [];
|
|
685
|
+
const consider = (path: string): void => {
|
|
686
|
+
let key = path;
|
|
687
|
+
try {
|
|
688
|
+
key = realpathSync(path);
|
|
689
|
+
} catch {
|
|
690
|
+
// keep the given path as the identity
|
|
691
|
+
}
|
|
692
|
+
if (seen.has(key) || files.length >= MAX_DISCOVERED_FILES) return;
|
|
693
|
+
seen.add(key);
|
|
694
|
+
const observed = observeLogFile(path);
|
|
695
|
+
if (observed) files.push(observed);
|
|
696
|
+
};
|
|
697
|
+
for (const dir of options.directories) {
|
|
698
|
+
let entries;
|
|
699
|
+
try {
|
|
700
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
701
|
+
} catch (error) {
|
|
702
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
703
|
+
if (code !== "ENOENT") warnings.push(`cannot list ${dir} (${errorMessage(error)})`);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
for (const entry of entries) {
|
|
707
|
+
if (!entry.isFile() || !LOG_FILE_NAME.test(entry.name)) continue;
|
|
708
|
+
consider(join(dir, entry.name));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
for (const path of options.extraPaths ?? []) consider(resolve(path));
|
|
712
|
+
if (files.length >= MAX_DISCOVERED_FILES)
|
|
713
|
+
warnings.push(`log discovery capped at ${MAX_DISCOVERED_FILES} files`);
|
|
714
|
+
return { files, warnings };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function pidLivenessProbe(pid: number): PidLiveness {
|
|
718
|
+
try {
|
|
719
|
+
process.kill(pid, 0);
|
|
720
|
+
return "alive";
|
|
721
|
+
} catch (error) {
|
|
722
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
723
|
+
if (code === "ESRCH") return "dead";
|
|
724
|
+
if (code === "EPERM") return "alive";
|
|
725
|
+
return "unknown";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export interface CollectDaemonProvenanceOptions {
|
|
730
|
+
readonly recordState?: CanonicalDaemonInfoState;
|
|
731
|
+
readonly pidLiveness?: (pid: number) => PidLiveness;
|
|
732
|
+
readonly now?: () => Date;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** io — inspect the record, discover log files, and build the report. */
|
|
736
|
+
export function collectDaemonProvenanceReport(
|
|
737
|
+
options: CollectDaemonProvenanceOptions = {},
|
|
738
|
+
): DaemonProvenanceReport {
|
|
739
|
+
const namespace = resolveRuntimeNamespace();
|
|
740
|
+
const recordPath = getCanonicalDaemonInfoPath();
|
|
741
|
+
const recordState = options.recordState ?? inspectCanonicalDaemonInfo();
|
|
742
|
+
const extraPaths: string[] = [];
|
|
743
|
+
if (recordState.status === "valid") {
|
|
744
|
+
for (const stream of [
|
|
745
|
+
recordState.info.provenance?.stdout,
|
|
746
|
+
recordState.info.provenance?.stderr,
|
|
747
|
+
]) {
|
|
748
|
+
if (stream?.kind === "file" && stream.path) extraPaths.push(stream.path);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const directories = [
|
|
752
|
+
...new Set([namespace.logsDir, namespace.daemonInfoDir, namespace.stateHome]),
|
|
753
|
+
];
|
|
754
|
+
const discovered = discoverLogFiles({ directories, extraPaths });
|
|
755
|
+
return buildDaemonProvenanceReport({
|
|
756
|
+
namespace: {
|
|
757
|
+
mode: namespace.mode,
|
|
758
|
+
stateHome: namespace.stateHome,
|
|
759
|
+
daemonInfoDir: namespace.daemonInfoDir,
|
|
760
|
+
logsDir: namespace.logsDir,
|
|
761
|
+
recordPath,
|
|
762
|
+
},
|
|
763
|
+
recordState,
|
|
764
|
+
files: discovered.files,
|
|
765
|
+
pidLiveness: options.pidLiveness ?? pidLivenessProbe,
|
|
766
|
+
...(options.now ? { now: options.now } : {}),
|
|
767
|
+
warnings: discovered.warnings,
|
|
768
|
+
});
|
|
769
|
+
}
|