@edgehero/pi-dispatch 4.3.0 → 5.0.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/.env.example +94 -4
- package/README.md +20 -7
- package/deploy/console.service +52 -0
- package/deploy/docker-compose.yml +3 -1
- package/deploy/pi-dispatch-valkey.container +4 -2
- package/deploy/sandbox-gateway.service +23 -0
- package/deploy/worker-env-wrapper.cmd +6 -0
- package/deploy/worker-env-wrapper.sh +10 -1
- package/package.json +117 -19
- package/src/agent-snapshot.mjs +49 -0
- package/src/answerer-runtime.mjs +194 -0
- package/src/answerer.mjs +178 -0
- package/src/approval-policy.mjs +76 -0
- package/src/approval-runtime.mjs +188 -0
- package/src/approval-session.mjs +137 -0
- package/src/approval-state.mjs +170 -0
- package/src/auth-cli.mjs +86 -0
- package/src/auth-connections.mjs +145 -0
- package/src/auth-policy.mjs +32 -0
- package/src/auth-store.mjs +97 -0
- package/src/azure-host.mjs +66 -4
- package/src/azure-prompt.mjs +5 -4
- package/src/backend-conformance.mjs +34 -2
- package/src/backend-local.mjs +103 -34
- package/src/backend-openshell.mjs +1540 -0
- package/src/backend-podman.mjs +63 -2
- package/src/backend-registry.mjs +61 -4
- package/src/backends.mjs +395 -6
- package/src/bullmq-internals.mjs +208 -0
- package/src/canary-cli.mjs +28 -0
- package/src/canary-lock.mjs +41 -0
- package/src/canary-runtime.mjs +237 -0
- package/src/canary.mjs +53 -0
- package/src/cancel-cli.mjs +47 -34
- package/src/cancel-state.mjs +213 -15
- package/src/capabilities.mjs +36 -1
- package/src/capacity-cli.mjs +28 -8
- package/src/capacity-records.mjs +75 -16
- package/src/capacity.mjs +65 -0
- package/src/chat-cli.mjs +288 -0
- package/src/chat-fork.mjs +547 -0
- package/src/chat-mode.mjs +156 -0
- package/src/chat-view.mjs +720 -0
- package/src/checkpoint-paths.mjs +113 -0
- package/src/cli.mjs +128 -41
- package/src/comment-tables.mjs +322 -0
- package/src/community-auth-adapters.mjs +87 -0
- package/src/community-auth-helper.mjs +76 -0
- package/src/community-auth-metadata.mjs +6 -0
- package/src/community-auth-output-pins.mjs +38 -0
- package/src/community-auth-patches.mjs +168 -0
- package/src/community-auth-pins.mjs +38 -0
- package/src/config.mjs +199 -30
- package/src/connection.mjs +83 -7
- package/src/console-answer.mjs +113 -0
- package/src/console-audit.mjs +604 -0
- package/src/console-bootstrap.mjs +159 -0
- package/src/console-client.mjs +848 -0
- package/src/console-probe.mjs +106 -0
- package/src/console-tokens.mjs +459 -0
- package/src/container-names.mjs +51 -0
- package/src/container-spec.mjs +20 -1
- package/src/cpu-reserve.mjs +4 -1
- package/src/defer-reasons.mjs +200 -0
- package/src/defer-write.mjs +208 -0
- package/src/deployment-venue.mjs +23 -6
- package/src/docker-run.mjs +11 -0
- package/src/doctor-bundles.mjs +130 -0
- package/src/doctor-model-proposals.mjs +109 -0
- package/src/doctor-openshell.mjs +238 -0
- package/src/doctor-results.mjs +602 -0
- package/src/doctor-spawner.mjs +215 -0
- package/src/doctor.mjs +1663 -198
- package/src/dollar-budget.mjs +212 -3
- package/src/endpoint-status.mjs +100 -0
- package/src/env-allowlist.mjs +103 -9
- package/src/env-file.mjs +6 -4
- package/src/file-expect.mjs +47 -0
- package/src/forgejo-auth.mjs +4 -0
- package/src/forgejo-host.mjs +127 -31
- package/src/forgejo-prompt.mjs +5 -4
- package/src/forges.mjs +73 -15
- package/src/fork-job.mjs +58 -0
- package/src/fork-policy.mjs +23 -0
- package/src/fork-store.mjs +338 -0
- package/src/get-token.mjs +1 -1
- package/src/git-dirty.mjs +119 -14
- package/src/git-hardening.mjs +85 -0
- package/src/github-host.mjs +31 -6
- package/src/github-prompt.mjs +7 -16
- package/src/gitlab-host.mjs +42 -3
- package/src/gitlab-prompt.mjs +5 -4
- package/src/held-document.mjs +23 -0
- package/src/held-replies.mjs +325 -0
- package/src/held-settle.mjs +266 -0
- package/src/hook-runs.mjs +188 -0
- package/src/host-budget.mjs +8 -5
- package/src/host-clone.mjs +72 -0
- package/src/host-loads.mjs +557 -0
- package/src/host-registry.mjs +66 -6
- package/src/host-telemetry.mjs +92 -0
- package/src/http-body.mjs +78 -0
- package/src/image-preflight.mjs +156 -13
- package/src/import-pi.mjs +60 -172
- package/src/inbox-relay.mjs +482 -0
- package/src/inbox-settle.mjs +196 -0
- package/src/index.mjs +740 -129
- package/src/init.mjs +35 -5
- package/src/job-data-version.mjs +75 -0
- package/src/job-id.mjs +30 -15
- package/src/job-layout.mjs +22 -0
- package/src/job-size.mjs +16 -0
- package/src/live-jobs.mjs +14 -6
- package/src/live-probes.mjs +6 -2
- package/src/live-transcript.mjs +872 -0
- package/src/loaded-record.mjs +609 -0
- package/src/mcp-config.mjs +1 -1
- package/src/mcp-preflight.mjs +36 -5
- package/src/model-attempts.mjs +84 -0
- package/src/model-catalog.mjs +83 -0
- package/src/model-cooldowns.mjs +22 -0
- package/src/model-failover-protocol.mjs +11 -0
- package/src/model-failover.mjs +134 -0
- package/src/model-observation.mjs +50 -0
- package/src/model-proposal-protocol.mjs +21 -0
- package/src/model-proposals.mjs +75 -0
- package/src/model-ref.mjs +67 -10
- package/src/model-roles-file.mjs +24 -0
- package/src/model-roles.mjs +73 -0
- package/src/model-status.mjs +34 -0
- package/src/models-json.mjs +4 -1
- package/src/money.mjs +1 -1
- package/src/oauth-openai.mjs +121 -0
- package/src/older-state.mjs +249 -0
- package/src/on-failure.mjs +17 -13
- package/src/open-browser.mjs +31 -2
- package/src/opener-cli.mjs +135 -0
- package/src/opener-protocol.mjs +27 -0
- package/src/opener-terminal.mjs +128 -0
- package/src/openshell-cli.mjs +155 -0
- package/src/openshell-policy.mjs +163 -0
- package/src/outbox.mjs +14 -0
- package/src/output-cap.mjs +1 -0
- package/src/package-canary-record.mjs +26 -0
- package/src/package-canary.mjs +135 -0
- package/src/package-names.mjs +9 -0
- package/src/package-stage.mjs +418 -0
- package/src/packages.mjs +24 -16
- package/src/parked.mjs +2 -1
- package/src/pause-windows.mjs +136 -17
- package/src/podman-stack.mjs +40 -14
- package/src/portfolio-snapshot.mjs +26 -7
- package/src/prepare-github.mjs +41 -77
- package/src/prepare-local.mjs +32 -2
- package/src/prepare.mjs +45 -11
- package/src/private-store-boundary.mjs +70 -0
- package/src/processor.mjs +1554 -167
- package/src/projects.mjs +23 -0
- package/src/prompt-fence.mjs +18 -0
- package/src/provider-key.mjs +7 -0
- package/src/provider-steering.mjs +1 -0
- package/src/question-context.mjs +35 -0
- package/src/question-group-runtime.mjs +91 -0
- package/src/question-groups.mjs +171 -0
- package/src/queue.mjs +296 -55
- package/src/reason-catalog.mjs +448 -0
- package/src/receiver-status.mjs +230 -0
- package/src/record-reason.mjs +37 -0
- package/src/recovery-queue.mjs +566 -0
- package/src/redact-secrets.mjs +219 -0
- package/src/reserve-estimate.mjs +101 -0
- package/src/reserved-env.mjs +36 -1
- package/src/retention.mjs +67 -0
- package/src/run-budget-ledger.mjs +283 -0
- package/src/run-checkpoint.mjs +627 -0
- package/src/run-container.mjs +749 -33
- package/src/run-history.mjs +791 -45
- package/src/run-mirror.mjs +75 -7
- package/src/run-progress.mjs +751 -0
- package/src/runtime-observations.mjs +3 -1
- package/src/runtime-settings.mjs +272 -37
- package/src/sandbox-cli.mjs +78 -2
- package/src/sandbox-deployment.mjs +51 -0
- package/src/sandbox-gateway-protocol.mjs +378 -0
- package/src/sandbox-hosts.mjs +100 -0
- package/src/sandbox-inventory.mjs +83 -0
- package/src/sandbox-policy.mjs +225 -0
- package/src/sandbox-prepare-requests.mjs +145 -0
- package/src/sandbox-prepare.mjs +285 -0
- package/src/sandbox-refs.mjs +70 -0
- package/src/sandbox-session-monitor.mjs +4 -0
- package/src/sandbox-session-watch.mjs +124 -0
- package/src/sandbox-sessions.mjs +161 -0
- package/src/sandbox-store.mjs +63 -6
- package/src/sandbox-workspace-guard.mjs +39 -0
- package/src/sandbox.mjs +27 -200
- package/src/scheduler-stall-guard.mjs +14 -8
- package/src/schedules.mjs +19 -6
- package/src/secret-bundles.mjs +245 -0
- package/src/secret-profiles.mjs +103 -3
- package/src/secrets.mjs +19 -9
- package/src/service-env.mjs +38 -5
- package/src/service.mjs +638 -106
- package/src/session-key.mjs +60 -0
- package/src/session-store.mjs +168 -10
- package/src/setup-contract.mjs +40 -0
- package/src/setup-egress.mjs +113 -0
- package/src/setup-host-entry.mjs +33 -0
- package/src/setup-host.mjs +242 -0
- package/src/setup-lock.mjs +41 -0
- package/src/size-records.mjs +15 -3
- package/src/start.mjs +1353 -135
- package/src/status-comment.mjs +415 -0
- package/src/stderr-transcript.mjs +318 -0
- package/src/steer-ack.mjs +122 -0
- package/src/steer-cli.mjs +213 -0
- package/src/steer-framing.mjs +185 -0
- package/src/steer-inbox.mjs +693 -0
- package/src/store-version.mjs +139 -0
- package/src/subscription-config.mjs +56 -0
- package/src/subscription-limits.mjs +3 -0
- package/src/subscription-quota.mjs +259 -0
- package/src/subscription-runtime.mjs +123 -0
- package/src/subscription-status.mjs +42 -0
- package/src/subscription-wait.mjs +11 -0
- package/src/subscriptions.mjs +7 -1
- package/src/thinking-level.mjs +19 -0
- package/src/touch-outbox.mjs +61 -0
- package/src/touch-state.mjs +133 -0
- package/src/triggers-file.mjs +299 -39
- package/src/triggers.mjs +782 -40
- package/src/trust-paths.mjs +171 -0
- package/src/up.mjs +398 -51
- package/src/updates-cli.mjs +61 -0
- package/src/updates-look.mjs +127 -0
- package/src/updates-reconcile.mjs +182 -0
- package/src/updates-subscription-adapters.mjs +8 -0
- package/src/updates.mjs +252 -0
- package/src/valkey-auth.mjs +32 -9
- package/src/valkey-endpoint.mjs +14 -4
- package/src/wait-for.mjs +1 -1
- package/src/wait-state.mjs +62 -3
- package/src/watch-board.mjs +38 -0
- package/src/watch-closer.mjs +1 -1
- package/src/watch-contract.mjs +102 -0
- package/src/watch-decision.mjs +78 -0
- package/src/watch-drainer.mjs +14 -0
- package/src/watch-issue-adapter.mjs +44 -0
- package/src/watch-issue.mjs +111 -0
- package/src/watch-record.mjs +10 -0
- package/src/watch-runtime.mjs +167 -0
- package/src/watch-store.mjs +183 -0
- package/src/watch-trace.mjs +34 -0
- package/src/workspace-checkpoint.mjs +1750 -0
- package/src/zero-touch-fixes.mjs +99 -0
- package/src/zero-touch.mjs +225 -0
- package/src/repeat-slot.mjs +0 -13
package/src/backend-local.mjs
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* calls docker directly. That refusal is now gone because the seam is real.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { JOB_NAME_PREFIX, jobContainerName } from "./container-names.mjs";
|
|
24
25
|
import { execFile } from "node:child_process";
|
|
25
26
|
import { promisify } from "node:util";
|
|
26
27
|
import { scrubCredentials } from "./redact.mjs";
|
|
@@ -51,7 +52,9 @@ export function reaperExec({ execFileFn = execFile, timeoutMs = REAPER_STEP_TIME
|
|
|
51
52
|
// 1 MiB, the facts readers' own); every other step keeps the reaper's 30 s.
|
|
52
53
|
return (bin, args, opts = {}) =>
|
|
53
54
|
new Promise((resolve, reject) => {
|
|
54
|
-
|
|
55
|
+
// `env` (issue #684): a step that hands the CLI a credential through its own environment, never its argv
|
|
56
|
+
// (the openshell venue's `provider create --credential NAME`); every other step inherits the worker's.
|
|
57
|
+
execFileFn(bin, [...args], { timeout: opts.timeoutMs ?? timeoutMs, killSignal: "SIGKILL", maxBuffer: opts.maxBuffer ?? 1024 * 1024, ...(opts.env ? { env: opts.env } : {}) }, (err, stdout, stderr) => {
|
|
55
58
|
if (err) reject(Object.assign(err, { stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }));
|
|
56
59
|
else resolve({ stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
57
60
|
});
|
|
@@ -59,22 +62,8 @@ export function reaperExec({ execFileFn = execFile, timeoutMs = REAPER_STEP_TIME
|
|
|
59
62
|
}
|
|
60
63
|
const execReaperBounded = reaperExec();
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* readability. TWO sweeps match it as a SUBSTRING, both at boot in `start.mjs`: the container reaper's
|
|
65
|
-
* `docker ps` filter and the network reaper's `docker network ls` filter. They share the NAME rule
|
|
66
|
-
* (`isJobNamespace`) and deliberately not the STATE question -- see `reapNetwork` for why one lists running
|
|
67
|
-
* containers and the other asks about members in any state. The sandbox tooling is the
|
|
68
|
-
* counterpart rather than a third sweep -- it names itself `pi-sandbox-` precisely to stay OUTSIDE this
|
|
69
|
-
* namespace, so a worker restart cannot tear down the shell an operator is sitting in, and it reaps its own
|
|
70
|
-
* by job id rather than by name.
|
|
71
|
-
*
|
|
72
|
-
* Exported and imported rather than re-typed at each site for the reason `CONTAINER_GLOBAL_PI_DIR` is: the
|
|
73
|
-
* namespace and the filters that sweep it are ONE fact, and two literals in two modules is how a rename
|
|
74
|
-
* lands in the producer and not in the reaper, leaving every crashed worker's containers behind forever with
|
|
75
|
-
* both test suites green.
|
|
76
|
-
*/
|
|
77
|
-
export const JOB_NAME_PREFIX = "pi-job-";
|
|
65
|
+
// Naming is a leaf so operator guidance uses the runtime's exact rule without loading its implementation.
|
|
66
|
+
export { JOB_NAME_PREFIX, jobContainerName } from "./container-names.mjs";
|
|
78
67
|
|
|
79
68
|
/**
|
|
80
69
|
* The container states `docker network inspect` lists in `.Containers`, and therefore the ones the daemon
|
|
@@ -87,23 +76,7 @@ export const JOB_NAME_PREFIX = "pi-job-";
|
|
|
87
76
|
*/
|
|
88
77
|
export { ENDPOINT_LISTED_STATES };
|
|
89
78
|
|
|
90
|
-
|
|
91
|
-
* `pi-job-<jobId>`. The name a running job answers to, for `docker stop` on the 30-minute timeout, for the
|
|
92
|
-
* per-job egress network derived from it, and for the reaper's filter.
|
|
93
|
-
*
|
|
94
|
-
* Sanitised to the runtimes' own name rule, `[a-zA-Z0-9][a-zA-Z0-9_.-]*` (docker's `RestrictedNameChars`, and
|
|
95
|
-
* Podman's, measured), every other character becoming `_`, as `sanitizeJobId` does for file names. It said here that
|
|
96
|
-
* BullMQ ids are already that shape, and a cron job's is not: the job scheduler mints `repeat:<schedulerId>:<millis>`,
|
|
97
|
-
* so `pi-job-repeat:...` was refused at create (exit 125, measured on Podman 5.8.1) and so was its `-net` network, and
|
|
98
|
-
* every cron job ended `container-never-started` before it ever ran (issue #435). The prefix supplies the first
|
|
99
|
-
* character. Deterministic, because every path that must find the container again (the timeout's stop, the cancel,
|
|
100
|
-
* the network, the log sink) asks this function for the name rather than deriving it. Not injective: `repeat:a:1`
|
|
101
|
-
* and a literal `repeat_a_1` share a name. BullMQ ids are unique per queue and no producer here mints an id with `_`
|
|
102
|
-
* where a scheduler id has `:`, so it is a residual rather than a collision anything reaches.
|
|
103
|
-
*/
|
|
104
|
-
export function jobContainerName(jobId) {
|
|
105
|
-
return `${JOB_NAME_PREFIX}${String(jobId).replace(/[^A-Za-z0-9._-]/g, "_")}`;
|
|
106
|
-
}
|
|
79
|
+
|
|
107
80
|
|
|
108
81
|
/**
|
|
109
82
|
* Bundle the local backend's already-built functions into one checked value.
|
|
@@ -139,6 +112,19 @@ export const BACKEND_PROVIDED = ["name", "declares", "namePrefix", "containerNam
|
|
|
139
112
|
*/
|
|
140
113
|
export const BACKEND_VALUES = ["neverStartedExits"];
|
|
141
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Functions a bundle carries that `makeLocalBackend` builds ITSELF unless the caller passes its own (issue #645): the
|
|
117
|
+
* freezer verbs. Optional to pass rather than required like `BACKEND_FUNCTIONS`, because every caller of this factory
|
|
118
|
+
* (start.mjs and the tests) predates them and the local adapter knows how to spell them for docker; a test seam is
|
|
119
|
+
* the only reason to pass one. Still a NAMED list, so a misspelt `pasue` is refused as an unknown member like any other
|
|
120
|
+
* key rather than silently replaced by the default.
|
|
121
|
+
*
|
|
122
|
+
* Issue #681: the host budget's two venue questions joined them, for the same reason. `listJobContainers` lists this
|
|
123
|
+
* venue's leftover job containers for the boot seed and `containerGone` settles an orphan's hold; both are docker's
|
|
124
|
+
* here, and a bundle carries its own so the budget never asks one runtime about another's container.
|
|
125
|
+
*/
|
|
126
|
+
export const BACKEND_OPTIONAL_FUNCTIONS = ["pause", "unpause", "listJobContainers", "containerGone"];
|
|
127
|
+
|
|
142
128
|
export function makeLocalBackend(parts = {}) {
|
|
143
129
|
const { runContainer, imagePreflight, egressPreflight, stopContainer, reap } = parts ?? {};
|
|
144
130
|
const missing = Object.entries({ runContainer, imagePreflight, egressPreflight, stopContainer, reap })
|
|
@@ -155,9 +141,14 @@ export function makeLocalBackend(parts = {}) {
|
|
|
155
141
|
// control again, arriving through a dropped argument.
|
|
156
142
|
for (const key of Object.keys(parts ?? {})) {
|
|
157
143
|
if (BACKEND_FUNCTIONS.includes(key) || BACKEND_VALUES.includes(key)) continue;
|
|
144
|
+
if (BACKEND_OPTIONAL_FUNCTIONS.includes(key)) {
|
|
145
|
+
if (typeof parts[key] !== "function") throw new Error(`backend "${DEFAULT_BACKEND}": ${key} must be a function when it is passed`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
158
148
|
throw new Error(`backend "${DEFAULT_BACKEND}": unknown bundle member ${JSON.stringify(key)} (this factory takes ${BACKEND_FUNCTIONS.join(", ")} and sets ${BACKEND_PROVIDED.join(", ")} itself)`);
|
|
159
149
|
}
|
|
160
150
|
|
|
151
|
+
const verbs = makePauseVerbs({ bin: "docker" });
|
|
161
152
|
return {
|
|
162
153
|
name: DEFAULT_BACKEND,
|
|
163
154
|
// The declaration is READ from the table, never re-typed here. An adapter that stated its own
|
|
@@ -175,6 +166,14 @@ export function makeLocalBackend(parts = {}) {
|
|
|
175
166
|
egressPreflight,
|
|
176
167
|
stopContainer,
|
|
177
168
|
reap,
|
|
169
|
+
// The freezer verbs (issue #645), `pausable` in the table. Never throw; `{ ok: false, reason }` is the caller's cue to
|
|
170
|
+
// carry on without a frozen read.
|
|
171
|
+
pause: parts.pause ?? verbs.pause,
|
|
172
|
+
unpause: parts.unpause ?? verbs.unpause,
|
|
173
|
+
// The host budget's two questions about THIS venue's containers (issue #681), asked of docker because docker holds
|
|
174
|
+
// them. Before, the wiring derived the CLI from the job's venue name and sent every venue but podman to docker.
|
|
175
|
+
listJobContainers: parts.listJobContainers ?? makeJobContainerLister({ bin: "docker" }),
|
|
176
|
+
containerGone: parts.containerGone ?? makeContainerGone({ binOf: () => "docker" }),
|
|
178
177
|
// The integers this runtime uses for "the runner never ran". The processor asks the BACKEND rather
|
|
179
178
|
// than assuming docker's triple, because those numbers collide with the runner's own exit channel.
|
|
180
179
|
neverStartedExits: LOCAL_NEVER_STARTED_EXITS,
|
|
@@ -218,6 +217,76 @@ export function makeStopContainer({ exec = execDocker, bin = "docker" } = {}) {
|
|
|
218
217
|
};
|
|
219
218
|
}
|
|
220
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The bound on one `pause` or `unpause` (issue #645). 15 s, not `execDockerBounded`'s 5 s default: a freeze waits for
|
|
222
|
+
* every task in the container's cgroup to reach the frozen state, and a container mid-`fork` storm or in
|
|
223
|
+
* uninterruptible I/O takes visibly longer than a CLI round trip, while a caller holding a job's checkpoint cannot wait
|
|
224
|
+
* on a wedged daemon forever. Past it the verb answers `{ ok: false, reason: "timeout" }` and the child is SIGKILLed.
|
|
225
|
+
*/
|
|
226
|
+
export const PAUSE_TIMEOUT_MS = 15_000;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* A runtime's container name, which is also what keeps the argument from reading as a FLAG: `docker pause --help`
|
|
230
|
+
* exits 0 and freezes nothing, so a name beginning with `-` would come back as a successful pause of no container.
|
|
231
|
+
* `jobContainerName` only mints names of this shape (the runtimes' own rule), so refusing anything else costs no job.
|
|
232
|
+
*/
|
|
233
|
+
const RUNTIME_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
234
|
+
|
|
235
|
+
/** Whether `name` is a runtime container name the freezer verbs accept. Exported so a venue's own gate checks it FIRST. */
|
|
236
|
+
export function isRuntimeName(name) {
|
|
237
|
+
return typeof name === "string" && RUNTIME_NAME.test(name);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* `{ pause(name), unpause(name) }` for the venue's CLI (`docker pause` / `podman pause` and their `unpause`), each
|
|
242
|
+
* resolving `{ ok: true }` or `{ ok: false, reason }` and NEVER rejecting (issue #645, the `pausable` row).
|
|
243
|
+
*
|
|
244
|
+
* WHAT IT IS FOR. Pausing uses the cgroup freezer: every process in the container stops where it stands, nothing is
|
|
245
|
+
* killed, and the bind-mounted workspace holds still, so the host can read it as one consistent state while nothing in
|
|
246
|
+
* the container runs. That is the whole of it; a frozen container is granted nothing and its mounts do not change.
|
|
247
|
+
*
|
|
248
|
+
* NEVER THROWS, because its caller is a best-effort read beside a running job: a freeze that did not take means the
|
|
249
|
+
* read is taken live or skipped, never that the job fails. A failure is a fixed token (`timeout`, `exit-<n>`,
|
|
250
|
+
* `<bin>-not-found`, `spawn-failed`, `invalid-name`) and never the CLI's stderr, which can repeat a `DOCKER_HOST` with
|
|
251
|
+
* credentials in it (issue #339's reason for `execDockerBounded` not reading it by default).
|
|
252
|
+
*
|
|
253
|
+
* BOUNDED TWICE ON PURPOSE. `execDockerBounded` (the default `run`) settles on its own timer and kills the child, which
|
|
254
|
+
* is the bound in production; the race below is the same bound for an injected `run`, so "never hangs" is a property of
|
|
255
|
+
* the verb rather than of whichever seam a caller passed. `docker stop` is not bounded here, which is why this uses
|
|
256
|
+
* `execDockerBounded` and not `makeStopContainer`'s `exec`: the abort path does not await its stop and this caller does.
|
|
257
|
+
*/
|
|
258
|
+
export function makePauseVerbs({ bin = "docker", timeoutMs = PAUSE_TIMEOUT_MS, run = (cmd, args, opts) => execDockerBounded(args, { bin: cmd, timeoutMs: opts.timeoutMs }) } = {}) {
|
|
259
|
+
const verb = (word) =>
|
|
260
|
+
async function pauseVerb(name) {
|
|
261
|
+
if (!isRuntimeName(name)) return { ok: false, reason: "invalid-name" };
|
|
262
|
+
let timer = null;
|
|
263
|
+
const expired = new Promise((resolve) => {
|
|
264
|
+
timer = setTimeout(() => resolve({ code: null, error: { timedOut: true } }), timeoutMs);
|
|
265
|
+
});
|
|
266
|
+
let result;
|
|
267
|
+
try {
|
|
268
|
+
result = await Promise.race([Promise.resolve().then(() => run(bin, [word, name], { timeoutMs })), expired]);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
result = { code: null, error: err };
|
|
271
|
+
} finally {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
}
|
|
274
|
+
if (!result?.error && result?.code === 0) return { ok: true };
|
|
275
|
+
return { ok: false, reason: pauseFailure(bin, result) };
|
|
276
|
+
};
|
|
277
|
+
return { pause: verb("pause"), unpause: verb("unpause") };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The fixed token for a verb that did not take; values Node supplies only, never CLI text. */
|
|
281
|
+
function pauseFailure(bin, result) {
|
|
282
|
+
const error = result?.error ?? null;
|
|
283
|
+
if (error?.timedOut || error?.killed) return "timeout";
|
|
284
|
+
if (error?.code === "ENOENT") return `${bin}-not-found`;
|
|
285
|
+
if (typeof result?.code === "number") return `exit-${result.code}`;
|
|
286
|
+
if (typeof error?.code === "number") return `exit-${error.code}`;
|
|
287
|
+
return "spawn-failed";
|
|
288
|
+
}
|
|
289
|
+
|
|
221
290
|
/**
|
|
222
291
|
* What the CLI resolved, as a phrase that is never EMPTY. Four call sites interpolate an endpoint into a
|
|
223
292
|
* sentence of the form "resolves <this>, which is not shown to be on this host", and a context can carry no
|