@fungi.computer/computer 0.0.0-stage → 0.1.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/Dockerfile +85 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/container/computer-init.mjs +119 -0
- package/container/computer-start.sh +24 -0
- package/container/package.json +14 -0
- package/container/pnpm-lock.yaml +378 -0
- package/container/pnpm-workspace.yaml +5 -0
- package/container/resource-bounds.mjs +54 -0
- package/container/runtime-privileges.cc +37 -0
- package/container/runtime-privileges.mjs +11 -0
- package/container/terminal-assets/README.md +35 -0
- package/container/terminal-assets/bashrc +49 -0
- package/container/terminal-assets/git-prompt.sh +586 -0
- package/container/terminal-protocol.mjs +58 -0
- package/container/terminal-runtime.mjs +10 -0
- package/container/terminal-server.mjs +243 -0
- package/dist/apply-patch/LICENSE.codex +201 -0
- package/dist/apply-patch/build.mjs +98 -0
- package/dist/apply-patch/cache.mjs +168 -0
- package/dist/apply-patch/compile.mjs +199 -0
- package/dist/apply-patch/docker/darwin.Dockerfile +15 -0
- package/dist/apply-patch/docker/linux-arm64.Dockerfile +38 -0
- package/dist/apply-patch/docker/linux-x64.Dockerfile +23 -0
- package/dist/apply-patch/main.rs +320 -0
- package/dist/apply-patch/notices.mjs +44 -0
- package/dist/apply-patch/pin.mjs +8 -0
- package/dist/apply-patch/toolchain.mjs +39 -0
- package/dist/computer-backends.d.ts +94 -0
- package/dist/computer-backends.js +103 -0
- package/dist/computerd.cjs +18010 -0
- package/dist/container-policy.d.ts +7 -0
- package/dist/container-policy.js +24 -0
- package/dist/container-system-trust.d.ts +8 -0
- package/dist/container-system-trust.js +66 -0
- package/dist/egress-policy.d.ts +89 -0
- package/dist/egress-policy.js +240 -0
- package/dist/execution-result.d.ts +3 -0
- package/dist/execution-result.js +28 -0
- package/dist/files-patch.d.ts +20 -0
- package/dist/files-patch.js +108 -0
- package/dist/files.d.ts +75 -0
- package/dist/files.js +136 -0
- package/dist/fleet/admission.d.ts +14 -0
- package/dist/fleet/admission.js +10 -0
- package/dist/fleet/backing.d.ts +52 -0
- package/dist/fleet/backing.js +242 -0
- package/dist/fleet/computers.d.ts +62 -0
- package/dist/fleet/computers.js +35 -0
- package/dist/fleet/index.d.ts +11 -0
- package/dist/fleet/index.js +11 -0
- package/dist/fleet/inventory-wire.d.ts +29 -0
- package/dist/fleet/inventory-wire.js +15 -0
- package/dist/fleet/inventory.d.ts +24 -0
- package/dist/fleet/inventory.js +152 -0
- package/dist/fleet/lifecycle.d.ts +99 -0
- package/dist/fleet/lifecycle.js +418 -0
- package/dist/fleet/runtime-usage.d.ts +50 -0
- package/dist/fleet/runtime-usage.js +145 -0
- package/dist/fleet/slots.d.ts +30 -0
- package/dist/fleet/slots.js +46 -0
- package/dist/fleet/wire.d.ts +228 -0
- package/dist/fleet/wire.js +127 -0
- package/dist/host-config.d.ts +12 -0
- package/dist/host-config.js +10 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +1237 -0
- package/dist/light-shell.d.ts +44 -0
- package/dist/light-shell.js +376 -0
- package/dist/local-backend.d.ts +52 -0
- package/dist/local-backend.js +143 -0
- package/dist/metered-container-host.d.ts +26 -0
- package/dist/metered-container-host.js +48 -0
- package/dist/on-box-vm.d.ts +119 -0
- package/dist/on-box-vm.js +294 -0
- package/dist/operation-contract.d.ts +456 -0
- package/dist/operation-contract.js +450 -0
- package/dist/operation-record.d.ts +50 -0
- package/dist/operation-record.js +162 -0
- package/dist/preview.d.ts +55 -0
- package/dist/preview.js +151 -0
- package/dist/process-inventory.d.ts +80 -0
- package/dist/process-inventory.js +427 -0
- package/dist/pull-pending.d.ts +26 -0
- package/dist/pull-pending.js +35 -0
- package/dist/team-candidate-builder.mjs +28220 -0
- package/dist/transport-backend.d.ts +3 -0
- package/dist/transport-backend.js +99 -0
- package/dist/workspace-stream.d.ts +2 -0
- package/dist/workspace-stream.js +31 -0
- package/package.json +126 -3
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { z } from "zod";
|
|
54
|
+
import { computerProcessSchema, processStartInputSchema, mintComputerProcessId, ComputerProcessLimitError, ComputerPortReservedError, ComputerPortConflictError, ComputerPortLimitError, isComputerReservedPort, maximumExposedPorts, maximumProcessRecords, maximumRunningProcesses, } from "./operation-contract.js";
|
|
55
|
+
import { ComputerOperationConflictError } from "./operation-record.js";
|
|
56
|
+
function toProcess(row) {
|
|
57
|
+
return computerProcessSchema.parse({
|
|
58
|
+
id: row.id,
|
|
59
|
+
command: row.command,
|
|
60
|
+
cwd: row.cwd,
|
|
61
|
+
port: row.port,
|
|
62
|
+
state: row.state,
|
|
63
|
+
exitCode: row.exit_code,
|
|
64
|
+
sync: row.sync,
|
|
65
|
+
startedAt: row.started_at,
|
|
66
|
+
endedAt: row.ended_at,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Bump when `computer_processes`' columns change. Pre-release data is
|
|
71
|
+
* disposable (no migrations before launch): a mismatch resets the table
|
|
72
|
+
* to the fresh schema instead of a strict-table write throwing on an old
|
|
73
|
+
* row shape. Tracked in a dedicated row, not `PRAGMA user_version`: that
|
|
74
|
+
* pragma returns SQLITE_AUTH against a real Durable Object's SQLite
|
|
75
|
+
* storage (confirmed in workerd; node:sqlite does not enforce the same
|
|
76
|
+
* sandbox, so a unit test against it alone would hide this).
|
|
77
|
+
*/
|
|
78
|
+
const processSchemaVersion = 2;
|
|
79
|
+
/** Computer-owned replay identities; computerd owns execution and exit observations. */
|
|
80
|
+
export class ComputerProcessInventory {
|
|
81
|
+
#storage;
|
|
82
|
+
#now;
|
|
83
|
+
constructor(storage, now) {
|
|
84
|
+
this.#storage = storage;
|
|
85
|
+
this.#now = now;
|
|
86
|
+
storage.transactionSync(() => {
|
|
87
|
+
storage.sql.exec("CREATE TABLE IF NOT EXISTS computer_schema_meta (id INTEGER PRIMARY KEY CHECK (id = 0), version INTEGER NOT NULL) STRICT");
|
|
88
|
+
const seenVersion = storage.sql
|
|
89
|
+
.exec("SELECT version FROM computer_schema_meta WHERE id = 0")
|
|
90
|
+
.toArray()[0]?.version ?? 0;
|
|
91
|
+
if (seenVersion !== processSchemaVersion) {
|
|
92
|
+
storage.sql.exec("DROP TABLE IF EXISTS computer_processes");
|
|
93
|
+
storage.sql.exec("DROP TABLE IF EXISTS computer_exposed_ports");
|
|
94
|
+
storage.sql.exec("DELETE FROM computer_schema_meta WHERE id = 0");
|
|
95
|
+
storage.sql.exec("INSERT INTO computer_schema_meta (id, version) VALUES (0, ?)", processSchemaVersion);
|
|
96
|
+
}
|
|
97
|
+
storage.sql.exec("CREATE TABLE IF NOT EXISTS computer_processes (id TEXT PRIMARY KEY, request_scope TEXT NOT NULL, request_id TEXT NOT NULL, input_hash TEXT NOT NULL, command TEXT NOT NULL, cwd TEXT, port INTEGER, state TEXT NOT NULL CHECK (state IN ('running','exited')), exit_code INTEGER, sync TEXT NOT NULL DEFAULT 'unknown' CHECK (sync IN ('synced','pending','unknown')), started_at INTEGER NOT NULL, ended_at INTEGER, CHECK ((state = 'running') = (ended_at IS NULL)), UNIQUE(request_scope, request_id)) STRICT");
|
|
98
|
+
storage.sql.exec("CREATE TABLE IF NOT EXISTS computer_exposed_ports (port INTEGER PRIMARY KEY CHECK (port BETWEEN 1 AND 65535), process_id TEXT) STRICT");
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
#matching(input, inputHash, accept) {
|
|
102
|
+
const row = this.#storage.sql
|
|
103
|
+
.exec("SELECT * FROM computer_processes WHERE request_scope = ? AND request_id = ?", input.requestScope, input.requestId)
|
|
104
|
+
.toArray()[0];
|
|
105
|
+
if (row === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
accept?.();
|
|
108
|
+
if (row.input_hash !== inputHash)
|
|
109
|
+
throw new ComputerOperationConflictError();
|
|
110
|
+
const process = toProcess(row);
|
|
111
|
+
// Admission may still be dispatching: a receipt alone does not prove liveness.
|
|
112
|
+
return process.state === "running"
|
|
113
|
+
? { ...process, state: "uncertain" }
|
|
114
|
+
: process;
|
|
115
|
+
}
|
|
116
|
+
/** A replay does not boot a backend or redispatch an exited command. */
|
|
117
|
+
replay(input, inputHash, accept) {
|
|
118
|
+
return this.#storage.transactionSync(() => {
|
|
119
|
+
return this.#matching(input, inputHash, accept);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Reserve the process and only a newly created exposure in one transaction. */
|
|
123
|
+
admit(input, inputHash, accept) {
|
|
124
|
+
return this.#storage.transactionSync(() => {
|
|
125
|
+
accept();
|
|
126
|
+
const existing = this.#matching(input, inputHash);
|
|
127
|
+
if (existing !== undefined)
|
|
128
|
+
return { process: existing, created: false };
|
|
129
|
+
const now = this.#now();
|
|
130
|
+
const running = this.#storage.sql
|
|
131
|
+
.exec("SELECT COUNT(*) AS count FROM computer_processes WHERE state = 'running' AND request_scope = ?", input.requestScope)
|
|
132
|
+
.toArray()[0]?.count ?? maximumRunningProcesses;
|
|
133
|
+
if (running >= maximumRunningProcesses)
|
|
134
|
+
throw new ComputerProcessLimitError();
|
|
135
|
+
const id = mintComputerProcessId();
|
|
136
|
+
if (input.port !== undefined) {
|
|
137
|
+
if (isComputerReservedPort(input.port))
|
|
138
|
+
throw new ComputerPortReservedError(input.port);
|
|
139
|
+
const exposure = this.#storage.sql
|
|
140
|
+
.exec("SELECT process_id FROM computer_exposed_ports WHERE port = ?", input.port)
|
|
141
|
+
.toArray()[0];
|
|
142
|
+
if (exposure !== undefined && exposure.process_id !== null)
|
|
143
|
+
throw new ComputerPortConflictError();
|
|
144
|
+
if (exposure === undefined) {
|
|
145
|
+
const ports = this.#storage.sql
|
|
146
|
+
.exec("SELECT COUNT(*) AS count FROM computer_exposed_ports")
|
|
147
|
+
.toArray()[0]?.count ?? maximumExposedPorts;
|
|
148
|
+
if (ports >= maximumExposedPorts)
|
|
149
|
+
throw new ComputerPortLimitError();
|
|
150
|
+
this.#storage.sql.exec("INSERT INTO computer_exposed_ports (port, process_id) VALUES (?, ?)", input.port, id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
this.#storage.sql.exec("INSERT INTO computer_processes (id, request_scope, request_id, input_hash, command, cwd, port, state, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?)", id, input.requestScope, input.requestId, inputHash, input.command, input.cwd ?? null, input.port ?? null, now);
|
|
154
|
+
// Each scope retains its own newest 64 rows, including running processes.
|
|
155
|
+
this.#storage.sql.exec("DELETE FROM computer_processes WHERE request_scope = ? AND state = 'exited' AND id NOT IN (SELECT id FROM computer_processes WHERE request_scope = ? AND state = 'exited' ORDER BY started_at DESC, id DESC LIMIT MAX(0, ? - (SELECT COUNT(*) FROM computer_processes WHERE request_scope = ? AND state = 'running')))", input.requestScope, input.requestScope, maximumProcessRecords, input.requestScope);
|
|
156
|
+
return { process: this.#require(id), created: true };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
exited(id, exitCode, sync = "unknown") {
|
|
160
|
+
return this.#storage.transactionSync(() => {
|
|
161
|
+
this.#storage.sql.exec("UPDATE computer_processes SET state = 'exited', exit_code = ?, sync = ?, ended_at = ? WHERE id = ? AND state = 'running'", exitCode, sync, this.#now(), id);
|
|
162
|
+
this.#storage.sql.exec("DELETE FROM computer_exposed_ports WHERE process_id = ?", id);
|
|
163
|
+
return this.#require(id);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** Retain a failed dispatch receipt: retrying that key cannot create new work. */
|
|
167
|
+
release(id) {
|
|
168
|
+
const row = this.get(id);
|
|
169
|
+
if (row?.state === "running")
|
|
170
|
+
this.exited(id, null);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Upgrade an already-exited row's sync verdict once a background drain
|
|
174
|
+
* (one that gave up synchronously at its deadline) resolves it. A
|
|
175
|
+
* no-op if the row was evicted or never exited: pending rows must be
|
|
176
|
+
* updatable here, not only running ones.
|
|
177
|
+
*/
|
|
178
|
+
resync(id, sync) {
|
|
179
|
+
this.#storage.transactionSync(() => {
|
|
180
|
+
this.#storage.sql.exec("UPDATE computer_processes SET sync = ? WHERE id = ? AND state = 'exited'", sync, id);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
get(id) {
|
|
184
|
+
const row = this.#storage.sql
|
|
185
|
+
.exec("SELECT * FROM computer_processes WHERE id = ?", id)
|
|
186
|
+
.toArray()[0];
|
|
187
|
+
return row === undefined ? undefined : toProcess(row);
|
|
188
|
+
}
|
|
189
|
+
list() {
|
|
190
|
+
return this.#storage.sql
|
|
191
|
+
.exec("SELECT * FROM computer_processes ORDER BY started_at DESC, id DESC")
|
|
192
|
+
.toArray()
|
|
193
|
+
.map(toProcess);
|
|
194
|
+
}
|
|
195
|
+
#require(id) {
|
|
196
|
+
const found = this.get(id);
|
|
197
|
+
if (found === undefined)
|
|
198
|
+
throw new Error(`Computer process record is missing: ${id}`);
|
|
199
|
+
return found;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** How long a tail read may stay silent before the process counts as live. */
|
|
203
|
+
export const liveProbeMs = 750;
|
|
204
|
+
const lostCodeSchema = z.object({ code: z.enum(["EEXEC_LOST", "ENOENT"]) });
|
|
205
|
+
const busyCodeSchema = z.object({ code: z.literal("EEXEC_BUSY") });
|
|
206
|
+
/**
|
|
207
|
+
* A replaced container runtime (the package's fence) or an execution computerd
|
|
208
|
+
* no longer holds (its retention elapsed) both prove the process is gone.
|
|
209
|
+
*/
|
|
210
|
+
function lostExecution(error) {
|
|
211
|
+
return lostCodeSchema.safeParse(error).success;
|
|
212
|
+
}
|
|
213
|
+
/** Classify a failed attach: busy is live, lost is gone, anything else is unknown. */
|
|
214
|
+
function failedObservation(error) {
|
|
215
|
+
if (error === undefined)
|
|
216
|
+
return { state: "uncertain" };
|
|
217
|
+
// computerd admits one live subscriber; the Computer's own watcher holding
|
|
218
|
+
// it proves the execution is still live.
|
|
219
|
+
if (busyCodeSchema.safeParse(error).success)
|
|
220
|
+
return { state: "running" };
|
|
221
|
+
return lostExecution(error)
|
|
222
|
+
? { state: "exited", exitCode: null, sync: "unknown" }
|
|
223
|
+
: { state: "uncertain" };
|
|
224
|
+
}
|
|
225
|
+
async function readExitCode(runtime, id) {
|
|
226
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
227
|
+
try {
|
|
228
|
+
const replay = __addDisposableResource(env_1, await runtime.getExec(id, { resume: "full" }), false);
|
|
229
|
+
const reader = replay.getReader();
|
|
230
|
+
let exitCode = null;
|
|
231
|
+
for (;;) {
|
|
232
|
+
const next = await reader.read();
|
|
233
|
+
if (next.done)
|
|
234
|
+
break;
|
|
235
|
+
if (next.value.name === "exit")
|
|
236
|
+
exitCode = next.value.code;
|
|
237
|
+
}
|
|
238
|
+
return exitCode;
|
|
239
|
+
}
|
|
240
|
+
catch (e_1) {
|
|
241
|
+
env_1.error = e_1;
|
|
242
|
+
env_1.hasError = true;
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
__disposeResources(env_1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* How long a drain may keep waiting for EOF once an exit has already been
|
|
250
|
+
* observed, before giving up: a backgrounded child (for example
|
|
251
|
+
* `node server.js &`) can hold the transport open long past its own
|
|
252
|
+
* parent's exit, and nothing here may hang on that forever.
|
|
253
|
+
*/
|
|
254
|
+
export const postExitDrainDeadlineMs = 2_000;
|
|
255
|
+
const drainTimedOut = Symbol("computer-process-drain-timed-out");
|
|
256
|
+
/**
|
|
257
|
+
* Keep reading past the deadline so a slow-but-real pull still lands as
|
|
258
|
+
* "synced" instead of leaving the row stuck at "pending" forever; an
|
|
259
|
+
* error gives up without claiming anything new. The caller already
|
|
260
|
+
* handed off disposal to whoever awaits this.
|
|
261
|
+
*/
|
|
262
|
+
async function drainInBackground(reader) {
|
|
263
|
+
try {
|
|
264
|
+
for (;;) {
|
|
265
|
+
const next = await reader.read();
|
|
266
|
+
if (next.done)
|
|
267
|
+
return "synced";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (cause) {
|
|
271
|
+
console.error("Computer process background drain failed", cause);
|
|
272
|
+
return "pending";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Keep reading `reader` past an already-observed exit, bounded: the read
|
|
277
|
+
* that reaches EOF is the one that runs computerd's post-command pull
|
|
278
|
+
* (the contract in computer-execution-backends.md), so the ordinary case
|
|
279
|
+
* still waits for it and reports "synced". If the deadline passes
|
|
280
|
+
* first, hands back a `settleLater` promise that keeps draining in the
|
|
281
|
+
* background and resolves once that real EOF finally arrives -- the
|
|
282
|
+
* caller must keep the reader alive for it (not dispose it) and is
|
|
283
|
+
* responsible for keeping its own host alive too (a Durable Object's
|
|
284
|
+
* `waitUntil`), and for applying the eventual upgrade. This is also what
|
|
285
|
+
* keeps a backgrounded child holding the transport open (for example
|
|
286
|
+
* `node server.js &`) from hanging the caller synchronously.
|
|
287
|
+
*/
|
|
288
|
+
async function drainPastExit(reader) {
|
|
289
|
+
let timer;
|
|
290
|
+
const expired = new Promise((resolve) => {
|
|
291
|
+
timer = setTimeout(() => {
|
|
292
|
+
resolve(drainTimedOut);
|
|
293
|
+
}, postExitDrainDeadlineMs);
|
|
294
|
+
});
|
|
295
|
+
try {
|
|
296
|
+
for (;;) {
|
|
297
|
+
const next = await Promise.race([reader.read(), expired]);
|
|
298
|
+
if (next === drainTimedOut)
|
|
299
|
+
return { sync: "pending", settleLater: drainInBackground(reader) };
|
|
300
|
+
if (next.done)
|
|
301
|
+
return { sync: "synced" };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Observe one execution without consuming its output: a tail attach emits
|
|
310
|
+
* nothing for a live process, closes at once for an exited one, and fails
|
|
311
|
+
* for one computerd no longer holds.
|
|
312
|
+
*/
|
|
313
|
+
export async function observeProcess(runtime, id) {
|
|
314
|
+
let tail;
|
|
315
|
+
try {
|
|
316
|
+
tail = await runtime.getExec(id, { resume: "tail" });
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
return failedObservation(error instanceof Error ? error : undefined);
|
|
320
|
+
}
|
|
321
|
+
const reader = tail.getReader();
|
|
322
|
+
// Not a `using` disposal: the "pending" branch below hands the reader
|
|
323
|
+
// to a background continuation and must not dispose it out from under
|
|
324
|
+
// that continuation.
|
|
325
|
+
const dispose = () => {
|
|
326
|
+
reader.releaseLock();
|
|
327
|
+
tail[Symbol.dispose]();
|
|
328
|
+
};
|
|
329
|
+
const silent = Symbol("silent");
|
|
330
|
+
let timer;
|
|
331
|
+
let settleLater;
|
|
332
|
+
try {
|
|
333
|
+
const first = await Promise.race([
|
|
334
|
+
reader.read(),
|
|
335
|
+
new Promise((resolve) => {
|
|
336
|
+
timer = setTimeout(() => {
|
|
337
|
+
resolve(silent);
|
|
338
|
+
}, liveProbeMs);
|
|
339
|
+
}),
|
|
340
|
+
]);
|
|
341
|
+
if (first === silent)
|
|
342
|
+
return { state: "running" };
|
|
343
|
+
if (first.done) {
|
|
344
|
+
try {
|
|
345
|
+
return {
|
|
346
|
+
state: "exited",
|
|
347
|
+
exitCode: await readExitCode(runtime, id),
|
|
348
|
+
sync: "unknown",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
catch (cause) {
|
|
352
|
+
console.error("Computer process observation cleanup failed", cause);
|
|
353
|
+
return { state: "exited", exitCode: null, sync: "unknown" };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (first.value.name === "exit") {
|
|
357
|
+
// Keep draining to EOF, bounded, before reporting exited: that read
|
|
358
|
+
// is the one that runs computerd's post-command Workspace pull.
|
|
359
|
+
// Returning immediately disposes the reader first (the `finally`
|
|
360
|
+
// below cancels it), which skips that pull instead of waiting for
|
|
361
|
+
// it; waiting unboundedly can hang on a backgrounded child that
|
|
362
|
+
// still holds the transport open past this execution's own exit.
|
|
363
|
+
const exitCode = first.value.code;
|
|
364
|
+
const drained = await drainPastExit(reader);
|
|
365
|
+
settleLater = drained.settleLater;
|
|
366
|
+
if (settleLater !== undefined)
|
|
367
|
+
return { state: "exited", exitCode, sync: drained.sync, settleLater };
|
|
368
|
+
return { state: "exited", exitCode, sync: drained.sync };
|
|
369
|
+
}
|
|
370
|
+
return { state: "running" };
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
return failedObservation(error instanceof Error ? error : undefined);
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
clearTimeout(timer);
|
|
377
|
+
if (settleLater === undefined) {
|
|
378
|
+
await reader.cancel().catch((cause) => {
|
|
379
|
+
console.error("Computer process observation cleanup failed", cause);
|
|
380
|
+
return undefined;
|
|
381
|
+
});
|
|
382
|
+
dispose();
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
void settleLater.finally(dispose);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Drain `run` to its exit, bounded past that exit the same way
|
|
391
|
+
* `observeProcess` is, and determine whether its files reached the
|
|
392
|
+
* Workspace. `undefined` if the stream broke or was replaced before any
|
|
393
|
+
* exit was observed -- there is nothing to record.
|
|
394
|
+
*/
|
|
395
|
+
export async function drainProcessExit(run) {
|
|
396
|
+
const reader = run.getReader();
|
|
397
|
+
const dispose = () => {
|
|
398
|
+
reader.releaseLock();
|
|
399
|
+
run[Symbol.dispose]();
|
|
400
|
+
};
|
|
401
|
+
let exitCode;
|
|
402
|
+
let sync = "unknown";
|
|
403
|
+
let settleLater;
|
|
404
|
+
try {
|
|
405
|
+
for (;;) {
|
|
406
|
+
const next = await reader.read();
|
|
407
|
+
if (next.done)
|
|
408
|
+
break;
|
|
409
|
+
if (next.value.name === "exit") {
|
|
410
|
+
exitCode = next.value.code;
|
|
411
|
+
const drained = await drainPastExit(reader);
|
|
412
|
+
sync = drained.sync;
|
|
413
|
+
settleLater = drained.settleLater;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
if (settleLater === undefined)
|
|
420
|
+
dispose();
|
|
421
|
+
}
|
|
422
|
+
if (exitCode === undefined)
|
|
423
|
+
return undefined;
|
|
424
|
+
if (settleLater !== undefined)
|
|
425
|
+
return { exitCode, sync, settleLater: settleLater.finally(dispose) };
|
|
426
|
+
return { exitCode, sync };
|
|
427
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SyncRetryScheduler } from "@fungi.computer/cloudflare-computer";
|
|
2
|
+
/**
|
|
3
|
+
* A faithful, in-memory delegate for `@cloudflare/computer`'s own
|
|
4
|
+
* `retryScheduler` option: the library's documented host-owned durable
|
|
5
|
+
* storage boundary for a post-command pull that failed (`onPullPending`,
|
|
6
|
+
* from `runPostPull`'s catch branch -- "the workspace library cannot own
|
|
7
|
+
* a Durable Object alarm"). `get`, `schedule` and `clear` store and return
|
|
8
|
+
* the library's real intents, so its own dedup-by-runtimeId and
|
|
9
|
+
* exponential backoff (`Workspace.retryPendingSync`) stay correct for
|
|
10
|
+
* whoever wires an alarm to drive it. A wrapper that invents its own
|
|
11
|
+
* signal instead -- for example, one whose `get` always reports no
|
|
12
|
+
* existing intent, to make the library call `schedule` on every failure
|
|
13
|
+
* -- would break that dedup and backoff for an actual retry.
|
|
14
|
+
*
|
|
15
|
+
* This Computer does not yet call `retryPendingSync` from anywhere (no
|
|
16
|
+
* Durable Object alarm is wired to it), so a pending backend only clears
|
|
17
|
+
* when that retry runs and succeeds; it does not self-heal if the
|
|
18
|
+
* underlying problem goes away on its own. It is also in-memory only: it
|
|
19
|
+
* does not survive a Durable Object eviction. Both are known, bounded
|
|
20
|
+
* limitations, not a second compatibility path -- the real fix is wiring
|
|
21
|
+
* an alarm to `retryPendingSync`, a separate change.
|
|
22
|
+
*/
|
|
23
|
+
export declare class PullPendingTracker {
|
|
24
|
+
#private;
|
|
25
|
+
readonly scheduler: SyncRetryScheduler;
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A faithful, in-memory delegate for `@cloudflare/computer`'s own
|
|
3
|
+
* `retryScheduler` option: the library's documented host-owned durable
|
|
4
|
+
* storage boundary for a post-command pull that failed (`onPullPending`,
|
|
5
|
+
* from `runPostPull`'s catch branch -- "the workspace library cannot own
|
|
6
|
+
* a Durable Object alarm"). `get`, `schedule` and `clear` store and return
|
|
7
|
+
* the library's real intents, so its own dedup-by-runtimeId and
|
|
8
|
+
* exponential backoff (`Workspace.retryPendingSync`) stay correct for
|
|
9
|
+
* whoever wires an alarm to drive it. A wrapper that invents its own
|
|
10
|
+
* signal instead -- for example, one whose `get` always reports no
|
|
11
|
+
* existing intent, to make the library call `schedule` on every failure
|
|
12
|
+
* -- would break that dedup and backoff for an actual retry.
|
|
13
|
+
*
|
|
14
|
+
* This Computer does not yet call `retryPendingSync` from anywhere (no
|
|
15
|
+
* Durable Object alarm is wired to it), so a pending backend only clears
|
|
16
|
+
* when that retry runs and succeeds; it does not self-heal if the
|
|
17
|
+
* underlying problem goes away on its own. It is also in-memory only: it
|
|
18
|
+
* does not survive a Durable Object eviction. Both are known, bounded
|
|
19
|
+
* limitations, not a second compatibility path -- the real fix is wiring
|
|
20
|
+
* an alarm to `retryPendingSync`, a separate change.
|
|
21
|
+
*/
|
|
22
|
+
export class PullPendingTracker {
|
|
23
|
+
#intents = new Map();
|
|
24
|
+
scheduler = {
|
|
25
|
+
get: (backend) => Promise.resolve(this.#intents.get(backend)),
|
|
26
|
+
schedule: (intent) => {
|
|
27
|
+
this.#intents.set(intent.backend, intent);
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
},
|
|
30
|
+
clear: (backend) => {
|
|
31
|
+
this.#intents.delete(backend);
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|