sentinelayer-cli 0.46.1 → 0.46.2
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/create-sentinelayer.js +2 -2
- package/bin/sentinelayer-cli.js +2 -2
- package/bin/sl.js +2 -2
- package/package.json +1 -1
- package/src/cli-bootstrap.js +20 -0
- package/src/cli.js +3 -1
- package/src/commands/session.js +47 -14
- package/src/legacy-cli.js +0 -10
- package/src/session/listener-process.js +112 -25
package/bin/sentinelayer-cli.js
CHANGED
package/bin/sl.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { restrictProgramLookupToPath } from "./program-lookup.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Entry for the bin scripts. Checks the runtime before the rest of the CLI is loaded: importing
|
|
7
|
+
* cli.js loads every command module, and some modules do work as they load (for example, creating
|
|
8
|
+
* ~/.sentinelayer), so this module and program-lookup.js are the only ones a bin script imports
|
|
9
|
+
* statically. runCli repeats the check for callers that import cli.js directly.
|
|
10
|
+
*/
|
|
11
|
+
export async function bootstrapCli() {
|
|
12
|
+
const programLookup = restrictProgramLookupToPath();
|
|
13
|
+
if (!programLookup.ok) {
|
|
14
|
+
console.error(programLookup.message);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const { runCli } = await import("./cli.js");
|
|
19
|
+
await runCli();
|
|
20
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -244,7 +244,9 @@ export async function buildCliProgram({
|
|
|
244
244
|
|
|
245
245
|
export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
246
246
|
// Programs named without a path come from PATH only, never from the working directory, before
|
|
247
|
-
// anything is spawned. On a Windows runtime that cannot guarantee that, nothing runs.
|
|
247
|
+
// anything is spawned. On a Windows runtime that cannot guarantee that, nothing runs. The bin
|
|
248
|
+
// scripts check this before loading this module (cli-bootstrap.js); here it covers callers that
|
|
249
|
+
// import runCli directly.
|
|
248
250
|
const programLookup = restrictProgramLookupToPath();
|
|
249
251
|
if (!programLookup.ok) {
|
|
250
252
|
console.error(programLookup.message);
|
package/src/commands/session.js
CHANGED
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
import { fetchSessionListeners, formatListenerLine } from "../session/listeners.js";
|
|
77
77
|
import {
|
|
78
78
|
getListenerProcessStatus,
|
|
79
|
+
LISTENER_PROCESS_SCAN_RETRY_TIMEOUT_MS,
|
|
79
80
|
removeListenerPidRecord,
|
|
80
81
|
requestListenerProcessStop,
|
|
81
82
|
summarizeLocalListenerProcesses,
|
|
@@ -5149,16 +5150,17 @@ export function registerSessionCommand(program) {
|
|
|
5149
5150
|
let localProcessScan = {
|
|
5150
5151
|
ok: false,
|
|
5151
5152
|
reason: listeners.length > 0 ? "not_scanned" : "no_listener_rows",
|
|
5152
|
-
|
|
5153
|
-
|
|
5153
|
+
// Not scanned, so no count: null, never 0, so a reader that skips `ok` can't read "no duplicates".
|
|
5154
|
+
processCount: null,
|
|
5155
|
+
duplicateProcessCount: null,
|
|
5154
5156
|
agents: [],
|
|
5155
5157
|
};
|
|
5156
5158
|
if (listeners.length > 0) {
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5159
|
+
const summary = await summarizeLocalListenerProcesses(
|
|
5160
|
+
normalizedSessionId,
|
|
5161
|
+
listeners.map((row) => row.agentId),
|
|
5162
|
+
);
|
|
5163
|
+
if (summary.ok) {
|
|
5162
5164
|
const byAgent = new Map(summary.agents.map((entry) => [entry.agentId, entry]));
|
|
5163
5165
|
listeners = listeners.map((row) => {
|
|
5164
5166
|
const local = byAgent.get(row.agentId) || {};
|
|
@@ -5176,12 +5178,13 @@ export function registerSessionCommand(program) {
|
|
|
5176
5178
|
duplicateProcessCount: Number(summary.duplicateProcessCount || 0),
|
|
5177
5179
|
agents: summary.agents,
|
|
5178
5180
|
};
|
|
5179
|
-
}
|
|
5181
|
+
} else {
|
|
5182
|
+
// The rows stay unannotated: an unread process table is not "no local listeners".
|
|
5180
5183
|
localProcessScan = {
|
|
5181
5184
|
ok: false,
|
|
5182
|
-
reason:
|
|
5183
|
-
processCount:
|
|
5184
|
-
duplicateProcessCount:
|
|
5185
|
+
reason: summary.reason,
|
|
5186
|
+
processCount: null,
|
|
5187
|
+
duplicateProcessCount: null,
|
|
5185
5188
|
agents: [],
|
|
5186
5189
|
};
|
|
5187
5190
|
}
|
|
@@ -5231,6 +5234,11 @@ export function registerSessionCommand(program) {
|
|
|
5231
5234
|
else if (row.status === "idle") console.log(pc.cyan(` ${line}`));
|
|
5232
5235
|
else console.log(pc.gray(` ${line}`));
|
|
5233
5236
|
}
|
|
5237
|
+
if (!localProcessScan.ok) {
|
|
5238
|
+
console.log(pc.yellow(
|
|
5239
|
+
`Local listener processes: unknown. The local process check did not complete (${localProcessScan.reason}), so duplicate local listeners were not checked.`,
|
|
5240
|
+
));
|
|
5241
|
+
}
|
|
5234
5242
|
const duplicateLocalRows = listeners.filter((row) => Number(row.localDuplicateProcessCount || 0) > 0);
|
|
5235
5243
|
if (duplicateLocalRows.length > 0) {
|
|
5236
5244
|
const summary = duplicateLocalRows
|
|
@@ -5484,9 +5492,23 @@ export function registerSessionCommand(program) {
|
|
|
5484
5492
|
const singletonEnabled = maxPolls === null && !options.allowDuplicate;
|
|
5485
5493
|
let listenerPidRecordActive = false;
|
|
5486
5494
|
if (singletonEnabled) {
|
|
5487
|
-
|
|
5495
|
+
let existingListener = await getListenerProcessStatus(normalizedSessionId, agentId, {
|
|
5488
5496
|
targetPath,
|
|
5489
5497
|
});
|
|
5498
|
+
if (existingListener.state === "unknown") {
|
|
5499
|
+
// A busy machine can need longer than the first limit: check once more, slower.
|
|
5500
|
+
existingListener = await getListenerProcessStatus(normalizedSessionId, agentId, {
|
|
5501
|
+
targetPath,
|
|
5502
|
+
scanTimeoutMs: LISTENER_PROCESS_SCAN_RETRY_TIMEOUT_MS,
|
|
5503
|
+
});
|
|
5504
|
+
}
|
|
5505
|
+
if (existingListener.state === "unknown") {
|
|
5506
|
+
// Starting without an answer could add a second listener, so refuse, with or
|
|
5507
|
+
// without --force: --force replaces the listener the check finds.
|
|
5508
|
+
throw new Error(
|
|
5509
|
+
`Could not confirm whether a session listener is already running for session ${normalizedSessionId} as ${agentId}: the local process check did not complete (${existingListener.reason}), including one retry with a longer time limit. No listener was started. Run the command again, or use --allow-duplicate to start without this check (it can then run alongside an existing listener). --force does not skip the check: it stops the listener the check finds, then starts this one.`,
|
|
5510
|
+
);
|
|
5511
|
+
}
|
|
5490
5512
|
if (existingListener.running && !options.force) {
|
|
5491
5513
|
throw new Error(
|
|
5492
5514
|
`A session listener is already running for session ${normalizedSessionId} as ${agentId} (pid ${existingListener.pid}). Use --force to take over or --allow-duplicate if you intentionally need more than one.`,
|
|
@@ -5494,8 +5516,19 @@ export function registerSessionCommand(program) {
|
|
|
5494
5516
|
}
|
|
5495
5517
|
if (existingListener.running && options.force) {
|
|
5496
5518
|
const stopMatchesResult = await stopMatchingListenerProcesses(normalizedSessionId, agentId);
|
|
5519
|
+
if (!stopMatchesResult.ok) {
|
|
5520
|
+
// The listener being replaced is known (a verified pid record or a scan moments
|
|
5521
|
+
// ago), so stopping it and starting this one never adds a listener. Only the
|
|
5522
|
+
// cleanup of extra copies is skipped, and that is said rather than assumed.
|
|
5523
|
+
console.error(
|
|
5524
|
+
pc.yellow(
|
|
5525
|
+
`The local process check did not complete (${stopMatchesResult.reason}); replacing listener pid ${existingListener.pid} only. Other copies of this listener, if any, were not checked.`,
|
|
5526
|
+
),
|
|
5527
|
+
);
|
|
5528
|
+
}
|
|
5529
|
+
const scannedStops = stopMatchesResult.ok ? stopMatchesResult.results : [];
|
|
5497
5530
|
const stoppedPids = new Set(
|
|
5498
|
-
|
|
5531
|
+
scannedStops
|
|
5499
5532
|
.filter((result) => result.stopped)
|
|
5500
5533
|
.map((result) => Number(result.pid)),
|
|
5501
5534
|
);
|
|
@@ -5504,7 +5537,7 @@ export function registerSessionCommand(program) {
|
|
|
5504
5537
|
explicitStopResult = await requestListenerProcessStop(existingListener.pid);
|
|
5505
5538
|
}
|
|
5506
5539
|
const failedStops = [
|
|
5507
|
-
...
|
|
5540
|
+
...scannedStops.filter((result) => !result.stopped),
|
|
5508
5541
|
...(explicitStopResult && !explicitStopResult.stopped
|
|
5509
5542
|
? [{ pid: existingListener.pid, ...explicitStopResult }]
|
|
5510
5543
|
: []),
|
package/src/legacy-cli.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
import crypto from "node:crypto";
|
|
4
2
|
import fs from "node:fs";
|
|
5
3
|
import fsp from "node:fs/promises";
|
|
@@ -8,7 +6,6 @@ import process from "node:process";
|
|
|
8
6
|
import { spawnSync } from "node:child_process";
|
|
9
7
|
import { createInterface } from "node:readline/promises";
|
|
10
8
|
import { stdin as input, stdout as output } from "node:process";
|
|
11
|
-
import { pathToFileURL } from "node:url";
|
|
12
9
|
|
|
13
10
|
import open from "open";
|
|
14
11
|
import pc from "picocolors";
|
|
@@ -3764,10 +3761,3 @@ export async function runLegacyCliWithErrorHandling(rawArgs = process.argv.slice
|
|
|
3764
3761
|
}
|
|
3765
3762
|
}
|
|
3766
3763
|
|
|
3767
|
-
const invokedAsEntrypoint =
|
|
3768
|
-
process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
3769
|
-
|
|
3770
|
-
if (invokedAsEntrypoint) {
|
|
3771
|
-
runLegacyCliWithErrorHandling();
|
|
3772
|
-
}
|
|
3773
|
-
|
|
@@ -13,6 +13,12 @@ const LISTENER_DIR_NAME = "listeners";
|
|
|
13
13
|
const GLOBAL_LISTENER_DIR_NAME = "session-listeners";
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
15
|
|
|
16
|
+
// Time allowed for one scan of the process table. A busy machine can need longer than the
|
|
17
|
+
// first limit, so a caller that must not guess (the one-listener check in `session listen`)
|
|
18
|
+
// retries once with the longer one before it reports the check as incomplete.
|
|
19
|
+
export const LISTENER_PROCESS_SCAN_TIMEOUT_MS = 3_000;
|
|
20
|
+
export const LISTENER_PROCESS_SCAN_RETRY_TIMEOUT_MS = 10_000;
|
|
21
|
+
|
|
16
22
|
function normalizeString(value) {
|
|
17
23
|
return String(value || "").trim();
|
|
18
24
|
}
|
|
@@ -88,7 +94,7 @@ function normalizeProcessRows(rows = []) {
|
|
|
88
94
|
.filter((row) => Number.isInteger(row.pid) && row.pid > 0 && row.commandLine);
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
async function listWindowsProcesses() {
|
|
97
|
+
async function listWindowsProcesses({ timeoutMs }) {
|
|
92
98
|
const command = [
|
|
93
99
|
"$ErrorActionPreference = 'Stop';",
|
|
94
100
|
"$rows = Get-CimInstance Win32_Process |",
|
|
@@ -99,7 +105,7 @@ async function listWindowsProcesses() {
|
|
|
99
105
|
const { stdout } = await execFileAsync(
|
|
100
106
|
"powershell.exe",
|
|
101
107
|
["-NoProfile", "-NonInteractive", "-Command", command],
|
|
102
|
-
{ windowsHide: true, timeout:
|
|
108
|
+
{ windowsHide: true, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024 },
|
|
103
109
|
);
|
|
104
110
|
const text = normalizeString(stdout);
|
|
105
111
|
if (!text) return [];
|
|
@@ -107,9 +113,9 @@ async function listWindowsProcesses() {
|
|
|
107
113
|
return normalizeProcessRows(Array.isArray(parsed) ? parsed : [parsed]);
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
async function listPosixProcesses() {
|
|
116
|
+
async function listPosixProcesses({ timeoutMs }) {
|
|
111
117
|
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,command="], {
|
|
112
|
-
timeout:
|
|
118
|
+
timeout: timeoutMs,
|
|
113
119
|
maxBuffer: 10 * 1024 * 1024,
|
|
114
120
|
});
|
|
115
121
|
return String(stdout || "")
|
|
@@ -121,30 +127,73 @@ async function listPosixProcesses() {
|
|
|
121
127
|
.filter(Boolean);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
// Resolves to the process rows, or rejects when the table cannot be read (a timeout, a
|
|
131
|
+
// missing tool, unreadable output). It never turns a failed read into an empty table.
|
|
132
|
+
async function listProcessTable({ timeoutMs = LISTENER_PROCESS_SCAN_TIMEOUT_MS } = {}) {
|
|
133
|
+
return process.platform === "win32"
|
|
134
|
+
? listWindowsProcesses({ timeoutMs })
|
|
135
|
+
: listPosixProcesses({ timeoutMs });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The probes `session listen` uses; replaced only through setListenerProcessProbesForTests.
|
|
139
|
+
let activeListProcessTable = listProcessTable;
|
|
140
|
+
let activeReadProcessCommandLine = readProcessCommandLine;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Test seam: replace the process-table scan and the per-pid command-line read that the
|
|
144
|
+
* listener commands use. Call with no argument to restore the real probes.
|
|
145
|
+
*/
|
|
146
|
+
export function setListenerProcessProbesForTests({ listProcesses = null, readCommandLine = null } = {}) {
|
|
147
|
+
activeListProcessTable = listProcesses || listProcessTable;
|
|
148
|
+
activeReadProcessCommandLine = readCommandLine || readProcessCommandLine;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function processScanFailureReason(error) {
|
|
152
|
+
const code = normalizeString(error?.code);
|
|
153
|
+
// execFile kills the scan at its timeout and reports it as killed with no code.
|
|
154
|
+
if (code === "ETIMEDOUT" || (!code && error?.killed)) return "process_scan_timeout";
|
|
155
|
+
if (error instanceof SyntaxError) return "process_scan_unreadable";
|
|
156
|
+
return code ? `process_scan_failed:${code}` : "process_scan_failed";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// One scan of the process table: { ok: true, rows } or { ok: false, reason }.
|
|
160
|
+
async function scanProcessTable(listProcesses, timeoutMs) {
|
|
161
|
+
let rows;
|
|
125
162
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
} catch {
|
|
130
|
-
return [];
|
|
163
|
+
rows = await listProcesses({ timeoutMs });
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return { ok: false, reason: processScanFailureReason(error) };
|
|
131
166
|
}
|
|
167
|
+
if (!Array.isArray(rows)) {
|
|
168
|
+
return { ok: false, reason: "process_scan_unreadable" };
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, rows: normalizeProcessRows(rows) };
|
|
132
171
|
}
|
|
133
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Listener processes for this session and agent: { ok: true, matches } when the process
|
|
175
|
+
* table was read, { ok: false, reason } when it could not be. A failed scan has no
|
|
176
|
+
* `matches`, so it can never be read as "no listener".
|
|
177
|
+
*/
|
|
134
178
|
export async function listMatchingListenerProcesses(
|
|
135
179
|
sessionId,
|
|
136
180
|
agentId,
|
|
137
181
|
{
|
|
138
182
|
excludePid = process.pid,
|
|
139
|
-
|
|
183
|
+
timeoutMs = LISTENER_PROCESS_SCAN_TIMEOUT_MS,
|
|
184
|
+
_listProcesses = activeListProcessTable,
|
|
140
185
|
} = {},
|
|
141
186
|
) {
|
|
142
|
-
const
|
|
187
|
+
const scan = await scanProcessTable(_listProcesses, timeoutMs);
|
|
188
|
+
if (!scan.ok) return scan;
|
|
143
189
|
const excluded = Number(excludePid);
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
matches: scan.rows.filter((row) =>
|
|
193
|
+
row.pid !== excluded &&
|
|
194
|
+
listenerCommandLineMatches(row.commandLine, { sessionId, agentId })
|
|
195
|
+
),
|
|
196
|
+
};
|
|
148
197
|
}
|
|
149
198
|
|
|
150
199
|
export async function summarizeLocalListenerProcesses(
|
|
@@ -152,16 +201,20 @@ export async function summarizeLocalListenerProcesses(
|
|
|
152
201
|
agentIds = [],
|
|
153
202
|
{
|
|
154
203
|
excludePid = process.pid,
|
|
155
|
-
|
|
204
|
+
timeoutMs = LISTENER_PROCESS_SCAN_TIMEOUT_MS,
|
|
205
|
+
_listProcesses = activeListProcessTable,
|
|
156
206
|
} = {},
|
|
157
207
|
) {
|
|
158
208
|
const normalizedSessionId = normalizeString(sessionId);
|
|
159
209
|
const uniqueAgentIds = [...new Set((Array.isArray(agentIds) ? agentIds : [])
|
|
160
210
|
.map((agentId) => normalizeString(agentId))
|
|
161
211
|
.filter(Boolean))];
|
|
162
|
-
const
|
|
212
|
+
const scan = await scanProcessTable(_listProcesses, timeoutMs);
|
|
213
|
+
if (!scan.ok) {
|
|
214
|
+
return { ok: false, reason: scan.reason, sessionId: normalizedSessionId };
|
|
215
|
+
}
|
|
163
216
|
const excluded = Number(excludePid);
|
|
164
|
-
const candidates = rows.filter((row) => row.pid !== excluded);
|
|
217
|
+
const candidates = scan.rows.filter((row) => row.pid !== excluded);
|
|
165
218
|
const agents = uniqueAgentIds.map((agentId) => {
|
|
166
219
|
const matches = candidates
|
|
167
220
|
.filter((row) => listenerCommandLineMatches(row.commandLine, {
|
|
@@ -178,6 +231,7 @@ export async function summarizeLocalListenerProcesses(
|
|
|
178
231
|
};
|
|
179
232
|
});
|
|
180
233
|
return {
|
|
234
|
+
ok: true,
|
|
181
235
|
sessionId: normalizedSessionId,
|
|
182
236
|
processCount: agents.reduce((sum, agent) => sum + agent.processCount, 0),
|
|
183
237
|
duplicateProcessCount: agents.reduce((sum, agent) => sum + agent.duplicateProcessCount, 0),
|
|
@@ -367,12 +421,13 @@ async function listenerStatusFromRecord(
|
|
|
367
421
|
sessionId,
|
|
368
422
|
agentId,
|
|
369
423
|
recordScope,
|
|
370
|
-
_readProcessCommandLine =
|
|
424
|
+
_readProcessCommandLine = activeReadProcessCommandLine,
|
|
371
425
|
} = {},
|
|
372
426
|
) {
|
|
373
427
|
const alive = isProcessAlive(record.pid);
|
|
374
428
|
if (!alive) {
|
|
375
429
|
return {
|
|
430
|
+
state: "not_running",
|
|
376
431
|
running: false,
|
|
377
432
|
pid: null,
|
|
378
433
|
stale: true,
|
|
@@ -384,6 +439,7 @@ async function listenerStatusFromRecord(
|
|
|
384
439
|
const commandLine = await _readProcessCommandLine(record.pid);
|
|
385
440
|
if (!listenerCommandLineMatches(commandLine, { sessionId, agentId })) {
|
|
386
441
|
return {
|
|
442
|
+
state: "not_running",
|
|
387
443
|
running: false,
|
|
388
444
|
pid: null,
|
|
389
445
|
stale: true,
|
|
@@ -396,6 +452,7 @@ async function listenerStatusFromRecord(
|
|
|
396
452
|
};
|
|
397
453
|
}
|
|
398
454
|
return {
|
|
455
|
+
state: "running",
|
|
399
456
|
running: true,
|
|
400
457
|
pid: Number(record.pid),
|
|
401
458
|
stale: false,
|
|
@@ -406,14 +463,21 @@ async function listenerStatusFromRecord(
|
|
|
406
463
|
};
|
|
407
464
|
}
|
|
408
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Whether a listener for this session and agent is running on this machine. `state` is
|
|
468
|
+
* "running", "not_running" or "unknown". A live pid record whose command line matches is
|
|
469
|
+
* "running" without a scan. Otherwise the process table decides, and when it cannot be read
|
|
470
|
+
* the answer is "unknown" (`running: null`, with `reason`), never "not_running".
|
|
471
|
+
*/
|
|
409
472
|
export async function getListenerProcessStatus(
|
|
410
473
|
sessionId,
|
|
411
474
|
agentId,
|
|
412
475
|
{
|
|
413
476
|
targetPath = process.cwd(),
|
|
414
477
|
homeDir = os.homedir(),
|
|
415
|
-
|
|
416
|
-
|
|
478
|
+
scanTimeoutMs = LISTENER_PROCESS_SCAN_TIMEOUT_MS,
|
|
479
|
+
_readProcessCommandLine = activeReadProcessCommandLine,
|
|
480
|
+
_listProcesses = activeListProcessTable,
|
|
417
481
|
} = {},
|
|
418
482
|
) {
|
|
419
483
|
const records = [
|
|
@@ -440,11 +504,24 @@ export async function getListenerProcessStatus(
|
|
|
440
504
|
}
|
|
441
505
|
staleStatuses.push(status);
|
|
442
506
|
}
|
|
443
|
-
const
|
|
507
|
+
const scan = await listMatchingListenerProcesses(sessionId, agentId, {
|
|
508
|
+
timeoutMs: scanTimeoutMs,
|
|
444
509
|
_listProcesses,
|
|
445
510
|
});
|
|
511
|
+
if (!scan.ok) {
|
|
512
|
+
return {
|
|
513
|
+
state: "unknown",
|
|
514
|
+
running: null,
|
|
515
|
+
pid: null,
|
|
516
|
+
reason: scan.reason,
|
|
517
|
+
record: staleStatuses[0]?.record || null,
|
|
518
|
+
listenerKey: normalizeListenerProcessKey(agentId),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const matchingProcesses = scan.matches;
|
|
446
522
|
if (matchingProcesses.length > 0) {
|
|
447
523
|
return {
|
|
524
|
+
state: "running",
|
|
448
525
|
running: true,
|
|
449
526
|
pid: matchingProcesses[0].pid,
|
|
450
527
|
stale: false,
|
|
@@ -458,6 +535,7 @@ export async function getListenerProcessStatus(
|
|
|
458
535
|
}
|
|
459
536
|
if (staleStatuses.length === 0) {
|
|
460
537
|
return {
|
|
538
|
+
state: "not_running",
|
|
461
539
|
running: false,
|
|
462
540
|
pid: null,
|
|
463
541
|
stale: false,
|
|
@@ -468,6 +546,10 @@ export async function getListenerProcessStatus(
|
|
|
468
546
|
return staleStatuses[0];
|
|
469
547
|
}
|
|
470
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Stops every listener process the scan finds for this session and agent. When the process
|
|
551
|
+
* table cannot be read it stops nothing and returns { ok: false, reason }.
|
|
552
|
+
*/
|
|
471
553
|
export async function stopMatchingListenerProcesses(
|
|
472
554
|
sessionId,
|
|
473
555
|
agentId,
|
|
@@ -475,13 +557,17 @@ export async function stopMatchingListenerProcesses(
|
|
|
475
557
|
excludePid = process.pid,
|
|
476
558
|
timeoutMs = 3000,
|
|
477
559
|
pollIntervalMs = 100,
|
|
478
|
-
|
|
560
|
+
scanTimeoutMs = LISTENER_PROCESS_SCAN_TIMEOUT_MS,
|
|
561
|
+
_listProcesses = activeListProcessTable,
|
|
479
562
|
} = {},
|
|
480
563
|
) {
|
|
481
|
-
const
|
|
564
|
+
const scan = await listMatchingListenerProcesses(sessionId, agentId, {
|
|
482
565
|
excludePid,
|
|
566
|
+
timeoutMs: scanTimeoutMs,
|
|
483
567
|
_listProcesses,
|
|
484
568
|
});
|
|
569
|
+
if (!scan.ok) return scan;
|
|
570
|
+
const matches = scan.matches;
|
|
485
571
|
const results = [];
|
|
486
572
|
for (const match of matches) {
|
|
487
573
|
const result = await requestListenerProcessStop(match.pid, {
|
|
@@ -496,6 +582,7 @@ export async function stopMatchingListenerProcesses(
|
|
|
496
582
|
}
|
|
497
583
|
const failed = results.filter((result) => !result.stopped);
|
|
498
584
|
return {
|
|
585
|
+
ok: true,
|
|
499
586
|
matches,
|
|
500
587
|
results,
|
|
501
588
|
stoppedCount: results.filter((result) => result.stopped).length,
|