@adhdev/daemon-core 1.0.18-rc.1 → 1.0.18-rc.10
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/dist/cli-adapters/cli-state-engine.d.ts +77 -0
- package/dist/cli-adapters/pty-transport.d.ts +2 -1
- package/dist/commands/process-lifecycle.d.ts +43 -0
- package/dist/commands/upgrade-helper.d.ts +7 -0
- package/dist/commands/windows-atomic-upgrade.d.ts +63 -0
- package/dist/index.js +5818 -4496
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5853 -4525
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/mesh-disk-retention.d.ts +105 -0
- package/dist/mesh/mesh-ledger.d.ts +28 -1
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/dist/providers/cli-provider-instance-types.d.ts +2 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/cli-adapters/cli-state-engine.ts +204 -3
- package/src/cli-adapters/provider-cli-adapter.ts +39 -5
- package/src/cli-adapters/pty-transport.d.ts +2 -1
- package/src/cli-adapters/pty-transport.ts +1 -1
- package/src/cli-adapters/session-host-transport.ts +8 -3
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -2
- package/src/commands/process-lifecycle.ts +248 -0
- package/src/commands/upgrade-helper.ts +146 -79
- package/src/commands/windows-atomic-upgrade.ts +631 -0
- package/src/config/mesh-json-config.ts +8 -0
- package/src/mesh/coordinator-prompt.ts +256 -10
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-ledger.ts +72 -0
- package/src/mesh/mesh-queue-assignment.ts +126 -1
- package/src/mesh/mesh-reconcile-loop.ts +49 -0
- package/src/mesh/mesh-runtime-store.ts +21 -0
- package/src/providers/cli-provider-instance-types.ts +25 -0
- package/src/providers/cli-provider-instance.ts +116 -0
- package/src/session-host/managed-host.ts +34 -0
|
@@ -707,8 +707,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
707
707
|
}
|
|
708
708
|
});
|
|
709
709
|
|
|
710
|
-
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
711
|
-
|
|
710
|
+
this.ptyProcess.onExit(({ exitCode, signal }: { exitCode: number | null; signal?: number | null }) => {
|
|
711
|
+
// Preserve the unknown case: a null exitCode (signal-terminated or
|
|
712
|
+
// otherwise unreported) is logged as "unknown", never as exit 0.
|
|
713
|
+
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode === null || exitCode === undefined ? 'unknown' : exitCode}${signal ? ` (signal ${signal})` : ''}`);
|
|
712
714
|
this.flushPendingOutputParse();
|
|
713
715
|
this.ptyProcess = null;
|
|
714
716
|
this.engine.onPtyExit();
|
|
@@ -1072,7 +1074,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1072
1074
|
|
|
1073
1075
|
getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
|
|
1074
1076
|
const allowParse = options.allowParse !== false;
|
|
1075
|
-
|
|
1077
|
+
let startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
1078
|
+
// (fix: kimi startup-gate stale-approval bypass) startupParseGate can
|
|
1079
|
+
// still be open by the time a real approval is requested AND resolved
|
|
1080
|
+
// — this ad-hoc parse of recentOutputBuffer runs independently of the
|
|
1081
|
+
// settled-eval loop and previously had no staleness protection at all,
|
|
1082
|
+
// so it kept re-surfacing the identical already-resolved modal for as
|
|
1083
|
+
// long as its text remained anywhere in the rolling buffer, bypassing
|
|
1084
|
+
// engine.activeModal (which resolveModal() had already correctly
|
|
1085
|
+
// cleared). Reuse the SAME discriminator the settle loop uses instead
|
|
1086
|
+
// of inventing a second one here.
|
|
1087
|
+
if (startupModal && this.engine.isStaleResolvedApproval(startupModal, { screenText: this.terminalScreen.getText(), accumulatedBuffer: this.accumulatedBuffer })) {
|
|
1088
|
+
startupModal = null;
|
|
1089
|
+
}
|
|
1076
1090
|
const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
|
|
1077
1091
|
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
1078
1092
|
: null;
|
|
@@ -1179,7 +1193,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1179
1193
|
// and let a later evaluate find the complete modal.
|
|
1180
1194
|
const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
|
|
1181
1195
|
&& liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
|
|
1182
|
-
|
|
1196
|
+
// (fix: kimi live-detect stale-approval bypass) This fallback is
|
|
1197
|
+
// NOT gated by startupParseGate — it runs on every getStatus()
|
|
1198
|
+
// call (including the periodic background status heartbeat)
|
|
1199
|
+
// for as long as isWaitingForResponse stays true, which for a
|
|
1200
|
+
// provider that keeps "thinking" after the approved tool call
|
|
1201
|
+
// can be the whole rest of the turn. Its own unguarded re-parse
|
|
1202
|
+
// of recentOutputBuffer/terminalScreen previously kept
|
|
1203
|
+
// re-writing engine.activeModal directly — bypassing setStatus
|
|
1204
|
+
// (so rawStatus never even flipped) and bypassing
|
|
1205
|
+
// applyWaitingApproval's staleness guard entirely, silently
|
|
1206
|
+
// re-corrupting the engine's own state moments after
|
|
1207
|
+
// resolveModal() had correctly cleared it. Reuse the same
|
|
1208
|
+
// engine-owned discriminator here too.
|
|
1209
|
+
if (liveModal && buttonsOk && !this.engine.isStaleResolvedApproval(liveModal, { screenText: this.terminalScreen.getText(), accumulatedBuffer: this.accumulatedBuffer })) {
|
|
1183
1210
|
effectiveModal = liveModal;
|
|
1184
1211
|
if (!this.engine.activeModal) this.engine.activeModal = liveModal;
|
|
1185
1212
|
}
|
|
@@ -2474,7 +2501,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2474
2501
|
|
|
2475
2502
|
getDebugState(): Record<string, any> {
|
|
2476
2503
|
const screenText = sanitizeTerminalText(this.terminalScreen.getText());
|
|
2477
|
-
|
|
2504
|
+
let startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
2505
|
+
// (fix: kimi startup-gate stale-approval bypass) See the matching
|
|
2506
|
+
// comment in getStatus() — this ad-hoc startup-gate parse bypassed
|
|
2507
|
+
// engine.activeModal's staleness protection entirely. Reuse the same
|
|
2508
|
+
// engine-owned discriminator rather than duplicating it here.
|
|
2509
|
+
if (startupModal && this.engine.isStaleResolvedApproval(startupModal, { screenText: this.terminalScreen.getText(), accumulatedBuffer: this.accumulatedBuffer })) {
|
|
2510
|
+
startupModal = null;
|
|
2511
|
+
}
|
|
2478
2512
|
const startupDetectedStatus = this.startupParseGate && !startupModal
|
|
2479
2513
|
? this.runDetectStatus(this.recentOutputBuffer || screenText)
|
|
2480
2514
|
: null;
|
|
@@ -37,7 +37,8 @@ export interface PtyRuntimeTransport {
|
|
|
37
37
|
getMetadata?(): PtyRuntimeMetadata | null;
|
|
38
38
|
onData(callback: (data: string) => void): void;
|
|
39
39
|
onExit(callback: (info: {
|
|
40
|
-
exitCode: number;
|
|
40
|
+
exitCode: number | null;
|
|
41
|
+
signal?: number | null;
|
|
41
42
|
}) => void): void;
|
|
42
43
|
}
|
|
43
44
|
export interface PtyTransportFactory {
|
|
@@ -62,7 +62,7 @@ export interface PtyRuntimeTransport {
|
|
|
62
62
|
updateMeta?(meta: Record<string, unknown>, replace?: boolean): void;
|
|
63
63
|
getMetadata?(): PtyRuntimeMetadata | null;
|
|
64
64
|
onData(callback: (data: string) => void): void;
|
|
65
|
-
onExit(callback: (info: { exitCode: number }) => void): void;
|
|
65
|
+
onExit(callback: (info: { exitCode: number | null; signal?: number | null }) => void): void;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export interface PtyTransportFactory {
|
|
@@ -42,7 +42,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
42
42
|
|
|
43
43
|
private readonly client: SessionHostClient;
|
|
44
44
|
private readonly dataCallbacks = new Set<(data: string) => void>();
|
|
45
|
-
private readonly exitCallbacks = new Set<(info: { exitCode: number }) => void>();
|
|
45
|
+
private readonly exitCallbacks = new Set<(info: { exitCode: number | null; signal?: number | null }) => void>();
|
|
46
46
|
private readonly pendingOutput: string[] = [];
|
|
47
47
|
private operationChain = Promise.resolve();
|
|
48
48
|
private unsubscribe: (() => void) | null = null;
|
|
@@ -79,7 +79,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
onExit(callback: (info: { exitCode: number }) => void): void {
|
|
82
|
+
onExit(callback: (info: { exitCode: number | null; signal?: number | null }) => void): void {
|
|
83
83
|
this.exitCallbacks.add(callback);
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -361,8 +361,13 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
361
361
|
return;
|
|
362
362
|
}
|
|
363
363
|
if (event.type === 'session_exit') {
|
|
364
|
+
// Preserve the nullable/unknown exitCode and signal exactly as the
|
|
365
|
+
// session host reported them — never collapse null to 0, which would
|
|
366
|
+
// make a signal-terminated process indistinguishable from a clean exit.
|
|
367
|
+
const exitCode = typeof event.exitCode === 'number' ? event.exitCode : null;
|
|
368
|
+
const signal = typeof event.signal === 'number' ? event.signal : null;
|
|
364
369
|
for (const callback of this.exitCallbacks) {
|
|
365
|
-
callback({ exitCode
|
|
370
|
+
callback({ exitCode, signal });
|
|
366
371
|
}
|
|
367
372
|
void this.closeClient(false);
|
|
368
373
|
}
|
|
@@ -115,8 +115,12 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
115
115
|
try {
|
|
116
116
|
const { readOperatingNotes } = await import('../../mesh/mesh-ledger.js');
|
|
117
117
|
// readOperatingNotes filters out tombstoned (forgotten) notes so a
|
|
118
|
-
// retracted lesson never rides into the prompt. Newest last
|
|
119
|
-
|
|
118
|
+
// retracted lesson never rides into the prompt. Newest last.
|
|
119
|
+
// Phase 2 (d): the byte-budget/count cap now bounds the injected list
|
|
120
|
+
// (selectOperatingNotesForPrompt), so read a larger candidate tail than
|
|
121
|
+
// the old fixed 20 and let injection-side ranking + budget do the
|
|
122
|
+
// bounding. Keep a sane store-read ceiling to avoid unbounded arrays.
|
|
123
|
+
const noteEntries = readOperatingNotes(id, { tail: 100 });
|
|
120
124
|
const notes = noteEntries
|
|
121
125
|
.map((e) => {
|
|
122
126
|
const p = (e.payload || {}) as Record<string, unknown>;
|
|
@@ -131,6 +135,17 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
131
135
|
category,
|
|
132
136
|
createdAt: typeof p.createdAt === 'string' ? p.createdAt : e.timestamp,
|
|
133
137
|
sourceCoordinator: typeof p.sourceCoordinator === 'string' ? p.sourceCoordinator : undefined,
|
|
138
|
+
// Operating-notes lifecycle: thread pinned/expiresAt so the
|
|
139
|
+
// injection-side selection can honor them. Legacy notes lack
|
|
140
|
+
// these → pinned defaults false, expiry governed by category TTL.
|
|
141
|
+
pinned: p.pinned === true,
|
|
142
|
+
...(typeof p.expiresAt === 'string' ? { expiresAt: p.expiresAt } : {}),
|
|
143
|
+
// Phase 2 (b)/(c): thread the ledger id + supersedes/subjectKey
|
|
144
|
+
// so version-supersede targeting and same-class folding work.
|
|
145
|
+
// Legacy notes lack them → no supersede, fold by leading [tag].
|
|
146
|
+
noteId: e.id,
|
|
147
|
+
...(typeof p.supersedes === 'string' ? { supersedes: p.supersedes } : {}),
|
|
148
|
+
...(typeof p.subjectKey === 'string' ? { subjectKey: p.subjectKey } : {}),
|
|
134
149
|
};
|
|
135
150
|
})
|
|
136
151
|
.filter((n): n is NonNullable<typeof n> => n !== null);
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { execFileSync, type ExecFileSyncOptions } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ProcessLifecycleOptions {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
execFileSync?: typeof import('child_process').execFileSync;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OwnedProcessInfo {
|
|
10
|
+
pid: number;
|
|
11
|
+
commandLine: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function defaultExecFileSync(): typeof import('child_process').execFileSync {
|
|
15
|
+
return execFileSync;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getWindowsProcessCommandLine(
|
|
19
|
+
pid: number,
|
|
20
|
+
exec: typeof import('child_process').execFileSync,
|
|
21
|
+
): string | null {
|
|
22
|
+
const pidFilter = `ProcessId=${pid}`;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const psOut = exec('powershell.exe', [
|
|
26
|
+
'-NoProfile',
|
|
27
|
+
'-NonInteractive',
|
|
28
|
+
'-ExecutionPolicy', 'Bypass',
|
|
29
|
+
'-Command',
|
|
30
|
+
`(Get-CimInstance Win32_Process -Filter "${pidFilter}").CommandLine`,
|
|
31
|
+
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
32
|
+
const text = String(psOut).trim();
|
|
33
|
+
if (text) return text;
|
|
34
|
+
} catch {
|
|
35
|
+
// fall through to wmic fallback
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const wmicOut = exec('wmic', [
|
|
40
|
+
'process', 'where', pidFilter, 'get', 'CommandLine',
|
|
41
|
+
], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
42
|
+
const text = String(wmicOut).trim();
|
|
43
|
+
if (text) return text;
|
|
44
|
+
} catch {
|
|
45
|
+
// noop
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getProcessCommandLine(
|
|
52
|
+
pid: number,
|
|
53
|
+
options: ProcessLifecycleOptions = {},
|
|
54
|
+
): string | null {
|
|
55
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
56
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
57
|
+
const platform = options.platform ?? process.platform;
|
|
58
|
+
|
|
59
|
+
if (platform === 'win32') {
|
|
60
|
+
return getWindowsProcessCommandLine(pid, exec);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const text = String(exec('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
timeout: 3000,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
68
|
+
})).trim();
|
|
69
|
+
return text || null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract the script argument (the token after the node executable) from a
|
|
77
|
+
* command line. Handles both quoted and unquoted executables/scripts.
|
|
78
|
+
*/
|
|
79
|
+
export function parseNodeScriptPath(commandLine: string | null): string | null {
|
|
80
|
+
if (!commandLine) return null;
|
|
81
|
+
let rest = commandLine.trim();
|
|
82
|
+
|
|
83
|
+
// Skip the leading executable token.
|
|
84
|
+
if (rest.startsWith('"')) {
|
|
85
|
+
const end = rest.indexOf('"', 1);
|
|
86
|
+
if (end === -1) return null;
|
|
87
|
+
rest = rest.slice(end + 1).trim();
|
|
88
|
+
} else {
|
|
89
|
+
const idx = rest.search(/\s/);
|
|
90
|
+
if (idx === -1) return null;
|
|
91
|
+
rest = rest.slice(idx + 1).trim();
|
|
92
|
+
}
|
|
93
|
+
if (!rest) return null;
|
|
94
|
+
|
|
95
|
+
if (rest.startsWith('"')) {
|
|
96
|
+
const end = rest.indexOf('"', 1);
|
|
97
|
+
return end === -1 ? rest.slice(1) : rest.slice(1, end);
|
|
98
|
+
}
|
|
99
|
+
const idx = rest.search(/\s/);
|
|
100
|
+
return idx === -1 ? rest : rest.slice(0, idx);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeWindowsPath(value: string): string {
|
|
104
|
+
return value.toLowerCase().replace(/\//g, '\\').replace(/\\+$/, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isCommandLineUnderPrefix(commandLine: string | null, prefix: string): boolean {
|
|
108
|
+
if (!commandLine) return false;
|
|
109
|
+
const needle = normalizeWindowsPath(prefix);
|
|
110
|
+
// Command-line paths may quote Windows separators as either single or
|
|
111
|
+
// doubled backslashes; collapse doubles before matching.
|
|
112
|
+
const haystack = commandLine.split('\\\\').join('\\').toLowerCase();
|
|
113
|
+
return haystack.includes(needle);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function killProcess(pid: number, options: ProcessLifecycleOptions = {}): boolean {
|
|
117
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
118
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
119
|
+
const platform = options.platform ?? process.platform;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (platform === 'win32') {
|
|
123
|
+
exec('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
|
124
|
+
stdio: 'ignore',
|
|
125
|
+
windowsHide: true,
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
process.kill(pid, 'SIGTERM');
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean> {
|
|
137
|
+
const start = Date.now();
|
|
138
|
+
while (Date.now() - start < timeoutMs) {
|
|
139
|
+
try {
|
|
140
|
+
process.kill(pid, 0);
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
142
|
+
} catch {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List Node processes whose command line places them under any of the supplied
|
|
151
|
+
* prefixes. Windows-only — the versioned-prefix lifecycle this supports does not
|
|
152
|
+
* exist on POSIX, so the helper is a no-op there.
|
|
153
|
+
*/
|
|
154
|
+
export function listOwnedNodeProcesses(options: {
|
|
155
|
+
prefixes: readonly string[];
|
|
156
|
+
excludePids?: readonly number[];
|
|
157
|
+
markers?: readonly string[];
|
|
158
|
+
} & ProcessLifecycleOptions): OwnedProcessInfo[] {
|
|
159
|
+
const platform = options.platform ?? process.platform;
|
|
160
|
+
if (platform !== 'win32') return [];
|
|
161
|
+
|
|
162
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
163
|
+
const prefixes = options.prefixes.map((p) => normalizeWindowsPath(p));
|
|
164
|
+
const exclude = new Set((options.excludePids ?? []).filter((n) => Number.isFinite(n) && n > 0));
|
|
165
|
+
// Command-line paths use Windows separators; normalize marker separators the
|
|
166
|
+
// same way so a marker like "dist/cli/index.js" matches "dist\cli\index.js".
|
|
167
|
+
const markers = (options.markers ?? []).map((m) => m.toLowerCase().replace(/\//g, '\\'));
|
|
168
|
+
|
|
169
|
+
let pids: number[] = [];
|
|
170
|
+
try {
|
|
171
|
+
const out = String(exec('powershell.exe', [
|
|
172
|
+
'-NoProfile',
|
|
173
|
+
'-NonInteractive',
|
|
174
|
+
'-ExecutionPolicy', 'Bypass',
|
|
175
|
+
'-Command',
|
|
176
|
+
'Get-Process node -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id | ConvertTo-Json -Compress',
|
|
177
|
+
], { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true })).trim();
|
|
178
|
+
if (out) {
|
|
179
|
+
const parsed = JSON.parse(out);
|
|
180
|
+
pids = Array.isArray(parsed) ? parsed : [parsed];
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const results: OwnedProcessInfo[] = [];
|
|
187
|
+
for (const pid of pids) {
|
|
188
|
+
if (!Number.isFinite(pid) || pid <= 0 || exclude.has(pid)) continue;
|
|
189
|
+
const commandLine = getProcessCommandLine(pid, { platform, execFileSync: exec });
|
|
190
|
+
if (!commandLine) continue;
|
|
191
|
+
const lower = commandLine.toLowerCase();
|
|
192
|
+
const underPrefix = prefixes.some((prefix) => lower.includes(prefix));
|
|
193
|
+
if (!underPrefix) continue;
|
|
194
|
+
if (markers.length > 0 && !markers.some((marker) => lower.includes(marker))) continue;
|
|
195
|
+
results.push({ pid, commandLine });
|
|
196
|
+
}
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function stopOwnedProcesses(options: {
|
|
201
|
+
processes: readonly OwnedProcessInfo[];
|
|
202
|
+
waitMs?: number;
|
|
203
|
+
} & ProcessLifecycleOptions): Promise<{ stopped: number; survivors: OwnedProcessInfo[] }> {
|
|
204
|
+
const waitMs = options.waitMs ?? 15_000;
|
|
205
|
+
const killed = new Set<number>();
|
|
206
|
+
|
|
207
|
+
for (const p of options.processes) {
|
|
208
|
+
if (killProcess(p.pid, options)) killed.add(p.pid);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const survivors: OwnedProcessInfo[] = [];
|
|
212
|
+
for (const p of options.processes) {
|
|
213
|
+
if (!killed.has(p.pid)) {
|
|
214
|
+
survivors.push(p);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const exited = await waitForPidExit(p.pid, waitMs);
|
|
218
|
+
if (!exited) survivors.push(p);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A process counts as stopped only when it left the process table; a failed
|
|
222
|
+
// kill or a post-kill timeout both land it in `survivors`.
|
|
223
|
+
return { stopped: options.processes.length - survivors.length, survivors };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function stopOwnedProcessesForPrefixes(options: {
|
|
227
|
+
prefixes: readonly string[];
|
|
228
|
+
excludePids?: readonly number[];
|
|
229
|
+
markers?: readonly string[];
|
|
230
|
+
waitMs?: number;
|
|
231
|
+
log?: (message: string) => void;
|
|
232
|
+
} & ProcessLifecycleOptions): Promise<{ stopped: number; survivors: OwnedProcessInfo[] }> {
|
|
233
|
+
const processes = listOwnedNodeProcesses(options);
|
|
234
|
+
if (processes.length === 0) return { stopped: 0, survivors: [] };
|
|
235
|
+
|
|
236
|
+
options.log?.(`Stopping ${processes.length} owned process(es) under prefixes: ${options.prefixes.join(', ')}`);
|
|
237
|
+
const result = await stopOwnedProcesses({
|
|
238
|
+
processes,
|
|
239
|
+
waitMs: options.waitMs,
|
|
240
|
+
platform: options.platform,
|
|
241
|
+
execFileSync: options.execFileSync,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (result.survivors.length > 0) {
|
|
245
|
+
options.log?.(`Could not stop ${result.survivors.length} owned process(es): ${result.survivors.map((s) => s.pid).join(', ')}`);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|