pi-lens 3.8.69 → 3.8.71
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/CHANGELOG.md +99 -0
- package/README.md +31 -13
- package/dist/clients/bootstrap.js +3 -1
- package/dist/clients/bus-events-logger.js +53 -0
- package/dist/clients/bus-publish.js +45 -2
- package/dist/clients/cache/rule-cache.js +9 -5
- package/dist/clients/cache-manager.js +19 -13
- package/dist/clients/code-quality-warnings.js +27 -0
- package/dist/clients/dead-code-client.js +5 -23
- package/dist/clients/diagnostic-dispositions.js +298 -0
- package/dist/clients/diagnostics-publish.js +38 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +144 -6
- package/dist/clients/dispatch/dispatcher.js +6 -1
- package/dist/clients/dispatch/facts/function-facts.js +83 -0
- package/dist/clients/dispatch/integration.js +19 -4
- package/dist/clients/dispatch/rules/async-noise.js +1 -1
- package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +1 -1
- package/dist/clients/dispatch/rules/cors-wildcard.js +1 -1
- package/dist/clients/dispatch/rules/error-obscuring.js +2 -1
- package/dist/clients/dispatch/rules/error-swallowing.js +2 -1
- package/dist/clients/dispatch/rules/framework-call-noise.js +67 -0
- package/dist/clients/dispatch/rules/high-complexity.js +12 -1
- package/dist/clients/dispatch/rules/high-fan-out.js +14 -3
- package/dist/clients/dispatch/rules/high-import-coupling.js +1 -1
- package/dist/clients/dispatch/rules/missing-error-propagation.js +1 -1
- package/dist/clients/dispatch/rules/no-commented-credentials.js +1 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +1 -1
- package/dist/clients/dispatch/rules/placeholder-comments.js +1 -1
- package/dist/clients/dispatch/rules/unsafe-boundary.js +1 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +75 -18
- package/dist/clients/dispatch/runners/lsp.js +23 -34
- package/dist/clients/dispatch/runners/oxlint.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +68 -0
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +4 -1
- package/dist/clients/disposition-logger.js +39 -0
- package/dist/clients/disposition-publish.js +109 -0
- package/dist/clients/file-utils.js +94 -33
- package/dist/clients/format-events-publish.js +299 -0
- package/dist/clients/git-tracked-ignore.js +208 -0
- package/dist/clients/gitleaks-client.js +40 -5
- package/dist/clients/installer/index.js +123 -6
- package/dist/clients/instance-reaper.js +229 -2
- package/dist/clients/instance-registry.js +141 -2
- package/dist/clients/json-cache-read.js +65 -0
- package/dist/clients/knip-client.js +35 -38
- package/dist/clients/language-profile.js +11 -11
- package/dist/clients/lens-engine.js +12 -0
- package/dist/clients/lens-map.js +1079 -0
- package/dist/clients/log-cleanup.js +61 -19
- package/dist/clients/lsp/aggregation.js +69 -2
- package/dist/clients/lsp/cascade-tier.js +31 -19
- package/dist/clients/lsp/client.js +124 -14
- package/dist/clients/lsp/config.js +27 -1
- package/dist/clients/lsp/index.js +1028 -68
- package/dist/clients/lsp/server-strategies.js +17 -8
- package/dist/clients/lsp/server.js +80 -28
- package/dist/clients/lsp/tsserver-sync.js +145 -0
- package/dist/clients/lsp/workspace-diagnostics-cache.js +172 -0
- package/dist/clients/lsp-budget.js +148 -0
- package/dist/clients/mcp/session.js +9 -0
- package/dist/clients/middle-man-analysis.js +260 -0
- package/dist/clients/module-report.js +173 -11
- package/dist/clients/ndjson-logger.js +24 -0
- package/dist/clients/opengrep-client.js +217 -0
- package/dist/clients/opengrep-config.js +2 -10
- package/dist/clients/path-utils.js +66 -0
- package/dist/clients/pipeline.js +5 -1
- package/dist/clients/project-diagnostics/cache.js +5 -12
- package/dist/clients/project-diagnostics/extractors.js +21 -1
- package/dist/clients/project-diagnostics/fresh-fetch.js +275 -0
- package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +40 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +46 -0
- package/dist/clients/project-snapshot.js +3 -6
- package/dist/clients/quiet-window.js +57 -2
- package/dist/clients/read-expansion.js +2 -3
- package/dist/clients/read-guard-tool-lines.js +10 -0
- package/dist/clients/read-guard.js +1 -1
- package/dist/clients/resource-sampler.js +254 -0
- package/dist/clients/review-graph/builder.js +322 -42
- package/dist/clients/review-graph/import-resolvers.js +53 -19
- package/dist/clients/review-graph/symbol-id.js +57 -0
- package/dist/clients/runtime-agent-end.js +26 -0
- package/dist/clients/runtime-coordinator.js +9 -1
- package/dist/clients/runtime-session.js +93 -17
- package/dist/clients/runtime-tool-call.js +789 -0
- package/dist/clients/runtime-tool-result.js +15 -2
- package/dist/clients/runtime-turn.js +62 -13
- package/dist/clients/safe-spawn.js +48 -2
- package/dist/clients/session-state-store.js +6 -9
- package/dist/clients/sgconfig.js +2 -9
- package/dist/clients/source-filter.js +17 -25
- package/dist/clients/source-walker.js +66 -0
- package/dist/clients/startup-scan.js +105 -43
- package/dist/clients/symbol-containment.js +24 -0
- package/dist/clients/test-runner-client.js +544 -29
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/typos-config.js +2 -11
- package/dist/clients/widget-state.js +83 -1
- package/dist/clients/write-ordering-guard.js +58 -0
- package/dist/clients/zizmor-config.js +29 -10
- package/dist/index.js +25511 -19414
- package/dist/mcp/server.js +95 -19
- package/dist/tools/activate-tools.js +72 -0
- package/dist/tools/ast-dump.js +2 -8
- package/dist/tools/lens-diagnostic-mark.js +158 -0
- package/dist/tools/lens-diagnostics.js +327 -33
- package/dist/tools/lsp-diagnostics.js +525 -105
- package/dist/tools/module-report.js +18 -5
- package/docs/agent-tools.md +136 -0
- package/docs/ast-grep_rules_catalog.md +564 -0
- package/docs/astplayground.md +178 -0
- package/docs/audit1.md +177 -0
- package/docs/custom-rules.md +231 -0
- package/docs/dependencies.md +54 -0
- package/docs/dispositions.md +85 -0
- package/docs/env_variables.md +10 -0
- package/docs/environment-variables.md +83 -0
- package/docs/fable.md +140 -0
- package/docs/features.md +338 -0
- package/docs/globalconfig.md +79 -0
- package/docs/language-coverage.md +48 -0
- package/docs/lsp-capability-matrix.md +157 -0
- package/docs/lsp-latency-benchmark.md +169 -0
- package/docs/mcp.md +438 -0
- package/docs/module-report-read-symbol.md +258 -0
- package/docs/servercapabilities.md +118 -0
- package/docs/subagent-compat.md +281 -0
- package/docs/tools.md +16 -0
- package/docs/tools_improvement2.md +345 -0
- package/docs/tree-sitter_rules_catalog.md +296 -0
- package/docs/usage.md +129 -0
- package/docs/word-index.md +150 -0
- package/package.json +10 -1
- package/rules/ast-grep-rules/rule-tests/find-import-file-without-extension-test.yml +4 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +5 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +9 -0
- package/rules/ast-grep-rules/rules/array-callback-return-js.yml +1 -1
- package/rules/ast-grep-rules/rules/find-import-file-without-extension.yml +3 -1
- package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +13 -1
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +12 -0
- package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +1 -1
- package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +1 -1
- package/skills/pi-lens-ast-grep/SKILL.md +3 -1
- package/dist/clients/production-readiness.js +0 -493
|
@@ -196,7 +196,7 @@ export const TOOLS = [
|
|
|
196
196
|
checkCommand: "jscpd",
|
|
197
197
|
checkArgs: ["--version"],
|
|
198
198
|
installStrategy: "npm",
|
|
199
|
-
packageName: "jscpd@
|
|
199
|
+
packageName: "jscpd@5.0.12", // v4's packaging bug (reprism dep missing lib/languages/) is gone in v5's ground-up Rust rewrite — verified: real per-platform native binary (jscpd-windows-x64-msvc etc. via optionalDependencies, no missing-dir regression), --min-lines/--min-tokens/--reporters/--output/--ignore all unchanged, JSON schema fields read by clients/jscpd-client.ts's parseReport() (statistics.total.*, duplicates[].firstFile/secondFile.name+start, .lines, .tokens) are identical, and it's ~50x faster on this repo (4.1s -> 76ms detection time) — closes #582
|
|
200
200
|
binaryName: "jscpd",
|
|
201
201
|
},
|
|
202
202
|
// Structural search and dead code detection
|
|
@@ -675,6 +675,48 @@ export const TOOLS = [
|
|
|
675
675
|
treeMarker: "bin",
|
|
676
676
|
},
|
|
677
677
|
},
|
|
678
|
+
{
|
|
679
|
+
// lua-language-server (#564, split from #241) — same self-contained native
|
|
680
|
+
// TREE BUNDLE shape as clangd: bin/lua-language-server[.exe] + bundled
|
|
681
|
+
// locale/meta files, no external runtime. UNLIKE clangd, the release
|
|
682
|
+
// archive has NO wrapping version dir (verified by inspecting the actual
|
|
683
|
+
// 3.18.2 linux-x64 .tar.gz and win32-x64 .zip contents: `bin/`, `LICENSE`,
|
|
684
|
+
// `locale/`, … sit at archive root) — so stripComponents:0, not 1.
|
|
685
|
+
// LuaServer launches `<bundle>/bin/lua-language-server` directly.
|
|
686
|
+
// checkCommand documents the binary but is unused for resolution (tree
|
|
687
|
+
// bundles resolve only via the extract dir + treeMarker). Platform-matched
|
|
688
|
+
// url: LuaLS publishes darwin/linux x64+arm64 and win32 x64 (no win32/arm64
|
|
689
|
+
// build as of 3.18.2 → undefined, graceful unavailable); asset naming
|
|
690
|
+
// verified against the live GitHub release listing, not guessed.
|
|
691
|
+
id: "lua-language-server",
|
|
692
|
+
name: "lua-language-server",
|
|
693
|
+
checkCommand: "lua-language-server",
|
|
694
|
+
checkArgs: ["--version"],
|
|
695
|
+
installStrategy: "archive",
|
|
696
|
+
binaryName: "lua-language-server",
|
|
697
|
+
archive: {
|
|
698
|
+
url: (platform, arch) => {
|
|
699
|
+
const version = "3.18.2";
|
|
700
|
+
const base = `https://github.com/LuaLS/lua-language-server/releases/download/${version}`;
|
|
701
|
+
if (platform === "linux")
|
|
702
|
+
return arch === "arm64"
|
|
703
|
+
? `${base}/lua-language-server-${version}-linux-arm64.tar.gz`
|
|
704
|
+
: `${base}/lua-language-server-${version}-linux-x64.tar.gz`;
|
|
705
|
+
if (platform === "darwin")
|
|
706
|
+
return arch === "arm64"
|
|
707
|
+
? `${base}/lua-language-server-${version}-darwin-arm64.tar.gz`
|
|
708
|
+
: `${base}/lua-language-server-${version}-darwin-x64.tar.gz`;
|
|
709
|
+
if (platform === "win32")
|
|
710
|
+
return arch === "arm64"
|
|
711
|
+
? undefined
|
|
712
|
+
: `${base}/lua-language-server-${version}-win32-x64.zip`;
|
|
713
|
+
return undefined;
|
|
714
|
+
},
|
|
715
|
+
kind: "zip",
|
|
716
|
+
stripComponents: 0,
|
|
717
|
+
treeMarker: "bin",
|
|
718
|
+
},
|
|
719
|
+
},
|
|
678
720
|
{
|
|
679
721
|
id: "actionlint",
|
|
680
722
|
name: "actionlint",
|
|
@@ -1183,6 +1225,7 @@ export function resetProbeCacheStateForTesting() {
|
|
|
1183
1225
|
_probeCacheDirty = false;
|
|
1184
1226
|
resolvedPathCache.clear();
|
|
1185
1227
|
ensureInFlight.clear();
|
|
1228
|
+
lastManagedInstallVersion.clear();
|
|
1186
1229
|
if (_probeCacheFlushTimer !== null) {
|
|
1187
1230
|
clearTimeout(_probeCacheFlushTimer);
|
|
1188
1231
|
_probeCacheFlushTimer = null;
|
|
@@ -1238,11 +1281,44 @@ async function isCommandAvailable(command, _args) {
|
|
|
1238
1281
|
export function isLspTransportRequiredError(output) {
|
|
1239
1282
|
return /Connection (?:input|output) stream is not set|Use arguments of createConnection/i.test(output);
|
|
1240
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Parse the exact version pinned in a TOOLS `packageName` spec, e.g.
|
|
1286
|
+
* `"jscpd@3.5.10"` -> `"3.5.10"`. Scoped packages (`"@ast-grep/cli"`) have no
|
|
1287
|
+
* pin unless a version is appended after the package name
|
|
1288
|
+
* (`"@scope/pkg@1.2.3"`) — `lastIndexOf("@")` on a bare scope marker (index 0)
|
|
1289
|
+
* correctly reports "no pin" rather than mistaking the scope for a version.
|
|
1290
|
+
* Returns undefined when packageName has no explicit `@version` suffix.
|
|
1291
|
+
*/
|
|
1292
|
+
function parsePinnedVersion(packageName) {
|
|
1293
|
+
const at = packageName.lastIndexOf("@");
|
|
1294
|
+
if (at <= 0)
|
|
1295
|
+
return undefined;
|
|
1296
|
+
return packageName.slice(at + 1) || undefined;
|
|
1297
|
+
}
|
|
1298
|
+
/** Extract the first semver-ish token (e.g. "3.5.10") from `--version` output. */
|
|
1299
|
+
function extractVersionToken(output) {
|
|
1300
|
+
return output.match(/\d+\.\d+\.\d+(?:[-+][\w.]+)?/)?.[0];
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Version reported by the last successful `--version` probe of a pi-lens
|
|
1304
|
+
* managed local npm install, keyed by toolId. Populated only inside
|
|
1305
|
+
* getToolPath()'s managed-local-install checks below — i.e. only when that
|
|
1306
|
+
* code path already spawns verifyToolBinary anyway (cache hits in
|
|
1307
|
+
* ensureTool()'s fast paths never reach getToolPath, so this never adds a new
|
|
1308
|
+
* spawn). Consumed by ensureTool() to detect drift against the tool's current
|
|
1309
|
+
* `packageName` pin (#589) — deliberately scoped to installStrategy "npm"
|
|
1310
|
+
* with an explicit version pin; unpinned tools and non-npm strategies (github/
|
|
1311
|
+
* maven/archive) never populate or read this map.
|
|
1312
|
+
*/
|
|
1313
|
+
const lastManagedInstallVersion = new Map();
|
|
1241
1314
|
/**
|
|
1242
1315
|
* Verify a tool binary actually works by running --version
|
|
1243
|
-
* This catches broken symlinks, partial installs, and corrupted binaries
|
|
1316
|
+
* This catches broken symlinks, partial installs, and corrupted binaries.
|
|
1317
|
+
* `onVersionOutput`, when provided, receives the raw stdout on a successful
|
|
1318
|
+
* (exit 0) probe — used to piggyback version-pin drift detection onto this
|
|
1319
|
+
* already-happening spawn instead of adding a new one (#589).
|
|
1244
1320
|
*/
|
|
1245
|
-
async function verifyToolBinary(binPath) {
|
|
1321
|
+
async function verifyToolBinary(binPath, onVersionOutput) {
|
|
1246
1322
|
return new Promise((resolve) => {
|
|
1247
1323
|
const isWindows = process.platform === "win32";
|
|
1248
1324
|
const hasKnownWindowsExt = /\.(cmd|exe|ps1)$/i.test(binPath);
|
|
@@ -1277,6 +1353,7 @@ async function verifyToolBinary(binPath) {
|
|
|
1277
1353
|
proc.on("exit", (code) => {
|
|
1278
1354
|
if (code === 0) {
|
|
1279
1355
|
debugLog(`Verified: ${binPath} (version: ${stdout.trim()})`);
|
|
1356
|
+
onVersionOutput?.(stdout);
|
|
1280
1357
|
resolve(true);
|
|
1281
1358
|
}
|
|
1282
1359
|
else if (isLspTransportRequiredError(`${stdout}\n${stderr}`)) {
|
|
@@ -1491,6 +1568,23 @@ export async function getToolPath(toolId) {
|
|
|
1491
1568
|
if (tool.installStrategy === "archive" && !tool.archive?.launcher) {
|
|
1492
1569
|
return getArchiveTreeBundlePath(tool);
|
|
1493
1570
|
}
|
|
1571
|
+
// Version-pin drift detection (#589) is scoped to npm-strategy tools with an
|
|
1572
|
+
// explicit `@version` pin — everything else (unpinned npm entries, pip/gem,
|
|
1573
|
+
// github/maven/archive) has no drift signal to piggyback on here. Clear any
|
|
1574
|
+
// stale entry up front so a miss on the checks below never leaves a prior
|
|
1575
|
+
// call's version lingering for ensureTool() to misread.
|
|
1576
|
+
const pinnedVersion = tool.installStrategy === "npm" && tool.packageName
|
|
1577
|
+
? parsePinnedVersion(tool.packageName)
|
|
1578
|
+
: undefined;
|
|
1579
|
+
if (pinnedVersion)
|
|
1580
|
+
lastManagedInstallVersion.delete(toolId);
|
|
1581
|
+
const recordVersion = pinnedVersion
|
|
1582
|
+
? (output) => {
|
|
1583
|
+
const seen = extractVersionToken(output);
|
|
1584
|
+
if (seen)
|
|
1585
|
+
lastManagedInstallVersion.set(toolId, seen);
|
|
1586
|
+
}
|
|
1587
|
+
: undefined;
|
|
1494
1588
|
// Fast path: check local npm install first (where auto-install places tools).
|
|
1495
1589
|
// This avoids the ~2-5s overhead of spawning npm global probes and PATH
|
|
1496
1590
|
// searches for tools we already manage locally.
|
|
@@ -1500,7 +1594,7 @@ export async function getToolPath(toolId) {
|
|
|
1500
1594
|
const cmdPath = `${localBase}.cmd`;
|
|
1501
1595
|
try {
|
|
1502
1596
|
await fs.access(cmdPath);
|
|
1503
|
-
if (await verifyToolBinary(cmdPath)) {
|
|
1597
|
+
if (await verifyToolBinary(cmdPath, recordVersion)) {
|
|
1504
1598
|
return cmdPath;
|
|
1505
1599
|
}
|
|
1506
1600
|
logSessionStart(`auto-install verify: ${cmdPath} exists but is broken, will reinstall`);
|
|
@@ -1513,7 +1607,7 @@ export async function getToolPath(toolId) {
|
|
|
1513
1607
|
const exePath = `${localBase}.exe`;
|
|
1514
1608
|
try {
|
|
1515
1609
|
await fs.access(exePath);
|
|
1516
|
-
if (await verifyToolBinary(exePath)) {
|
|
1610
|
+
if (await verifyToolBinary(exePath, recordVersion)) {
|
|
1517
1611
|
return exePath;
|
|
1518
1612
|
}
|
|
1519
1613
|
logSessionStart(`auto-install verify: ${exePath} exists but is broken, will reinstall`);
|
|
@@ -1524,7 +1618,7 @@ export async function getToolPath(toolId) {
|
|
|
1524
1618
|
}
|
|
1525
1619
|
try {
|
|
1526
1620
|
await fs.access(localBase);
|
|
1527
|
-
if (await verifyToolBinary(localBase)) {
|
|
1621
|
+
if (await verifyToolBinary(localBase, recordVersion)) {
|
|
1528
1622
|
return localBase;
|
|
1529
1623
|
}
|
|
1530
1624
|
logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
|
|
@@ -2653,6 +2747,29 @@ export async function ensureTool(toolId, opts) {
|
|
|
2653
2747
|
// Check if already installed.
|
|
2654
2748
|
const existingPath = await getToolPath(toolId);
|
|
2655
2749
|
if (existingPath) {
|
|
2750
|
+
// Version-pin drift (#589): getToolPath() above just spawned
|
|
2751
|
+
// verifyToolBinary on the managed local install anyway (this is the
|
|
2752
|
+
// slow path — fast paths 1/2 above already returned before reaching
|
|
2753
|
+
// here), so lastManagedInstallVersion was populated for free if this
|
|
2754
|
+
// is a pinned npm tool. Compare it to the current pin and, on
|
|
2755
|
+
// mismatch, route through the EXISTING forceReinstall codepath rather
|
|
2756
|
+
// than resolving to a known-stale binary. Piggybacks entirely on the
|
|
2757
|
+
// probe-cache's ~once-per-24h/once-per-session cadence — no new spawn.
|
|
2758
|
+
const tool = TOOLS.find((t) => t.id === toolId);
|
|
2759
|
+
const pinnedVersion = tool?.installStrategy === "npm" && tool.packageName
|
|
2760
|
+
? parsePinnedVersion(tool.packageName)
|
|
2761
|
+
: undefined;
|
|
2762
|
+
if (pinnedVersion) {
|
|
2763
|
+
const seenVersion = lastManagedInstallVersion.get(toolId);
|
|
2764
|
+
if (seenVersion && seenVersion !== pinnedVersion) {
|
|
2765
|
+
lastManagedInstallVersion.delete(toolId);
|
|
2766
|
+
logSessionStart(`auto-install ensure ${toolId}: version drift (installed ${seenVersion} != pinned ${pinnedVersion}) — forcing reinstall (${Date.now() - ensureStartMs}ms)`);
|
|
2767
|
+
return ensureTool(toolId, {
|
|
2768
|
+
forceReinstall: true,
|
|
2769
|
+
allowInstall: opts?.allowInstall,
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2656
2773
|
resolvedPathCache.set(toolId, existingPath);
|
|
2657
2774
|
void updateProbeCache(toolId, existingPath);
|
|
2658
2775
|
logSessionStart(`auto-install ensure ${toolId}: already available at ${existingPath} (${Date.now() - ensureStartMs}ms)`);
|
|
@@ -199,8 +199,10 @@ export function decideOrphanReaping(registry, isPidAlive, matchProcess, now = Da
|
|
|
199
199
|
}
|
|
200
200
|
// --- Impure liveness / identity / kill helpers ---
|
|
201
201
|
/** `process.kill(pid, 0)` liveness check: ESRCH ⇒ dead, anything else
|
|
202
|
-
* (EPERM, or no error thrown at all) ⇒ conservatively alive.
|
|
203
|
-
|
|
202
|
+
* (EPERM, or no error thrown at all) ⇒ conservatively alive. Exported so
|
|
203
|
+
* other registry consumers (clients/lsp-budget.ts, #449 slice 2) reuse this
|
|
204
|
+
* exact liveness check rather than inventing a second one. */
|
|
205
|
+
export function realIsPidAlive(pid) {
|
|
204
206
|
if (!Number.isFinite(pid) || pid <= 0)
|
|
205
207
|
return false;
|
|
206
208
|
try {
|
|
@@ -407,6 +409,231 @@ async function killPidTree(pid) {
|
|
|
407
409
|
}
|
|
408
410
|
}
|
|
409
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* #658: known pi-lens-managed LSP/scanner binary names (clients/lsp/server.ts
|
|
414
|
+
* spawn candidates), used by the registry-INDEPENDENT backstop sweep below.
|
|
415
|
+
* `opengrep-core` is opengrep's own native subprocess (not spawned directly by
|
|
416
|
+
* pi-lens, but still part of the tree we manage). `yaml-language-server` and
|
|
417
|
+
* `typescript-language-server` are node-launched — their OS image name is
|
|
418
|
+
* `node`/`node.exe`, so matching is done against the full command line
|
|
419
|
+
* (script path), not the process image name, exactly like the existing
|
|
420
|
+
* marker-search's WQL LIKE query below.
|
|
421
|
+
*/
|
|
422
|
+
export const MANAGED_BINARY_NAMES = [
|
|
423
|
+
"ast-grep",
|
|
424
|
+
"opengrep-core",
|
|
425
|
+
"opengrep",
|
|
426
|
+
"marksman",
|
|
427
|
+
"zizmor",
|
|
428
|
+
"typos-lsp",
|
|
429
|
+
"yaml-language-server",
|
|
430
|
+
"typescript-language-server",
|
|
431
|
+
];
|
|
432
|
+
/**
|
|
433
|
+
* PURE decision function for the #658 registry-independent backstop: given a
|
|
434
|
+
* live OS-process snapshot (already filtered to known managed binary names)
|
|
435
|
+
* and the current registry snapshot, decide which processes are backstop-
|
|
436
|
+
* kill-eligible. Zero I/O — unit-testable with fake data, mirroring
|
|
437
|
+
* `decideOrphanReaping`.
|
|
438
|
+
*
|
|
439
|
+
* A process is eligible ONLY when ALL of:
|
|
440
|
+
* - its pid is NOT already tracked in any instance's `lspChildren[]` (tracked
|
|
441
|
+
* pids stay owned by the registry-driven `decideOrphanReaping` path above —
|
|
442
|
+
* this backstop must never race or duplicate that logic);
|
|
443
|
+
* - its reported parent pid is a verifiable, well-formed pid (finite,
|
|
444
|
+
* positive, and not equal to its own pid) — an unresolvable/malformed
|
|
445
|
+
* parent pid is UNVERIFIABLE, never treated as "confirmed dead" (this is a
|
|
446
|
+
* stricter contract than `realIsPidAlive`'s own conservatism, because here
|
|
447
|
+
* an invalid value means "the OS couldn't tell us", not "confirmed gone");
|
|
448
|
+
* - `isPidAlive(parentPid)` reports the parent as dead.
|
|
449
|
+
*
|
|
450
|
+
* Note the direction of the ambiguity guard: if `parentPid` itself was
|
|
451
|
+
* recycled onto an unrelated live process, `isPidAlive` conservatively
|
|
452
|
+
* reports "alive" and the process is (safely) left alone — a false negative,
|
|
453
|
+
* never a false positive kill. Binary name alone is never sufficient (name
|
|
454
|
+
* matching only decides which processes are candidates for this check at
|
|
455
|
+
* all); a live parent is never overridden "however unfamiliar" the process.
|
|
456
|
+
*/
|
|
457
|
+
export function decideBackstopOrphanReaping(processes, registry, isPidAlive) {
|
|
458
|
+
const trackedPids = new Set();
|
|
459
|
+
for (const instance of registry) {
|
|
460
|
+
for (const child of instance.lspChildren)
|
|
461
|
+
trackedPids.add(child.pid);
|
|
462
|
+
}
|
|
463
|
+
const out = [];
|
|
464
|
+
for (const proc of processes) {
|
|
465
|
+
if (trackedPids.has(proc.pid))
|
|
466
|
+
continue; // owned by the registry-driven reaper
|
|
467
|
+
if (!Number.isFinite(proc.parentPid) || proc.parentPid <= 0)
|
|
468
|
+
continue; // unverifiable
|
|
469
|
+
if (proc.parentPid === proc.pid)
|
|
470
|
+
continue; // malformed data guard
|
|
471
|
+
if (isPidAlive(proc.parentPid))
|
|
472
|
+
continue; // live parent — never kill
|
|
473
|
+
out.push(proc);
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
/** Enumerate live OS processes whose command line contains one of
|
|
478
|
+
* `MANAGED_BINARY_NAMES` (#658), independent of the instance registry.
|
|
479
|
+
* Windows: one batched CIM/WQL query (mirrors `findPidsByMarkerWindows`'s
|
|
480
|
+
* query pattern). POSIX: `ps -eo pid=,ppid=,args=`, filtered in JS. Returns
|
|
481
|
+
* `{pid, parentPid, command}` rows. Best-effort: any failure ⇒ []. */
|
|
482
|
+
async function enumerateManagedProcesses() {
|
|
483
|
+
if (isWindows) {
|
|
484
|
+
const clauses = MANAGED_BINARY_NAMES.map((name) => `CommandLine LIKE '%${escapeWqlLikeValue(name)}%'`).join(" OR ");
|
|
485
|
+
const psScript = `Get-CimInstance Win32_Process -Filter "${clauses}" ` +
|
|
486
|
+
`| ForEach-Object { "$($_.ProcessId)\t$($_.ParentProcessId)\t$($_.CommandLine)" }`;
|
|
487
|
+
return new Promise((resolve) => {
|
|
488
|
+
try {
|
|
489
|
+
const powershell = windowsExe("WindowsPowerShell\\v1.0\\powershell.exe");
|
|
490
|
+
const child = nodeSpawn(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
491
|
+
let out = "";
|
|
492
|
+
child.stdout?.on("data", (chunk) => {
|
|
493
|
+
out += chunk.toString();
|
|
494
|
+
});
|
|
495
|
+
child.once("error", () => resolve([]));
|
|
496
|
+
child.once("close", () => {
|
|
497
|
+
const results = [];
|
|
498
|
+
for (const line of out.split(/\r?\n/)) {
|
|
499
|
+
const firstTab = line.indexOf("\t");
|
|
500
|
+
if (firstTab <= 0)
|
|
501
|
+
continue;
|
|
502
|
+
const secondTab = line.indexOf("\t", firstTab + 1);
|
|
503
|
+
if (secondTab <= 0)
|
|
504
|
+
continue;
|
|
505
|
+
const pid = Number(line.slice(0, firstTab).trim());
|
|
506
|
+
const parentPid = Number(line.slice(firstTab + 1, secondTab).trim());
|
|
507
|
+
const command = line.slice(secondTab + 1);
|
|
508
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
509
|
+
results.push({ pid, parentPid, command });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
resolve(results);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
resolve([]);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
// POSIX: enumerate everything, filter in JS by managed-name substring —
|
|
521
|
+
// there is no single-query WQL-style server-side filter available.
|
|
522
|
+
return new Promise((resolve) => {
|
|
523
|
+
try {
|
|
524
|
+
const child = nodeSpawn(posixPsPath(), ["-eo", "pid=,ppid=,args="], {
|
|
525
|
+
shell: false,
|
|
526
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
527
|
+
});
|
|
528
|
+
let out = "";
|
|
529
|
+
child.stdout?.on("data", (chunk) => {
|
|
530
|
+
out += chunk.toString();
|
|
531
|
+
});
|
|
532
|
+
child.once("error", () => resolve([]));
|
|
533
|
+
child.once("close", () => {
|
|
534
|
+
const results = [];
|
|
535
|
+
for (const line of out.split(/\r?\n/)) {
|
|
536
|
+
// Linear parse (S8786/S6594): " pid ppid args here".
|
|
537
|
+
const trimmed = line.trimStart();
|
|
538
|
+
if (!trimmed)
|
|
539
|
+
continue;
|
|
540
|
+
let i = 0;
|
|
541
|
+
while (i < trimmed.length && trimmed[i] >= "0" && trimmed[i] <= "9")
|
|
542
|
+
i++;
|
|
543
|
+
if (i === 0)
|
|
544
|
+
continue;
|
|
545
|
+
const pid = Number(trimmed.slice(0, i));
|
|
546
|
+
let rest = trimmed.slice(i);
|
|
547
|
+
let k = 0;
|
|
548
|
+
while (k < rest.length && (rest[k] === " " || rest[k] === "\t"))
|
|
549
|
+
k++;
|
|
550
|
+
rest = rest.slice(k);
|
|
551
|
+
let j = 0;
|
|
552
|
+
while (j < rest.length && rest[j] >= "0" && rest[j] <= "9")
|
|
553
|
+
j++;
|
|
554
|
+
if (j === 0)
|
|
555
|
+
continue;
|
|
556
|
+
const parentPid = Number(rest.slice(0, j));
|
|
557
|
+
let m = j;
|
|
558
|
+
while (m < rest.length && (rest[m] === " " || rest[m] === "\t"))
|
|
559
|
+
m++;
|
|
560
|
+
const args = rest.slice(m);
|
|
561
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
562
|
+
continue;
|
|
563
|
+
const lowerArgs = args.toLowerCase();
|
|
564
|
+
if (MANAGED_BINARY_NAMES.some((name) => lowerArgs.includes(name.toLowerCase()))) {
|
|
565
|
+
results.push({ pid, parentPid, command: args });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
resolve(results);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
resolve([]);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Fire-and-forget REGISTRY-INDEPENDENT backstop sweep (#658): finds
|
|
578
|
+
* pi-lens-managed LSP/scanner processes the registry-driven `sweepOrphans`
|
|
579
|
+
* can never see again once their registry trace is lost (a stale-heartbeat
|
|
580
|
+
* entry removal, or a `killPidTree` call that failed silently) — enumerates
|
|
581
|
+
* live OS processes by known binary name, and kills any that are both
|
|
582
|
+
* untracked and have a confirmed-dead parent. A strictly ADDITIVE second
|
|
583
|
+
* layer: `sweepOrphans`'s registry-driven path is untouched and remains the
|
|
584
|
+
* cheap, correct common case.
|
|
585
|
+
*
|
|
586
|
+
* Kill-attempt retry: deliberately NO separate retry-tracking state. If
|
|
587
|
+
* `killPidTree` fails silently, the process stays alive, untracked, and its
|
|
588
|
+
* parent stays dead — so it stays classified as backstop-kill-eligible and
|
|
589
|
+
* this exact sweep simply tries again at the next `session_start`, with zero
|
|
590
|
+
* extra bookkeeping. This only breaks if the orphan's *parent* pid gets
|
|
591
|
+
* reused by a live process in the interim (closes the window), which is the
|
|
592
|
+
* same residual risk `decideOrphanReaping`'s registry path already accepts.
|
|
593
|
+
* A future hardening (making `killPidTree` return a success signal so a
|
|
594
|
+
* failed kill can be logged distinctly from "already gone") is a reasonable
|
|
595
|
+
* follow-up, not required for correctness here.
|
|
596
|
+
*
|
|
597
|
+
* Never throws — every step is wrapped so a reap failure cannot block or
|
|
598
|
+
* crash the caller (session_start).
|
|
599
|
+
*/
|
|
600
|
+
export async function sweepUntrackedOrphans() {
|
|
601
|
+
if (!isInstanceRegistryEnabled())
|
|
602
|
+
return;
|
|
603
|
+
const startedAt = Date.now();
|
|
604
|
+
try {
|
|
605
|
+
const [registry, processes] = await Promise.all([
|
|
606
|
+
readInstanceRegistry(),
|
|
607
|
+
enumerateManagedProcesses(),
|
|
608
|
+
]);
|
|
609
|
+
if (processes.length === 0)
|
|
610
|
+
return;
|
|
611
|
+
const candidates = decideBackstopOrphanReaping(processes, registry, realIsPidAlive);
|
|
612
|
+
let killedCount = 0;
|
|
613
|
+
for (const proc of candidates) {
|
|
614
|
+
await killPidTree(proc.pid);
|
|
615
|
+
killedCount++;
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
logLatency({
|
|
619
|
+
type: "phase",
|
|
620
|
+
phase: "orphan_backstop_reaped",
|
|
621
|
+
filePath: "",
|
|
622
|
+
durationMs: Date.now() - startedAt,
|
|
623
|
+
metadata: {
|
|
624
|
+
scanned: processes.length,
|
|
625
|
+
killed: killedCount,
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// best-effort logging only
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
// The sweep must never throw out of session_start.
|
|
635
|
+
}
|
|
636
|
+
}
|
|
410
637
|
/**
|
|
411
638
|
* Fire-and-forget orphan sweep: reads the registry, decides what's dead via
|
|
412
639
|
* `decideOrphanReaping`, kills orphaned LSP children (by pid, with a
|
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
import * as fs from "node:fs";
|
|
24
24
|
import * as path from "node:path";
|
|
25
25
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
26
|
+
// #735: reuse the #449/#525 reaper's exact conservative liveness check
|
|
27
|
+
// (`process.kill(pid, 0)`, ESRCH-only-means-dead) rather than inventing a
|
|
28
|
+
// second one — see realIsPidAlive's own docstring, which already calls out
|
|
29
|
+
// clients/lsp-budget.ts as a precedent consumer. This creates a live-binding
|
|
30
|
+
// import cycle with instance-reaper.ts (which imports readInstanceRegistry/
|
|
31
|
+
// isInstanceRegistryEnabled from here); safe under Node ESM because every
|
|
32
|
+
// use on both sides happens inside function bodies, never at module-
|
|
33
|
+
// evaluation time, so both modules are fully initialized before either
|
|
34
|
+
// import is actually invoked.
|
|
35
|
+
import { realIsPidAlive } from "./instance-reaper.js";
|
|
26
36
|
import { normalizeFilePath } from "./path-utils.js";
|
|
27
37
|
function registryPath() {
|
|
28
38
|
return path.join(getGlobalPiLensDir(), "instances.json");
|
|
@@ -138,7 +148,8 @@ export async function registerInstance(projectRoot) {
|
|
|
138
148
|
});
|
|
139
149
|
await writeRegistryAsync({ instances: others });
|
|
140
150
|
}
|
|
141
|
-
/** Update this process's heartbeat/rss
|
|
151
|
+
/** Update this process's heartbeat/rss (and, since #620, host CPU% + live
|
|
152
|
+
* LSP children's rss/CPU%). Cheap — safe to call every turn end. */
|
|
142
153
|
export async function updateHeartbeat(patch = {}) {
|
|
143
154
|
if (!isInstanceRegistryEnabled())
|
|
144
155
|
return;
|
|
@@ -153,10 +164,24 @@ export async function updateHeartbeat(patch = {}) {
|
|
|
153
164
|
}
|
|
154
165
|
const now = new Date().toISOString();
|
|
155
166
|
const current = file.instances[idx];
|
|
167
|
+
const lspChildren = patch.childUsage
|
|
168
|
+
? current.lspChildren.map((child) => {
|
|
169
|
+
const usage = patch.childUsage?.[child.pid];
|
|
170
|
+
if (!usage)
|
|
171
|
+
return child;
|
|
172
|
+
return {
|
|
173
|
+
...child,
|
|
174
|
+
rssBytes: usage.rssBytes ?? child.rssBytes,
|
|
175
|
+
cpuPercent: usage.cpuPercent ?? child.cpuPercent,
|
|
176
|
+
};
|
|
177
|
+
})
|
|
178
|
+
: current.lspChildren;
|
|
156
179
|
file.instances[idx] = {
|
|
157
180
|
...current,
|
|
158
181
|
rssBytes: patch.rssBytes ?? process.memoryUsage().rss,
|
|
159
|
-
|
|
182
|
+
cpuPercent: patch.cpuPercent ?? current.cpuPercent,
|
|
183
|
+
lspChildren,
|
|
184
|
+
lspChildCount: lspChildren.length,
|
|
160
185
|
heartbeatAt: now,
|
|
161
186
|
};
|
|
162
187
|
await writeRegistryAsync(file);
|
|
@@ -236,3 +261,117 @@ export function deregisterInstance() {
|
|
|
236
261
|
return; // nothing to remove
|
|
237
262
|
writeRegistrySync({ instances: remaining });
|
|
238
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* PURE aggregation over a registry snapshot: "how much CPU/RAM is pi-lens
|
|
266
|
+
* attributable to, right now, across every process it owns" (#620) — the
|
|
267
|
+
* host of every registered instance plus every one of its live LSP children.
|
|
268
|
+
* Missing/unsampled `rssBytes`/`cpuPercent` (best-effort sampling can fail)
|
|
269
|
+
* are treated as 0 for summation purposes — never as a full instance to
|
|
270
|
+
* exclude, since a partially-sampled instance's other numbers are still real
|
|
271
|
+
* data worth surfacing.
|
|
272
|
+
*
|
|
273
|
+
* Does NOT include transient analyzer children (jscpd/knip/etc.) — those are
|
|
274
|
+
* short-lived and sampled separately per-invocation via
|
|
275
|
+
* clients/resource-sampler.ts into clients/latency-logger.ts, not carried in
|
|
276
|
+
* the registry (see the module docstring's scope note).
|
|
277
|
+
*
|
|
278
|
+
* #735: `isPidAlive`, when supplied, drops any instance whose owning pid is
|
|
279
|
+
* confirmed dead BEFORE aggregation — a hard-killed pi process otherwise
|
|
280
|
+
* leaves a registry entry with heartbeat-cached RSS that reads as a live,
|
|
281
|
+
* resource-consuming instance until it eventually ages out (up to
|
|
282
|
+
* `STALE_HEARTBEAT_MS`, see clients/instance-reaper.ts). Dropped rather than
|
|
283
|
+
* flagged `stale: true`: a dead pid is unambiguous (unlike heartbeat
|
|
284
|
+
* staleness, which the reaper deliberately treats as "maybe idle-but-alive"
|
|
285
|
+
* and never uses to justify removing/hiding anything) — the wire shape and
|
|
286
|
+
* every existing caller (chiefly `pilens_health`'s headline instance
|
|
287
|
+
* count/RSS/CPU numbers) simply expects a footprint of currently-live
|
|
288
|
+
* instances. Left `undefined` for pure/synchronous callers (incl. every
|
|
289
|
+
* pre-#735 unit test) that pass a plain snapshot with no intent to check OS
|
|
290
|
+
* process state — no filtering happens, preserving prior behavior exactly.
|
|
291
|
+
* No pid-reuse identity check is applied here (unlike the reaper's child-pid
|
|
292
|
+
* `matchProcess`): `InstanceEntry` never recorded the host's own command
|
|
293
|
+
* line to verify against (same gap #525 called out for the reaper's PARENT
|
|
294
|
+
* pid, deliberately left unfixed there too), and a health-report false
|
|
295
|
+
* positive is a much smaller blast radius than the reaper's mistaken kill —
|
|
296
|
+
* so plain liveness is judged sufficient here.
|
|
297
|
+
*/
|
|
298
|
+
export function computeResourceFootprint(instances, isPidAlive) {
|
|
299
|
+
const liveInstances = isPidAlive
|
|
300
|
+
? instances.filter((instance) => isPidAlive(instance.pid))
|
|
301
|
+
: instances;
|
|
302
|
+
const perInstance = liveInstances.map((instance) => {
|
|
303
|
+
const lspChildRssBytes = instance.lspChildren.reduce((sum, child) => sum + (child.rssBytes ?? 0), 0);
|
|
304
|
+
const lspChildCpuPercent = instance.lspChildren.reduce((sum, child) => sum + (child.cpuPercent ?? 0), 0);
|
|
305
|
+
return {
|
|
306
|
+
pid: instance.pid,
|
|
307
|
+
projectRoot: instance.projectRoot,
|
|
308
|
+
rssBytes: instance.rssBytes ?? 0,
|
|
309
|
+
cpuPercent: instance.cpuPercent ?? 0,
|
|
310
|
+
lspChildCount: instance.lspChildren.length,
|
|
311
|
+
lspChildRssBytes,
|
|
312
|
+
lspChildCpuPercent,
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
let totalRssBytes = 0;
|
|
316
|
+
let totalCpuPercent = 0;
|
|
317
|
+
let totalLspChildCount = 0;
|
|
318
|
+
for (const inst of perInstance) {
|
|
319
|
+
totalRssBytes += inst.rssBytes + inst.lspChildRssBytes;
|
|
320
|
+
totalCpuPercent += inst.cpuPercent + inst.lspChildCpuPercent;
|
|
321
|
+
totalLspChildCount += inst.lspChildCount;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
instanceCount: perInstance.length,
|
|
325
|
+
totalRssBytes,
|
|
326
|
+
totalCpuPercent,
|
|
327
|
+
totalLspChildCount,
|
|
328
|
+
perInstance,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Read the live registry and compute the aggregate footprint — the query
|
|
333
|
+
* side of "how much CPU/RAM is pi-lens using right now" (#620). Best-effort
|
|
334
|
+
* (readInstanceRegistry never throws); the answer only reflects whatever
|
|
335
|
+
* heartbeats have landed so far.
|
|
336
|
+
*
|
|
337
|
+
* #735: defaults to the reaper's `realIsPidAlive` so dead-pid registry
|
|
338
|
+
* entries (a hard-killed pi process) are excluded from both the returned
|
|
339
|
+
* footprint AND — opportunistically, fire-and-forget, best-effort — pruned
|
|
340
|
+
* from the on-disk registry, the same "entry removal only, never blocks the
|
|
341
|
+
* caller" convention `sweepOrphans`/`pruneDeadInstances` already use.
|
|
342
|
+
* Pruning here is a bonus cleanup, not a substitute for the reaper sweep:
|
|
343
|
+
* this path only prunes pids this particular read happened to find dead,
|
|
344
|
+
* while the reaper sweep is the authoritative, scheduled cleanup. Injectable
|
|
345
|
+
* so tests can pass a fake predicate (or omit filtering entirely by passing
|
|
346
|
+
* a function that always returns true) without touching real OS process
|
|
347
|
+
* state.
|
|
348
|
+
*/
|
|
349
|
+
export async function getResourceFootprint(isPidAlive = realIsPidAlive) {
|
|
350
|
+
const instances = await readInstanceRegistry();
|
|
351
|
+
const deadPids = new Set(instances.filter((instance) => !isPidAlive(instance.pid)).map((instance) => instance.pid));
|
|
352
|
+
if (deadPids.size > 0) {
|
|
353
|
+
// Fire-and-forget: a health-report read must never block on, or fail
|
|
354
|
+
// because of, a registry write.
|
|
355
|
+
prunePids(deadPids).catch(() => {
|
|
356
|
+
// best-effort — a dead-pid entry that fails to prune here is simply
|
|
357
|
+
// re-evaluated (and re-dropped from the report) on the next read, and
|
|
358
|
+
// remains catchable by the scheduled reaper sweep regardless.
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return computeResourceFootprint(instances, isPidAlive);
|
|
362
|
+
}
|
|
363
|
+
/** Best-effort removal of specific dead pids' entries from the on-disk
|
|
364
|
+
* registry (#735). Re-reads immediately before writing (rather than reusing
|
|
365
|
+
* an earlier snapshot) to narrow — not eliminate — the last-writer-wins race
|
|
366
|
+
* already accepted for this module's read-modify-write model (see the
|
|
367
|
+
* module docstring). Mirrors clients/instance-reaper.ts's
|
|
368
|
+
* `pruneDeadInstances`, kept local here rather than imported to avoid
|
|
369
|
+
* reaching back across the same import edge `realIsPidAlive` already
|
|
370
|
+
* crosses in the other direction. */
|
|
371
|
+
async function prunePids(deadPids) {
|
|
372
|
+
const file = await readRegistryAsync();
|
|
373
|
+
const remaining = file.instances.filter((entry) => !deadPids.has(entry.pid));
|
|
374
|
+
if (remaining.length === file.instances.length)
|
|
375
|
+
return;
|
|
376
|
+
await writeRegistryAsync({ instances: remaining });
|
|
377
|
+
}
|