@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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent registry of spawned `code-server` editor instances.
|
|
3
|
+
*
|
|
4
|
+
* Persists PIDs to ~/.pi/dashboard/editor-pids.json so that, after a non-graceful
|
|
5
|
+
* dashboard shutdown (SIGKILL, crash, OOM, force-quit), the next server boot can
|
|
6
|
+
* sweep and SIGTERM/SIGKILL orphan code-server processes that were reparented to
|
|
7
|
+
* init/launchd.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the persistence + boot-sweep pattern of `headless-pid-registry.ts` but
|
|
10
|
+
* KILLS live orphans (not reclaim) — editor instances are dashboard-internal,
|
|
11
|
+
* unreachable after restart, and the user expects a clean state.
|
|
12
|
+
*/
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
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
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { readJsonFile, writeJsonFile } from "./json-store.js";
|
|
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";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_PID_FILE = path.join(os.homedir(), ".pi", "dashboard", "editor-pids.json");
|
|
25
|
+
|
|
26
|
+
/** Grace period between SIGTERM and SIGKILL escalation. */
|
|
27
|
+
const SIGKILL_GRACE_MS = 1000;
|
|
28
|
+
|
|
29
|
+
/** Marker that uniquely identifies a dashboard-spawned code-server cmdline. */
|
|
30
|
+
const DASHBOARD_DATA_DIR_MARKER = path.join(os.homedir(), ".pi", "dashboard", "editors") + path.sep;
|
|
31
|
+
|
|
32
|
+
export interface PersistedEditorEntry {
|
|
33
|
+
id: string;
|
|
34
|
+
pid: number;
|
|
35
|
+
port: number;
|
|
36
|
+
cwd: string;
|
|
37
|
+
dataDir: string;
|
|
38
|
+
/** ISO 8601 timestamp */
|
|
39
|
+
spawnedAt: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface EditorPidFileData {
|
|
43
|
+
entries: PersistedEditorEntry[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EditorPidRegistry {
|
|
47
|
+
/** Record a newly-ready editor instance. */
|
|
48
|
+
register(entry: Omit<PersistedEditorEntry, "spawnedAt"> & { spawnedAt?: number | string }): void;
|
|
49
|
+
/** Remove an entry by editor id. */
|
|
50
|
+
remove(id: string): void;
|
|
51
|
+
/** Number of in-memory tracked entries (testing aid). */
|
|
52
|
+
size(): number;
|
|
53
|
+
/** Sweep persisted entries on server boot, killing verified orphans. */
|
|
54
|
+
cleanupOrphans(): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface EditorPidRegistryOptions {
|
|
58
|
+
pidFilePath?: string;
|
|
59
|
+
/** Override cmdline lookup (testing). */
|
|
60
|
+
getCmdline?: (pid: number) => string | null;
|
|
61
|
+
/** Override process-alive check (testing). */
|
|
62
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
63
|
+
/** Override kill (testing). Returns true if signal was delivered. */
|
|
64
|
+
kill?: (pid: number, signal: NodeJS.Signals) => boolean;
|
|
65
|
+
/** Override grace ms between SIGTERM and SIGKILL (testing). */
|
|
66
|
+
graceMs?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Default cross-platform process command-line lookup. */
|
|
70
|
+
function defaultGetCmdline(pid: number): string | null {
|
|
71
|
+
try {
|
|
72
|
+
if (process.platform === "linux") {
|
|
73
|
+
const file = `/proc/${pid}/cmdline`;
|
|
74
|
+
if (!existsSync(file)) return null;
|
|
75
|
+
// /proc cmdline is NUL-separated
|
|
76
|
+
return readFileSync(file, "utf-8").replace(/\0/g, " ").trim();
|
|
77
|
+
}
|
|
78
|
+
if (process.platform === "darwin") {
|
|
79
|
+
const out = execSync(`ps -p ${pid} -o command=`, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
80
|
+
return out.trim() || null;
|
|
81
|
+
}
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
const out = execSync(`wmic process where ProcessId=${pid} get CommandLine /value`, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
84
|
+
const m = out.match(/CommandLine=(.*)/);
|
|
85
|
+
return m ? m[1].trim() : null;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
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. */
|
|
95
|
+
function defaultIsProcessAlive(pid: number): boolean {
|
|
96
|
+
return platformIsProcessAlive(pid);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function defaultKill(pid: number, signal: NodeJS.Signals): boolean {
|
|
100
|
+
try {
|
|
101
|
+
killPidWithGroup(pid, signal);
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Verify that `cmdline` looks like a dashboard-spawned code-server. */
|
|
109
|
+
export function isDashboardOwnedCodeServer(cmdline: string | null): boolean {
|
|
110
|
+
if (!cmdline) return false;
|
|
111
|
+
// Must reference --user-data-dir under ~/.pi/dashboard/editors/
|
|
112
|
+
return cmdline.includes("--user-data-dir") && cmdline.includes(DASHBOARD_DATA_DIR_MARKER);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createEditorPidRegistry(options: EditorPidRegistryOptions = {}): EditorPidRegistry {
|
|
116
|
+
const pidFilePath = options.pidFilePath ?? DEFAULT_PID_FILE;
|
|
117
|
+
const getCmdline = options.getCmdline ?? defaultGetCmdline;
|
|
118
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
119
|
+
const kill = options.kill ?? defaultKill;
|
|
120
|
+
const graceMs = options.graceMs ?? SIGKILL_GRACE_MS;
|
|
121
|
+
|
|
122
|
+
// In-memory mirror of the file (id → entry).
|
|
123
|
+
const entries = new Map<string, PersistedEditorEntry>();
|
|
124
|
+
|
|
125
|
+
function persist(): void {
|
|
126
|
+
try {
|
|
127
|
+
const data: EditorPidFileData = { entries: [...entries.values()] };
|
|
128
|
+
writeJsonFile(pidFilePath, data);
|
|
129
|
+
} catch {
|
|
130
|
+
// Best-effort: persistence failures must not break editor lifecycle.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
register(entry) {
|
|
136
|
+
const spawnedAt =
|
|
137
|
+
typeof entry.spawnedAt === "string"
|
|
138
|
+
? entry.spawnedAt
|
|
139
|
+
: new Date(entry.spawnedAt ?? Date.now()).toISOString();
|
|
140
|
+
entries.set(entry.id, {
|
|
141
|
+
id: entry.id,
|
|
142
|
+
pid: entry.pid,
|
|
143
|
+
port: entry.port,
|
|
144
|
+
cwd: entry.cwd,
|
|
145
|
+
dataDir: entry.dataDir,
|
|
146
|
+
spawnedAt,
|
|
147
|
+
});
|
|
148
|
+
persist();
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
remove(id) {
|
|
152
|
+
if (entries.delete(id)) persist();
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
size() {
|
|
156
|
+
return entries.size;
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
async cleanupOrphans() {
|
|
160
|
+
if (isUnsafeTestHomeScan()) {
|
|
161
|
+
console.warn("[editor-pid-registry] cleanupOrphans() blocked: running under vitest with real HOME");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const data = readJsonFile<EditorPidFileData>(pidFilePath, { entries: [] });
|
|
165
|
+
const persisted = Array.isArray(data?.entries) ? data.entries : [];
|
|
166
|
+
|
|
167
|
+
let killed = 0;
|
|
168
|
+
const toKill: PersistedEditorEntry[] = [];
|
|
169
|
+
|
|
170
|
+
for (const entry of persisted) {
|
|
171
|
+
if (!isAlive(entry.pid)) continue;
|
|
172
|
+
const cmdline = getCmdline(entry.pid);
|
|
173
|
+
if (!isDashboardOwnedCodeServer(cmdline)) continue;
|
|
174
|
+
toKill.push(entry);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const entry of toKill) {
|
|
178
|
+
kill(entry.pid, "SIGTERM");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (toKill.length > 0) {
|
|
182
|
+
await new Promise((r) => setTimeout(r, graceMs));
|
|
183
|
+
for (const entry of toKill) {
|
|
184
|
+
if (isAlive(entry.pid)) {
|
|
185
|
+
kill(entry.pid, "SIGKILL");
|
|
186
|
+
}
|
|
187
|
+
killed++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Reset to whatever the new server has registered so far (initially nothing).
|
|
192
|
+
persist();
|
|
193
|
+
|
|
194
|
+
if (killed > 0) {
|
|
195
|
+
console.log(`[editor-pid-registry] cleaned ${killed} orphan${killed === 1 ? "" : "s"}`);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -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 });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime fix for node-pty spawn-helper permissions.
|
|
3
|
+
*
|
|
4
|
+
* On macOS/Linux, the prebuilt spawn-helper binary may lack the execute bit
|
|
5
|
+
* (especially in Electron bundles where npm hoisting skips the postinstall fix).
|
|
6
|
+
* This module finds and fixes all spawn-helper binaries at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Called once when the terminal manager is created.
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
|
|
14
|
+
let fixed = false;
|
|
15
|
+
|
|
16
|
+
export function fixPtyPermissions(): void {
|
|
17
|
+
if (fixed || process.platform === "win32") return;
|
|
18
|
+
fixed = true;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Resolve node-pty's actual location (works with hoisting)
|
|
22
|
+
const require_ = createRequire(import.meta.url);
|
|
23
|
+
const ptyMain = require_.resolve("node-pty");
|
|
24
|
+
const ptyDir = path.dirname(ptyMain);
|
|
25
|
+
const prebuildsDir = path.join(ptyDir, "..", "prebuilds");
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(prebuildsDir)) return;
|
|
28
|
+
|
|
29
|
+
for (const dir of fs.readdirSync(prebuildsDir)) {
|
|
30
|
+
const helper = path.join(prebuildsDir, dir, "spawn-helper");
|
|
31
|
+
try {
|
|
32
|
+
const stat = fs.statSync(helper);
|
|
33
|
+
if (!(stat.mode & 0o111)) {
|
|
34
|
+
fs.chmodSync(helper, 0o755);
|
|
35
|
+
console.log(`[pty] Fixed spawn-helper permissions: ${helper}`);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// spawn-helper doesn't exist for this platform, skip
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// node-pty not installed or not resolvable, skip silently
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -3,11 +3,13 @@
|
|
|
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";
|
|
12
|
+
import { isUnsafeTestHomeScan } from "./test-env-guard.js";
|
|
11
13
|
|
|
12
14
|
/** Default PID file path */
|
|
13
15
|
const DEFAULT_PID_FILE = path.join(os.homedir(), ".pi", "dashboard", "headless-pids.json");
|
|
@@ -81,15 +83,6 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
81
83
|
return data.entries ?? [];
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
function isProcessAlive(pid: number): boolean {
|
|
85
|
-
try {
|
|
86
|
-
process.kill(pid, 0);
|
|
87
|
-
return true;
|
|
88
|
-
} catch {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
86
|
return {
|
|
94
87
|
register(pid: number, cwd: string, proc: ChildProcess) {
|
|
95
88
|
entries.set(pid, { pid, cwd, process: proc, spawnedAt: Date.now() });
|
|
@@ -123,12 +116,9 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
123
116
|
for (const entry of entries.values()) {
|
|
124
117
|
if (entry.sessionId === sessionId) {
|
|
125
118
|
try {
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
const signal = "SIGTERM";
|
|
130
|
-
const pid = process.platform === "win32" ? entry.pid : -entry.pid;
|
|
131
|
-
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");
|
|
132
122
|
entries.delete(entry.pid);
|
|
133
123
|
persist();
|
|
134
124
|
return true;
|
|
@@ -148,10 +138,13 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
148
138
|
},
|
|
149
139
|
|
|
150
140
|
killAll() {
|
|
151
|
-
|
|
141
|
+
if (isUnsafeTestHomeScan()) {
|
|
142
|
+
console.warn("[headless-pid-registry] killAll() blocked: running under vitest with real HOME");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
152
145
|
for (const [pid] of entries) {
|
|
153
146
|
try {
|
|
154
|
-
|
|
147
|
+
killPidWithGroup(pid, "SIGTERM");
|
|
155
148
|
} catch {
|
|
156
149
|
// Process may have already exited
|
|
157
150
|
}
|
|
@@ -166,6 +159,10 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
166
159
|
},
|
|
167
160
|
|
|
168
161
|
cleanupOrphans() {
|
|
162
|
+
if (isUnsafeTestHomeScan()) {
|
|
163
|
+
console.warn("[headless-pid-registry] cleanupOrphans() blocked: running under vitest with real HOME");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
169
166
|
const persisted = loadFromDisk();
|
|
170
167
|
const now = Date.now();
|
|
171
168
|
|
|
@@ -181,8 +178,7 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
181
178
|
if (age > MAX_ORPHAN_AGE_MS) {
|
|
182
179
|
// Very old orphan — kill (process group on Unix, direct on Windows)
|
|
183
180
|
try {
|
|
184
|
-
|
|
185
|
-
process.kill(pid, "SIGTERM");
|
|
181
|
+
killPidWithGroup(entry.pid, "SIGTERM");
|
|
186
182
|
} catch {
|
|
187
183
|
// Already dead
|
|
188
184
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install signal + exit handlers that release the per-HOME dashboard lock.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `home-lock.ts` so the pure lock-acquisition logic stays
|
|
5
|
+
* trivially testable. See change: single-dashboard-per-home.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ReleaseFn = () => Promise<void>;
|
|
9
|
+
|
|
10
|
+
export interface InstallReleaseHandlersOptions {
|
|
11
|
+
/** Inject a fake `process`-like object for tests. */
|
|
12
|
+
proc?: NodeJS.Process;
|
|
13
|
+
/** Inject a logger (defaults to `console`). */
|
|
14
|
+
log?: (msg: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register SIGINT / SIGTERM / SIGHUP / SIGBREAK handlers and an `exit`
|
|
19
|
+
* fallback that call `release()` exactly once. The handler is idempotent;
|
|
20
|
+
* multiple signals will not double-release.
|
|
21
|
+
*
|
|
22
|
+
* Windows:
|
|
23
|
+
* - SIGINT + SIGBREAK are emitted by Node. SIGBREAK fires on Ctrl+Break.
|
|
24
|
+
* - SIGHUP does not exist on Windows; the registration is a no-op there.
|
|
25
|
+
* - `taskkill /F` bypasses all signals — the stale-detection path in
|
|
26
|
+
* `proper-lockfile` (staleDuration 10s) handles this case on next boot.
|
|
27
|
+
*
|
|
28
|
+
* Returns a function that removes the handlers (useful for tests).
|
|
29
|
+
*/
|
|
30
|
+
export function installReleaseHandlers(
|
|
31
|
+
release: ReleaseFn,
|
|
32
|
+
options: InstallReleaseHandlersOptions = {},
|
|
33
|
+
): () => void {
|
|
34
|
+
const proc = options.proc ?? process;
|
|
35
|
+
const log = options.log ?? ((m: string) => console.log(m));
|
|
36
|
+
|
|
37
|
+
let releasing = false;
|
|
38
|
+
const doRelease = async (signal: string) => {
|
|
39
|
+
if (releasing) return;
|
|
40
|
+
releasing = true;
|
|
41
|
+
try {
|
|
42
|
+
await release();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
log(`[home-lock] release on ${signal} failed: ${(err as Error).message ?? err}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const sigintHandler = () => { void doRelease("SIGINT").then(() => proc.exit(0)); };
|
|
49
|
+
const sigtermHandler = () => { void doRelease("SIGTERM").then(() => proc.exit(0)); };
|
|
50
|
+
const sighupHandler = () => { void doRelease("SIGHUP").then(() => proc.exit(0)); };
|
|
51
|
+
const sigbreakHandler = () => { void doRelease("SIGBREAK").then(() => proc.exit(0)); };
|
|
52
|
+
// `exit` is synchronous — we can't await. Best effort: fire and move on;
|
|
53
|
+
// the async release will race the exit. `proper-lockfile` also removes its
|
|
54
|
+
// own lockfile on exit via its own exit hook as a safety net.
|
|
55
|
+
const exitHandler = () => { void release().catch(() => { /* ignore */ }); };
|
|
56
|
+
|
|
57
|
+
proc.on("SIGINT", sigintHandler);
|
|
58
|
+
proc.on("SIGTERM", sigtermHandler);
|
|
59
|
+
// SIGHUP + SIGBREAK may be undefined on Windows / some environments —
|
|
60
|
+
// registering still works (Node just never fires them there).
|
|
61
|
+
proc.on("SIGHUP", sighupHandler);
|
|
62
|
+
proc.on("SIGBREAK" as NodeJS.Signals, sigbreakHandler);
|
|
63
|
+
proc.on("exit", exitHandler);
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
proc.off("SIGINT", sigintHandler);
|
|
67
|
+
proc.off("SIGTERM", sigtermHandler);
|
|
68
|
+
proc.off("SIGHUP", sighupHandler);
|
|
69
|
+
proc.off("SIGBREAK" as NodeJS.Signals, sigbreakHandler);
|
|
70
|
+
proc.off("exit", exitHandler);
|
|
71
|
+
};
|
|
72
|
+
}
|