@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
|
@@ -73,8 +73,22 @@ export async function discoverAndBroadcastSessions(deps: SessionBootstrapDeps):
|
|
|
73
73
|
} as any);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
// Initial OpenSpec poll for all known directories
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// Initial OpenSpec poll for all known directories.
|
|
77
|
+
//
|
|
78
|
+
// NOTE: `refreshOpenSpec` / `pollOpenSpec` is currently synchronous internally
|
|
79
|
+
// (spawnSync per change) — on Windows with many active changes (~19) and
|
|
80
|
+
// multiple pinned directories this can block the event loop for minutes,
|
|
81
|
+
// making the HTTP server unresponsive during startup. We intentionally do
|
|
82
|
+
// NOT await it here so HTTP + WebSocket startup completes immediately;
|
|
83
|
+
// openspec data populates in the background and pushes `openspec_update`
|
|
84
|
+
// broadcasts to browsers as each directory finishes.
|
|
85
|
+
//
|
|
86
|
+
// A proper fix is to migrate the openspec Recipe to async spawn; tracked
|
|
87
|
+
// separately. See change: consolidate-tool-resolution.
|
|
88
|
+
void Promise.all(
|
|
89
|
+
directoryService.knownDirectories().map(async (cwd) => {
|
|
90
|
+
try { await directoryService.refreshOpenSpec(cwd); }
|
|
91
|
+
catch (err) { console.error(`[dashboard] initial openspec poll failed for ${cwd}:`, err); }
|
|
92
|
+
}),
|
|
79
93
|
);
|
|
80
94
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Session diff extraction — scans session events for file changes
|
|
3
3
|
* and optionally enriches with git diffs.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { resolve, relative, isAbsolute } from "node:path";
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { resolve, relative, isAbsolute, sep as pathSep } from "node:path";
|
|
7
|
+
import * as git from "@blackbelt-technology/pi-dashboard-shared/platform/git.js";
|
|
7
8
|
import type { DashboardEvent } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
8
9
|
import type { FileChangeEvent, FileDiffEntry, EditOperation } from "@blackbelt-technology/pi-dashboard-shared/diff-types.js";
|
|
9
10
|
import { isGitRepo } from "./git-operations.js";
|
|
@@ -105,7 +106,11 @@ function normalizePath(rawPath: string, cwd: string): string | null {
|
|
|
105
106
|
return null;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
// Normalize to posix separators. These paths are embedded into git diff
|
|
110
|
+
// headers (`diff --git a/<path> b/<path>`) which expect forward slashes,
|
|
111
|
+
// and are also used by the client for display and URL construction.
|
|
112
|
+
// See change: fix-windows-server-parity.
|
|
113
|
+
return pathSep === "/" ? rel : rel.split(pathSep).join("/");
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
/**
|
|
@@ -130,32 +135,27 @@ export function enrichWithGitDiff(
|
|
|
130
135
|
|
|
131
136
|
const enriched = files.map((file) => {
|
|
132
137
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
timeout: GIT_TIMEOUT,
|
|
138
|
-
}).trim();
|
|
138
|
+
// Delegate to the shared git tool module. The runner handles
|
|
139
|
+
// windowsHide, timeout, argv-array escaping (no shell), and the
|
|
140
|
+
// "no diff" exit-1 tolerance. See change: platform-command-executor.
|
|
141
|
+
const diff = git.diffOr({ cwd, path: file.path }).trim();
|
|
139
142
|
|
|
140
143
|
if (diff) {
|
|
141
144
|
return { ...file, gitDiff: diff };
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
// No diff from HEAD — try untracked (new file)
|
|
145
|
-
const status =
|
|
146
|
-
cwd,
|
|
147
|
-
encoding: "utf-8",
|
|
148
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
149
|
-
timeout: GIT_TIMEOUT,
|
|
150
|
-
}).trim();
|
|
148
|
+
const status = git.statusPorcelainOr({ cwd, path: file.path }).trim();
|
|
151
149
|
|
|
152
150
|
if (status.startsWith("??") || status.startsWith("A")) {
|
|
153
|
-
// Untracked or newly added — generate synthetic diff
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
// Untracked or newly added — generate synthetic diff.
|
|
152
|
+
// Read via fs.readFileSync rather than `cat` for cross-platform
|
|
153
|
+
// support (Windows has no `cat`). See change: fix-windows-server-parity.
|
|
154
|
+
const absPath = resolve(cwd, file.path);
|
|
155
|
+
if (!existsSync(absPath)) {
|
|
156
|
+
return file;
|
|
157
|
+
}
|
|
158
|
+
const content = readFileSync(absPath, "utf-8");
|
|
159
159
|
const lines = content.split("\n");
|
|
160
160
|
const diffLines = [
|
|
161
161
|
`diff --git a/${file.path} b/${file.path}`,
|
|
@@ -4,18 +4,25 @@
|
|
|
4
4
|
import * as pty from "node-pty";
|
|
5
5
|
import type { IPty } from "node-pty";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { fixPtyPermissions } from "./fix-pty-permissions.js";
|
|
7
8
|
import type { TerminalSession, TerminalControlMessage } from "@blackbelt-technology/pi-dashboard-shared/terminal-types.js";
|
|
8
9
|
import type { WebSocket } from "ws";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_BUFFER_SIZE = 256 * 1024; // 256KB
|
|
11
12
|
|
|
13
|
+
// Delegate shell detection to the shared platform primitive. Back-compat
|
|
14
|
+
// wrapper preserved so callers (and tests) that import `detectShell` from
|
|
15
|
+
// this module continue to work. See change: consolidate-platform-handlers.
|
|
16
|
+
import {
|
|
17
|
+
detectShell as platformDetectShell,
|
|
18
|
+
getTerminalEnvHints as platformTerminalEnvHints,
|
|
19
|
+
} from "@blackbelt-technology/pi-dashboard-shared/platform/shell.js";
|
|
20
|
+
import { killProcess } from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
|
|
21
|
+
|
|
12
22
|
/** Detect the appropriate shell for the current platform. */
|
|
13
23
|
export function detectShell(platform?: string): string {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return process.env.COMSPEC || "powershell.exe";
|
|
17
|
-
}
|
|
18
|
-
return process.env.SHELL || "/bin/bash";
|
|
24
|
+
// Keep the old `platform?: string` signature; coerce to the shared primitive's opts.
|
|
25
|
+
return platformDetectShell(platform ? { platform: platform as NodeJS.Platform } : undefined);
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
/** Circular buffer for PTY output replay. */
|
|
@@ -99,6 +106,9 @@ function generateId(): string {
|
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
export function createTerminalManager(options?: TerminalManagerOptions): TerminalManager {
|
|
109
|
+
// Fix node-pty spawn-helper permissions at runtime (defense in depth)
|
|
110
|
+
fixPtyPermissions();
|
|
111
|
+
|
|
102
112
|
const entries = new Map<string, TerminalEntry>();
|
|
103
113
|
const bufferSize = options?.bufferSize ?? DEFAULT_BUFFER_SIZE;
|
|
104
114
|
|
|
@@ -106,10 +116,7 @@ export function createTerminalManager(options?: TerminalManagerOptions): Termina
|
|
|
106
116
|
const shell = detectShell();
|
|
107
117
|
const id = generateId();
|
|
108
118
|
|
|
109
|
-
const env = { ...process.env } as Record<string, string>;
|
|
110
|
-
if (process.platform === "win32" && !env.TERM) {
|
|
111
|
-
env.TERM = "cygwin";
|
|
112
|
-
}
|
|
119
|
+
const env = { ...process.env, ...platformTerminalEnvHints() } as Record<string, string>;
|
|
113
120
|
|
|
114
121
|
const p = pty.spawn(shell, [], {
|
|
115
122
|
cwd,
|
|
@@ -207,18 +214,56 @@ export function createTerminalManager(options?: TerminalManagerOptions): Termina
|
|
|
207
214
|
function kill(id: string): void {
|
|
208
215
|
const entry = entries.get(id);
|
|
209
216
|
if (!entry) throw new Error(`Terminal ${id} not found`);
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
|
|
218
|
+
// Windows: node-pty's kill(signal) uses TerminateProcess on the shell
|
|
219
|
+
// handle, which (a) ignores the signal string, and (b) does not kill
|
|
220
|
+
// child processes of the shell (python.exe, node.exe, etc.). Worse, its
|
|
221
|
+
// onExit callback is not always fired after external kills, so the
|
|
222
|
+
// terminal entry would stay in the map forever — which is exactly why
|
|
223
|
+
// the X button "doesn't work" on Windows. Route through platform's
|
|
224
|
+
// killProcess() so taskkill /F /T /PID does a genuine tree kill.
|
|
225
|
+
//
|
|
226
|
+
// POSIX: keep the SIGHUP → SIGKILL idiom — interactive shells honor
|
|
227
|
+
// SIGHUP, giving them a chance to clean up tty state before we escalate.
|
|
228
|
+
if (process.platform === "win32") {
|
|
229
|
+
const pid = entry.pty.pid;
|
|
230
|
+
if (typeof pid === "number") {
|
|
231
|
+
void killProcess(pid, { timeoutMs: 2000 }).catch(() => { /* surfaced via onExit fallback below */ });
|
|
232
|
+
} else {
|
|
233
|
+
try { entry.pty.kill(); } catch { /* best-effort */ }
|
|
216
234
|
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
} else {
|
|
236
|
+
entry.pty.kill("SIGHUP");
|
|
237
|
+
const escalation = setTimeout(() => {
|
|
238
|
+
if (entries.has(id)) {
|
|
239
|
+
try { entry.pty.kill("SIGKILL"); } catch {}
|
|
240
|
+
}
|
|
241
|
+
}, 1000);
|
|
242
|
+
const disposeEsc = entry.pty.onExit(() => {
|
|
243
|
+
clearTimeout(escalation);
|
|
244
|
+
disposeEsc.dispose();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fallback cleanup: if node-pty's onExit doesn't fire within 3s (common
|
|
249
|
+
// on Windows ConPTY after external termination), simulate it so the
|
|
250
|
+
// terminal entry is removed, clients are disconnected, and the server
|
|
251
|
+
// broadcasts terminal_removed. Without this, the X click never
|
|
252
|
+
// completes from the user's perspective.
|
|
253
|
+
const fallback = setTimeout(() => {
|
|
254
|
+
const stale = entries.get(id);
|
|
255
|
+
if (!stale) return; // onExit already ran
|
|
256
|
+
stale.session = { ...stale.session, status: "ended" };
|
|
257
|
+
for (const ws of stale.clients) {
|
|
258
|
+
try { ws.close(); } catch { /* ignore */ }
|
|
259
|
+
}
|
|
260
|
+
stale.clients.clear();
|
|
261
|
+
entries.delete(id);
|
|
262
|
+
options?.onExit?.(id);
|
|
263
|
+
}, 3000);
|
|
264
|
+
const disposeFb = entry.pty.onExit(() => {
|
|
265
|
+
clearTimeout(fallback);
|
|
266
|
+
disposeFb.dispose();
|
|
222
267
|
});
|
|
223
268
|
}
|
|
224
269
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defense-in-depth guard against destructive PID-registry sweeps during tests.
|
|
3
|
+
*
|
|
4
|
+
* Production startup code paths (headlessPidRegistry.cleanupOrphans/killAll,
|
|
5
|
+
* editorPidRegistry.cleanupOrphans) read PID files and send SIGTERM. If they
|
|
6
|
+
* ever run under vitest AGAINST the developer's real $HOME, they can kill
|
|
7
|
+
* live pi sessions.
|
|
8
|
+
*
|
|
9
|
+
* This guard returns true when:
|
|
10
|
+
* - we appear to be inside a vitest run (VITEST env var), AND
|
|
11
|
+
* - HOME still points at the real user home (tripwire missed).
|
|
12
|
+
*
|
|
13
|
+
* Callers SHOULD `console.warn` and return without performing destructive work
|
|
14
|
+
* when this returns true.
|
|
15
|
+
*
|
|
16
|
+
* Normal production servers (VITEST not set) always get `false` and behave as
|
|
17
|
+
* before.
|
|
18
|
+
*/
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
|
|
21
|
+
export function isUnsafeTestHomeScan(): boolean {
|
|
22
|
+
if (process.env.VITEST !== "true") return false;
|
|
23
|
+
const currentHome = process.env.HOME ?? "";
|
|
24
|
+
const realHome = os.userInfo().homedir;
|
|
25
|
+
return currentHome === "" || currentHome === realHome;
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createTestServer — boot a real DashboardServer on OS-assigned ports for
|
|
3
|
+
* integration tests, with safe defaults (no auto-shutdown, no tunnel).
|
|
4
|
+
*
|
|
5
|
+
* Use with the `setup-home` vitest setupFile (in @blackbelt-technology/pi-dashboard-shared/test-support)
|
|
6
|
+
* so that HOME is also isolated.
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* const { server, httpPort, piPort, stop } = await createTestServer();
|
|
10
|
+
* const res = await fetch(`http://localhost:${httpPort}/api/health`);
|
|
11
|
+
* ...
|
|
12
|
+
* await stop();
|
|
13
|
+
*/
|
|
14
|
+
import { createServer, type DashboardServer, type ServerConfig } from "../server.js";
|
|
15
|
+
|
|
16
|
+
export interface TestServerHandle {
|
|
17
|
+
server: DashboardServer;
|
|
18
|
+
httpPort: number;
|
|
19
|
+
piPort: number;
|
|
20
|
+
stop: () => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TestServerOverrides = Partial<ServerConfig>;
|
|
24
|
+
|
|
25
|
+
const DEFAULTS: ServerConfig = {
|
|
26
|
+
port: 0,
|
|
27
|
+
piPort: 0,
|
|
28
|
+
dev: true,
|
|
29
|
+
autoShutdown: false,
|
|
30
|
+
shutdownIdleSeconds: 999,
|
|
31
|
+
tunnel: false,
|
|
32
|
+
editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function createTestServer(
|
|
36
|
+
overrides: TestServerOverrides = {},
|
|
37
|
+
): Promise<TestServerHandle> {
|
|
38
|
+
const config: ServerConfig = { ...DEFAULTS, ...overrides };
|
|
39
|
+
const server = await createServer(config);
|
|
40
|
+
await server.start();
|
|
41
|
+
|
|
42
|
+
const httpPort = server.httpPort();
|
|
43
|
+
const piPort = server.piPort();
|
|
44
|
+
if (httpPort == null || piPort == null) {
|
|
45
|
+
await server.stop();
|
|
46
|
+
throw new Error(
|
|
47
|
+
`createTestServer: failed to resolve ports (httpPort=${httpPort}, piPort=${piPort})`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
server,
|
|
53
|
+
httpPort,
|
|
54
|
+
piPort,
|
|
55
|
+
stop: async () => {
|
|
56
|
+
try {
|
|
57
|
+
await server.stop();
|
|
58
|
+
} catch {
|
|
59
|
+
// best-effort — tests may race on shutdown
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import os from "node:os";
|
|
10
|
-
import { execSync, spawn, type ChildProcess } from "
|
|
10
|
+
import { execSync, spawn, type ChildProcess } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
11
|
+
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js";
|
|
12
|
+
import {
|
|
13
|
+
isProcessAlive,
|
|
14
|
+
killProcess,
|
|
15
|
+
killPidWithGroup,
|
|
16
|
+
} from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
|
|
17
|
+
|
|
18
|
+
const zrokResolver = new ToolResolver({ processExecPath: process.execPath });
|
|
11
19
|
import type { TunnelStatus } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
12
20
|
import { CONFIG_FILE } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
13
21
|
|
|
@@ -27,17 +35,20 @@ const SPAWN_TIMEOUT_MS = 30_000;
|
|
|
27
35
|
let activeProcess: ChildProcess | null = null;
|
|
28
36
|
let activeTunnelUrl: string | null = null;
|
|
29
37
|
let zrokAvailable: boolean | null = null;
|
|
38
|
+
// Serialization: any concurrent createTunnel() call while one is already in
|
|
39
|
+
// flight returns the same promise instead of spawning a second zrok process.
|
|
40
|
+
// Without this, a UI double-click or a race between startup auto-connect and
|
|
41
|
+
// an explicit `/api/tunnel-connect` created two parallel reservations and
|
|
42
|
+
// two running `zrok share` processes for the same port.
|
|
43
|
+
let pendingCreate: Promise<string | null> | null = null;
|
|
30
44
|
|
|
31
45
|
// ── Binary Detection ────────────────────────────────────────────────
|
|
32
46
|
|
|
33
47
|
function checkZrokOnPath(): boolean {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
48
|
+
// Delegate binary lookup to the shared platform primitive (handles the
|
|
49
|
+
// where/which split on Windows vs Unix, managed-bin search, and login
|
|
50
|
+
// shell fallback). See change: consolidate-platform-handlers.
|
|
51
|
+
return zrokResolver.which("zrok") !== null;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
/**
|
|
@@ -87,30 +98,22 @@ export function removeZrokPid(): void {
|
|
|
87
98
|
|
|
88
99
|
// ── Stale Process Cleanup ───────────────────────────────────────────
|
|
89
100
|
|
|
90
|
-
/**
|
|
91
|
-
* Check if a process is alive by sending signal 0.
|
|
92
|
-
*/
|
|
93
|
-
function isProcessAlive(pid: number): boolean {
|
|
94
|
-
try {
|
|
95
|
-
process.kill(pid, 0);
|
|
96
|
-
return true;
|
|
97
|
-
} catch {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
101
|
/**
|
|
103
102
|
* Clean up stale zrok processes from previous server runs.
|
|
104
|
-
* Reads PID file, kills process if running
|
|
103
|
+
* Reads PID file, kills process if running (via the platform helper so
|
|
104
|
+
* Windows uses `taskkill /F /T /PID`), removes PID file.
|
|
105
|
+
* See change: route-kill-paths-through-platform.
|
|
105
106
|
*/
|
|
106
|
-
export function cleanupStaleZrok(): void {
|
|
107
|
+
export async function cleanupStaleZrok(): Promise<void> {
|
|
107
108
|
const pid = readZrokPid();
|
|
108
109
|
if (pid === null) return;
|
|
109
110
|
|
|
110
111
|
if (isProcessAlive(pid)) {
|
|
111
112
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
const result = await killProcess(pid, { timeoutMs: 2000 });
|
|
114
|
+
if (result.ok) {
|
|
115
|
+
console.log(`Killed stale zrok process (PID ${pid})`);
|
|
116
|
+
}
|
|
114
117
|
} catch (err: any) {
|
|
115
118
|
console.warn(`Failed to kill stale zrok process (PID ${pid}): ${err.message}`);
|
|
116
119
|
}
|
|
@@ -160,6 +163,75 @@ function saveReservedToken(token: string): void {
|
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Release a reserved share via `zrok release <token>`. Best-effort, non-throwing.
|
|
168
|
+
* Returns true if the release command exited cleanly, false otherwise. Callers
|
|
169
|
+
* should invoke this whenever abandoning a reserved token so the zrok edge
|
|
170
|
+
* doesn't keep an orphaned reservation record (which is what causes stale
|
|
171
|
+
* URLs like `tgbdzzvlar6b.share.zrok.io` to persist after the agent dies).
|
|
172
|
+
*/
|
|
173
|
+
export function releaseShare(token: string): boolean {
|
|
174
|
+
if (!token) return false;
|
|
175
|
+
try {
|
|
176
|
+
execSync(`zrok release ${token}`, {
|
|
177
|
+
timeout: 10_000,
|
|
178
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
179
|
+
});
|
|
180
|
+
return true;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Scan `ps` for orphan `zrok share` processes that point at the given port
|
|
188
|
+
* via `--override-endpoint http://localhost:<port>` and SIGTERM them.
|
|
189
|
+
*
|
|
190
|
+
* This complements `cleanupStaleZrok` (which only knows about the single PID
|
|
191
|
+
* in our pid-file): when the retry logic in `createTunnel` leaks processes
|
|
192
|
+
* across failures, or when a previous server instance crashed, the pid-file
|
|
193
|
+
* loses track of them. On startup we scavenge them directly from the process
|
|
194
|
+
* table so a fresh tunnel doesn't compete with orphans.
|
|
195
|
+
*
|
|
196
|
+
* Returns the list of PIDs we killed.
|
|
197
|
+
*/
|
|
198
|
+
export function scavengeOrphanZrokProcesses(port: number): number[] {
|
|
199
|
+
const killed: number[] = [];
|
|
200
|
+
let output = "";
|
|
201
|
+
try {
|
|
202
|
+
output = execSync("ps -ax -o pid=,args=", {
|
|
203
|
+
encoding: "utf-8",
|
|
204
|
+
timeout: 5_000,
|
|
205
|
+
}).toString();
|
|
206
|
+
} catch {
|
|
207
|
+
return killed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const endpointMarker = `--override-endpoint http://localhost:${port}`;
|
|
211
|
+
for (const line of output.split(/\r?\n/)) {
|
|
212
|
+
const trimmed = line.trim();
|
|
213
|
+
if (!trimmed) continue;
|
|
214
|
+
if (!trimmed.includes("zrok share")) continue;
|
|
215
|
+
if (!trimmed.includes(endpointMarker)) continue;
|
|
216
|
+
const m = trimmed.match(/^(\d+)\s+/);
|
|
217
|
+
if (!m) continue;
|
|
218
|
+
const pid = parseInt(m[1], 10);
|
|
219
|
+
if (!Number.isFinite(pid) || pid <= 0) continue;
|
|
220
|
+
if (pid === process.pid) continue; // never kill ourselves
|
|
221
|
+
try {
|
|
222
|
+
// Group-kill on Unix so zrok's child workers die with it; taskkill /T
|
|
223
|
+
// already handles the tree on Windows (killPidWithGroup routes the
|
|
224
|
+
// platform-correct path).
|
|
225
|
+
killPidWithGroup(pid, "SIGTERM");
|
|
226
|
+
killed.push(pid);
|
|
227
|
+
console.log(`Scavenged orphan zrok process (PID ${pid})`);
|
|
228
|
+
} catch {
|
|
229
|
+
// Process may have exited between ps and kill — ignore
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return killed;
|
|
233
|
+
}
|
|
234
|
+
|
|
163
235
|
/**
|
|
164
236
|
* Create a reserved share via `zrok reserve public`.
|
|
165
237
|
* Returns the share token or null on failure.
|
|
@@ -195,7 +267,29 @@ function reserveShare(port: number): Promise<string | null> {
|
|
|
195
267
|
* On subsequent runs, reuses the reserved token.
|
|
196
268
|
* Returns URL or null on failure.
|
|
197
269
|
*/
|
|
198
|
-
export function createTunnel(
|
|
270
|
+
export function createTunnel(
|
|
271
|
+
port: number,
|
|
272
|
+
reservedToken?: string,
|
|
273
|
+
retriesLeft: number = 1,
|
|
274
|
+
): Promise<string | null> {
|
|
275
|
+
// Fast path: another caller is already creating a tunnel — join that promise.
|
|
276
|
+
if (pendingCreate) return pendingCreate;
|
|
277
|
+
// Fast path: tunnel already up — return its URL without spawning.
|
|
278
|
+
if (activeTunnelUrl) return Promise.resolve(activeTunnelUrl);
|
|
279
|
+
|
|
280
|
+
const promise = _createTunnelInner(port, reservedToken, retriesLeft);
|
|
281
|
+
pendingCreate = promise;
|
|
282
|
+
promise.finally(() => {
|
|
283
|
+
if (pendingCreate === promise) pendingCreate = null;
|
|
284
|
+
});
|
|
285
|
+
return promise;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function _createTunnelInner(
|
|
289
|
+
port: number,
|
|
290
|
+
reservedToken?: string,
|
|
291
|
+
retriesLeft: number = 1,
|
|
292
|
+
): Promise<string | null> {
|
|
199
293
|
return new Promise(async (resolve) => {
|
|
200
294
|
if (!detectZrokBinary()) {
|
|
201
295
|
resolve(null);
|
|
@@ -209,7 +303,11 @@ export function createTunnel(port: number, reservedToken?: string): Promise<stri
|
|
|
209
303
|
return;
|
|
210
304
|
}
|
|
211
305
|
|
|
212
|
-
//
|
|
306
|
+
// Track whether this call reserved the token itself (so we know to
|
|
307
|
+
// release it if we subsequently time out or fail — the caller-provided
|
|
308
|
+
// `reservedToken` is owned by the caller / config and must not be released
|
|
309
|
+
// on transient timeouts).
|
|
310
|
+
const callerProvidedToken = !!reservedToken;
|
|
213
311
|
let token = reservedToken;
|
|
214
312
|
if (!token) {
|
|
215
313
|
token = await reserveShare(port) ?? undefined;
|
|
@@ -228,12 +326,27 @@ export function createTunnel(port: number, reservedToken?: string): Promise<stri
|
|
|
228
326
|
detached: false,
|
|
229
327
|
});
|
|
230
328
|
|
|
231
|
-
// Timeout: kill process if URL not parsed in time
|
|
329
|
+
// Timeout: kill process if URL not parsed in time. Escalate SIGTERM
|
|
330
|
+
// → SIGKILL after a grace period so a wedged zrok doesn't keep a stale
|
|
331
|
+
// reservation attached after we've moved on. If we reserved the token
|
|
332
|
+
// just-in-time within this call, release it on the zrok edge too so we
|
|
333
|
+
// don't leak a dead reservation (root cause of stale URLs like
|
|
334
|
+
// `tgbdzzvlar6b.share.zrok.io`).
|
|
232
335
|
const timeout = setTimeout(() => {
|
|
233
336
|
if (!resolved) {
|
|
234
337
|
resolved = true;
|
|
235
338
|
console.warn("zrok tunnel creation timed out (30s)");
|
|
236
|
-
try {
|
|
339
|
+
try {
|
|
340
|
+
if (child.pid != null) killPidWithGroup(child.pid, "SIGTERM");
|
|
341
|
+
else child.kill("SIGTERM");
|
|
342
|
+
} catch { /* already dead */ }
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
try {
|
|
345
|
+
if (child.pid != null) killPidWithGroup(child.pid, "SIGKILL");
|
|
346
|
+
else child.kill("SIGKILL");
|
|
347
|
+
} catch { /* already dead */ }
|
|
348
|
+
}, 2_000);
|
|
349
|
+
if (token && !callerProvidedToken) releaseShare(token);
|
|
237
350
|
removeZrokPid();
|
|
238
351
|
resolve(null);
|
|
239
352
|
}
|
|
@@ -270,10 +383,20 @@ export function createTunnel(port: number, reservedToken?: string): Promise<stri
|
|
|
270
383
|
if (!resolved) {
|
|
271
384
|
resolved = true;
|
|
272
385
|
clearTimeout(timeout);
|
|
273
|
-
// If reserved share failed, token may be expired
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
386
|
+
// If reserved share failed, token may be expired or already attached
|
|
387
|
+
// to an orphan process. Release it on the zrok edge before retrying so
|
|
388
|
+
// we don't leak dead reservations (which is what produced stale URLs
|
|
389
|
+
// like `tgbdzzvlar6b.share.zrok.io` pointing at nothing).
|
|
390
|
+
if (token && retriesLeft > 0) {
|
|
391
|
+
console.warn(`Reserved share failed (code ${code}), releasing token ${token} and creating new reservation...`);
|
|
392
|
+
releaseShare(token);
|
|
393
|
+
// Bypass the mutex wrapper so we don't self-deadlock: call the inner
|
|
394
|
+
// implementation directly for the retry attempt.
|
|
395
|
+
resolve(_createTunnelInner(port, undefined, retriesLeft - 1));
|
|
396
|
+
} else if (token) {
|
|
397
|
+
console.warn(`Reserved share failed (code ${code}) and retry budget exhausted; releasing token ${token}`);
|
|
398
|
+
releaseShare(token);
|
|
399
|
+
resolve(null);
|
|
277
400
|
} else {
|
|
278
401
|
console.warn(`zrok process exited before producing URL (code ${code})`);
|
|
279
402
|
resolve(null);
|
|
@@ -291,20 +414,35 @@ export function createTunnel(port: number, reservedToken?: string): Promise<stri
|
|
|
291
414
|
|
|
292
415
|
/**
|
|
293
416
|
* Stop the active tunnel. Kills the subprocess and removes PID file.
|
|
417
|
+
* Also sweeps any orphan zrok processes bound to the given port so restart
|
|
418
|
+
* paths (which call `deleteTunnel` then spawn a new server) don't leave
|
|
419
|
+
* dead reservations attached to the zrok edge.
|
|
294
420
|
*/
|
|
295
|
-
export async function deleteTunnel(): Promise<void> {
|
|
421
|
+
export async function deleteTunnel(port?: number): Promise<void> {
|
|
296
422
|
const child = activeProcess;
|
|
297
423
|
activeProcess = null;
|
|
298
424
|
activeTunnelUrl = null;
|
|
299
425
|
|
|
300
426
|
if (child) {
|
|
301
427
|
try {
|
|
302
|
-
child.
|
|
428
|
+
if (child.pid != null) {
|
|
429
|
+
// Route through the platform helper so Windows gets taskkill
|
|
430
|
+
// semantics (tree-kill). See change: route-kill-paths-through-platform.
|
|
431
|
+
await killProcess(child.pid, { timeoutMs: 2000 });
|
|
432
|
+
} else {
|
|
433
|
+
child.kill("SIGTERM");
|
|
434
|
+
}
|
|
303
435
|
} catch (err: any) {
|
|
304
436
|
console.warn(`zrok tunnel cleanup failed: ${err.message}`);
|
|
305
437
|
}
|
|
306
438
|
}
|
|
307
439
|
removeZrokPid();
|
|
440
|
+
|
|
441
|
+
// Belt-and-braces: sweep any orphan zrok processes that escaped pid-file
|
|
442
|
+
// tracking (e.g. from a previous crash or a failed retry chain).
|
|
443
|
+
if (typeof port === "number") {
|
|
444
|
+
try { scavengeOrphanZrokProcesses(port); } catch { /* best-effort */ }
|
|
445
|
+
}
|
|
308
446
|
}
|
|
309
447
|
|
|
310
448
|
/**
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackbelt-technology/pi-dashboard-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared types and utilities for pi-dashboard",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
6
9
|
"exports": {
|
|
7
10
|
"./*.js": "./src/*.ts",
|
|
8
11
|
"./*": "./src/*"
|
|
@@ -10,6 +13,10 @@
|
|
|
10
13
|
"files": [
|
|
11
14
|
"src/"
|
|
12
15
|
],
|
|
13
|
-
"dependencies": {
|
|
14
|
-
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"bonjour-service": "^1.3.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"memfs": "^4.57.2"
|
|
21
|
+
}
|
|
15
22
|
}
|