@catheadowl/ukp 0.2.4 → 0.2.6
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/README.md +15 -7
- package/docs/remote-deployment.md +101 -16
- package/package.json +1 -1
- package/src/capabilities/door-health.ts +480 -0
- package/src/capabilities/remote-client.ts +201 -21
- package/src/capabilities/rg.ts +147 -20
- package/src/capabilities/tls-identity.ts +3 -2
- package/src/cli.ts +7 -1
- package/src/commands/diagnose.ts +80 -2
- package/src/commands/guide-content/remote.txt +4 -2
- package/src/commands/guide-content/rg.txt +35 -0
- package/src/commands/guide.ts +11 -4
- package/src/commands/init.ts +27 -0
- package/src/commands/rg.ts +45 -14
- package/src/commands/serve-task.ts +334 -44
- package/src/commands/serve.ts +101 -19
- package/src/server.ts +113 -65
- package/src/spawn-relay.ts +57 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/** Windows resident-door health model (O-020 ruling B2): the read-only
|
|
2
|
+
* checks behind `ukp diagnose --door`. A "door" here is what
|
|
3
|
+
* `ukp serve --print-task` sets up: a scheduled task whose action runs
|
|
4
|
+
* wscript.exe on a hidden launcher (start-*-hidden.vbs) under
|
|
5
|
+
* %USERPROFILE%\.ukp, which runs the start script (start-*.cmd, carrying
|
|
6
|
+
* the serve line and the token) and owns door.log. Multi-door hosts rename
|
|
7
|
+
* the trio per door (field shape: ukp-door / ukp-door-<name>), so
|
|
8
|
+
* discovery matches the task ACTION (wscript + a .vbs under .ukp), never a
|
|
9
|
+
* fixed task name.
|
|
10
|
+
*
|
|
11
|
+
* Read-only by construction: the system facts arrive as one PowerShell
|
|
12
|
+
* snapshot (task roster, process table, LISTEN sockets) - no process is
|
|
13
|
+
* killed, no file written, no task registered or unregistered, and the
|
|
14
|
+
* "port bind dry-run" is a listener-table lookup, not a real bind (a real
|
|
15
|
+
* listen could pop a firewall prompt and race the launcher's crash retry).
|
|
16
|
+
* The start script is read but only the serve line's --endpoint/--select/
|
|
17
|
+
* --host/--port values are ever echoed; the UKP_SERVE_TOKEN line stays in the
|
|
18
|
+
* file (secret-free output, D-090 convention). Repair knowledge is
|
|
19
|
+
* pointers only: `ukp serve --print-task` section 5 and the deployment
|
|
20
|
+
* handbook - diagnose says what is wrong, never fixes it. */
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// System snapshot (injectable in full for tests)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export interface DoorTaskRow {
|
|
30
|
+
name: string;
|
|
31
|
+
state: string;
|
|
32
|
+
execute: string;
|
|
33
|
+
arguments: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DoorProcessRow {
|
|
37
|
+
pid: number;
|
|
38
|
+
ppid: number;
|
|
39
|
+
name: string;
|
|
40
|
+
commandLine: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DoorListenerRow {
|
|
44
|
+
address: string;
|
|
45
|
+
port: number;
|
|
46
|
+
pid: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DoorSystemSnapshot {
|
|
50
|
+
tasks: DoorTaskRow[];
|
|
51
|
+
processes: DoorProcessRow[];
|
|
52
|
+
listeners: DoorListenerRow[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type DoorSystemProbe = () => DoorSystemSnapshot;
|
|
56
|
+
|
|
57
|
+
/** Default file reader for the operator-authored door files. */
|
|
58
|
+
export function defaultReadDoorFile(path: string): string {
|
|
59
|
+
return readFileSync(path, "utf8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The one-shot PowerShell collector. Everything diagnose needs about
|
|
63
|
+
* tasks, processes, and listeners rides in a single spawn (each direct
|
|
64
|
+
* bun->tool edge is Defender-taxed on win32; powershell as the carrier is
|
|
65
|
+
* the measured-fast shape, see spawn-relay.ts) and comes back as JSON.
|
|
66
|
+
* `[Console]::OutputEncoding` is set so non-ASCII task names survive the
|
|
67
|
+
* pipe as UTF-8. */
|
|
68
|
+
export function defaultDoorSystemProbe(): DoorSystemSnapshot {
|
|
69
|
+
const script = [
|
|
70
|
+
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
71
|
+
"$tasks = @(Get-ScheduledTask -ErrorAction SilentlyContinue | ForEach-Object {",
|
|
72
|
+
" $a = @($_.Actions)[0]",
|
|
73
|
+
' [pscustomobject]@{ name = $_.TaskName; state = [string]$_.State; execute = [string]$a.Execute; arguments = [string]$a.Arguments }',
|
|
74
|
+
"})",
|
|
75
|
+
"$processes = @(Get-CimInstance Win32_Process | ForEach-Object {",
|
|
76
|
+
' [pscustomobject]@{ pid = [int]$_.ProcessId; ppid = [int]$_.ParentProcessId; name = [string]$_.Name; commandLine = [string]$_.CommandLine }',
|
|
77
|
+
"})",
|
|
78
|
+
"$listeners = @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ForEach-Object {",
|
|
79
|
+
' [pscustomobject]@{ address = [string]$_.LocalAddress; port = [int]$_.LocalPort; pid = [int]$_.OwningProcess }',
|
|
80
|
+
"})",
|
|
81
|
+
'[pscustomobject]@{ tasks = $tasks; processes = $processes; listeners = $listeners } | ConvertTo-Json -Depth 4 -Compress',
|
|
82
|
+
].join("\n");
|
|
83
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
84
|
+
const result = spawnSync("powershell", ["-NoProfile", "-EncodedCommand", encoded], {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
timeout: 20_000,
|
|
88
|
+
});
|
|
89
|
+
if (result.error) {
|
|
90
|
+
throw new Error(`could not run powershell for the door snapshot: ${result.error.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (result.status !== 0) {
|
|
93
|
+
throw new Error(`the door snapshot query failed (powershell exit ${result.status})`);
|
|
94
|
+
}
|
|
95
|
+
const raw = (result.stdout ?? "").replace(/^\uFEFF/, "").trim();
|
|
96
|
+
if (raw.length === 0) {
|
|
97
|
+
throw new Error("the door snapshot query returned nothing");
|
|
98
|
+
}
|
|
99
|
+
const parsed = JSON.parse(raw) as Partial<DoorSystemSnapshot>;
|
|
100
|
+
return {
|
|
101
|
+
tasks: (parsed.tasks ?? []).map((row) => ({ ...row, arguments: row.arguments ?? "" })),
|
|
102
|
+
processes: (parsed.processes ?? []).map((row) => ({
|
|
103
|
+
pid: Number(row.pid),
|
|
104
|
+
ppid: Number(row.ppid),
|
|
105
|
+
name: row.name ?? "",
|
|
106
|
+
commandLine: row.commandLine ?? "",
|
|
107
|
+
})),
|
|
108
|
+
listeners: (parsed.listeners ?? []).map((row) => ({
|
|
109
|
+
address: row.address ?? "",
|
|
110
|
+
port: Number(row.port),
|
|
111
|
+
pid: Number(row.pid),
|
|
112
|
+
})),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Door discovery and per-door evaluation
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export interface DoorReport {
|
|
121
|
+
taskName: string;
|
|
122
|
+
taskState: string;
|
|
123
|
+
/** Resolved launcher (.vbs) path; absent when the action path could not
|
|
124
|
+
* be resolved (task is then structurally failed). */
|
|
125
|
+
launcherPath?: string;
|
|
126
|
+
scriptPath?: string;
|
|
127
|
+
serve?: { endpoint?: string; select?: string[]; host?: string; port?: number };
|
|
128
|
+
/** Main root-to-leaf chain (wscript -> cmd -> ... -> bun); absent when no
|
|
129
|
+
* launcher process was found. */
|
|
130
|
+
tree?: Array<{ pid: number; name: string }>;
|
|
131
|
+
port?: {
|
|
132
|
+
port: number;
|
|
133
|
+
listening: boolean;
|
|
134
|
+
holderPid?: number;
|
|
135
|
+
holderName?: string;
|
|
136
|
+
inTree?: boolean;
|
|
137
|
+
};
|
|
138
|
+
log?: {
|
|
139
|
+
path: string;
|
|
140
|
+
banner: boolean;
|
|
141
|
+
lastListening?: string;
|
|
142
|
+
eaddrinuse: boolean;
|
|
143
|
+
sharingViolation: boolean;
|
|
144
|
+
};
|
|
145
|
+
warnings: string[];
|
|
146
|
+
status: "ok" | "warning" | "failed";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface DoorEvaluationEnv {
|
|
150
|
+
homeDir: string;
|
|
151
|
+
readTextFile: (path: string) => string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeKeyPath(path: string): string {
|
|
155
|
+
return path.replaceAll("/", "\\").toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stripSurroundingQuotes(value: string): string {
|
|
159
|
+
return value.trim().replace(/^"+/, "").replace(/"+$/, "");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A door task = an action running wscript.exe on a .vbs under a `.ukp`
|
|
163
|
+
* directory (the print-task launcher shape, rename-tolerant: any task/vbs
|
|
164
|
+
* name qualifies as long as the shape holds). */
|
|
165
|
+
export function isDoorTaskAction(execute: string, args: string): boolean {
|
|
166
|
+
if (!/wscript\.exe$/i.test(execute.trim())) return false;
|
|
167
|
+
const clean = stripSurroundingQuotes(args).replaceAll("/", "\\");
|
|
168
|
+
return /\\.ukp\\.*\.vbs$/i.test(clean);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Task action argument -> launcher path. Only %USERPROFILE% (the
|
|
172
|
+
* template's variable) is expanded; any other %VAR% marks the action
|
|
173
|
+
* unresolvable from userland. */
|
|
174
|
+
export function resolveDoorLauncherPath(
|
|
175
|
+
args: string,
|
|
176
|
+
homeDir: string,
|
|
177
|
+
): { path: string; resolved: boolean } {
|
|
178
|
+
const clean = stripSurroundingQuotes(args);
|
|
179
|
+
const expanded = clean.replace(/%USERPROFILE%/gi, homeDir);
|
|
180
|
+
return { path: expanded, resolved: !expanded.includes("%") };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The .vbs path from a live wscript command line (last quoted .vbs token). */
|
|
184
|
+
function vbsPathFromCommandLine(commandLine: string): string | undefined {
|
|
185
|
+
const matches = [...commandLine.matchAll(/"([^"]+\.vbs)"/gi)].map((m) => m[1]!);
|
|
186
|
+
return matches.at(-1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The serve line's flags from the start script text; undefined pieces are
|
|
190
|
+
* simply absent. Only these values are ever echoed - the token line in the
|
|
191
|
+
* same file is never reproduced. `--select` (ADR-REM-010 subset doors)
|
|
192
|
+
* parses to the name list. */
|
|
193
|
+
export function parseServeLine(scriptText: string): { endpoint?: string; select?: string[]; host?: string; port?: number } | undefined {
|
|
194
|
+
const line = scriptText
|
|
195
|
+
.split(/\r?\n/)
|
|
196
|
+
.map((entry) => entry.trim())
|
|
197
|
+
.find((entry) => /^ukp\s+serve(\s|$)/.test(entry));
|
|
198
|
+
if (line === undefined) return undefined;
|
|
199
|
+
const endpoint = /--endpoint\s+(\S+)/.exec(line)?.[1];
|
|
200
|
+
const selectRaw = /--select\s+(\S+)/.exec(line)?.[1];
|
|
201
|
+
const host = /--host\s+(\S+)/.exec(line)?.[1];
|
|
202
|
+
const portRaw = /--port\s+(\d+)/.exec(line)?.[1];
|
|
203
|
+
return {
|
|
204
|
+
...(endpoint !== undefined ? { endpoint } : {}),
|
|
205
|
+
...(selectRaw !== undefined ? { select: selectRaw.split(",") } : {}),
|
|
206
|
+
...(host !== undefined ? { host } : {}),
|
|
207
|
+
...(portRaw !== undefined ? { port: Number(portRaw) } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** door.log path: the live cmd process carries the full redirect target;
|
|
212
|
+
* statically, the vbs names the log file fragment under .ukp. */
|
|
213
|
+
function findLogPath(liveCmdCommandLine: string | undefined, vbsText: string | undefined, launcherPath: string): string | undefined {
|
|
214
|
+
const live = liveCmdCommandLine !== undefined ? />>\s*"([^"]+)"/.exec(liveCmdCommandLine)?.[1] : undefined;
|
|
215
|
+
if (live !== undefined) return live;
|
|
216
|
+
const fragment = vbsText !== undefined ? /\\\.ukp\\([^\r\n"']+?\.log)/i.exec(vbsText)?.[1] : undefined;
|
|
217
|
+
if (fragment !== undefined) {
|
|
218
|
+
const dir = launcherPath.slice(0, Math.max(launcherPath.lastIndexOf("\\"), launcherPath.lastIndexOf("/")));
|
|
219
|
+
return `${dir}\\${fragment.replaceAll("/", "\\")}`;
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function evaluateDoor(report: DoorReport, snapshot: DoorSystemSnapshot, env: DoorEvaluationEnv): DoorReport {
|
|
225
|
+
const warnings = report.warnings;
|
|
226
|
+
const failed = report.status === "failed";
|
|
227
|
+
|
|
228
|
+
// Launcher script readability (operator-authored; tolerance, not failure).
|
|
229
|
+
let vbsText: string | undefined;
|
|
230
|
+
if (report.launcherPath !== undefined) {
|
|
231
|
+
try {
|
|
232
|
+
vbsText = env.readTextFile(report.launcherPath);
|
|
233
|
+
} catch {
|
|
234
|
+
warnings.push(
|
|
235
|
+
`launcher script not found at ${report.launcherPath} - the task references a deleted file; remove the task ('ukp serve --print-task' section 5, level 2) or restore the file`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Start script (the -hidden.vbs -> .cmd naming convention) and its serve line.
|
|
241
|
+
let scriptText: string | undefined;
|
|
242
|
+
if (report.scriptPath !== undefined) {
|
|
243
|
+
try {
|
|
244
|
+
scriptText = env.readTextFile(report.scriptPath);
|
|
245
|
+
} catch {
|
|
246
|
+
warnings.push(
|
|
247
|
+
`start script not found at ${report.scriptPath} - the door's port cannot be determined; restore it or remove the door ('ukp serve --print-task' section 5)`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (scriptText !== undefined) {
|
|
252
|
+
const serve = parseServeLine(scriptText);
|
|
253
|
+
if (serve === undefined) {
|
|
254
|
+
warnings.push(
|
|
255
|
+
`no 'ukp serve' line found in ${report.scriptPath} - the door's port cannot be determined from the script`,
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
report.serve = serve;
|
|
259
|
+
if (serve.port === undefined) {
|
|
260
|
+
warnings.push(`the serve line in ${report.scriptPath} carries no --port - the door's port cannot be determined`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Process tree: the wscript whose command line names this door's vbs.
|
|
266
|
+
const launcherKey = report.launcherPath !== undefined ? normalizeKeyPath(report.launcherPath) : undefined;
|
|
267
|
+
const root = snapshot.processes.find((proc) => {
|
|
268
|
+
if (!/^wscript\.exe$/i.test(proc.name)) return false;
|
|
269
|
+
const vbs = vbsPathFromCommandLine(proc.commandLine);
|
|
270
|
+
return vbs !== undefined && launcherKey !== undefined && normalizeKeyPath(vbs) === launcherKey;
|
|
271
|
+
});
|
|
272
|
+
let treePidSet: Set<number> | undefined;
|
|
273
|
+
if (root !== undefined) {
|
|
274
|
+
const children = new Map<number, DoorProcessRow[]>();
|
|
275
|
+
for (const proc of snapshot.processes) {
|
|
276
|
+
const list = children.get(proc.ppid) ?? [];
|
|
277
|
+
list.push(proc);
|
|
278
|
+
children.set(proc.ppid, list);
|
|
279
|
+
}
|
|
280
|
+
const treePids = new Set<number>([root.pid]);
|
|
281
|
+
const chain: Array<{ pid: number; name: string }> = [{ pid: root.pid, name: root.name.replace(/\.exe$/i, "") }];
|
|
282
|
+
let frontier: DoorProcessRow[] = [root];
|
|
283
|
+
while (frontier.length > 0) {
|
|
284
|
+
// Main chain = first child per level; siblings only join the pid set.
|
|
285
|
+
const next: DoorProcessRow[] = [];
|
|
286
|
+
for (const parent of frontier) {
|
|
287
|
+
for (const child of children.get(parent.pid) ?? []) {
|
|
288
|
+
treePids.add(child.pid);
|
|
289
|
+
next.push(child);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (next.length > 0) {
|
|
293
|
+
chain.push({ pid: next[0]!.pid, name: next[0]!.name.replace(/\.exe$/i, "") });
|
|
294
|
+
}
|
|
295
|
+
frontier = next;
|
|
296
|
+
}
|
|
297
|
+
treePidSet = treePids;
|
|
298
|
+
report.tree = chain;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Port cross-check: the listener table decides, never a real bind.
|
|
302
|
+
const port = report.serve?.port;
|
|
303
|
+
if (port !== undefined) {
|
|
304
|
+
const holder = snapshot.listeners.find((entry) => entry.port === port);
|
|
305
|
+
if (holder === undefined) {
|
|
306
|
+
report.port = { port, listening: false };
|
|
307
|
+
} else {
|
|
308
|
+
const holderName = snapshot.processes.find((proc) => proc.pid === holder.pid)?.name ?? "(unknown)";
|
|
309
|
+
const inTree = treePidSet?.has(holder.pid) ?? false;
|
|
310
|
+
report.port = {
|
|
311
|
+
port,
|
|
312
|
+
listening: true,
|
|
313
|
+
holderPid: holder.pid,
|
|
314
|
+
holderName: holderName.replace(/\.exe$/i, ""),
|
|
315
|
+
inTree,
|
|
316
|
+
};
|
|
317
|
+
if (!inTree) {
|
|
318
|
+
warnings.push(
|
|
319
|
+
`port ${port} is held by ${holderName.replace(/\.exe$/i, "")}(${holder.pid}) OUTSIDE this door's process tree - a leftover listener the door cannot bind past (its log would carry EADDRINUSE); find it with netstat -ano | findstr :${port}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// door.log tail triage (ISSUE-016 vocabulary made actionable).
|
|
326
|
+
if (report.launcherPath !== undefined) {
|
|
327
|
+
const liveCmd =
|
|
328
|
+
root !== undefined
|
|
329
|
+
? snapshot.processes.find((proc) => proc.ppid === root!.pid && /^cmd\.exe$/i.test(proc.name))
|
|
330
|
+
: undefined;
|
|
331
|
+
const logPath = findLogPath(liveCmd?.commandLine, vbsText, report.launcherPath);
|
|
332
|
+
if (logPath === undefined) {
|
|
333
|
+
warnings.push("the door.log path could not be determined (neither the live launcher nor the vbs names it)");
|
|
334
|
+
} else {
|
|
335
|
+
try {
|
|
336
|
+
const tail = env.readTextFile(logPath).slice(-8192);
|
|
337
|
+
// Latest-evidence triage: the log is append-only, so a failure
|
|
338
|
+
// signature only counts when it lands AFTER the newest serving
|
|
339
|
+
// banner (a banner that follows supersedes the failure - the door
|
|
340
|
+
// restarted or the artifacts were regenerated; field door 8570
|
|
341
|
+
// carries ISSUE-016-era scar lines under a healthy banner).
|
|
342
|
+
const lastBanner = Math.max(
|
|
343
|
+
tail.lastIndexOf("serving endpoint '"),
|
|
344
|
+
tail.lastIndexOf("serving host door"),
|
|
345
|
+
);
|
|
346
|
+
const lastEaddrinuse = tail.lastIndexOf("EADDRINUSE");
|
|
347
|
+
const lastSharingViolation = tail.lastIndexOf("being used by another process");
|
|
348
|
+
const banner = lastBanner !== -1;
|
|
349
|
+
const lastListening = [...tail.matchAll(/listening: (\S+)/g)].map((m) => m[1]!).at(-1);
|
|
350
|
+
const eaddrinuse = lastEaddrinuse > lastBanner;
|
|
351
|
+
const sharingViolation = lastSharingViolation > lastBanner;
|
|
352
|
+
report.log = { path: logPath, banner, lastListening, eaddrinuse, sharingViolation };
|
|
353
|
+
if (!banner) {
|
|
354
|
+
warnings.push(
|
|
355
|
+
`no serving banner in the tail of ${logPath} - the door never reached serving in what the log remembers`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (eaddrinuse) {
|
|
359
|
+
warnings.push(
|
|
360
|
+
`door.log tail carries EADDRINUSE as its latest evidence - the port was already taken when the door last tried to bind; see the port row above, then restart the door by killing the launcher root (taskkill /PID <wscript-pid> /T /F) and Start-ScheduledTask -TaskName ${report.taskName}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (sharingViolation) {
|
|
364
|
+
warnings.push(
|
|
365
|
+
"door.log carries 'being used by another process' as its latest evidence - the double-redirect deadlock shape (a redirect on the serve line inside the start script while the hidden launcher also redirects); regenerate the artifacts with a current ukp ('ukp serve --print-task') - the serve line must carry no redirect",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
warnings.push(`no readable door.log at ${logPath} - the door has not logged a start here`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Door-level findings.
|
|
375
|
+
if (report.tree === undefined) {
|
|
376
|
+
if (/^running$/i.test(report.taskState)) {
|
|
377
|
+
warnings.push(
|
|
378
|
+
`the task state says Running but no launcher process exists - the door crashed past the launcher's bounded retries or was killed; start it again (Start-ScheduledTask -TaskName ${report.taskName})`,
|
|
379
|
+
);
|
|
380
|
+
} else {
|
|
381
|
+
warnings.push(
|
|
382
|
+
`door not running - the task is registered but nothing holds the process tree; start it with Start-ScheduledTask -TaskName ${report.taskName}, or remove it ('ukp serve --print-task' section 5)`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
} else if (report.port?.listening === false) {
|
|
386
|
+
warnings.push(
|
|
387
|
+
`the door's process tree is alive but nothing listens on port ${port} - the serve process may be mid-retry or failed to bind; check the log tail above`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
report.status = failed ? "failed" : warnings.length > 0 ? "warning" : "ok";
|
|
392
|
+
return report;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Snapshot -> per-door reports. Pure apart from the injected file reader. */
|
|
396
|
+
export function evaluateDoorSnapshot(snapshot: DoorSystemSnapshot, env: DoorEvaluationEnv): DoorReport[] {
|
|
397
|
+
const reports: DoorReport[] = [];
|
|
398
|
+
for (const task of snapshot.tasks) {
|
|
399
|
+
if (!isDoorTaskAction(task.execute, task.arguments)) continue;
|
|
400
|
+
const report: DoorReport = { taskName: task.name, taskState: task.state, warnings: [], status: "ok" };
|
|
401
|
+
const resolved = resolveDoorLauncherPath(task.arguments, env.homeDir);
|
|
402
|
+
if (!resolved.resolved) {
|
|
403
|
+
report.status = "failed";
|
|
404
|
+
report.warnings.push(
|
|
405
|
+
`the task action could not be resolved to a launcher path: ${task.arguments} (only %USERPROFILE% is expanded)`,
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
report.launcherPath = resolved.path;
|
|
409
|
+
if (/-hidden\.vbs$/i.test(resolved.path)) {
|
|
410
|
+
report.scriptPath = resolved.path.replace(/-hidden\.vbs$/i, ".cmd");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
reports.push(evaluateDoor(report, snapshot, env));
|
|
414
|
+
}
|
|
415
|
+
return reports.sort((a, b) => a.taskName.localeCompare(b.taskName));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// Rendering (diagnose wire vocabulary: key: value rows, status:, warning:)
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
export function renderDoorReport(report: DoorReport): string[] {
|
|
423
|
+
const lines = [
|
|
424
|
+
`== door: ${report.taskName} ==`,
|
|
425
|
+
`task: ${report.taskState}`,
|
|
426
|
+
...(report.launcherPath !== undefined ? [`launcher: ${report.launcherPath}`] : []),
|
|
427
|
+
];
|
|
428
|
+
if (report.scriptPath !== undefined) {
|
|
429
|
+
lines.push(`script: ${report.scriptPath}`);
|
|
430
|
+
}
|
|
431
|
+
if (report.serve !== undefined) {
|
|
432
|
+
const shape = report.serve.endpoint !== undefined
|
|
433
|
+
? `endpoint ${report.serve.endpoint}`
|
|
434
|
+
: report.serve.select !== undefined
|
|
435
|
+
? `subset ${report.serve.select.join(",")}`
|
|
436
|
+
: "host door";
|
|
437
|
+
const host = report.serve.host !== undefined ? `, host ${report.serve.host}` : "";
|
|
438
|
+
const port = report.serve.port !== undefined ? `, port ${report.serve.port}` : "";
|
|
439
|
+
lines.push(`serve: ${shape}${host}${port}`);
|
|
440
|
+
}
|
|
441
|
+
lines.push(
|
|
442
|
+
report.tree !== undefined
|
|
443
|
+
? `tree: ${report.tree.map((node) => `${node.name}(${node.pid})`).join(" > ")}`
|
|
444
|
+
: "tree: (no launcher process)",
|
|
445
|
+
);
|
|
446
|
+
if (report.port !== undefined) {
|
|
447
|
+
if (!report.port.listening) {
|
|
448
|
+
lines.push(`port: ${report.port.port} free (nothing listens)`);
|
|
449
|
+
} else if (report.port.inTree) {
|
|
450
|
+
lines.push(`port: ${report.port.port} listening (pid ${report.port.holderPid}, inside the door tree)`);
|
|
451
|
+
} else {
|
|
452
|
+
lines.push(`port: ${report.port.port} listening (pid ${report.port.holderPid}, OUTSIDE the door tree)`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (report.log !== undefined) {
|
|
456
|
+
const tail = report.log.banner
|
|
457
|
+
? report.log.lastListening !== undefined
|
|
458
|
+
? `banner present (last: listening: ${report.log.lastListening})`
|
|
459
|
+
: "banner present"
|
|
460
|
+
: "no serving banner in the tail";
|
|
461
|
+
lines.push(`log: ${report.log.path}`, `log_tail: ${tail}`);
|
|
462
|
+
}
|
|
463
|
+
for (const warning of report.warnings) {
|
|
464
|
+
lines.push(`warning: ${warning}`);
|
|
465
|
+
}
|
|
466
|
+
lines.push(`status: ${report.status}`);
|
|
467
|
+
return lines;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function renderDoorHealthReport(reports: readonly DoorReport[]): string {
|
|
471
|
+
if (reports.length === 0) {
|
|
472
|
+
return [
|
|
473
|
+
"no resident doors found on this host (no scheduled task launches a .ukp",
|
|
474
|
+
"hidden launcher). This is not an error - doors exist only where",
|
|
475
|
+
"'ukp serve --print-task' was applied; the ssh:// path runs none.",
|
|
476
|
+
"",
|
|
477
|
+
].join("\n");
|
|
478
|
+
}
|
|
479
|
+
return `${reports.map((report) => renderDoorReport(report).join("\n")).join("\n\n")}\n`;
|
|
480
|
+
}
|