@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.0
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/AGENTS.md +67 -116
- package/README.md +93 -7
- package/docs/architecture.md +408 -9
- package/package.json +6 -4
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/bridge.ts +69 -2
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +16 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +13 -7
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +8 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +211 -10
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +56 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +71 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +63 -46
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DirectoryService — server-side directory-scoped operations.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Session discovery and event loading (unchanged).
|
|
6
|
+
* - OpenSpec polling with an mtime-gated cache, configurable interval,
|
|
7
|
+
* concurrency cap (semaphore), and deterministic per-cwd jitter to
|
|
8
|
+
* flatten the CPU envelope.
|
|
9
|
+
* - Pi resources scanning on its own slower cadence (5× openspec interval)
|
|
10
|
+
* so it does not stack onto the openspec burst.
|
|
11
|
+
*
|
|
12
|
+
* See change: optimize-openspec-poll-burst for the cost model.
|
|
5
13
|
*/
|
|
6
|
-
import
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
buildOpenSpecData,
|
|
18
|
+
pollOpenSpecAsync,
|
|
19
|
+
runOpenSpecList,
|
|
20
|
+
runOpenSpecStatus,
|
|
21
|
+
} from "@blackbelt-technology/pi-dashboard-shared/openspec-poller.js";
|
|
22
|
+
import { DEFAULT_OPENSPEC_POLL, type OpenSpecPollConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
23
|
+
import { createSemaphore, type Semaphore } from "@blackbelt-technology/pi-dashboard-shared/semaphore.js";
|
|
7
24
|
import { discoverSessionsForCwd } from "./session-discovery.js";
|
|
8
25
|
import { replayEntriesAsEvents } from "@blackbelt-technology/pi-dashboard-shared/state-replay.js";
|
|
9
26
|
import { scanPiResources } from "./pi-resource-scanner.js";
|
|
10
|
-
import type { OpenSpecData } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
27
|
+
import type { OpenSpecData, OpenSpecChange } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
11
28
|
import type { PiResourcesResult } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
12
29
|
import type { PreferencesStore } from "./preferences-store.js";
|
|
13
30
|
import type { SessionManager } from "./memory-session-manager.js";
|
|
14
31
|
|
|
15
|
-
const POLL_INTERVAL = 30_000;
|
|
16
|
-
|
|
17
32
|
import type { DiscoveredSession } from "./session-discovery.js";
|
|
18
33
|
export type { DiscoveredSession } from "./session-discovery.js";
|
|
19
34
|
|
|
@@ -33,34 +48,88 @@ export interface DirectoryService {
|
|
|
33
48
|
discoverSessions(cwd: string): DiscoveredSession[];
|
|
34
49
|
loadSessionEvents(sessionId: string, sessionFile: string): Promise<LoadResult>;
|
|
35
50
|
getOpenSpecData(cwd: string): OpenSpecData | undefined;
|
|
51
|
+
/** Force refresh: bypasses the mtime gate. Still honors the semaphore. */
|
|
36
52
|
refreshOpenSpec(cwd: string): Promise<OpenSpecData>;
|
|
53
|
+
/** Gated poll: respects `changeDetection` config and the semaphore. Returns cached data. */
|
|
54
|
+
pollDirectoryGated(cwd: string): Promise<OpenSpecData>;
|
|
37
55
|
getPiResources(cwd: string): PiResourcesResult | undefined;
|
|
38
56
|
refreshPiResources(cwd: string): Promise<PiResourcesResult>;
|
|
39
57
|
startPolling(onChange: (cwd: string, data: OpenSpecData) => void): void;
|
|
40
58
|
stopPolling(): void;
|
|
59
|
+
/** Apply a new OpenSpecPollConfig without losing cache. Safe to call mid-stream. */
|
|
60
|
+
reconfigurePolling(config: OpenSpecPollConfig): void;
|
|
41
61
|
onDirectoryAdded(cwd: string): Promise<DirectoryAddedResult>;
|
|
42
62
|
}
|
|
43
63
|
|
|
64
|
+
// ── Jitter ─────────────────────────────────────────────────────────
|
|
65
|
+
// 32-bit FNV-1a hash — cheap, stable, well-distributed for short strings.
|
|
66
|
+
export function fnv1a32(s: string): number {
|
|
67
|
+
let h = 0x811c9dc5;
|
|
68
|
+
for (let i = 0; i < s.length; i++) {
|
|
69
|
+
h ^= s.charCodeAt(i);
|
|
70
|
+
h = Math.imul(h, 0x01000193);
|
|
71
|
+
}
|
|
72
|
+
return h >>> 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function phaseOffsetMs(cwd: string, jitterSeconds: number): number {
|
|
76
|
+
if (!Number.isFinite(jitterSeconds) || jitterSeconds <= 0) return 0;
|
|
77
|
+
return fnv1a32(cwd) % (jitterSeconds * 1000);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── mtime helpers ──────────────────────────────────────────────────
|
|
81
|
+
function statMtimeOr(p: string): number | undefined {
|
|
82
|
+
try {
|
|
83
|
+
return fs.statSync(p).mtimeMs;
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Per-directory cache ────────────────────────────────────────────
|
|
90
|
+
type PerChangeEntry = {
|
|
91
|
+
mtimeMs: number | undefined;
|
|
92
|
+
change: OpenSpecChange;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type DirCache = {
|
|
96
|
+
/** mtime of `<cwd>/openspec/changes/` when we last ran `openspec list`. */
|
|
97
|
+
listMtimeMs: number | undefined;
|
|
98
|
+
/** Cached list-result entries (raw shape from openspec list). */
|
|
99
|
+
listResult: Array<{ name: string; status: string; completedTasks: number; totalTasks: number }> | undefined;
|
|
100
|
+
changes: Map<string, PerChangeEntry>;
|
|
101
|
+
/** Last built OpenSpecData (what we broadcast). */
|
|
102
|
+
data: OpenSpecData | undefined;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function emptyDirCache(): DirCache {
|
|
106
|
+
return { listMtimeMs: undefined, listResult: undefined, changes: new Map(), data: undefined };
|
|
107
|
+
}
|
|
108
|
+
|
|
44
109
|
export function createDirectoryService(
|
|
45
110
|
preferencesStore: PreferencesStore,
|
|
46
111
|
sessionManager: SessionManager,
|
|
112
|
+
initialConfig?: Partial<OpenSpecPollConfig>,
|
|
47
113
|
): DirectoryService {
|
|
48
|
-
|
|
114
|
+
let cfg: OpenSpecPollConfig = { ...DEFAULT_OPENSPEC_POLL, ...(initialConfig ?? {}) };
|
|
115
|
+
|
|
116
|
+
const caches = new Map<string, DirCache>();
|
|
49
117
|
const piResourcesCache = new Map<string, PiResourcesResult>();
|
|
118
|
+
|
|
119
|
+
let semaphore: Semaphore = createSemaphore(cfg.maxConcurrentSpawns);
|
|
120
|
+
|
|
50
121
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
122
|
+
let piResourcesTimer: ReturnType<typeof setInterval> | null = null;
|
|
51
123
|
let onChangeCallback: ((cwd: string, data: OpenSpecData) => void) | null = null;
|
|
124
|
+
const scheduledPhaseTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
52
125
|
|
|
53
126
|
// In-progress session loads for dedup
|
|
54
127
|
const loadingSet = new Set<string>();
|
|
55
128
|
|
|
56
129
|
function computeKnownDirectories(): string[] {
|
|
57
130
|
const dirs = new Set<string>();
|
|
58
|
-
for (const dir of preferencesStore.getPinnedDirectories())
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
for (const session of sessionManager.listAll()) {
|
|
62
|
-
dirs.add(session.cwd);
|
|
63
|
-
}
|
|
131
|
+
for (const dir of preferencesStore.getPinnedDirectories()) dirs.add(dir);
|
|
132
|
+
for (const session of sessionManager.listAll()) dirs.add(session.cwd);
|
|
64
133
|
return Array.from(dirs);
|
|
65
134
|
}
|
|
66
135
|
|
|
@@ -69,7 +138,6 @@ export function createDirectoryService(
|
|
|
69
138
|
}
|
|
70
139
|
|
|
71
140
|
async function loadSessionEvents(sessionId: string, sessionFile: string): Promise<LoadResult> {
|
|
72
|
-
// Dedup: wait if already loading
|
|
73
141
|
if (loadingSet.has(sessionId)) {
|
|
74
142
|
return { success: false, events: [], error: "already_loading" };
|
|
75
143
|
}
|
|
@@ -88,34 +156,190 @@ export function createDirectoryService(
|
|
|
88
156
|
}
|
|
89
157
|
}
|
|
90
158
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
159
|
+
// ── Core gated poll ──────────────────────────────────────────────
|
|
160
|
+
// Contract:
|
|
161
|
+
// - `force=true` bypasses both the list-mtime and per-change-mtime gates.
|
|
162
|
+
// - Every CLI spawn goes through the shared semaphore.
|
|
163
|
+
// - Cache is updated atomically per directory: on any failure the
|
|
164
|
+
// old cache stays intact.
|
|
165
|
+
async function pollOne(cwd: string, force: boolean): Promise<OpenSpecData> {
|
|
166
|
+
const cache = caches.get(cwd) ?? emptyDirCache();
|
|
167
|
+
const gateEnabled = cfg.changeDetection === "mtime" && !force;
|
|
168
|
+
|
|
169
|
+
const changesRoot = path.join(cwd, "openspec", "changes");
|
|
170
|
+
const rootMtime = statMtimeOr(changesRoot);
|
|
171
|
+
|
|
172
|
+
// If the directory doesn't exist, short-circuit (matches old behavior).
|
|
173
|
+
if (rootMtime === undefined) {
|
|
174
|
+
const empty: OpenSpecData = { initialized: false, changes: [] };
|
|
175
|
+
cache.data = empty;
|
|
176
|
+
cache.listMtimeMs = undefined;
|
|
177
|
+
cache.listResult = undefined;
|
|
178
|
+
cache.changes.clear();
|
|
179
|
+
caches.set(cwd, cache);
|
|
180
|
+
return empty;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Step 1: list (gated) ──
|
|
184
|
+
let listResult: typeof cache.listResult = cache.listResult;
|
|
185
|
+
const listCacheValid = gateEnabled && cache.listMtimeMs === rootMtime && cache.listResult !== undefined;
|
|
186
|
+
if (!listCacheValid) {
|
|
187
|
+
const raw = await semaphore.run(() => runOpenSpecList(cwd));
|
|
188
|
+
if (!raw || !Array.isArray(raw.changes)) {
|
|
189
|
+
const empty: OpenSpecData = { initialized: false, changes: [] };
|
|
190
|
+
cache.data = empty;
|
|
191
|
+
cache.listMtimeMs = rootMtime;
|
|
192
|
+
cache.listResult = undefined;
|
|
193
|
+
cache.changes.clear();
|
|
194
|
+
caches.set(cwd, cache);
|
|
195
|
+
return empty;
|
|
196
|
+
}
|
|
197
|
+
listResult = raw.changes;
|
|
198
|
+
cache.listMtimeMs = rootMtime;
|
|
199
|
+
cache.listResult = listResult;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Prune cache for changes no longer present.
|
|
203
|
+
const liveNames = new Set((listResult ?? []).map((c) => c.name));
|
|
204
|
+
for (const key of Array.from(cache.changes.keys())) {
|
|
205
|
+
if (!liveNames.has(key)) cache.changes.delete(key);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Step 2: per-change status (gated) ──
|
|
209
|
+
const statusResults = new Map<string, { artifacts?: Array<{ id: string; status: string }>; isComplete?: boolean } | null>();
|
|
210
|
+
|
|
211
|
+
await Promise.all((listResult ?? []).map(async (c) => {
|
|
212
|
+
const changeDir = path.join(changesRoot, c.name);
|
|
213
|
+
const changeMtime = statMtimeOr(changeDir);
|
|
214
|
+
const cached = cache.changes.get(c.name);
|
|
215
|
+
|
|
216
|
+
if (gateEnabled && cached && cached.mtimeMs !== undefined && cached.mtimeMs === changeMtime) {
|
|
217
|
+
// Cache hit. Reuse the artifacts/isComplete from the cached OpenSpecChange.
|
|
218
|
+
statusResults.set(c.name, {
|
|
219
|
+
artifacts: cached.change.artifacts.map((a) => ({ id: a.id, status: a.status })),
|
|
220
|
+
...(cached.change.isComplete !== undefined ? { isComplete: cached.change.isComplete } : {}),
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const status = await semaphore.run(() => runOpenSpecStatus(cwd, c.name));
|
|
226
|
+
statusResults.set(c.name, status);
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
// ── Step 3: build + cache + return ──
|
|
230
|
+
const data = buildOpenSpecData({ changes: listResult ?? [] }, statusResults);
|
|
231
|
+
|
|
232
|
+
// Update per-change cache with the mtimes we just observed.
|
|
233
|
+
for (const change of data.changes) {
|
|
234
|
+
const changeDir = path.join(changesRoot, change.name);
|
|
235
|
+
const changeMtime = statMtimeOr(changeDir);
|
|
236
|
+
cache.changes.set(change.name, { mtimeMs: changeMtime, change });
|
|
237
|
+
}
|
|
238
|
+
cache.data = data;
|
|
239
|
+
caches.set(cwd, cache);
|
|
94
240
|
return data;
|
|
95
241
|
}
|
|
96
242
|
|
|
243
|
+
async function refreshOpenSpec(cwd: string): Promise<OpenSpecData> {
|
|
244
|
+
try {
|
|
245
|
+
return await pollOne(cwd, true);
|
|
246
|
+
} catch {
|
|
247
|
+
// Fall back to the legacy monolithic path so "refresh" never silently fails.
|
|
248
|
+
const data = await pollOpenSpecAsync(cwd);
|
|
249
|
+
const cache = caches.get(cwd) ?? emptyDirCache();
|
|
250
|
+
cache.data = data;
|
|
251
|
+
caches.set(cwd, cache);
|
|
252
|
+
return data;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function pollDirectoryGated(cwd: string): Promise<OpenSpecData> {
|
|
257
|
+
return pollOne(cwd, false);
|
|
258
|
+
}
|
|
259
|
+
|
|
97
260
|
async function refreshPiResourcesInternal(cwd: string): Promise<PiResourcesResult> {
|
|
98
261
|
const data = await scanPiResources(cwd);
|
|
99
262
|
piResourcesCache.set(cwd, data);
|
|
100
263
|
return data;
|
|
101
264
|
}
|
|
102
265
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
266
|
+
// ── Scheduler ────────────────────────────────────────────────────
|
|
267
|
+
const TICK_SLOW_WARN_MS = 5000;
|
|
268
|
+
const DEBUG_ENABLED =
|
|
269
|
+
typeof process !== "undefined" && typeof process.env?.DEBUG === "string" && /pi-dashboard|openspec-poll/.test(process.env.DEBUG);
|
|
270
|
+
|
|
271
|
+
let openspecTickInFlight = false;
|
|
272
|
+
async function scheduleOpenSpecTick() {
|
|
273
|
+
if (openspecTickInFlight) return;
|
|
274
|
+
openspecTickInFlight = true;
|
|
275
|
+
const tickStart = Date.now();
|
|
276
|
+
let spawnsBefore = 0;
|
|
277
|
+
let spawnsAfter = 0;
|
|
278
|
+
try {
|
|
279
|
+
const dirs = computeKnownDirectories();
|
|
280
|
+
// Track spawn count by hooking the semaphore's size(). Approximation.
|
|
281
|
+
spawnsBefore = semaphore.size();
|
|
282
|
+
await Promise.all(dirs.map((cwd) => new Promise<void>((resolve) => {
|
|
283
|
+
const delay = phaseOffsetMs(cwd, cfg.jitterSeconds);
|
|
284
|
+
const timer = setTimeout(async () => {
|
|
285
|
+
scheduledPhaseTimers.delete(timer);
|
|
286
|
+
try {
|
|
287
|
+
const prev = caches.get(cwd)?.data;
|
|
288
|
+
const prevJson = prev ? JSON.stringify(prev) : undefined;
|
|
289
|
+
const next = await pollDirectoryGated(cwd);
|
|
290
|
+
const nextJson = JSON.stringify(next);
|
|
291
|
+
if (nextJson !== prevJson) onChangeCallback?.(cwd, next);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
// Swallow — the next tick will retry.
|
|
294
|
+
console.error(`[openspec-poll] tick failed for ${cwd}:`, err);
|
|
295
|
+
} finally {
|
|
296
|
+
resolve();
|
|
297
|
+
}
|
|
298
|
+
}, delay);
|
|
299
|
+
scheduledPhaseTimers.add(timer);
|
|
300
|
+
})));
|
|
301
|
+
spawnsAfter = semaphore.size();
|
|
302
|
+
} finally {
|
|
303
|
+
openspecTickInFlight = false;
|
|
304
|
+
const durationMs = Date.now() - tickStart;
|
|
305
|
+
if (DEBUG_ENABLED) {
|
|
306
|
+
const dirs = computeKnownDirectories().length;
|
|
307
|
+
// eslint-disable-next-line no-console
|
|
308
|
+
console.log(`[openspec-poll] tick dirs=${dirs} queueBefore=${spawnsBefore} queueAfter=${spawnsAfter} durationMs=${durationMs}`);
|
|
117
309
|
}
|
|
118
|
-
|
|
310
|
+
if (durationMs > TICK_SLOW_WARN_MS) {
|
|
311
|
+
console.warn(`[openspec-poll] slow tick: ${durationMs}ms (threshold ${TICK_SLOW_WARN_MS}ms). Consider raising pollIntervalSeconds or lowering maxConcurrentSpawns.`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let piResourcesInFlight = false;
|
|
317
|
+
async function schedulePiResourcesTick() {
|
|
318
|
+
if (piResourcesInFlight) return;
|
|
319
|
+
piResourcesInFlight = true;
|
|
320
|
+
try {
|
|
321
|
+
await Promise.all(computeKnownDirectories().map(async (cwd) => {
|
|
322
|
+
try { await refreshPiResourcesInternal(cwd); }
|
|
323
|
+
catch { /* ignore, next tick retries */ }
|
|
324
|
+
}));
|
|
325
|
+
} finally {
|
|
326
|
+
piResourcesInFlight = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function installTimers() {
|
|
331
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
332
|
+
if (piResourcesTimer) clearInterval(piResourcesTimer);
|
|
333
|
+
pollTimer = setInterval(scheduleOpenSpecTick, cfg.pollIntervalSeconds * 1000);
|
|
334
|
+
// Pi resources change far less often; poll at 5× the openspec interval.
|
|
335
|
+
piResourcesTimer = setInterval(schedulePiResourcesTick, cfg.pollIntervalSeconds * 5 * 1000);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function stopTimers() {
|
|
339
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
340
|
+
if (piResourcesTimer) { clearInterval(piResourcesTimer); piResourcesTimer = null; }
|
|
341
|
+
for (const t of scheduledPhaseTimers) clearTimeout(t);
|
|
342
|
+
scheduledPhaseTimers.clear();
|
|
119
343
|
}
|
|
120
344
|
|
|
121
345
|
return {
|
|
@@ -124,10 +348,11 @@ export function createDirectoryService(
|
|
|
124
348
|
loadSessionEvents,
|
|
125
349
|
|
|
126
350
|
getOpenSpecData(cwd: string): OpenSpecData | undefined {
|
|
127
|
-
return
|
|
351
|
+
return caches.get(cwd)?.data;
|
|
128
352
|
},
|
|
129
353
|
|
|
130
354
|
refreshOpenSpec,
|
|
355
|
+
pollDirectoryGated,
|
|
131
356
|
|
|
132
357
|
getPiResources(cwd: string): PiResourcesResult | undefined {
|
|
133
358
|
return piResourcesCache.get(cwd);
|
|
@@ -139,18 +364,24 @@ export function createDirectoryService(
|
|
|
139
364
|
|
|
140
365
|
startPolling(onChange: (cwd: string, data: OpenSpecData) => void) {
|
|
141
366
|
onChangeCallback = onChange;
|
|
142
|
-
|
|
143
|
-
pollTimer = setInterval(pollAllDirectories, POLL_INTERVAL);
|
|
367
|
+
installTimers();
|
|
144
368
|
},
|
|
145
369
|
|
|
146
370
|
stopPolling() {
|
|
147
|
-
|
|
148
|
-
clearInterval(pollTimer);
|
|
149
|
-
pollTimer = null;
|
|
150
|
-
}
|
|
371
|
+
stopTimers();
|
|
151
372
|
onChangeCallback = null;
|
|
152
373
|
},
|
|
153
374
|
|
|
375
|
+
reconfigurePolling(newCfg: OpenSpecPollConfig) {
|
|
376
|
+
const oldInterval = cfg.pollIntervalSeconds;
|
|
377
|
+
cfg = { ...newCfg };
|
|
378
|
+
semaphore.setMax(cfg.maxConcurrentSpawns);
|
|
379
|
+
// Only re-install timers if they were running and the interval actually changed.
|
|
380
|
+
if (pollTimer && oldInterval !== cfg.pollIntervalSeconds) {
|
|
381
|
+
installTimers();
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
|
|
154
385
|
async onDirectoryAdded(cwd: string): Promise<DirectoryAddedResult> {
|
|
155
386
|
const [sessions, openspecData] = await Promise.all([
|
|
156
387
|
discoverSessions(cwd),
|
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
* Auto-detection of code-server / openvscode-server binary.
|
|
3
3
|
* Checks config override first, then PATH.
|
|
4
4
|
*/
|
|
5
|
-
import { execSync } from "node:child_process";
|
|
6
5
|
import type { EditorDetectionResult } from "@blackbelt-technology/pi-dashboard-shared/editor-types.js";
|
|
7
6
|
import type { EditorConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
8
|
-
import {
|
|
7
|
+
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js";
|
|
9
8
|
|
|
10
9
|
export const BINARIES_TO_CHECK = ["code-server", "openvscode-server"] as const;
|
|
11
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Look up a binary using the unified ToolResolver, which handles the
|
|
13
|
+
* where/which split (Windows vs Unix), managed-bin paths, and login-shell
|
|
14
|
+
* fallback for GUI apps. Previously used raw `which` which silently failed
|
|
15
|
+
* on Windows. See change: fix-windows-server-parity.
|
|
16
|
+
*/
|
|
12
17
|
export function whichBinary(name: string): string | null {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
18
|
+
const resolver = new ToolResolver({
|
|
19
|
+
processExecPath: process.execPath,
|
|
20
|
+
useLoginShell: true,
|
|
21
|
+
});
|
|
22
|
+
return resolver.which(name);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
let cachedResult: EditorDetectionResult | null = null;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Server-side lifecycle manager for code-server child processes.
|
|
3
3
|
* Spawns per-folder instances, tracks heartbeats, enforces idle timeout and max instances.
|
|
4
4
|
*/
|
|
5
|
-
import { spawn, type ChildProcess } from "
|
|
5
|
+
import { spawn, type ChildProcess } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
6
|
+
import { killPidWithGroup, killProcess } from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
|
|
6
7
|
import { createServer as createNetServer, Socket as NetSocket } from "node:net";
|
|
7
8
|
import { createHash, randomBytes } from "node:crypto";
|
|
8
9
|
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
@@ -329,9 +330,23 @@ export function createEditorManager(options: EditorManagerOptions): EditorManage
|
|
|
329
330
|
setStatus(inst, "stopped");
|
|
330
331
|
|
|
331
332
|
if (inst.process && !inst.process.killed) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
const pid = inst.process.pid;
|
|
334
|
+
if (pid != null) {
|
|
335
|
+
// Tree-kill the code-server subtree. On Windows this becomes
|
|
336
|
+
// `taskkill /F /T /PID` (async); on POSIX it's SIGTERM → 2s → SIGKILL
|
|
337
|
+
// of the process group. Fire-and-forget: `stop()` is synchronous
|
|
338
|
+
// by convention and callers don't await. See change:
|
|
339
|
+
// route-kill-paths-through-platform.
|
|
340
|
+
void killProcess(pid, { timeoutMs: 2000 }).catch(() => {
|
|
341
|
+
// Fallback to a direct pgroup SIGTERM if the platform helper
|
|
342
|
+
// couldn't complete (rare; mostly for already-dead processes).
|
|
343
|
+
try { killPidWithGroup(pid, "SIGTERM"); } catch { /* already dead */ }
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
// No PID yet (process hasn't started). Fall back to the raw
|
|
347
|
+
// ChildProcess.kill() which only signals the leaf.
|
|
348
|
+
try { inst.process.kill("SIGTERM"); } catch { /* already dead */ }
|
|
349
|
+
}
|
|
335
350
|
}
|
|
336
351
|
|
|
337
352
|
cleanup(id);
|
|
@@ -12,10 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import { execSync } from "node:child_process";
|
|
15
|
+
import { execSync } from "node:child_process"; // ban:child_process-ok editor orphan sweep uses `ps`/`taskkill` probe for bounded wait; tracked tech debt for migration to platform/process Recipe
|
|
16
16
|
import { readFileSync, existsSync } from "node:fs";
|
|
17
17
|
import { readJsonFile, writeJsonFile } from "./json-store.js";
|
|
18
18
|
import { isUnsafeTestHomeScan } from "./test-env-guard.js";
|
|
19
|
+
import {
|
|
20
|
+
isProcessAlive as platformIsProcessAlive,
|
|
21
|
+
killPidWithGroup,
|
|
22
|
+
} from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
|
|
19
23
|
|
|
20
24
|
const DEFAULT_PID_FILE = path.join(os.homedir(), ".pi", "dashboard", "editor-pids.json");
|
|
21
25
|
|
|
@@ -86,18 +90,15 @@ function defaultGetCmdline(pid: number): string | null {
|
|
|
86
90
|
return null;
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
/** Route through platform/process.ts so lint enforcement and cross-platform
|
|
94
|
+
* semantics (libuv signal 0 check, POSIX group kill) stay in one place. */
|
|
89
95
|
function defaultIsProcessAlive(pid: number): boolean {
|
|
90
|
-
|
|
91
|
-
process.kill(pid, 0);
|
|
92
|
-
return true;
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
+
return platformIsProcessAlive(pid);
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
function defaultKill(pid: number, signal: NodeJS.Signals): boolean {
|
|
99
100
|
try {
|
|
100
|
-
|
|
101
|
+
killPidWithGroup(pid, signal);
|
|
101
102
|
return true;
|
|
102
103
|
} catch {
|
|
103
104
|
return false;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static editor registry and detection logic.
|
|
3
3
|
* Detects available editors by checking for running processes + CLI on PATH.
|
|
4
|
+
* Uses shared platform primitives so the win32 / unix split is owned in one
|
|
5
|
+
* place. See change: consolidate-platform-handlers.
|
|
4
6
|
*/
|
|
5
|
-
import {
|
|
7
|
+
import { isProcessRunning as platformIsProcessRunning } from "@blackbelt-technology/pi-dashboard-shared/platform/process-scan.js";
|
|
8
|
+
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js";
|
|
6
9
|
|
|
7
10
|
export interface EditorEntry {
|
|
8
11
|
id: string;
|
|
@@ -50,32 +53,28 @@ export const EDITORS: EditorEntry[] = [
|
|
|
50
53
|
},
|
|
51
54
|
];
|
|
52
55
|
|
|
56
|
+
// Cached resolver for binary-availability checks (reads PATH via `where`/`which`).
|
|
57
|
+
const resolver = new ToolResolver({ processExecPath: process.execPath });
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Platform-unified process-running check. Re-exported for callers (and tests)
|
|
61
|
+
* that previously imported it from this module.
|
|
62
|
+
*/
|
|
53
63
|
export function isProcessRunning(pattern: string): boolean {
|
|
54
|
-
|
|
55
|
-
execSync(`pgrep -f "${pattern}"`, { stdio: "pipe" });
|
|
56
|
-
return true;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
64
|
+
return platformIsProcessRunning(pattern);
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
67
|
+
/**
|
|
68
|
+
* @deprecated Use `isProcessRunning(pattern)` — the shared primitive now
|
|
69
|
+
* handles the Windows (tasklist) vs Unix (pgrep) split internally. Kept as
|
|
70
|
+
* a thin alias for tests that still call it directly.
|
|
71
|
+
*/
|
|
72
|
+
export function isProcessRunningWin32(pattern: string): boolean {
|
|
73
|
+
return platformIsProcessRunning(pattern, { platform: "win32" });
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const result = execSync(`tasklist /FI "IMAGENAME eq ${pattern}" /NH`, { encoding: "utf-8", stdio: "pipe" });
|
|
75
|
-
return result.includes(pattern);
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
76
|
+
function isCliAvailable(cli: string): boolean {
|
|
77
|
+
return resolver.which(cli) !== null;
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
export function detectEditors(_cwd: string): DetectedEditor[] {
|
|
@@ -96,9 +95,7 @@ export function detectEditors(_cwd: string): DetectedEditor[] {
|
|
|
96
95
|
cli = editor.cli;
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
const running =
|
|
100
|
-
? isProcessRunningWin32(pattern)
|
|
101
|
-
: isProcessRunning(pattern);
|
|
98
|
+
const running = isProcessRunning(pattern);
|
|
102
99
|
|
|
103
100
|
if (running && isCliAvailable(cli)) {
|
|
104
101
|
results.push({ id: editor.id, name: editor.name });
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Tracks PID + cwd at spawn time, links to sessionId when the bridge connects.
|
|
4
4
|
* Persists entries to disk so a restarted server can clean up orphans.
|
|
5
5
|
*/
|
|
6
|
-
import type { ChildProcess } from "
|
|
6
|
+
import type { ChildProcess } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
7
7
|
import { EventEmitter } from "node:events";
|
|
8
8
|
import { readJsonFile, writeJsonFile } from "./json-store.js";
|
|
9
|
+
import { killPidWithGroup, isProcessAlive } from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import os from "node:os";
|
|
11
12
|
import { isUnsafeTestHomeScan } from "./test-env-guard.js";
|
|
@@ -82,15 +83,6 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
82
83
|
return data.entries ?? [];
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
function isProcessAlive(pid: number): boolean {
|
|
86
|
-
try {
|
|
87
|
-
process.kill(pid, 0);
|
|
88
|
-
return true;
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
86
|
return {
|
|
95
87
|
register(pid: number, cwd: string, proc: ChildProcess) {
|
|
96
88
|
entries.set(pid, { pid, cwd, process: proc, spawnedAt: Date.now() });
|
|
@@ -124,12 +116,9 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
124
116
|
for (const entry of entries.values()) {
|
|
125
117
|
if (entry.sessionId === sessionId) {
|
|
126
118
|
try {
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
const signal = "SIGTERM";
|
|
131
|
-
const pid = process.platform === "win32" ? entry.pid : -entry.pid;
|
|
132
|
-
process.kill(pid, signal);
|
|
119
|
+
// Delegate platform-specific pid-vs-group-pid handling to the
|
|
120
|
+
// shared primitive. See change: consolidate-platform-handlers.
|
|
121
|
+
killPidWithGroup(entry.pid, "SIGTERM");
|
|
133
122
|
entries.delete(entry.pid);
|
|
134
123
|
persist();
|
|
135
124
|
return true;
|
|
@@ -153,10 +142,9 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
153
142
|
console.warn("[headless-pid-registry] killAll() blocked: running under vitest with real HOME");
|
|
154
143
|
return;
|
|
155
144
|
}
|
|
156
|
-
const useGroup = process.platform !== "win32";
|
|
157
145
|
for (const [pid] of entries) {
|
|
158
146
|
try {
|
|
159
|
-
|
|
147
|
+
killPidWithGroup(pid, "SIGTERM");
|
|
160
148
|
} catch {
|
|
161
149
|
// Process may have already exited
|
|
162
150
|
}
|
|
@@ -190,8 +178,7 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
190
178
|
if (age > MAX_ORPHAN_AGE_MS) {
|
|
191
179
|
// Very old orphan — kill (process group on Unix, direct on Windows)
|
|
192
180
|
try {
|
|
193
|
-
|
|
194
|
-
process.kill(pid, "SIGTERM");
|
|
181
|
+
killPidWithGroup(entry.pid, "SIGTERM");
|
|
195
182
|
} catch {
|
|
196
183
|
// Already dead
|
|
197
184
|
}
|