@code-yeongyu/senpi 2026.9.24-3 → 2026.9.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/bundle/chunks/{app-server-command-OTRHYRIB.js → app-server-command-RB6QUPRI.js} +1 -1
- package/dist/bundle/chunks/{chunk-UM43CBL4.js → chunk-JJ3NJZBD.js} +118 -111
- package/dist/bundle/chunks/chunk-SGYEOIOJ.js +8 -0
- package/dist/bundle/chunks/chunk-YS5YEX4S.js +2 -0
- package/dist/bundle/chunks/{cli-main-TX6I4X4F.js → cli-main-KO53KYDN.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-UI5Q6FGY.js → interactive-mode-ABDNLJSI.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-D6HMI3RQ.js → multi-session-host-JV3MNWXZ.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-3UWH3ZZS.js → package-manager-cli-ZYYAVCXU.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-LXIZHJSJ.js → rpc-mode-Q2OUJURB.js} +1 -1
- package/dist/bundle/chunks/runtime-session-XBT646O5.js +2 -0
- package/dist/bundle/chunks/session-worker.js +184 -177
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/builtin/cursor-cli-oauth/stream.d.ts +7 -0
- package/dist/core/extensions/builtin/cursor-cli-oauth/stream.d.ts.map +1 -1
- package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js +23 -4
- package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/durable-command.d.ts +11 -16
- package/dist/core/extensions/builtin/terminal/durable-command.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/durable-command.js +97 -43
- package/dist/core/extensions/builtin/terminal/durable-command.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/durable-file.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/durable-file.js +11 -4
- package/dist/core/extensions/builtin/terminal/durable-file.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/extension-state.d.ts +26 -0
- package/dist/core/extensions/builtin/terminal/extension-state.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/extension-state.js +151 -0
- package/dist/core/extensions/builtin/terminal/extension-state.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/extension.js +32 -308
- package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/lease-file.d.ts +52 -0
- package/dist/core/extensions/builtin/terminal/lease-file.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/lease-file.js +205 -0
- package/dist/core/extensions/builtin/terminal/lease-file.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/lease-keeper.d.ts +33 -0
- package/dist/core/extensions/builtin/terminal/lease-keeper.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/lease-keeper.js +110 -0
- package/dist/core/extensions/builtin/terminal/lease-keeper.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/manager.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/manager.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/manager.js +6 -0
- package/dist/core/extensions/builtin/terminal/manager.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/manifest-lease.d.ts +48 -4
- package/dist/core/extensions/builtin/terminal/manifest-lease.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/manifest-lease.js +141 -59
- package/dist/core/extensions/builtin/terminal/manifest-lease.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/monitor-registry.js +15 -10
- package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/monitor-state-dir.d.ts +11 -0
- package/dist/core/extensions/builtin/terminal/monitor-state-dir.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/monitor-state-dir.js +28 -0
- package/dist/core/extensions/builtin/terminal/monitor-state-dir.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/orphan-reaper.d.ts +41 -0
- package/dist/core/extensions/builtin/terminal/orphan-reaper.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/orphan-reaper.js +131 -0
- package/dist/core/extensions/builtin/terminal/orphan-reaper.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/process-identity.d.ts +21 -0
- package/dist/core/extensions/builtin/terminal/process-identity.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/process-identity.js +34 -0
- package/dist/core/extensions/builtin/terminal/process-identity.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/process-start-probe.d.ts +6 -0
- package/dist/core/extensions/builtin/terminal/process-start-probe.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/process-start-probe.js +60 -0
- package/dist/core/extensions/builtin/terminal/process-start-probe.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/prompt.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/prompt.js +8 -4
- package/dist/core/extensions/builtin/terminal/prompt.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/restore-digest.d.ts +54 -0
- package/dist/core/extensions/builtin/terminal/restore-digest.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/restore-digest.js +133 -0
- package/dist/core/extensions/builtin/terminal/restore-digest.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/restore-session.d.ts +56 -0
- package/dist/core/extensions/builtin/terminal/restore-session.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/restore-session.js +238 -0
- package/dist/core/extensions/builtin/terminal/restore-session.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/restore.d.ts +52 -19
- package/dist/core/extensions/builtin/terminal/restore.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/restore.js +63 -138
- package/dist/core/extensions/builtin/terminal/restore.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +5 -0
- package/dist/core/extensions/builtin/terminal/runtime-session.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/runtime-session.js +17 -0
- package/dist/core/extensions/builtin/terminal/runtime-session.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/session-activity.d.ts +8 -0
- package/dist/core/extensions/builtin/terminal/session-activity.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/session-activity.js +41 -0
- package/dist/core/extensions/builtin/terminal/session-activity.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/shared.d.ts +14 -0
- package/dist/core/extensions/builtin/terminal/shared.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/shared.js +14 -0
- package/dist/core/extensions/builtin/terminal/shared.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +8 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.d.ts +15 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +148 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest.d.ts +15 -2
- package/dist/core/extensions/builtin/terminal/terminal-manifest.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-manifest.js +62 -11
- package/dist/core/extensions/builtin/terminal/terminal-manifest.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-state-gc.d.ts +17 -0
- package/dist/core/extensions/builtin/terminal/terminal-state-gc.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/terminal-state-gc.js +81 -0
- package/dist/core/extensions/builtin/terminal/terminal-state-gc.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts +5 -0
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/context.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/kill-bash.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/kill-bash.js +16 -1
- package/dist/core/extensions/builtin/terminal/tools/kill-bash.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/monitor-manifest-binding.d.ts +27 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-manifest-binding.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-manifest-binding.js +75 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-manifest-binding.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts +35 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js +67 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +14 -35
- package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/monitor.js +30 -110
- package/dist/core/extensions/builtin/terminal/tools/monitor.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/first-turn.d.ts +7 -6
- package/dist/core/extensions/builtin/todotools/first-turn.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/first-turn.js +8 -8
- package/dist/core/extensions/builtin/todotools/first-turn.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/index.js +1 -1
- package/dist/core/extensions/builtin/todotools/index.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/todo-format.js +1 -1
- package/dist/core/extensions/builtin/todotools/todo-format.js.map +1 -1
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.js +4 -0
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.js.map +1 -1
- package/dist/core/extensions/builtin/ttsr/prompts.js +1 -1
- package/dist/core/extensions/builtin/ttsr/prompts.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +6 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/docs/extensions.md +2 -0
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-desktop-engine/README.md +31 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.d.ts +38 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.js +117 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.d.ts +3 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.js +3 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.d.ts +42 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.js +79 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/check-prebuild-fresh.mjs +160 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/index.d.ts +20 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/index.js +23 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/prebuilds/darwin-arm64/senpi-desktop-engine +0 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/package.json +53 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/README.md +20 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.d.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/package.json +44 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/README.md +20 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.d.ts +38 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.d.ts +28 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.js +102 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.d.ts +14 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.js +71 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.d.ts +7 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.js +4 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.d.ts +66 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.js +8 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.d.ts +44 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.d.ts +59 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/package.json +45 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/README.md +27 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.d.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/package.json +44 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/README.md +20 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.d.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/package.json +44 -0
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cerebras.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.d.ts +2 -0
- package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.js +6 -0
- package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-bun.d.ts +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-bun.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-bun.js +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-bun.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-native.js +5 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-native.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-raw-tail.d.ts +11 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-raw-tail.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-raw-tail.js +24 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-raw-tail.js.map +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-types.d.ts +2 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-types.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session.d.ts +4 -5
- package/node_modules/@earendil-works/pi-pty/dist/session.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session.js +11 -16
- package/node_modules/@earendil-works/pi-pty/dist/session.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/native/prebuilds/darwin-arm64/senpi_pty.darwin-arm64.node +0 -0
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +22 -7
- package/dist/bundle/chunks/chunk-GFUZPFHN.js +0 -2
- package/dist/bundle/chunks/chunk-JBOV6VNK.js +0 -8
- package/dist/bundle/chunks/runtime-session-F3EXC44D.js +0 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-level primitives for the terminal lease. On a filesystem with hard links a lease file is
|
|
3
|
+
* never visible half-written: it is written to a private temp file first and then published with
|
|
4
|
+
* `link` (exclusive: fails with EEXIST when a lease exists) or `rename` (replace). Every removal of
|
|
5
|
+
* a lease, by an acquire or by the GC, goes through `reclaimInspected`: under an exclusive
|
|
6
|
+
* `<lease>.lock`, and only while the file still holds exactly the record that was judged stale, so
|
|
7
|
+
* a fresh lease another process just published is never deleted. A reclaim lock records the
|
|
8
|
+
* holder's identity (pid, boot and start instant); it is broken only when that process is gone or its
|
|
9
|
+
* pid now belongs to another process, and only if it still holds the record that was judged stale.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { link, open, readFile, rename, stat, unlink } from "node:fs/promises";
|
|
13
|
+
import { ownProcessStartedAtMs, processBootAtMs, sameBoot, sameProcessStart } from "./process-identity.js";
|
|
14
|
+
import { readProcessStartMs as defaultReadProcessStartMs } from "./process-start-probe.js";
|
|
15
|
+
export function errorCode(error) {
|
|
16
|
+
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") {
|
|
17
|
+
return error.code;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
export async function unlinkIfPresent(path) {
|
|
22
|
+
try {
|
|
23
|
+
await unlink(path);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (errorCode(error) !== "ENOENT")
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function readLeaseText(path) {
|
|
31
|
+
try {
|
|
32
|
+
return await readFile(path, "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (errorCode(error) === "ENOENT")
|
|
36
|
+
return undefined;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function writeTemp(path, content) {
|
|
41
|
+
const temp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
42
|
+
const file = await open(temp, "wx");
|
|
43
|
+
try {
|
|
44
|
+
await file.writeFile(content, "utf8");
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await file.close();
|
|
48
|
+
}
|
|
49
|
+
return temp;
|
|
50
|
+
}
|
|
51
|
+
/** Publish `content` at `path` only if nothing is there; throws EEXIST otherwise. Never half-written. */
|
|
52
|
+
export async function publishExclusive(path, content) {
|
|
53
|
+
const temp = await writeTemp(path, content);
|
|
54
|
+
try {
|
|
55
|
+
await link(temp, path);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const code = errorCode(error);
|
|
59
|
+
if (code !== "ENOTSUP" && code !== "EOPNOTSUPP" && code !== "EPERM")
|
|
60
|
+
throw error;
|
|
61
|
+
// A filesystem without hard links: fall back to an exclusive create (briefly visible empty).
|
|
62
|
+
const file = await open(path, "wx");
|
|
63
|
+
try {
|
|
64
|
+
await file.writeFile(content, "utf8");
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await file.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await unlinkIfPresent(temp);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function publishReplace(path, content) {
|
|
75
|
+
const temp = await writeTemp(path, content);
|
|
76
|
+
try {
|
|
77
|
+
await rename(temp, path);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await unlinkIfPresent(temp);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function pidAlive(pid) {
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, 0);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return errorCode(error) === "EPERM";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** A lock that cannot even be parsed was left by a crash mid-write; past this age it is abandoned. */
|
|
94
|
+
const UNREADABLE_LOCK_STALE_MS = 30_000;
|
|
95
|
+
function readLockHolder(raw) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (!("pid" in parsed) || !("bootAtMs" in parsed) || !("processStartedAtMs" in parsed))
|
|
101
|
+
return undefined;
|
|
102
|
+
const { pid, bootAtMs, processStartedAtMs } = parsed;
|
|
103
|
+
if (typeof pid !== "number" || typeof bootAtMs !== "number" || typeof processStartedAtMs !== "number") {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return { pid, bootAtMs, processStartedAtMs };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Whether a reclaim lock is held by the very process that wrote it; a dead or reused pid is stale. */
|
|
113
|
+
export async function reclaimLockState(lock, probes = {}) {
|
|
114
|
+
const raw = await readLeaseText(lock);
|
|
115
|
+
if (raw === undefined)
|
|
116
|
+
return { state: "free" };
|
|
117
|
+
const holder = readLockHolder(raw);
|
|
118
|
+
if (holder === undefined) {
|
|
119
|
+
const age = await stat(lock).then((info) => Date.now() - info.mtimeMs, () => 0);
|
|
120
|
+
return age > UNREADABLE_LOCK_STALE_MS ? { state: "stale", raw } : { state: "held" };
|
|
121
|
+
}
|
|
122
|
+
if (!sameBoot(holder.bootAtMs, processBootAtMs()))
|
|
123
|
+
return { state: "stale", raw };
|
|
124
|
+
if (holder.pid === process.pid) {
|
|
125
|
+
// Our own pid: held only by this very process (another reclaim in flight here), never by a
|
|
126
|
+
// crashed earlier process whose pid this one now wears.
|
|
127
|
+
return sameProcessStart(holder.processStartedAtMs, ownProcessStartedAtMs())
|
|
128
|
+
? { state: "held", holder }
|
|
129
|
+
: { state: "stale", raw };
|
|
130
|
+
}
|
|
131
|
+
if (!(probes.isProcessAlive ?? pidAlive)(holder.pid))
|
|
132
|
+
return { state: "stale", raw };
|
|
133
|
+
const observed = await (probes.readProcessStartMs ?? defaultReadProcessStartMs)(holder.pid).catch(() => undefined);
|
|
134
|
+
if (observed === undefined || sameProcessStart(holder.processStartedAtMs, observed))
|
|
135
|
+
return { state: "held", holder };
|
|
136
|
+
return { state: "stale", raw };
|
|
137
|
+
}
|
|
138
|
+
/** Remove a stale lock only if it still holds `raw`; a lock someone took meanwhile is put back. */
|
|
139
|
+
export async function breakStaleLock(lock, raw) {
|
|
140
|
+
const aside = `${lock}.${process.pid}.${randomUUID().slice(0, 8)}.broken`;
|
|
141
|
+
try {
|
|
142
|
+
await rename(lock, aside);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (errorCode(error) === "ENOENT")
|
|
146
|
+
return;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
const moved = await readFile(aside, "utf8");
|
|
150
|
+
if (moved !== raw) {
|
|
151
|
+
try {
|
|
152
|
+
await publishExclusive(lock, moved);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (errorCode(error) !== "EEXIST")
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await unlinkIfPresent(aside);
|
|
160
|
+
}
|
|
161
|
+
async function takeReclaimLock(lock, probes) {
|
|
162
|
+
const mine = JSON.stringify({
|
|
163
|
+
pid: process.pid,
|
|
164
|
+
bootAtMs: processBootAtMs(),
|
|
165
|
+
processStartedAtMs: ownProcessStartedAtMs(),
|
|
166
|
+
});
|
|
167
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
168
|
+
try {
|
|
169
|
+
await publishExclusive(lock, mine);
|
|
170
|
+
return mine;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (errorCode(error) !== "EEXIST")
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const seen = await reclaimLockState(lock, probes);
|
|
177
|
+
if (seen.state === "held")
|
|
178
|
+
return { holder: seen.holder };
|
|
179
|
+
if (seen.state === "stale")
|
|
180
|
+
await breakStaleLock(lock, seen.raw);
|
|
181
|
+
}
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Remove the lease at `path` only if it still holds `inspected`: `removed`, `changed` (another
|
|
186
|
+
* process replaced it meanwhile; left alone), or `busy` with the live process reclaiming it now.
|
|
187
|
+
*/
|
|
188
|
+
export async function reclaimInspected(path, inspected, probes = {}) {
|
|
189
|
+
const lock = `${path}.lock`;
|
|
190
|
+
const taken = await takeReclaimLock(lock, probes);
|
|
191
|
+
if (typeof taken !== "string")
|
|
192
|
+
return { outcome: "busy", ...(taken.holder ? { holder: taken.holder } : {}) };
|
|
193
|
+
try {
|
|
194
|
+
if ((await readLeaseText(path)) !== inspected)
|
|
195
|
+
return { outcome: "changed" };
|
|
196
|
+
await unlinkIfPresent(path);
|
|
197
|
+
return { outcome: "removed" };
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
// Release only our own lock: one broken while we held it may now belong to someone else.
|
|
201
|
+
if ((await readLeaseText(lock)) === taken)
|
|
202
|
+
await unlinkIfPresent(lock);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=lease-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease-file.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/lease-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC3G,OAAO,EAAE,kBAAkB,IAAI,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAE3F,MAAM,UAAU,SAAS,CAAC,KAAc;IACvC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtG,OAAO,KAAK,CAAC,IAAI,CAAC;IACnB,CAAC;IACD,OAAO,SAAS,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IACjD,IAAI,CAAC;QACJ,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAC;IAChD,CAAC;AACF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC/C,IAAI,CAAC;QACJ,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACpD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,OAAe;IACrD,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC;IACtE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC;QACJ,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACvC,CAAC;YAAS,CAAC;QACV,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC;AAED,yGAAyG;AACzG,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY,EAAE,OAAe;IACnE,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC5C,IAAI,CAAC;QACJ,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QAC9B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,OAAO;YAAE,MAAM,KAAK,CAAC;QACjF,6FAA6F;QAC7F,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACvC,CAAC;gBAAS,CAAC;YACV,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;IACF,CAAC;YAAS,CAAC;QACV,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;AACF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,OAAe;IACjE,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC5C,IAAI,CAAC;QACJ,MAAM,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW;IAC5B,IAAI,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,SAAS,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC;IACrC,CAAC;AACF,CAAC;AAmBD,sGAAsG;AACtG,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAExC,SAAS,cAAc,CAAC,GAAW;IAClC,IAAI,CAAC;QACJ,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC;QACpE,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,IAAI,MAAM,CAAC;YAAE,OAAO,SAAS,CAAC;QACzG,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,kBAAkB,EAAE,GAAG,MAAM,CAAC;QACrD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,OAAO,kBAAkB,KAAK,QAAQ,EAAE,CAAC;YACvG,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY,EAAE,MAAM,GAAe,EAAE;IAC3E,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChD,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAChC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,EACnC,GAAG,EAAE,CAAC,CAAC,CACP,CAAC;QACF,OAAO,GAAG,GAAG,wBAAwB,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACrF,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,eAAe,EAAE,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;IAClF,IAAI,MAAM,CAAC,GAAG,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC;QAChC,2FAA2F;QAC3F,wDAAwD;QACxD,OAAO,gBAAgB,CAAC,MAAM,CAAC,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;YAC1E,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;YAC3B,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;IAC5B,CAAC;IACD,IAAI,CAAC,CAAC,MAAM,CAAC,cAAc,IAAI,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;IACrF,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,kBAAkB,IAAI,yBAAyB,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACnH,IAAI,QAAQ,KAAK,SAAS,IAAI,gBAAgB,CAAC,MAAM,CAAC,kBAAkB,EAAE,QAAQ,CAAC;QAClF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAClC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAChC,CAAC;AAED,mGAAmG;AACnG,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,GAAW;IAC7D,MAAM,KAAK,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IAC1E,IAAI,CAAC;QACJ,MAAM,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;YAAE,OAAO;QAC1C,MAAM,KAAK,CAAC;IACb,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC5C,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC;YACJ,MAAM,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAC;QAChD,CAAC;IACF,CAAC;IACD,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,MAAkB;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3B,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,QAAQ,EAAE,eAAe,EAAE;QAC3B,kBAAkB,EAAE,qBAAqB,EAAE;KAC3C,CAAC,CAAC;IACH,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QACjD,IAAI,CAAC;YACJ,MAAM,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACnC,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAC;QAChD,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAClD,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1D,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO;YAAE,MAAM,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,EAAE,CAAC;AACX,CAAC;AAMD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,IAAY,EACZ,SAAiB,EACjB,MAAM,GAAe,EAAE;IAEvB,MAAM,IAAI,GAAG,GAAG,IAAI,OAAO,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAC7G,IAAI,CAAC;QACJ,IAAI,CAAC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,SAAS;YAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;QAC7E,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;QAC5B,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC;YAAS,CAAC;QACV,yFAAyF;QACzF,IAAI,CAAC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK;YAAE,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;IACxE,CAAC;AACF,CAAC","sourcesContent":["/**\n * File-level primitives for the terminal lease. On a filesystem with hard links a lease file is\n * never visible half-written: it is written to a private temp file first and then published with\n * `link` (exclusive: fails with EEXIST when a lease exists) or `rename` (replace). Every removal of\n * a lease, by an acquire or by the GC, goes through `reclaimInspected`: under an exclusive\n * `<lease>.lock`, and only while the file still holds exactly the record that was judged stale, so\n * a fresh lease another process just published is never deleted. A reclaim lock records the\n * holder's identity (pid, boot and start instant); it is broken only when that process is gone or its\n * pid now belongs to another process, and only if it still holds the record that was judged stale.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { link, open, readFile, rename, stat, unlink } from \"node:fs/promises\";\nimport { ownProcessStartedAtMs, processBootAtMs, sameBoot, sameProcessStart } from \"./process-identity.ts\";\nimport { readProcessStartMs as defaultReadProcessStartMs } from \"./process-start-probe.ts\";\n\nexport function errorCode(error: unknown): string | undefined {\n\tif (typeof error === \"object\" && error !== null && \"code\" in error && typeof error.code === \"string\") {\n\t\treturn error.code;\n\t}\n\treturn undefined;\n}\n\nexport async function unlinkIfPresent(path: string): Promise<void> {\n\ttry {\n\t\tawait unlink(path);\n\t} catch (error) {\n\t\tif (errorCode(error) !== \"ENOENT\") throw error;\n\t}\n}\n\nexport async function readLeaseText(path: string): Promise<string | undefined> {\n\ttry {\n\t\treturn await readFile(path, \"utf8\");\n\t} catch (error) {\n\t\tif (errorCode(error) === \"ENOENT\") return undefined;\n\t\tthrow error;\n\t}\n}\n\nasync function writeTemp(path: string, content: string): Promise<string> {\n\tconst temp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;\n\tconst file = await open(temp, \"wx\");\n\ttry {\n\t\tawait file.writeFile(content, \"utf8\");\n\t} finally {\n\t\tawait file.close();\n\t}\n\treturn temp;\n}\n\n/** Publish `content` at `path` only if nothing is there; throws EEXIST otherwise. Never half-written. */\nexport async function publishExclusive(path: string, content: string): Promise<void> {\n\tconst temp = await writeTemp(path, content);\n\ttry {\n\t\tawait link(temp, path);\n\t} catch (error) {\n\t\tconst code = errorCode(error);\n\t\tif (code !== \"ENOTSUP\" && code !== \"EOPNOTSUPP\" && code !== \"EPERM\") throw error;\n\t\t// A filesystem without hard links: fall back to an exclusive create (briefly visible empty).\n\t\tconst file = await open(path, \"wx\");\n\t\ttry {\n\t\t\tawait file.writeFile(content, \"utf8\");\n\t\t} finally {\n\t\t\tawait file.close();\n\t\t}\n\t} finally {\n\t\tawait unlinkIfPresent(temp);\n\t}\n}\n\nexport async function publishReplace(path: string, content: string): Promise<void> {\n\tconst temp = await writeTemp(path, content);\n\ttry {\n\t\tawait rename(temp, path);\n\t} catch (error) {\n\t\tawait unlinkIfPresent(temp);\n\t\tthrow error;\n\t}\n}\n\nfunction pidAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn errorCode(error) === \"EPERM\";\n\t}\n}\n\nexport interface LockProbes {\n\treadonly isProcessAlive?: (pid: number) => boolean;\n\treadonly readProcessStartMs?: (pid: number) => Promise<number | undefined>;\n}\n\n/** Who holds a reclaim lock: the same identity a lease records, so a reused pid is never mistaken for it. */\nexport interface ReclaimLockHolder {\n\treadonly pid: number;\n\treadonly bootAtMs: number;\n\treadonly processStartedAtMs: number;\n}\n\ntype LockState =\n\t| { readonly state: \"free\" }\n\t| { readonly state: \"held\"; readonly holder?: ReclaimLockHolder }\n\t| { readonly state: \"stale\"; readonly raw: string };\n\n/** A lock that cannot even be parsed was left by a crash mid-write; past this age it is abandoned. */\nconst UNREADABLE_LOCK_STALE_MS = 30_000;\n\nfunction readLockHolder(raw: string): ReclaimLockHolder | undefined {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(raw);\n\t\tif (typeof parsed !== \"object\" || parsed === null) return undefined;\n\t\tif (!(\"pid\" in parsed) || !(\"bootAtMs\" in parsed) || !(\"processStartedAtMs\" in parsed)) return undefined;\n\t\tconst { pid, bootAtMs, processStartedAtMs } = parsed;\n\t\tif (typeof pid !== \"number\" || typeof bootAtMs !== \"number\" || typeof processStartedAtMs !== \"number\") {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn { pid, bootAtMs, processStartedAtMs };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Whether a reclaim lock is held by the very process that wrote it; a dead or reused pid is stale. */\nexport async function reclaimLockState(lock: string, probes: LockProbes = {}): Promise<LockState> {\n\tconst raw = await readLeaseText(lock);\n\tif (raw === undefined) return { state: \"free\" };\n\tconst holder = readLockHolder(raw);\n\tif (holder === undefined) {\n\t\tconst age = await stat(lock).then(\n\t\t\t(info) => Date.now() - info.mtimeMs,\n\t\t\t() => 0,\n\t\t);\n\t\treturn age > UNREADABLE_LOCK_STALE_MS ? { state: \"stale\", raw } : { state: \"held\" };\n\t}\n\tif (!sameBoot(holder.bootAtMs, processBootAtMs())) return { state: \"stale\", raw };\n\tif (holder.pid === process.pid) {\n\t\t// Our own pid: held only by this very process (another reclaim in flight here), never by a\n\t\t// crashed earlier process whose pid this one now wears.\n\t\treturn sameProcessStart(holder.processStartedAtMs, ownProcessStartedAtMs())\n\t\t\t? { state: \"held\", holder }\n\t\t\t: { state: \"stale\", raw };\n\t}\n\tif (!(probes.isProcessAlive ?? pidAlive)(holder.pid)) return { state: \"stale\", raw };\n\tconst observed = await (probes.readProcessStartMs ?? defaultReadProcessStartMs)(holder.pid).catch(() => undefined);\n\tif (observed === undefined || sameProcessStart(holder.processStartedAtMs, observed))\n\t\treturn { state: \"held\", holder };\n\treturn { state: \"stale\", raw };\n}\n\n/** Remove a stale lock only if it still holds `raw`; a lock someone took meanwhile is put back. */\nexport async function breakStaleLock(lock: string, raw: string): Promise<void> {\n\tconst aside = `${lock}.${process.pid}.${randomUUID().slice(0, 8)}.broken`;\n\ttry {\n\t\tawait rename(lock, aside);\n\t} catch (error) {\n\t\tif (errorCode(error) === \"ENOENT\") return;\n\t\tthrow error;\n\t}\n\tconst moved = await readFile(aside, \"utf8\");\n\tif (moved !== raw) {\n\t\ttry {\n\t\t\tawait publishExclusive(lock, moved);\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"EEXIST\") throw error;\n\t\t}\n\t}\n\tawait unlinkIfPresent(aside);\n}\n\nasync function takeReclaimLock(lock: string, probes: LockProbes): Promise<string | { holder?: ReclaimLockHolder }> {\n\tconst mine = JSON.stringify({\n\t\tpid: process.pid,\n\t\tbootAtMs: processBootAtMs(),\n\t\tprocessStartedAtMs: ownProcessStartedAtMs(),\n\t});\n\tfor (let attempt = 0; attempt < 3; attempt += 1) {\n\t\ttry {\n\t\t\tawait publishExclusive(lock, mine);\n\t\t\treturn mine;\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"EEXIST\") throw error;\n\t\t}\n\t\tconst seen = await reclaimLockState(lock, probes);\n\t\tif (seen.state === \"held\") return { holder: seen.holder };\n\t\tif (seen.state === \"stale\") await breakStaleLock(lock, seen.raw);\n\t}\n\treturn {};\n}\n\nexport type ReclaimOutcome =\n\t| { readonly outcome: \"removed\" | \"changed\" }\n\t| { readonly outcome: \"busy\"; readonly holder?: ReclaimLockHolder };\n\n/**\n * Remove the lease at `path` only if it still holds `inspected`: `removed`, `changed` (another\n * process replaced it meanwhile; left alone), or `busy` with the live process reclaiming it now.\n */\nexport async function reclaimInspected(\n\tpath: string,\n\tinspected: string,\n\tprobes: LockProbes = {},\n): Promise<ReclaimOutcome> {\n\tconst lock = `${path}.lock`;\n\tconst taken = await takeReclaimLock(lock, probes);\n\tif (typeof taken !== \"string\") return { outcome: \"busy\", ...(taken.holder ? { holder: taken.holder } : {}) };\n\ttry {\n\t\tif ((await readLeaseText(path)) !== inspected) return { outcome: \"changed\" };\n\t\tawait unlinkIfPresent(path);\n\t\treturn { outcome: \"removed\" };\n\t} finally {\n\t\t// Release only our own lock: one broken while we held it may now belong to someone else.\n\t\tif ((await readLeaseText(lock)) === taken) await unlinkIfPresent(lock);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type AcquireTerminalLeaseResult, type LeaseHolder, type LeaseSelfIdentity } from "./manifest-lease.ts";
|
|
2
|
+
export declare const LEASE_KEEPER_INTERVAL_MS = 10000;
|
|
3
|
+
export type LeaseKeeperState = "idle" | "waiting" | "owner" | "stopped";
|
|
4
|
+
export interface LeaseKeeperOptions {
|
|
5
|
+
readonly dir: string;
|
|
6
|
+
readonly encodedSessionId: string;
|
|
7
|
+
readonly onTakeover: (lease: AcquireTerminalLeaseResult & {
|
|
8
|
+
acquired: true;
|
|
9
|
+
}) => void | Promise<void>;
|
|
10
|
+
/** A tick that failed (e.g. the state dir became unwritable); the keeper keeps waiting. */
|
|
11
|
+
readonly onError?: (error: unknown) => void;
|
|
12
|
+
readonly intervalMs?: number;
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
readonly self?: LeaseSelfIdentity;
|
|
15
|
+
readonly isProcessAlive?: (pid: number) => boolean;
|
|
16
|
+
readonly readProcessStartMs?: (pid: number) => Promise<number | undefined>;
|
|
17
|
+
}
|
|
18
|
+
export interface LeaseKeeper {
|
|
19
|
+
readonly state: LeaseKeeperState;
|
|
20
|
+
onTick?: () => void;
|
|
21
|
+
/** Wait on `holder`; null when it is unknown (e.g. the acquire failed), so every tick re-acquires. */
|
|
22
|
+
start(holder: LeaseHolder | null): void;
|
|
23
|
+
stop(): void;
|
|
24
|
+
/** Resolves after the tick currently in flight (if any) has settled; tests drain fake timers with it. */
|
|
25
|
+
settled(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The WAITER side of the lease: polls the file system only (stat + kill 0, never a subprocess)
|
|
29
|
+
* and hands the session over exactly once when the holder is gone. Losing the race to another
|
|
30
|
+
* waiter is not an error: this keeper simply keeps waiting on the new holder.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createLeaseKeeper(options: LeaseKeeperOptions): LeaseKeeper;
|
|
33
|
+
//# sourceMappingURL=lease-keeper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease-keeper.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/lease-keeper.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,KAAK,0BAA0B,EAE/B,KAAK,WAAW,EAChB,KAAK,iBAAiB,EAEtB,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAExE,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,0BAA0B,GAAG;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtG,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3E;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,sGAAsG;IACtG,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;IACxC,IAAI,IAAI,IAAI,CAAC;IACb,yGAAyG;IACzG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAqBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CA2E1E"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { acquireTerminalLease, releaseTerminalLease, } from "./manifest-lease.js";
|
|
4
|
+
export const LEASE_KEEPER_INTERVAL_MS = 10_000;
|
|
5
|
+
function isAlive(pid, probe) {
|
|
6
|
+
try {
|
|
7
|
+
if (probe)
|
|
8
|
+
return probe(pid);
|
|
9
|
+
process.kill(pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EPERM";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function leaseFileExists(path) {
|
|
17
|
+
try {
|
|
18
|
+
await stat(path);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The WAITER side of the lease: polls the file system only (stat + kill 0, never a subprocess)
|
|
27
|
+
* and hands the session over exactly once when the holder is gone. Losing the race to another
|
|
28
|
+
* waiter is not an error: this keeper simply keeps waiting on the new holder.
|
|
29
|
+
*/
|
|
30
|
+
export function createLeaseKeeper(options) {
|
|
31
|
+
const intervalMs = options.intervalMs ?? LEASE_KEEPER_INTERVAL_MS;
|
|
32
|
+
const path = join(options.dir, `${options.encodedSessionId}.lease`);
|
|
33
|
+
let state = "idle";
|
|
34
|
+
let holder = null;
|
|
35
|
+
let timer;
|
|
36
|
+
let inFlight = Promise.resolve();
|
|
37
|
+
const arm = () => {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
timer = undefined;
|
|
40
|
+
inFlight = tick().catch((error) => {
|
|
41
|
+
if (state === "waiting")
|
|
42
|
+
arm();
|
|
43
|
+
options.onError?.(error);
|
|
44
|
+
});
|
|
45
|
+
}, intervalMs);
|
|
46
|
+
timer.unref();
|
|
47
|
+
};
|
|
48
|
+
const tryAcquire = async () => {
|
|
49
|
+
const result = await acquireTerminalLease({
|
|
50
|
+
dir: options.dir,
|
|
51
|
+
encodedSessionId: options.encodedSessionId,
|
|
52
|
+
now: options.now,
|
|
53
|
+
self: options.self,
|
|
54
|
+
isProcessAlive: options.isProcessAlive,
|
|
55
|
+
readProcessStartMs: options.readProcessStartMs,
|
|
56
|
+
});
|
|
57
|
+
if (state !== "waiting") {
|
|
58
|
+
// stop() landed while the acquire was in flight: hand the lease straight back, or this
|
|
59
|
+
// process would hold a lease no generation of it can ever release or re-enter.
|
|
60
|
+
if (result.acquired)
|
|
61
|
+
await releaseTerminalLease(result);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (result.acquired) {
|
|
65
|
+
state = "owner";
|
|
66
|
+
timer = undefined;
|
|
67
|
+
await options.onTakeover(result);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
holder = result.holder;
|
|
71
|
+
arm();
|
|
72
|
+
};
|
|
73
|
+
const tick = async () => {
|
|
74
|
+
if (state !== "waiting")
|
|
75
|
+
return;
|
|
76
|
+
keeper.onTick?.();
|
|
77
|
+
const current = holder;
|
|
78
|
+
const gone = current === null || !(await leaseFileExists(path)) || !isAlive(current.pid, options.isProcessAlive);
|
|
79
|
+
if (state !== "waiting")
|
|
80
|
+
return;
|
|
81
|
+
if (gone) {
|
|
82
|
+
await tryAcquire();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
arm();
|
|
86
|
+
};
|
|
87
|
+
const keeper = {
|
|
88
|
+
get state() {
|
|
89
|
+
return state;
|
|
90
|
+
},
|
|
91
|
+
start(initialHolder) {
|
|
92
|
+
if (state === "stopped" || state === "owner")
|
|
93
|
+
return;
|
|
94
|
+
holder = initialHolder;
|
|
95
|
+
state = "waiting";
|
|
96
|
+
if (timer === undefined)
|
|
97
|
+
arm();
|
|
98
|
+
},
|
|
99
|
+
stop() {
|
|
100
|
+
if (timer !== undefined)
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
timer = undefined;
|
|
103
|
+
if (state !== "owner")
|
|
104
|
+
state = "stopped";
|
|
105
|
+
},
|
|
106
|
+
settled: () => inFlight,
|
|
107
|
+
};
|
|
108
|
+
return keeper;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=lease-keeper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease-keeper.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/lease-keeper.ts"],"names":[],"mappings":"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","sourcesContent":["import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n\ttype AcquireTerminalLeaseResult,\n\tacquireTerminalLease,\n\ttype LeaseHolder,\n\ttype LeaseSelfIdentity,\n\treleaseTerminalLease,\n} from \"./manifest-lease.ts\";\n\nexport const LEASE_KEEPER_INTERVAL_MS = 10_000;\n\nexport type LeaseKeeperState = \"idle\" | \"waiting\" | \"owner\" | \"stopped\";\n\nexport interface LeaseKeeperOptions {\n\treadonly dir: string;\n\treadonly encodedSessionId: string;\n\treadonly onTakeover: (lease: AcquireTerminalLeaseResult & { acquired: true }) => void | Promise<void>;\n\t/** A tick that failed (e.g. the state dir became unwritable); the keeper keeps waiting. */\n\treadonly onError?: (error: unknown) => void;\n\treadonly intervalMs?: number;\n\treadonly now?: () => number;\n\treadonly self?: LeaseSelfIdentity;\n\treadonly isProcessAlive?: (pid: number) => boolean;\n\treadonly readProcessStartMs?: (pid: number) => Promise<number | undefined>;\n}\n\nexport interface LeaseKeeper {\n\treadonly state: LeaseKeeperState;\n\tonTick?: () => void;\n\t/** Wait on `holder`; null when it is unknown (e.g. the acquire failed), so every tick re-acquires. */\n\tstart(holder: LeaseHolder | null): void;\n\tstop(): void;\n\t/** Resolves after the tick currently in flight (if any) has settled; tests drain fake timers with it. */\n\tsettled(): Promise<void>;\n}\n\nfunction isAlive(pid: number, probe?: (pid: number) => boolean): boolean {\n\ttry {\n\t\tif (probe) return probe(pid);\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"EPERM\";\n\t}\n}\n\nasync function leaseFileExists(path: string): Promise<boolean> {\n\ttry {\n\t\tawait stat(path);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * The WAITER side of the lease: polls the file system only (stat + kill 0, never a subprocess)\n * and hands the session over exactly once when the holder is gone. Losing the race to another\n * waiter is not an error: this keeper simply keeps waiting on the new holder.\n */\nexport function createLeaseKeeper(options: LeaseKeeperOptions): LeaseKeeper {\n\tconst intervalMs = options.intervalMs ?? LEASE_KEEPER_INTERVAL_MS;\n\tconst path = join(options.dir, `${options.encodedSessionId}.lease`);\n\tlet state: LeaseKeeperState = \"idle\";\n\tlet holder: LeaseHolder | null = null;\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tlet inFlight: Promise<void> = Promise.resolve();\n\n\tconst arm = (): void => {\n\t\ttimer = setTimeout(() => {\n\t\t\ttimer = undefined;\n\t\t\tinFlight = tick().catch((error: unknown) => {\n\t\t\t\tif (state === \"waiting\") arm();\n\t\t\t\toptions.onError?.(error);\n\t\t\t});\n\t\t}, intervalMs);\n\t\ttimer.unref();\n\t};\n\n\tconst tryAcquire = async (): Promise<void> => {\n\t\tconst result = await acquireTerminalLease({\n\t\t\tdir: options.dir,\n\t\t\tencodedSessionId: options.encodedSessionId,\n\t\t\tnow: options.now,\n\t\t\tself: options.self,\n\t\t\tisProcessAlive: options.isProcessAlive,\n\t\t\treadProcessStartMs: options.readProcessStartMs,\n\t\t});\n\t\tif (state !== \"waiting\") {\n\t\t\t// stop() landed while the acquire was in flight: hand the lease straight back, or this\n\t\t\t// process would hold a lease no generation of it can ever release or re-enter.\n\t\t\tif (result.acquired) await releaseTerminalLease(result);\n\t\t\treturn;\n\t\t}\n\t\tif (result.acquired) {\n\t\t\tstate = \"owner\";\n\t\t\ttimer = undefined;\n\t\t\tawait options.onTakeover(result);\n\t\t\treturn;\n\t\t}\n\t\tholder = result.holder;\n\t\tarm();\n\t};\n\n\tconst tick = async (): Promise<void> => {\n\t\tif (state !== \"waiting\") return;\n\t\tkeeper.onTick?.();\n\t\tconst current = holder;\n\t\tconst gone = current === null || !(await leaseFileExists(path)) || !isAlive(current.pid, options.isProcessAlive);\n\t\tif (state !== \"waiting\") return;\n\t\tif (gone) {\n\t\t\tawait tryAcquire();\n\t\t\treturn;\n\t\t}\n\t\tarm();\n\t};\n\n\tconst keeper: LeaseKeeper = {\n\t\tget state() {\n\t\t\treturn state;\n\t\t},\n\t\tstart(initialHolder) {\n\t\t\tif (state === \"stopped\" || state === \"owner\") return;\n\t\t\tholder = initialHolder;\n\t\t\tstate = \"waiting\";\n\t\t\tif (timer === undefined) arm();\n\t\t},\n\t\tstop() {\n\t\t\tif (timer !== undefined) clearTimeout(timer);\n\t\t\ttimer = undefined;\n\t\t\tif (state !== \"owner\") state = \"stopped\";\n\t\t},\n\t\tsettled: () => inFlight,\n\t};\n\treturn keeper;\n}\n"]}
|
|
@@ -39,6 +39,7 @@ export declare class TerminalManager {
|
|
|
39
39
|
* runtime id through unchanged; undefined when neither resolves. Bindings intentionally
|
|
40
40
|
* outlive PTY exit for a final output read, then drop when the session is pruned or torn down.
|
|
41
41
|
*/
|
|
42
|
+
monitorIdOf(runtimeId: string): string | undefined;
|
|
42
43
|
resolveId(idOrMonitorId: string): string | undefined;
|
|
43
44
|
list(): {
|
|
44
45
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEX,4BAA4B,EAE5B,sBAAsB,EACtB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAA0B,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG3F,YAAY,EAAE,4BAA4B,EAAE,CAAC;AAE7C,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;CACzC;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6C;IACtE,yHAAyH;IACzH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,YAAY,CAAK;IAEzB,YAAY,OAAO,GAAE,sBAA2B,EAG/C;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,UAAU,IAAI,MAAM,CAGvB;IAED,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAU7B;IAED,mFAAmF;IAC7E,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAyB9F;IAED,sEAAsE;IACtE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAM7C;IAED,+FAA+F;IAC/F,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAExD;IAED;;;;OAIG;IACH,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKnD;IAED,IAAI,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,sBAAsB,CAAA;KAAE,EAAE,CASxD;IAED,2FAA2F;IACrF,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMvC;IAED,gEAAgE;IAC1D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ9B;IAED,wFAAwF;IACxF,OAAO,CAAC,iBAAiB;CAczB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,4BAA4B,CAIrF"}
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEX,4BAA4B,EAE5B,sBAAsB,EACtB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAA0B,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG3F,YAAY,EAAE,4BAA4B,EAAE,CAAC;AAE7C,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;CACzC;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6C;IACtE,yHAAyH;IACzH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,YAAY,CAAK;IAEzB,YAAY,OAAO,GAAE,sBAA2B,EAG/C;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,UAAU,IAAI,MAAM,CAGvB;IAED,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAU7B;IAED,mFAAmF;IAC7E,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAyB9F;IAED,sEAAsE;IACtE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAM7C;IAED,+FAA+F;IAC/F,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAExD;IAED;;;;OAIG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjD;IAED,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKnD;IAED,IAAI,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,sBAAsB,CAAA;KAAE,EAAE,CASxD;IAED,2FAA2F;IACrF,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMvC;IAED,gEAAgE;IAC1D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ9B;IAED,wFAAwF;IACxF,OAAO,CAAC,iBAAiB;CAczB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,4BAA4B,CAIrF"}
|
|
@@ -84,6 +84,12 @@ export class TerminalManager {
|
|
|
84
84
|
* runtime id through unchanged; undefined when neither resolves. Bindings intentionally
|
|
85
85
|
* outlive PTY exit for a final output read, then drop when the session is pruned or torn down.
|
|
86
86
|
*/
|
|
87
|
+
monitorIdOf(runtimeId) {
|
|
88
|
+
for (const [monitorId, sessionId] of this.monitorIds)
|
|
89
|
+
if (sessionId === runtimeId)
|
|
90
|
+
return monitorId;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
87
93
|
resolveId(idOrMonitorId) {
|
|
88
94
|
if (idOrMonitorId.startsWith("mon_"))
|
|
89
95
|
return this.monitorIds.get(idOrMonitorId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tSessionRegistry,\n\tSessionRegistryCapacityError,\n\tTerminalSession,\n\tTerminalSessionOptions,\n} from \"@earendil-works/pi-pty\";\nimport { loadedPty, loadPty } from \"./pty.lazy.ts\";\nimport type { TerminalRuntimeOptions, TerminalRuntimeSession } from \"./runtime-session.ts\";\nimport { DEFAULT_MAX_SESSIONS } from \"./shared.ts\";\n\nexport type { SessionRegistryCapacityError };\n\nexport interface TerminalManagerOptions {\n\treadonly maxSessions?: number;\n\treadonly scrollback?: number;\n}\n\nexport interface CreatedTerminalSession {\n\treadonly id: string;\n\treadonly runtime: TerminalRuntimeSession;\n}\n\n/**\n * Owns the live terminal sessions for one agent session. Delegates id allocation,\n * capacity caps, LRU-exited pruning, and tree-kill teardown to the pi-pty\n * {@link SessionRegistry}, while keeping the richer {@link TerminalRuntimeSession}\n * wrappers (screen model + output buffer) in lock-step.\n */\nexport class TerminalManager {\n\tprivate registry: SessionRegistry<TerminalSession> | undefined;\n\tprivate readonly runtimes = new Map<string, TerminalRuntimeSession>();\n\t/** Stable \"mon_\" identities bound to runtime session ids; survives PTY exit, then drops on session prune or teardown. */\n\tprivate readonly monitorIds = new Map<string, string>();\n\tprivate readonly scrollback?: number;\n\tprivate readonly maxSessions: number;\n\tprivate readonly exited = new Set<string>();\n\tprivate reservations = 0;\n\n\tconstructor(options: TerminalManagerOptions = {}) {\n\t\tthis.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;\n\t\tthis.scrollback = options.scrollback;\n\t}\n\n\tget size(): number {\n\t\treturn this.runtimes.size;\n\t}\n\n\tget activeSize(): number {\n\t\tthis.reconcileRuntimes();\n\t\treturn this.runtimes.size - this.exited.size + this.reservations;\n\t}\n\n\treserve(): (() => void) | null {\n\t\tthis.reconcileRuntimes();\n\t\tif (this.runtimes.size - this.exited.size + this.reservations >= this.maxSessions) return null;\n\t\tthis.reservations += 1;\n\t\tlet released = false;\n\t\treturn () => {\n\t\t\tif (released) return;\n\t\t\treleased = true;\n\t\t\tthis.reservations -= 1;\n\t\t};\n\t}\n\n\t/** Spawn a new terminal session and register it under an allocated `bash_N` id. */\n\tasync create(command: string, options: TerminalSessionOptions): Promise<CreatedTerminalSession> {\n\t\tconst pty = await loadPty();\n\t\tconst release = this.reserve();\n\t\tif (!release) throw new pty.SessionRegistryCapacityError(this.maxSessions);\n\t\tthis.registry ??= new pty.SessionRegistry<TerminalSession>({ maxSessions: this.maxSessions });\n\t\tconst registry = this.registry;\n\t\tconst runtimeOptions: TerminalRuntimeOptions = { ...options, scrollback: this.scrollback };\n\t\tconst { TerminalRuntimeSession } = await import(\"./runtime-session.ts\");\n\t\tconst runtime = new TerminalRuntimeSession(command, runtimeOptions);\n\t\tlet entry: { id: string };\n\t\ttry {\n\t\t\tentry = await registry.create({ command, session: runtime.session });\n\t\t} catch (error) {\n\t\t\trelease();\n\t\t\truntime.dispose();\n\t\t\truntime.session.kill();\n\t\t\tthrow error;\n\t\t}\n\t\tthis.runtimes.set(entry.id, runtime);\n\t\truntime.session.onExit(() => {\n\t\t\tthis.exited.add(entry.id);\n\t\t});\n\t\trelease();\n\t\tthis.reconcileRuntimes();\n\t\treturn { id: entry.id, runtime };\n\t}\n\n\t/** Look up a live-or-exited session, refreshing its LRU timestamp. */\n\tget(id: string): TerminalRuntimeSession | null {\n\t\tconst registry = this.registry;\n\t\tif (!registry) return null;\n\t\tconst entry = registry.get(id);\n\t\tif (!entry) return null;\n\t\treturn this.runtimes.get(id) ?? null;\n\t}\n\n\t/** Bind a stable \"mon_\" id to its runtime session id; a later restore re-binds the same id. */\n\tbindMonitorId(monitorId: string, sessionId: string): void {\n\t\tthis.monitorIds.set(monitorId, sessionId);\n\t}\n\n\t/**\n\t * Resolve a \"mon_\" monitor id to its current runtime \"bash_N\" (or \"watch_N\"), passing a\n\t * runtime id through unchanged; undefined when neither resolves. Bindings intentionally\n\t * outlive PTY exit for a final output read, then drop when the session is pruned or torn down.\n\t */\n\tresolveId(idOrMonitorId: string): string | undefined {\n\t\tif (idOrMonitorId.startsWith(\"mon_\")) return this.monitorIds.get(idOrMonitorId);\n\t\tconst registry = this.registry;\n\t\tif (!registry) return undefined;\n\t\treturn registry.get(idOrMonitorId) ? idOrMonitorId : undefined;\n\t}\n\n\tlist(): { id: string; runtime: TerminalRuntimeSession }[] {\n\t\tconst registry = this.registry;\n\t\tif (!registry) return [];\n\t\tconst result: { id: string; runtime: TerminalRuntimeSession }[] = [];\n\t\tfor (const entry of registry.list()) {\n\t\t\tconst runtime = this.runtimes.get(entry.id);\n\t\t\tif (runtime) result.push({ id: entry.id, runtime });\n\t\t}\n\t\treturn result;\n\t}\n\n\t/** Tree-kill one session; the exited entry is kept for a final output read until swept. */\n\tasync stop(id: string): Promise<boolean> {\n\t\tconst registry = this.registry;\n\t\tif (!registry) return false;\n\t\tconst stopped = await registry.stop(id);\n\t\tthis.reconcileRuntimes();\n\t\treturn stopped;\n\t}\n\n\t/** Tree-kill every session and dispose all runtime wrappers. */\n\tasync teardown(): Promise<void> {\n\t\tconst registry = this.registry;\n\t\tif (registry) await registry.teardown();\n\t\tthis.registry = undefined;\n\t\tfor (const runtime of this.runtimes.values()) runtime.dispose();\n\t\tthis.runtimes.clear();\n\t\tthis.exited.clear();\n\t\tthis.monitorIds.clear();\n\t}\n\n\t/** Dispose runtime wrappers whose registry entry was pruned (capacity/LRU eviction). */\n\tprivate reconcileRuntimes(): void {\n\t\tconst registry = this.registry;\n\t\tif (!registry) return;\n\t\tconst liveIds = new Set(registry.list().map((entry) => entry.id));\n\t\tfor (const [id, runtime] of this.runtimes) {\n\t\t\tif (liveIds.has(id)) continue;\n\t\t\truntime.dispose();\n\t\t\tthis.runtimes.delete(id);\n\t\t\tthis.exited.delete(id);\n\t\t\tfor (const [monitorId, sessionId] of this.monitorIds) {\n\t\t\t\tif (sessionId === id) this.monitorIds.delete(monitorId);\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport function isCapacityError(error: unknown): error is SessionRegistryCapacityError {\n\tconst pty = loadedPty();\n\tif (pty) return error instanceof pty.SessionRegistryCapacityError;\n\treturn error instanceof Error && error.name === \"SessionRegistryCapacityError\";\n}\n"]}
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tSessionRegistry,\n\tSessionRegistryCapacityError,\n\tTerminalSession,\n\tTerminalSessionOptions,\n} from \"@earendil-works/pi-pty\";\nimport { loadedPty, loadPty } from \"./pty.lazy.ts\";\nimport type { TerminalRuntimeOptions, TerminalRuntimeSession } from \"./runtime-session.ts\";\nimport { DEFAULT_MAX_SESSIONS } from \"./shared.ts\";\n\nexport type { SessionRegistryCapacityError };\n\nexport interface TerminalManagerOptions {\n\treadonly maxSessions?: number;\n\treadonly scrollback?: number;\n}\n\nexport interface CreatedTerminalSession {\n\treadonly id: string;\n\treadonly runtime: TerminalRuntimeSession;\n}\n\n/**\n * Owns the live terminal sessions for one agent session. Delegates id allocation,\n * capacity caps, LRU-exited pruning, and tree-kill teardown to the pi-pty\n * {@link SessionRegistry}, while keeping the richer {@link TerminalRuntimeSession}\n * wrappers (screen model + output buffer) in lock-step.\n */\nexport class TerminalManager {\n\tprivate registry: SessionRegistry<TerminalSession> | undefined;\n\tprivate readonly runtimes = new Map<string, TerminalRuntimeSession>();\n\t/** Stable \"mon_\" identities bound to runtime session ids; survives PTY exit, then drops on session prune or teardown. */\n\tprivate readonly monitorIds = new Map<string, string>();\n\tprivate readonly scrollback?: number;\n\tprivate readonly maxSessions: number;\n\tprivate readonly exited = new Set<string>();\n\tprivate reservations = 0;\n\n\tconstructor(options: TerminalManagerOptions = {}) {\n\t\tthis.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;\n\t\tthis.scrollback = options.scrollback;\n\t}\n\n\tget size(): number {\n\t\treturn this.runtimes.size;\n\t}\n\n\tget activeSize(): number {\n\t\tthis.reconcileRuntimes();\n\t\treturn this.runtimes.size - this.exited.size + this.reservations;\n\t}\n\n\treserve(): (() => void) | null {\n\t\tthis.reconcileRuntimes();\n\t\tif (this.runtimes.size - this.exited.size + this.reservations >= this.maxSessions) return null;\n\t\tthis.reservations += 1;\n\t\tlet released = false;\n\t\treturn () => {\n\t\t\tif (released) return;\n\t\t\treleased = true;\n\t\t\tthis.reservations -= 1;\n\t\t};\n\t}\n\n\t/** Spawn a new terminal session and register it under an allocated `bash_N` id. */\n\tasync create(command: string, options: TerminalSessionOptions): Promise<CreatedTerminalSession> {\n\t\tconst pty = await loadPty();\n\t\tconst release = this.reserve();\n\t\tif (!release) throw new pty.SessionRegistryCapacityError(this.maxSessions);\n\t\tthis.registry ??= new pty.SessionRegistry<TerminalSession>({ maxSessions: this.maxSessions });\n\t\tconst registry = this.registry;\n\t\tconst runtimeOptions: TerminalRuntimeOptions = { ...options, scrollback: this.scrollback };\n\t\tconst { TerminalRuntimeSession } = await import(\"./runtime-session.ts\");\n\t\tconst runtime = new TerminalRuntimeSession(command, runtimeOptions);\n\t\tlet entry: { id: string };\n\t\ttry {\n\t\t\tentry = await registry.create({ command, session: runtime.session });\n\t\t} catch (error) {\n\t\t\trelease();\n\t\t\truntime.dispose();\n\t\t\truntime.session.kill();\n\t\t\tthrow error;\n\t\t}\n\t\tthis.runtimes.set(entry.id, runtime);\n\t\truntime.session.onExit(() => {\n\t\t\tthis.exited.add(entry.id);\n\t\t});\n\t\trelease();\n\t\tthis.reconcileRuntimes();\n\t\treturn { id: entry.id, runtime };\n\t}\n\n\t/** Look up a live-or-exited session, refreshing its LRU timestamp. */\n\tget(id: string): TerminalRuntimeSession | null {\n\t\tconst registry = this.registry;\n\t\tif (!registry) return null;\n\t\tconst entry = registry.get(id);\n\t\tif (!entry) return null;\n\t\treturn this.runtimes.get(id) ?? null;\n\t}\n\n\t/** Bind a stable \"mon_\" id to its runtime session id; a later restore re-binds the same id. */\n\tbindMonitorId(monitorId: string, sessionId: string): void {\n\t\tthis.monitorIds.set(monitorId, sessionId);\n\t}\n\n\t/**\n\t * Resolve a \"mon_\" monitor id to its current runtime \"bash_N\" (or \"watch_N\"), passing a\n\t * runtime id through unchanged; undefined when neither resolves. Bindings intentionally\n\t * outlive PTY exit for a final output read, then drop when the session is pruned or torn down.\n\t */\n\tmonitorIdOf(runtimeId: string): string | undefined {\n\t\tfor (const [monitorId, sessionId] of this.monitorIds) if (sessionId === runtimeId) return monitorId;\n\t\treturn undefined;\n\t}\n\n\tresolveId(idOrMonitorId: string): string | undefined {\n\t\tif (idOrMonitorId.startsWith(\"mon_\")) return this.monitorIds.get(idOrMonitorId);\n\t\tconst registry = this.registry;\n\t\tif (!registry) return undefined;\n\t\treturn registry.get(idOrMonitorId) ? idOrMonitorId : undefined;\n\t}\n\n\tlist(): { id: string; runtime: TerminalRuntimeSession }[] {\n\t\tconst registry = this.registry;\n\t\tif (!registry) return [];\n\t\tconst result: { id: string; runtime: TerminalRuntimeSession }[] = [];\n\t\tfor (const entry of registry.list()) {\n\t\t\tconst runtime = this.runtimes.get(entry.id);\n\t\t\tif (runtime) result.push({ id: entry.id, runtime });\n\t\t}\n\t\treturn result;\n\t}\n\n\t/** Tree-kill one session; the exited entry is kept for a final output read until swept. */\n\tasync stop(id: string): Promise<boolean> {\n\t\tconst registry = this.registry;\n\t\tif (!registry) return false;\n\t\tconst stopped = await registry.stop(id);\n\t\tthis.reconcileRuntimes();\n\t\treturn stopped;\n\t}\n\n\t/** Tree-kill every session and dispose all runtime wrappers. */\n\tasync teardown(): Promise<void> {\n\t\tconst registry = this.registry;\n\t\tif (registry) await registry.teardown();\n\t\tthis.registry = undefined;\n\t\tfor (const runtime of this.runtimes.values()) runtime.dispose();\n\t\tthis.runtimes.clear();\n\t\tthis.exited.clear();\n\t\tthis.monitorIds.clear();\n\t}\n\n\t/** Dispose runtime wrappers whose registry entry was pruned (capacity/LRU eviction). */\n\tprivate reconcileRuntimes(): void {\n\t\tconst registry = this.registry;\n\t\tif (!registry) return;\n\t\tconst liveIds = new Set(registry.list().map((entry) => entry.id));\n\t\tfor (const [id, runtime] of this.runtimes) {\n\t\t\tif (liveIds.has(id)) continue;\n\t\t\truntime.dispose();\n\t\t\tthis.runtimes.delete(id);\n\t\t\tthis.exited.delete(id);\n\t\t\tfor (const [monitorId, sessionId] of this.monitorIds) {\n\t\t\t\tif (sessionId === id) this.monitorIds.delete(monitorId);\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport function isCapacityError(error: unknown): error is SessionRegistryCapacityError {\n\tconst pty = loadedPty();\n\tif (pty) return error instanceof pty.SessionRegistryCapacityError;\n\treturn error instanceof Error && error.name === \"SessionRegistryCapacityError\";\n}\n"]}
|
|
@@ -1,24 +1,68 @@
|
|
|
1
|
+
export declare const LEASE_RECORD_VERSION = 2;
|
|
2
|
+
export interface LeaseSelfIdentity {
|
|
3
|
+
readonly pid: number;
|
|
4
|
+
readonly bootAtMs: number;
|
|
5
|
+
readonly processStartedAtMs: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `startedAtMs` duplicates `acquiredAtMs` on purpose: a senpi build that still runs the v1 lease
|
|
9
|
+
* reader parses `{pid, startedAtMs}` and must not see this file as unparseable (which it reclaims).
|
|
10
|
+
*/
|
|
11
|
+
export interface LeaseRecord {
|
|
12
|
+
readonly v?: number;
|
|
13
|
+
readonly token?: string;
|
|
14
|
+
readonly pid: number;
|
|
15
|
+
readonly startedAtMs: number;
|
|
16
|
+
readonly bootAtMs?: number;
|
|
17
|
+
readonly processStartedAtMs?: number;
|
|
18
|
+
readonly acquiredAtMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface LeaseHolder {
|
|
21
|
+
readonly pid: number;
|
|
22
|
+
readonly startedAtMs: number;
|
|
23
|
+
readonly bootAtMs?: number;
|
|
24
|
+
}
|
|
1
25
|
export type AcquireTerminalLeaseOptions = {
|
|
2
26
|
dir: string;
|
|
3
27
|
encodedSessionId: string;
|
|
4
28
|
pid?: number;
|
|
5
29
|
now?: () => number;
|
|
30
|
+
self?: LeaseSelfIdentity;
|
|
6
31
|
isProcessAlive?: (pid: number) => boolean;
|
|
32
|
+
readProcessStartMs?: (pid: number) => Promise<number | undefined>;
|
|
7
33
|
};
|
|
8
34
|
export type AcquireTerminalLeaseResult = {
|
|
9
35
|
acquired: true;
|
|
10
36
|
path: string;
|
|
11
37
|
pid: number;
|
|
38
|
+
token: string;
|
|
12
39
|
} | {
|
|
13
40
|
acquired: false;
|
|
14
|
-
holder:
|
|
15
|
-
pid: number;
|
|
16
|
-
startedAtMs: number;
|
|
17
|
-
};
|
|
41
|
+
holder: LeaseHolder;
|
|
18
42
|
};
|
|
43
|
+
export type LeaseClassification = "self" | "dead" | "reused" | "live-foreign";
|
|
44
|
+
export declare function currentLeaseToken(encodedSessionId: string): string | undefined;
|
|
45
|
+
/** Mark a generation's token dead the moment its shutdown starts, before any slow flush. */
|
|
46
|
+
export declare function retireLeaseToken(token: string): void;
|
|
47
|
+
export declare function readLeaseRecord(raw: string): LeaseRecord | "unparseable";
|
|
48
|
+
/**
|
|
49
|
+
* Decide what an existing lease is. A pid alone is never trusted: the holder is alive only when
|
|
50
|
+
* its pid answers AND its recorded start instant matches the process now wearing that pid
|
|
51
|
+
* (probed on this cold path only). A boot mismatch alone never reclaims an alive pid. Our own
|
|
52
|
+
* pid is "self" only when the file carries the token this process minted for the session (or a
|
|
53
|
+
* legacy record without one): a multi-session host holds many sessions under one pid, and a
|
|
54
|
+
* lease another live generation in this process owns is a live holder, not a re-entry.
|
|
55
|
+
*/
|
|
56
|
+
export declare function classifyLease(existing: LeaseRecord, self: LeaseSelfIdentity, probes: {
|
|
57
|
+
readonly isProcessAlive?: (pid: number) => boolean;
|
|
58
|
+
readonly readProcessStartMs: (pid: number) => Promise<number | undefined>;
|
|
59
|
+
readonly liveTokens?: ReadonlySet<string>;
|
|
60
|
+
}): Promise<LeaseClassification>;
|
|
19
61
|
export declare function acquireTerminalLease(options: AcquireTerminalLeaseOptions): Promise<AcquireTerminalLeaseResult>;
|
|
62
|
+
/** Release only the generation that acquired: a stale release from an earlier generation is a no-op. */
|
|
20
63
|
export declare function releaseTerminalLease(handle: {
|
|
21
64
|
path: string;
|
|
22
65
|
pid: number;
|
|
66
|
+
token?: string;
|
|
23
67
|
}): Promise<void>;
|
|
24
68
|
//# sourceMappingURL=manifest-lease.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-lease.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/manifest-lease.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"manifest-lease.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/manifest-lease.ts"],"names":[],"mappings":"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"}
|