pi-supernova 0.0.7 → 0.0.8
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/README.md +22 -15
- package/bottleneck.js +32 -41
- package/catalog.js +61 -15
- package/config.default.json +2 -1
- package/config.js +19 -10
- package/diff.js +12 -4
- package/format.js +95 -0
- package/guest-worker.js +344 -0
- package/host-bridge.js +130 -439
- package/index.js +75 -97
- package/omp-frame.js +68 -51
- package/package.json +6 -1
- package/parallel.js +19 -20
- package/patch.js +106 -0
- package/render-measure.js +63 -49
- package/render.js +193 -166
- package/runtime.js +266 -241
- package/snap.js +120 -101
- package/surface.js +13 -30
- package/vfs.js +138 -0
- package/workspace.js +112 -0
package/runtime.js
CHANGED
|
@@ -1,268 +1,293 @@
|
|
|
1
|
-
|
|
2
|
-
import { packageFinalReturn } from "./bottleneck.js";
|
|
3
|
-
import { parallel as runParallel, pipeline as runPipeline } from "./parallel.js";
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
4
2
|
import { performance } from "node:perf_hooks";
|
|
5
|
-
import {
|
|
3
|
+
import { packageFinalReturn } from "./bottleneck.js";
|
|
4
|
+
import { isFunction, isObject } from "./decode.js";
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
// Guest code runs in a worker thread (see guest-worker.js). The host thread
|
|
7
|
+
// owns the bridge and answers nova.* RPCs; a hard timeout or abort terminates
|
|
8
|
+
// the worker, which is the only way to stop a synchronous loop.
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const WORKER_URL = new URL("./guest-worker.js", import.meta.url);
|
|
11
|
+
const ABORT_MESSAGE = "supernova timed out or aborted: pass timeoutMs to allow longer runs, or split the program";
|
|
12
|
+
// Bun ignores worker resourceLimits, so a process-RSS watchdog backs up the V8 heap cap.
|
|
13
|
+
const MEMORY_POLL_MS = 50;
|
|
14
|
+
const MEMORY_SLACK = 1.5;
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
const trimmed = String(code || "").trim();
|
|
14
|
-
if (!trimmed) throw new Error("code must be a non-empty string");
|
|
16
|
+
const rssBytes = isFunction(process.memoryUsage?.rss) ? () => process.memoryUsage.rss() : () => process.memoryUsage().rss;
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
if (/^async\s+function\b/.test(trimmed) || /^function\b/.test(trimmed)) {
|
|
20
|
-
return `const __fn = (${trimmed});\nreturn await __fn();`;
|
|
21
|
-
}
|
|
22
|
-
return trimmed;
|
|
23
|
-
}
|
|
18
|
+
let idleWorker = null;
|
|
19
|
+
let runSeq = 0;
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
const {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
function spawnWorker(config) {
|
|
22
|
+
const { maxHeapMb = 512 } = config;
|
|
23
|
+
const worker = new Worker(WORKER_URL, {
|
|
24
|
+
resourceLimits: { maxOldGenerationSizeMb: maxHeapMb },
|
|
25
|
+
});
|
|
26
|
+
const handle = { worker, dead: false, ready: null };
|
|
27
|
+
handle.ready = new Promise((resolve, reject) => {
|
|
28
|
+
const onMessage = (msg) => {
|
|
29
|
+
if (msg?.op === "ready") {
|
|
30
|
+
cleanup();
|
|
31
|
+
resolve();
|
|
32
|
+
}
|
|
34
33
|
};
|
|
35
|
-
|
|
34
|
+
const onFail = (err) => {
|
|
35
|
+
cleanup();
|
|
36
|
+
handle.dead = true;
|
|
37
|
+
reject(err instanceof Error ? err : new Error("guest worker exited before ready (code " + err + ")"));
|
|
38
|
+
};
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
worker.off("message", onMessage);
|
|
41
|
+
worker.off("error", onFail);
|
|
42
|
+
worker.off("exit", onFail);
|
|
43
|
+
};
|
|
44
|
+
worker.on("message", onMessage);
|
|
45
|
+
worker.on("error", onFail);
|
|
46
|
+
worker.on("exit", onFail);
|
|
47
|
+
});
|
|
48
|
+
handle.ready.catch(() => {});
|
|
49
|
+
worker.on("exit", () => {
|
|
50
|
+
handle.dead = true;
|
|
51
|
+
if (idleWorker === handle) idleWorker = null;
|
|
52
|
+
});
|
|
53
|
+
return handle;
|
|
54
|
+
}
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
56
|
+
function setIdleRef(handle, idle) {
|
|
57
|
+
const fn = idle ? handle.worker.unref : handle.worker.ref;
|
|
58
|
+
if (isFunction(fn)) fn.call(handle.worker);
|
|
59
|
+
}
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
function acquireWorker(config) {
|
|
62
|
+
const handle = idleWorker && !idleWorker.dead ? idleWorker : spawnWorker(config);
|
|
63
|
+
idleWorker = null;
|
|
64
|
+
setIdleRef(handle, false);
|
|
65
|
+
return handle;
|
|
66
|
+
}
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
ok: false,
|
|
54
|
-
error: err instanceof Error ? err.message : String(err),
|
|
55
|
-
logs,
|
|
56
|
-
wallMs: Math.round(performance.now() - started),
|
|
57
|
-
};
|
|
68
|
+
function releaseWorker(handle) {
|
|
69
|
+
if (handle.dead) return;
|
|
70
|
+
if (idleWorker && idleWorker !== handle) {
|
|
71
|
+
void handle.worker.terminate();
|
|
72
|
+
return;
|
|
58
73
|
}
|
|
74
|
+
idleWorker = handle;
|
|
75
|
+
setIdleRef(handle, true);
|
|
76
|
+
}
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"tools",
|
|
66
|
-
"console",
|
|
67
|
-
"parallel",
|
|
68
|
-
"pipeline",
|
|
69
|
-
"read",
|
|
70
|
-
"write",
|
|
71
|
-
"edit",
|
|
72
|
-
"patch",
|
|
73
|
-
"surface",
|
|
74
|
-
"snap",
|
|
75
|
-
"bash",
|
|
76
|
-
"exec",
|
|
77
|
-
"speculate",
|
|
78
|
-
body,
|
|
79
|
-
);
|
|
80
|
-
} catch (err) {
|
|
81
|
-
return {
|
|
82
|
-
ok: false,
|
|
83
|
-
error: err instanceof Error ? err.message : String(err),
|
|
84
|
-
logs,
|
|
85
|
-
wallMs: Math.round(performance.now() - started),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
if (compiledCache.size >= COMPILED_CACHE_MAX) {
|
|
89
|
-
const first = compiledCache.keys().next().value;
|
|
90
|
-
if (first !== undefined) compiledCache.delete(first);
|
|
91
|
-
}
|
|
92
|
-
compiledCache.set(body, compiled);
|
|
93
|
-
}
|
|
78
|
+
function killWorker(handle) {
|
|
79
|
+
handle.dead = true;
|
|
80
|
+
if (idleWorker === handle) idleWorker = null;
|
|
81
|
+
void handle.worker.terminate();
|
|
82
|
+
}
|
|
94
83
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
84
|
+
/** Pre-spawn the guest worker so the first program does not pay startup cost. */
|
|
85
|
+
export function warmGuestWorker(config) {
|
|
86
|
+
if (idleWorker && !idleWorker.dead) return idleWorker.ready;
|
|
87
|
+
const handle = spawnWorker(config || {});
|
|
88
|
+
idleWorker = handle;
|
|
89
|
+
// Keep the loop alive only until the worker reports ready; an idle worker must not pin the process.
|
|
90
|
+
handle.ready.then(
|
|
91
|
+
() => { if (idleWorker === handle) setIdleRef(handle, true); },
|
|
92
|
+
() => {},
|
|
93
|
+
);
|
|
94
|
+
return handle.ready;
|
|
95
|
+
}
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
if (res.details?.batch && Array.isArray(res.details?.items)) {
|
|
113
|
-
return res.details.items;
|
|
114
|
-
}
|
|
115
|
-
return res.value;
|
|
116
|
-
}
|
|
117
|
-
return res;
|
|
118
|
-
};
|
|
97
|
+
/** Terminate every guest worker (tests, shutdown). */
|
|
98
|
+
export async function shutdownGuestWorkers() {
|
|
99
|
+
if (!idleWorker) return;
|
|
100
|
+
const handle = idleWorker;
|
|
101
|
+
idleWorker = null;
|
|
102
|
+
handle.dead = true;
|
|
103
|
+
await handle.worker.terminate();
|
|
104
|
+
}
|
|
119
105
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (!
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
106
|
+
const RPC_METHODS = {
|
|
107
|
+
call: (nova, args) => {
|
|
108
|
+
if (!isFunction(nova?.call)) throw new Error("nova.call unavailable");
|
|
109
|
+
return nova.call(args[0], args[1]);
|
|
110
|
+
},
|
|
111
|
+
callMany: async (nova, args) => {
|
|
112
|
+
if (!isFunction(nova?.callMany)) throw new Error("nova.callMany unavailable");
|
|
113
|
+
const wave = await nova.callMany(args[0]);
|
|
114
|
+
if (Array.isArray(wave)) return { results: [...wave], mode: wave.mode, reason: wave.reason };
|
|
115
|
+
return wave;
|
|
116
|
+
},
|
|
117
|
+
search: (nova, args) => {
|
|
118
|
+
if (!isFunction(nova?.search)) throw new Error("nova.search unavailable");
|
|
119
|
+
return nova.search(args[0], args[1]);
|
|
120
|
+
},
|
|
121
|
+
describe: (nova, args) => {
|
|
122
|
+
if (!isFunction(nova?.describe)) throw new Error("nova.describe unavailable");
|
|
123
|
+
return nova.describe(args[0]);
|
|
124
|
+
},
|
|
125
|
+
surface: (nova, args) => {
|
|
126
|
+
if (isFunction(nova?.surface)) return nova.surface(args[0]);
|
|
127
|
+
return nova.call("surface", { path: args[0] });
|
|
128
|
+
},
|
|
129
|
+
snap: (nova, args) => {
|
|
130
|
+
if (isFunction(nova?.snap)) return nova.snap(args[0], args[1]);
|
|
131
|
+
return nova.call("snap", { query: args[0], path: args[1] });
|
|
132
|
+
},
|
|
133
|
+
speculateBegin: (nova) => (isFunction(nova?.speculateBegin) ? nova.speculateBegin() : undefined),
|
|
134
|
+
speculateCommit: (nova) => (isFunction(nova?.speculateCommit) ? nova.speculateCommit() : undefined),
|
|
135
|
+
speculateRollback: (nova) => (isFunction(nova?.speculateRollback) ? nova.speculateRollback() : undefined),
|
|
136
|
+
};
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return unwrapValue(res);
|
|
136
|
-
};
|
|
137
|
-
const guestWrite = async (p, c) => unwrapValue(await nova.call("write", { path: p, content: c }));
|
|
138
|
-
const guestEdit = async (p, oldOrDiff, newText) => {
|
|
139
|
-
const res = await nova.call("edit", { path: p, oldText: oldOrDiff, newText });
|
|
140
|
-
return unwrapValue(res);
|
|
141
|
-
};
|
|
142
|
-
const guestPatch = async (p, d) => unwrapValue(await nova.call("apply_patch", { path: p, patch: d }));
|
|
143
|
-
const guestSurface = async (p) => {
|
|
144
|
-
const res = await (isFunction(nova.surface) ? nova.surface(p) : nova.call("surface", { path: p }));
|
|
145
|
-
return unwrapJsonValue(res);
|
|
146
|
-
};
|
|
147
|
-
const guestSnap = async (q, p) => {
|
|
148
|
-
const res = await (isFunction(nova.snap) ? nova.snap(q, p) : nova.call("snap", { query: q, path: p }));
|
|
149
|
-
return unwrapJsonValue(res);
|
|
150
|
-
};
|
|
151
|
-
const guestBash = async (cmd, opts) => {
|
|
152
|
-
const res = await nova.call("bash", { command: cmd, ...opts });
|
|
153
|
-
if (res?.ok === false) {
|
|
154
|
-
const detail = isString(res?.details) ? res.details : "";
|
|
155
|
-
throw new Error(res?.value || detail || `command failed: ${cmd}`);
|
|
156
|
-
}
|
|
157
|
-
return unwrapValue(res);
|
|
158
|
-
};
|
|
159
|
-
const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
160
|
-
const guestExec = async (cmd, args, opts) => {
|
|
161
|
-
const command = String(cmd ?? "").trim();
|
|
162
|
-
if (!command) throw new Error("exec requires command");
|
|
163
|
-
// exec("git status") is a shell line; exec("git", ["status"]) is argv.
|
|
164
|
-
if (!Array.isArray(args) || args.length === 0) {
|
|
165
|
-
return guestBash(command, opts);
|
|
166
|
-
}
|
|
167
|
-
const argv = [command, ...args].map(quoteShellArg).join(" ");
|
|
168
|
-
return guestBash(argv, opts);
|
|
169
|
-
};
|
|
170
|
-
const guestSpeculate = async (fn) => (isFunction(nova.speculate) ? nova.speculate(fn) : fn());
|
|
138
|
+
async function dispatchRpc(nova, method, args) {
|
|
139
|
+
const fn = RPC_METHODS[method];
|
|
140
|
+
if (!fn) throw new Error("unknown nova method: " + method);
|
|
141
|
+
return fn(nova, args);
|
|
142
|
+
}
|
|
171
143
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
guestRead,
|
|
181
|
-
guestWrite,
|
|
182
|
-
guestEdit,
|
|
183
|
-
guestPatch,
|
|
184
|
-
guestSurface,
|
|
185
|
-
guestSnap,
|
|
186
|
-
guestBash,
|
|
187
|
-
guestExec,
|
|
188
|
-
guestSpeculate,
|
|
189
|
-
),
|
|
190
|
-
);
|
|
191
|
-
runPromise.catch((err) => {
|
|
192
|
-
if (!settled) return;
|
|
193
|
-
pushLog(logs, [`[late guest error] ${err instanceof Error ? err.message : String(err)}`], config);
|
|
194
|
-
});
|
|
195
|
-
timeoutPromise.catch(() => {
|
|
196
|
-
});
|
|
144
|
+
async function loadAvailable(nova) {
|
|
145
|
+
if (!isFunction(nova?.names)) return [];
|
|
146
|
+
try {
|
|
147
|
+
return await nova.names();
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
197
152
|
|
|
153
|
+
async function prepareRun(options, fail) {
|
|
154
|
+
const { code, config, signal, nova } = options;
|
|
155
|
+
if (!String(code || "").trim()) return { failed: fail("code must be a non-empty string") };
|
|
156
|
+
const maxCode = config.maxCodeChars ?? 48000;
|
|
157
|
+
if (code.length > maxCode) return { failed: fail("code exceeds " + maxCode + " characters") };
|
|
158
|
+
if (signal?.aborted) return { failed: fail(ABORT_MESSAGE) };
|
|
159
|
+
const handle = acquireWorker(config);
|
|
198
160
|
try {
|
|
199
|
-
|
|
200
|
-
settled = true;
|
|
201
|
-
const packaged = packageFinalReturn(resultValue, logs, config);
|
|
202
|
-
return {
|
|
203
|
-
ok: true,
|
|
204
|
-
result: packaged.returnValue,
|
|
205
|
-
resultText: packaged.returnText,
|
|
206
|
-
returnTruncated: packaged.returnTruncated,
|
|
207
|
-
logs: packaged.logs,
|
|
208
|
-
logTruncated: packaged.logTruncated,
|
|
209
|
-
wallMs: Math.round(performance.now() - started),
|
|
210
|
-
};
|
|
161
|
+
await handle.ready;
|
|
211
162
|
} catch (err) {
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
ok: false,
|
|
215
|
-
error: err instanceof Error ? err.message : String(err),
|
|
216
|
-
logs,
|
|
217
|
-
wallMs: Math.round(performance.now() - started),
|
|
218
|
-
};
|
|
219
|
-
} finally {
|
|
220
|
-
settled = true;
|
|
221
|
-
timeoutPromise.clear();
|
|
163
|
+
return { failed: fail("guest worker failed to start: " + err?.message) };
|
|
222
164
|
}
|
|
165
|
+
const available = await loadAvailable(nova);
|
|
166
|
+
return { handle, worker: handle.worker, available };
|
|
223
167
|
}
|
|
224
168
|
|
|
225
|
-
function
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
.
|
|
238
|
-
|
|
169
|
+
function startWatchdogs({ timeoutMs, rssLimit, signal, onAbort, onMemoryExceeded }) {
|
|
170
|
+
const timer = setTimeout(onAbort, timeoutMs);
|
|
171
|
+
if (timer.unref) timer.unref();
|
|
172
|
+
const memTimer = setInterval(() => {
|
|
173
|
+
if (rssBytes() <= rssLimit) return;
|
|
174
|
+
onMemoryExceeded(rssBytes());
|
|
175
|
+
}, MEMORY_POLL_MS);
|
|
176
|
+
if (memTimer.unref) memTimer.unref();
|
|
177
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
178
|
+
return () => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
clearInterval(memTimer);
|
|
181
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
182
|
+
};
|
|
239
183
|
}
|
|
240
184
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
185
|
+
const MESSAGE_HANDLERS = {
|
|
186
|
+
log: (msg, ctx) => {
|
|
187
|
+
ctx.logs.push(msg.line);
|
|
188
|
+
},
|
|
189
|
+
rpc: (msg, ctx) => {
|
|
190
|
+
dispatchRpc(ctx.nova, msg.method, msg.args).then(
|
|
191
|
+
(value) => ctx.postResult({ op: "rpc:result", id: msg.id, ok: true, value }),
|
|
192
|
+
(err) => ctx.postResult({ op: "rpc:result", id: msg.id, ok: false, error: err instanceof Error ? err.message : String(err) }),
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
done: (msg, ctx) => {
|
|
196
|
+
const packaged = packageFinalReturn(msg.value, ctx.logs, ctx.config);
|
|
197
|
+
ctx.finish(
|
|
198
|
+
{
|
|
199
|
+
ok: true,
|
|
200
|
+
result: packaged.returnValue,
|
|
201
|
+
resultText: packaged.returnText,
|
|
202
|
+
returnTruncated: packaged.returnTruncated,
|
|
203
|
+
undefinedReturn: msg.undefinedReturn === true && msg.hasReturn === false,
|
|
204
|
+
logs: packaged.logs,
|
|
205
|
+
logTruncated: packaged.logTruncated,
|
|
206
|
+
wallMs: ctx.wall(),
|
|
207
|
+
},
|
|
208
|
+
true,
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
error: (msg, ctx) => {
|
|
212
|
+
const where = msg.location ? " (line " + msg.location.line + ":" + msg.location.col + ")" : "";
|
|
213
|
+
ctx.finish(ctx.fail(msg.message + where, ctx.logs), true);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export async function runGuestProgram(options) {
|
|
218
|
+
const { code, nova, config, signal, onTimeout } = options;
|
|
219
|
+
const started = performance.now();
|
|
220
|
+
const wall = () => Math.round(performance.now() - started);
|
|
221
|
+
const fail = (error, logs = []) => ({ ok: false, error, logs, wallMs: wall() });
|
|
222
|
+
const prepared = await prepareRun(options, fail);
|
|
223
|
+
if (prepared.failed) return prepared.failed;
|
|
224
|
+
const { handle, worker, available } = prepared;
|
|
225
|
+
const logs = [];
|
|
226
|
+
const runId = ++runSeq;
|
|
227
|
+
const { timeoutMs = 60000, maxHeapMb = 512 } = config;
|
|
228
|
+
const rssLimit = rssBytes() + maxHeapMb * MEMORY_SLACK * 1048576;
|
|
229
|
+
return await new Promise((resolve) => {
|
|
230
|
+
let finished = false;
|
|
231
|
+
let stop;
|
|
232
|
+
const finish = (outcome, keepWorker) => {
|
|
233
|
+
if (finished) return;
|
|
234
|
+
finished = true;
|
|
235
|
+
stop();
|
|
236
|
+
worker.off("message", onMessage);
|
|
237
|
+
worker.off("error", onError);
|
|
238
|
+
worker.off("exit", onExit);
|
|
239
|
+
if (keepWorker) releaseWorker(handle);
|
|
240
|
+
else killWorker(handle);
|
|
241
|
+
resolve(outcome);
|
|
242
|
+
};
|
|
243
|
+
const abort = () => {
|
|
244
|
+
try {
|
|
245
|
+
onTimeout?.();
|
|
246
|
+
} catch {}
|
|
247
|
+
finish(fail(ABORT_MESSAGE, logs), false);
|
|
248
|
+
};
|
|
249
|
+
const postResult = (msg) => {
|
|
250
|
+
if (finished) return;
|
|
251
|
+
try {
|
|
252
|
+
worker.postMessage(msg);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
worker.postMessage({ op: "rpc:result", id: msg.id, ok: false, error: "result not transferable: " + err?.message });
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const ctx = { logs, nova, config, wall, fail, finish, postResult };
|
|
258
|
+
const onMessage = (msg) => {
|
|
259
|
+
if (!isObject(msg)) return;
|
|
260
|
+
if (msg.runId !== runId) {
|
|
261
|
+
if (msg.op === "rpc") postResult({ op: "rpc:result", id: msg.id, ok: false, error: "stale run" });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const handler = MESSAGE_HANDLERS[msg.op];
|
|
265
|
+
if (handler) handler(msg, ctx);
|
|
266
|
+
};
|
|
267
|
+
const onError = (err) => finish(fail("guest crashed: " + err?.message, logs), false);
|
|
268
|
+
const onExit = (runCode) => finish(fail("guest exited (code " + runCode + ")", logs), false);
|
|
269
|
+
worker.on("message", onMessage);
|
|
270
|
+
worker.on("error", onError);
|
|
271
|
+
worker.on("exit", onExit);
|
|
272
|
+
stop = startWatchdogs({
|
|
273
|
+
timeoutMs,
|
|
274
|
+
rssLimit,
|
|
275
|
+
signal,
|
|
276
|
+
onAbort: abort,
|
|
277
|
+
onMemoryExceeded: (rss) => {
|
|
278
|
+
finish(fail(`guest exceeded memory limit (maxHeapMb=${maxHeapMb}, process rss grew to ${Math.round(rss / 1048576)} MB)`, logs), false);
|
|
279
|
+
try {
|
|
280
|
+
onTimeout?.();
|
|
281
|
+
} catch {}
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const { maxLogLines = 100, maxLogLineChars = 4096 } = config;
|
|
285
|
+
worker.postMessage({
|
|
286
|
+
op: "run",
|
|
287
|
+
runId,
|
|
288
|
+
code,
|
|
289
|
+
limits: { maxLogLines, maxLogLineChars },
|
|
290
|
+
available,
|
|
291
|
+
});
|
|
262
292
|
});
|
|
263
|
-
promise.clear = () => {
|
|
264
|
-
clearTimeout(timer);
|
|
265
|
-
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
266
|
-
};
|
|
267
|
-
return promise;
|
|
268
293
|
}
|