@blackbelt-technology/pi-agent-dashboard 0.2.9 → 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 +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -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/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- 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/prompt-expander.ts +25 -4
- 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 +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -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 +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -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__/cors.test.ts +34 -2
- 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-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -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__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- 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__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- 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-auth-routes.test.ts +13 -3
- 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__/recommended-routes.test.ts +389 -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__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- 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 +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- 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 +108 -9
- package/packages/server/src/browser-gateway.ts +16 -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 +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -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/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- 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/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- 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 +363 -26
- 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 +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- 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 +59 -3
- 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__/openspec-poller.test.ts +44 -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 +156 -0
- 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__/source-matching.test.ts +143 -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 +93 -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 +71 -49
- 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 +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -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
- package/packages/shared/src/types.ts +7 -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";
|
|
@@ -12,6 +13,7 @@ import type { EditorInstanceStatus, EditorDetectionResult } from "@blackbelt-tec
|
|
|
12
13
|
import type { EditorConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
13
14
|
import { detectCodeServerBinary, resetDetectionCache } from "./editor-detection.js";
|
|
14
15
|
import { buildSpawnEnv } from "./process-manager.js";
|
|
16
|
+
import type { EditorPidRegistry } from "./editor-pid-registry.js";
|
|
15
17
|
|
|
16
18
|
export interface EditorInstanceInfo {
|
|
17
19
|
id: string;
|
|
@@ -37,6 +39,8 @@ export interface EditorManagerOptions {
|
|
|
37
39
|
onStatusChange?: (cwd: string, id: string, status: EditorInstanceStatus) => void;
|
|
38
40
|
/** Override re-detection (for testing). When false, skip runtime re-detection. */
|
|
39
41
|
allowRedetection?: boolean;
|
|
42
|
+
/** Optional persistent PID registry for orphan cleanup across restarts. */
|
|
43
|
+
pidRegistry?: EditorPidRegistry;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
export interface EditorManager {
|
|
@@ -141,7 +145,7 @@ function toInfo(inst: InternalInstance): EditorInstanceInfo {
|
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
export function createEditorManager(options: EditorManagerOptions): EditorManager {
|
|
144
|
-
const { config, detection, onStatusChange, allowRedetection = true } = options;
|
|
148
|
+
const { config, detection, onStatusChange, allowRedetection = true, pidRegistry } = options;
|
|
145
149
|
const instances = new Map<string, InternalInstance>();
|
|
146
150
|
const cwdIndex = new Map<string, string>(); // cwd → id
|
|
147
151
|
const idleTimeoutMs = (config.idleTimeoutMinutes ?? 10) * 60 * 1000;
|
|
@@ -279,6 +283,7 @@ export function createEditorManager(options: EditorManagerOptions): EditorManage
|
|
|
279
283
|
child.on("error", (err) => {
|
|
280
284
|
console.error(`[editor-manager] code-server error for ${cwd}:`, err.message);
|
|
281
285
|
setStatus(inst, "stopped");
|
|
286
|
+
pidRegistry?.remove(id);
|
|
282
287
|
cleanup(id);
|
|
283
288
|
});
|
|
284
289
|
|
|
@@ -287,6 +292,7 @@ export function createEditorManager(options: EditorManagerOptions): EditorManage
|
|
|
287
292
|
console.log(`[editor-manager] code-server exited (code=${code}) for ${cwd}`);
|
|
288
293
|
setStatus(inst, "stopped");
|
|
289
294
|
}
|
|
295
|
+
pidRegistry?.remove(id);
|
|
290
296
|
cleanup(id);
|
|
291
297
|
});
|
|
292
298
|
|
|
@@ -298,6 +304,16 @@ export function createEditorManager(options: EditorManagerOptions): EditorManage
|
|
|
298
304
|
}
|
|
299
305
|
|
|
300
306
|
setStatus(inst, "ready");
|
|
307
|
+
if (pidRegistry && typeof child.pid === "number") {
|
|
308
|
+
pidRegistry.register({
|
|
309
|
+
id,
|
|
310
|
+
pid: child.pid,
|
|
311
|
+
port,
|
|
312
|
+
cwd,
|
|
313
|
+
dataDir,
|
|
314
|
+
spawnedAt: inst.lastHeartbeat,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
301
317
|
startIdleTimer(inst);
|
|
302
318
|
return toInfo(inst);
|
|
303
319
|
}
|
|
@@ -306,13 +322,31 @@ export function createEditorManager(options: EditorManagerOptions): EditorManage
|
|
|
306
322
|
const inst = instances.get(id);
|
|
307
323
|
if (!inst) return;
|
|
308
324
|
|
|
325
|
+
// Remove from persistent registry FIRST so a crash mid-stop
|
|
326
|
+
// leaves the registry consistent on the next boot.
|
|
327
|
+
pidRegistry?.remove(id);
|
|
328
|
+
|
|
309
329
|
clearIdleTimer(inst);
|
|
310
330
|
setStatus(inst, "stopped");
|
|
311
331
|
|
|
312
332
|
if (inst.process && !inst.process.killed) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
}
|
|
316
350
|
}
|
|
317
351
|
|
|
318
352
|
cleanup(id);
|