@alisio/core 0.5.2 → 0.6.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/dist/analysis/janitor.d.ts +48 -0
- package/dist/analysis/janitor.js +170 -18
- package/dist/analysis/jobs.d.ts +11 -2
- package/dist/analysis/jobs.js +13 -0
- package/dist/analysis/oci.d.ts +11 -2
- package/dist/analysis/oci.js +20 -4
- package/dist/analysis/python/sources.js +2 -1
- package/dist/analysis/rerun.d.ts +1 -1
- package/dist/analysis/status.d.ts +3 -1
- package/dist/analysis/status.js +3 -0
- package/dist/application.d.ts +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +26 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.js +188 -6
- package/dist/core/call-internals.d.ts +22 -0
- package/dist/core/call-internals.js +9 -0
- package/dist/core/contracts.d.ts +2 -2
- package/dist/core/runner.js +111 -38
- package/dist/forge/broker.d.ts +51 -0
- package/dist/forge/broker.js +244 -0
- package/dist/forge/budget.d.ts +27 -0
- package/dist/forge/budget.js +60 -0
- package/dist/forge/catalog-view.d.ts +24 -0
- package/dist/forge/catalog-view.js +43 -0
- package/dist/forge/errors.d.ts +26 -0
- package/dist/forge/errors.js +49 -0
- package/dist/forge/guides/javascript.d.ts +25 -0
- package/dist/forge/guides/javascript.js +90 -0
- package/dist/forge/guides/provider.d.ts +42 -0
- package/dist/forge/guides/provider.js +96 -0
- package/dist/forge/guides/python-environment.d.ts +15 -0
- package/dist/forge/guides/python-environment.js +81 -0
- package/dist/forge/guides/python.d.ts +38 -0
- package/dist/forge/guides/python.js +170 -0
- package/dist/forge/javascript/engine.d.ts +44 -0
- package/dist/forge/javascript/engine.js +229 -0
- package/dist/forge/javascript/hosts/factory.d.ts +25 -0
- package/dist/forge/javascript/hosts/factory.js +21 -0
- package/dist/forge/javascript/hosts/framing.d.ts +17 -0
- package/dist/forge/javascript/hosts/framing.js +49 -0
- package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
- package/dist/forge/javascript/hosts/isolate-main.js +63 -0
- package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/process-entry.js +3 -0
- package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
- package/dist/forge/javascript/hosts/process-host.js +160 -0
- package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/worker-entry.js +3 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +18 -0
- package/dist/forge/javascript/hosts/worker-host.js +101 -0
- package/dist/forge/javascript/prelude.d.ts +2 -0
- package/dist/forge/javascript/prelude.js +333 -0
- package/dist/forge/javascript/probe.d.ts +2 -0
- package/dist/forge/javascript/probe.js +53 -0
- package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
- package/dist/forge/javascript/runtime-manifest.js +14 -0
- package/dist/forge/javascript/vm-runner.d.ts +21 -0
- package/dist/forge/javascript/vm-runner.js +301 -0
- package/dist/forge/javascript/wasm-asset.d.ts +8 -0
- package/dist/forge/javascript/wasm-asset.js +8 -0
- package/dist/forge/javascript/wasm.d.ts +15 -0
- package/dist/forge/javascript/wasm.js +54 -0
- package/dist/forge/journal.d.ts +105 -0
- package/dist/forge/journal.js +174 -0
- package/dist/forge/kernel.d.ts +76 -0
- package/dist/forge/kernel.js +338 -0
- package/dist/forge/limits.d.ts +38 -0
- package/dist/forge/limits.js +73 -0
- package/dist/forge/nested.d.ts +96 -0
- package/dist/forge/nested.js +214 -0
- package/dist/forge/ports.d.ts +120 -0
- package/dist/forge/ports.js +1 -0
- package/dist/forge/progress.d.ts +24 -0
- package/dist/forge/progress.js +76 -0
- package/dist/forge/protocol.d.ts +110 -0
- package/dist/forge/protocol.js +171 -0
- package/dist/forge/python/backend-selector.d.ts +38 -0
- package/dist/forge/python/backend-selector.js +111 -0
- package/dist/forge/python/backend.d.ts +89 -0
- package/dist/forge/python/backend.js +1 -0
- package/dist/forge/python/engine.d.ts +84 -0
- package/dist/forge/python/engine.js +279 -0
- package/dist/forge/python/frame-parser.d.ts +24 -0
- package/dist/forge/python/frame-parser.js +107 -0
- package/dist/forge/python/frame-transport.d.ts +40 -0
- package/dist/forge/python/frame-transport.js +101 -0
- package/dist/forge/python/import-scan.d.ts +22 -0
- package/dist/forge/python/import-scan.js +133 -0
- package/dist/forge/python/managed-backend.d.ts +30 -0
- package/dist/forge/python/managed-backend.js +111 -0
- package/dist/forge/python/oci-backend.d.ts +27 -0
- package/dist/forge/python/oci-backend.js +84 -0
- package/dist/forge/python/pyodide-backend.d.ts +40 -0
- package/dist/forge/python/pyodide-backend.js +289 -0
- package/dist/forge/python/pyodide-install.d.ts +41 -0
- package/dist/forge/python/pyodide-install.js +167 -0
- package/dist/forge/python/pyodide-manifest.d.ts +42 -0
- package/dist/forge/python/pyodide-manifest.js +376 -0
- package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
- package/dist/forge/python/pyodide-runner-source.js +3 -0
- package/dist/forge/python/rss.d.ts +2 -0
- package/dist/forge/python/rss.js +33 -0
- package/dist/forge/python/scratch-copy.d.ts +6 -0
- package/dist/forge/python/scratch-copy.js +61 -0
- package/dist/forge/python/settle.d.ts +6 -0
- package/dist/forge/python/settle.js +21 -0
- package/dist/forge/python/status.d.ts +8 -0
- package/dist/forge/python/status.js +15 -0
- package/dist/forge/python/wasm-node.d.ts +59 -0
- package/dist/forge/python/wasm-node.js +220 -0
- package/dist/forge/python/wiring.d.ts +39 -0
- package/dist/forge/python/wiring.js +47 -0
- package/dist/forge/registry.d.ts +26 -0
- package/dist/forge/registry.js +83 -0
- package/dist/forge/result.d.ts +58 -0
- package/dist/forge/result.js +86 -0
- package/dist/forge/routing-rule.d.ts +19 -0
- package/dist/forge/routing-rule.js +31 -0
- package/dist/forge/runtime.d.ts +64 -0
- package/dist/forge/runtime.js +88 -0
- package/dist/forge/scratch-files.d.ts +36 -0
- package/dist/forge/scratch-files.js +94 -0
- package/dist/forge/scratch-provider.d.ts +41 -0
- package/dist/forge/scratch-provider.js +79 -0
- package/dist/forge/scratch-quota.d.ts +8 -0
- package/dist/forge/scratch-quota.js +25 -0
- package/dist/forge/selector.d.ts +35 -0
- package/dist/forge/selector.js +51 -0
- package/dist/forge/session-dir.d.ts +59 -0
- package/dist/forge/session-dir.js +239 -0
- package/dist/forge/session-path.d.ts +31 -0
- package/dist/forge/session-path.js +61 -0
- package/dist/forge/slots.d.ts +16 -0
- package/dist/forge/slots.js +47 -0
- package/dist/forge/state-delta.d.ts +54 -0
- package/dist/forge/state-delta.js +99 -0
- package/dist/forge/state-machine.d.ts +20 -0
- package/dist/forge/state-machine.js +40 -0
- package/dist/forge/state-store.d.ts +25 -0
- package/dist/forge/state-store.js +50 -0
- package/dist/forge/task-log.d.ts +9 -0
- package/dist/forge/task-log.js +42 -0
- package/dist/forge/verdict.d.ts +20 -0
- package/dist/forge/verdict.js +28 -0
- package/dist/forge-isolate.d.ts +6 -0
- package/dist/forge-isolate.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/resources/context.d.ts +2 -1
- package/dist/resources/context.js +2 -1
- package/dist/runtime/access.d.ts +34 -0
- package/dist/runtime/access.js +80 -0
- package/dist/runtime/process.d.ts +2 -1
- package/dist/runtime/process.js +5 -5
- package/dist/runtime/store.d.ts +11 -0
- package/dist/runtime/store.js +103 -0
- package/dist/tools/analysis.d.ts +25 -2
- package/dist/tools/analysis.js +315 -267
- package/dist/tools/execute.js +6 -7
- package/dist/tools/forge-guide.d.ts +6 -0
- package/dist/tools/forge-guide.js +60 -0
- package/dist/tools/js-run.d.ts +19 -0
- package/dist/tools/js-run.js +126 -0
- package/dist/tools/standard.d.ts +7 -1
- package/dist/tools/standard.js +44 -43
- package/package.json +7 -2
|
@@ -58,3 +58,51 @@ export declare class AnalysisJanitor {
|
|
|
58
58
|
private pruneEmptyParents;
|
|
59
59
|
private dropDatasets;
|
|
60
60
|
}
|
|
61
|
+
export interface ScratchSweepReport {
|
|
62
|
+
/** Why nothing ran: swept less than 24 h ago, or another process is sweeping. */
|
|
63
|
+
skipped?: "recent" | "locked";
|
|
64
|
+
sessionsDeleted: number;
|
|
65
|
+
bytesFreed: number;
|
|
66
|
+
errors: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Retention of the volatile Forja session directories (spec §9.4): every `<root>/<workspace>/
|
|
70
|
+
* <rootSessionId>` folder goes when its root session no longer exists or its last activity
|
|
71
|
+
* (newest file in it) is older than `retentionDays`; `0` disables the deletion.
|
|
72
|
+
*
|
|
73
|
+
* Before deleting, the folder must be a real directory (never a symlink) under a real directory
|
|
74
|
+
* under a verified root, its `.alisio-session` marker must name the same session as the folder,
|
|
75
|
+
* and its real path must stay inside the real root. `rm` never follows a symlink it meets inside.
|
|
76
|
+
* Same schedule as the analysis janitor: at most once every 24 h (mark under `<state>/forge/`),
|
|
77
|
+
* with a lock between processes; deletions are best effort and independent.
|
|
78
|
+
*/
|
|
79
|
+
export declare class ScratchJanitor {
|
|
80
|
+
private options;
|
|
81
|
+
private timer?;
|
|
82
|
+
private stopped;
|
|
83
|
+
constructor(options: {
|
|
84
|
+
/** State root; holds the sweep mark and lock under `forge/`. */
|
|
85
|
+
stateRoot: string;
|
|
86
|
+
/** The scratch roots to sweep: `<tmp>/alisio-<uid>` and `<state>/scratch`. */
|
|
87
|
+
roots: () => string[];
|
|
88
|
+
/** Whether the root session still exists in the store. */
|
|
89
|
+
sessionExists: (rootSessionId: string) => boolean;
|
|
90
|
+
/** Read at every sweep, so a changed setting applies without a restart. */
|
|
91
|
+
retentionDays: () => number;
|
|
92
|
+
now?: () => number;
|
|
93
|
+
initialDelayMs?: number;
|
|
94
|
+
});
|
|
95
|
+
private now;
|
|
96
|
+
private get dir();
|
|
97
|
+
lastSweep(): Promise<number | undefined>;
|
|
98
|
+
/** Launches the sweep in the background; never delays the caller and never keeps Node alive. */
|
|
99
|
+
start(): void;
|
|
100
|
+
stop(): void;
|
|
101
|
+
private arm;
|
|
102
|
+
sweep(options?: {
|
|
103
|
+
force?: boolean;
|
|
104
|
+
}): Promise<ScratchSweepReport>;
|
|
105
|
+
private sweepRoot;
|
|
106
|
+
/** Whether `folder` is a verified session folder that is due; adds its size to the report. */
|
|
107
|
+
private expired;
|
|
108
|
+
}
|
package/dist/analysis/janitor.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Deletions are best effort and independent: one that fails is counted and the sweep goes on.
|
|
15
15
|
*/
|
|
16
|
-
import { mkdir, readFile, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
17
|
-
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
16
|
+
import { lstat, mkdir, readdir, readFile, realpath, rm, rmdir, stat, writeFile, } from "node:fs/promises";
|
|
17
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
18
|
+
import { readSessionMarker } from "../forge/session-dir.js";
|
|
18
19
|
const DAY = 86_400_000;
|
|
19
20
|
export const SWEEP_INTERVAL_MS = DAY;
|
|
20
21
|
/** A lock older than this belongs to a dead process. */
|
|
@@ -57,6 +58,27 @@ async function exists(path) {
|
|
|
57
58
|
return false;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Exclusive lock file `<dir>/<name>` between processes sharing the state folder; undefined when a
|
|
63
|
+
* live holder has it. A lock older than `STALE_LOCK_MS` belongs to a dead process and is replaced.
|
|
64
|
+
*/
|
|
65
|
+
async function acquireLock(dir, name) {
|
|
66
|
+
await mkdir(dir, { recursive: true });
|
|
67
|
+
const file = join(dir, name);
|
|
68
|
+
for (let attempt = 0; attempt < 2; attempt++)
|
|
69
|
+
try {
|
|
70
|
+
await writeFile(file, `${process.pid}\n`, { flag: "wx" });
|
|
71
|
+
return () => rm(file, { force: true });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
const info = await stat(file).catch(() => undefined);
|
|
75
|
+
// Real time on purpose: the lock belongs to a live process, not to the injected clock.
|
|
76
|
+
if (info && Date.now() - info.mtimeMs < STALE_LOCK_MS)
|
|
77
|
+
return undefined;
|
|
78
|
+
await rm(file, { force: true });
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
60
82
|
export class AnalysisJanitor {
|
|
61
83
|
options;
|
|
62
84
|
timer;
|
|
@@ -125,22 +147,8 @@ export class AnalysisJanitor {
|
|
|
125
147
|
return report;
|
|
126
148
|
}
|
|
127
149
|
/** Exclusive lock between processes sharing the state folder; undefined when someone holds it. */
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const file = join(this.dir, ".sweep.lock");
|
|
131
|
-
for (let attempt = 0; attempt < 2; attempt++)
|
|
132
|
-
try {
|
|
133
|
-
await writeFile(file, `${process.pid}\n`, { flag: "wx" });
|
|
134
|
-
return () => rm(file, { force: true });
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
const info = await stat(file).catch(() => undefined);
|
|
138
|
-
// Real time on purpose: the lock belongs to a live process, not to the injected clock.
|
|
139
|
-
if (info && Date.now() - info.mtimeMs < STALE_LOCK_MS)
|
|
140
|
-
return undefined;
|
|
141
|
-
await rm(file, { force: true });
|
|
142
|
-
}
|
|
143
|
-
return undefined;
|
|
150
|
+
lock() {
|
|
151
|
+
return acquireLock(this.dir, ".sweep.lock");
|
|
144
152
|
}
|
|
145
153
|
async expireArtifacts(policy, now, report) {
|
|
146
154
|
if (policy.artifactsDays <= 0)
|
|
@@ -262,3 +270,147 @@ export class AnalysisJanitor {
|
|
|
262
270
|
}
|
|
263
271
|
}
|
|
264
272
|
}
|
|
273
|
+
/** Newest modification time and total size under `dir`, without following symlinks. */
|
|
274
|
+
async function treeFacts(dir) {
|
|
275
|
+
const info = await lstat(dir);
|
|
276
|
+
let newest = info.mtimeMs;
|
|
277
|
+
let bytes = 0;
|
|
278
|
+
if (!info.isDirectory())
|
|
279
|
+
return { newest, bytes: info.size };
|
|
280
|
+
for (const entry of await readdir(dir)) {
|
|
281
|
+
const facts = await treeFacts(join(dir, entry)).catch(() => undefined);
|
|
282
|
+
if (!facts)
|
|
283
|
+
continue;
|
|
284
|
+
newest = Math.max(newest, facts.newest);
|
|
285
|
+
bytes += facts.bytes;
|
|
286
|
+
}
|
|
287
|
+
return { newest, bytes };
|
|
288
|
+
}
|
|
289
|
+
/** A directory that is really one (not a symlink to one). */
|
|
290
|
+
async function realDirectory(path) {
|
|
291
|
+
const info = await lstat(path).catch(() => undefined);
|
|
292
|
+
return !!info && info.isDirectory() && !info.isSymbolicLink();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Retention of the volatile Forja session directories (spec §9.4): every `<root>/<workspace>/
|
|
296
|
+
* <rootSessionId>` folder goes when its root session no longer exists or its last activity
|
|
297
|
+
* (newest file in it) is older than `retentionDays`; `0` disables the deletion.
|
|
298
|
+
*
|
|
299
|
+
* Before deleting, the folder must be a real directory (never a symlink) under a real directory
|
|
300
|
+
* under a verified root, its `.alisio-session` marker must name the same session as the folder,
|
|
301
|
+
* and its real path must stay inside the real root. `rm` never follows a symlink it meets inside.
|
|
302
|
+
* Same schedule as the analysis janitor: at most once every 24 h (mark under `<state>/forge/`),
|
|
303
|
+
* with a lock between processes; deletions are best effort and independent.
|
|
304
|
+
*/
|
|
305
|
+
export class ScratchJanitor {
|
|
306
|
+
options;
|
|
307
|
+
timer;
|
|
308
|
+
stopped = false;
|
|
309
|
+
constructor(options) {
|
|
310
|
+
this.options = options;
|
|
311
|
+
}
|
|
312
|
+
now() {
|
|
313
|
+
return (this.options.now ?? Date.now)();
|
|
314
|
+
}
|
|
315
|
+
get dir() {
|
|
316
|
+
return join(this.options.stateRoot, "forge");
|
|
317
|
+
}
|
|
318
|
+
async lastSweep() {
|
|
319
|
+
try {
|
|
320
|
+
const value = Number((await readFile(join(this.dir, ".last-scratch-sweep"), "utf8")).trim());
|
|
321
|
+
return Number.isFinite(value) ? value : undefined;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** Launches the sweep in the background; never delays the caller and never keeps Node alive. */
|
|
328
|
+
start() {
|
|
329
|
+
this.stopped = false;
|
|
330
|
+
this.arm(this.options.initialDelayMs ?? 30_000);
|
|
331
|
+
}
|
|
332
|
+
stop() {
|
|
333
|
+
this.stopped = true;
|
|
334
|
+
if (this.timer)
|
|
335
|
+
clearTimeout(this.timer);
|
|
336
|
+
this.timer = undefined;
|
|
337
|
+
}
|
|
338
|
+
arm(delay) {
|
|
339
|
+
if (this.stopped)
|
|
340
|
+
return;
|
|
341
|
+
this.timer = setTimeout(() => {
|
|
342
|
+
void this.sweep()
|
|
343
|
+
.catch(() => undefined)
|
|
344
|
+
.finally(() => this.arm(SWEEP_INTERVAL_MS));
|
|
345
|
+
}, delay);
|
|
346
|
+
this.timer.unref();
|
|
347
|
+
}
|
|
348
|
+
async sweep(options = {}) {
|
|
349
|
+
const report = { sessionsDeleted: 0, bytesFreed: 0, errors: 0 };
|
|
350
|
+
const now = this.now();
|
|
351
|
+
const last = await this.lastSweep();
|
|
352
|
+
if (!options.force && last !== undefined && now - last < SWEEP_INTERVAL_MS)
|
|
353
|
+
return { ...report, skipped: "recent" };
|
|
354
|
+
const release = await acquireLock(this.dir, ".scratch-sweep.lock");
|
|
355
|
+
if (!release)
|
|
356
|
+
return { ...report, skipped: "locked" };
|
|
357
|
+
try {
|
|
358
|
+
await writeFile(join(this.dir, ".last-scratch-sweep"), `${now}\n`);
|
|
359
|
+
const days = this.options.retentionDays();
|
|
360
|
+
if (days <= 0)
|
|
361
|
+
return report;
|
|
362
|
+
for (const root of this.options.roots())
|
|
363
|
+
await this.sweepRoot(root, days, now, report);
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
await release();
|
|
367
|
+
}
|
|
368
|
+
return report;
|
|
369
|
+
}
|
|
370
|
+
async sweepRoot(root, days, now, report) {
|
|
371
|
+
if (!(await realDirectory(root)))
|
|
372
|
+
return;
|
|
373
|
+
let realRoot;
|
|
374
|
+
try {
|
|
375
|
+
realRoot = await realpath(root);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
for (const workspace of await readdir(root).catch(() => [])) {
|
|
381
|
+
const workspaceDir = join(root, workspace);
|
|
382
|
+
if (!(await realDirectory(workspaceDir)))
|
|
383
|
+
continue;
|
|
384
|
+
for (const id of await readdir(workspaceDir).catch(() => [])) {
|
|
385
|
+
const folder = join(workspaceDir, id);
|
|
386
|
+
try {
|
|
387
|
+
if (await this.expired(folder, id, realRoot, days, now, report)) {
|
|
388
|
+
await rm(folder, { recursive: true, force: true });
|
|
389
|
+
report.sessionsDeleted++;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
report.errors++;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Drop the workspace folder when its last session went (rmdir only removes an empty one).
|
|
397
|
+
await rmdir(workspaceDir).catch(() => undefined);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** Whether `folder` is a verified session folder that is due; adds its size to the report. */
|
|
401
|
+
async expired(folder, id, realRoot, days, now, report) {
|
|
402
|
+
if (!(await realDirectory(folder)))
|
|
403
|
+
return false;
|
|
404
|
+
const marker = await readSessionMarker(folder);
|
|
405
|
+
if (!marker || marker.sessionId !== basename(folder) || marker.sessionId !== id)
|
|
406
|
+
return false;
|
|
407
|
+
const real = await realpath(folder);
|
|
408
|
+
if (!real.startsWith(realRoot + sep))
|
|
409
|
+
return false;
|
|
410
|
+
const facts = await treeFacts(folder);
|
|
411
|
+
const due = !this.options.sessionExists(id) || now - facts.newest > days * DAY;
|
|
412
|
+
if (due)
|
|
413
|
+
report.bytesFreed += facts.bytes;
|
|
414
|
+
return due;
|
|
415
|
+
}
|
|
416
|
+
}
|
package/dist/analysis/jobs.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { SqlDatabase } from "@alisio/sdk";
|
|
2
|
+
/** Where an execution ran (`analysis_executions.runtime`). */
|
|
3
|
+
export type AnalysisBackendId = "managed" | "oci" | "wasm";
|
|
2
4
|
export type ExecutionStatus = "running" | "completed" | "failed" | "cancelled" | "timed_out";
|
|
3
5
|
export interface JobPaths {
|
|
4
6
|
id: string;
|
|
@@ -34,7 +36,7 @@ export interface ExecutionDetails {
|
|
|
34
36
|
sessionId: string;
|
|
35
37
|
rootSessionId: string;
|
|
36
38
|
workspace: string;
|
|
37
|
-
runtime:
|
|
39
|
+
runtime: AnalysisBackendId;
|
|
38
40
|
status: ExecutionStatus;
|
|
39
41
|
scriptSha256: string;
|
|
40
42
|
exitCode?: number;
|
|
@@ -52,6 +54,8 @@ export declare class AnalysisJobs {
|
|
|
52
54
|
now?: () => number;
|
|
53
55
|
});
|
|
54
56
|
private now;
|
|
57
|
+
/** The store's database (Forja's journal and state live in it too). */
|
|
58
|
+
get database(): SqlDatabase;
|
|
55
59
|
/** Creates the job folder (script, runtime helpers, empty input/work/staging/logs) and its row. */
|
|
56
60
|
create(input: {
|
|
57
61
|
sessionId: string;
|
|
@@ -60,7 +64,7 @@ export declare class AnalysisJobs {
|
|
|
60
64
|
runId?: string;
|
|
61
65
|
callId?: string;
|
|
62
66
|
code: string;
|
|
63
|
-
runtime:
|
|
67
|
+
runtime: AnalysisBackendId;
|
|
64
68
|
/** The execution this one repeats (a rerun); recorded in the row and in `job.json`. */
|
|
65
69
|
rerunOf?: string;
|
|
66
70
|
}): Promise<JobPaths & {
|
|
@@ -74,6 +78,11 @@ export declare class AnalysisJobs {
|
|
|
74
78
|
addInput(job: JobPaths, source: string, preferred?: string): Promise<JobInput>;
|
|
75
79
|
/** `input/inputs.json`: what each input is (the script reads it with `alisio_runtime.datasets.inputs()`). */
|
|
76
80
|
writeInputsManifest(job: JobPaths, entries: unknown[]): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* `work/.forge/state.json`: the Forja state snapshot a script reads with `forge.state.get`. A
|
|
83
|
+
* copy, written fresh for every run; changes travel back as protocol messages, never through it.
|
|
84
|
+
*/
|
|
85
|
+
writeForgeState(job: JobPaths, values: Record<string, unknown>): Promise<void>;
|
|
77
86
|
/** Adds the inputs to `job.json` (atomically), once they are copied into `input/`. */
|
|
78
87
|
recordInputs(job: JobPaths, recorded: RecordedInputs): Promise<void>;
|
|
79
88
|
/** The inputs a finished job recorded; `undefined` when `job.json` is missing or has none. */
|
package/dist/analysis/jobs.js
CHANGED
|
@@ -21,6 +21,10 @@ export class AnalysisJobs {
|
|
|
21
21
|
now() {
|
|
22
22
|
return (this.options.now ?? Date.now)();
|
|
23
23
|
}
|
|
24
|
+
/** The store's database (Forja's journal and state live in it too). */
|
|
25
|
+
get database() {
|
|
26
|
+
return this.options.db;
|
|
27
|
+
}
|
|
24
28
|
/** Creates the job folder (script, runtime helpers, empty input/work/staging/logs) and its row. */
|
|
25
29
|
async create(input) {
|
|
26
30
|
const id = newId("exec", this.now());
|
|
@@ -84,6 +88,15 @@ export class AnalysisJobs {
|
|
|
84
88
|
async writeInputsManifest(job, entries) {
|
|
85
89
|
await writeFile(join(job.input, "inputs.json"), `${JSON.stringify(entries, null, 2)}\n`);
|
|
86
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* `work/.forge/state.json`: the Forja state snapshot a script reads with `forge.state.get`. A
|
|
93
|
+
* copy, written fresh for every run; changes travel back as protocol messages, never through it.
|
|
94
|
+
*/
|
|
95
|
+
async writeForgeState(job, values) {
|
|
96
|
+
const dir = join(job.work, ".forge");
|
|
97
|
+
await mkdir(dir, { recursive: true });
|
|
98
|
+
await writeFile(join(dir, "state.json"), JSON.stringify(values));
|
|
99
|
+
}
|
|
87
100
|
/** Adds the inputs to `job.json` (atomically), once they are copied into `input/`. */
|
|
88
101
|
async recordInputs(job, recorded) {
|
|
89
102
|
const file = join(job.root, "job.json");
|
package/dist/analysis/oci.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface OciMounts {
|
|
|
13
13
|
input: string;
|
|
14
14
|
work: string;
|
|
15
15
|
staging: string;
|
|
16
|
+
/** The Forja scratchpad, mounted read-only at `/job/scratch` (Forja spec §9.4). Optional. */
|
|
17
|
+
scratch?: string;
|
|
16
18
|
}
|
|
17
19
|
/** Paths inside the container (spec §8.3). */
|
|
18
20
|
export declare const CONTAINER: {
|
|
@@ -20,6 +22,7 @@ export declare const CONTAINER: {
|
|
|
20
22
|
readonly input: "/job/input";
|
|
21
23
|
readonly work: "/job/work";
|
|
22
24
|
readonly out: "/job/out";
|
|
25
|
+
readonly scratch: "/job/scratch";
|
|
23
26
|
};
|
|
24
27
|
export declare const PIDS_LIMIT = 256;
|
|
25
28
|
export declare const TMPFS_SIZE = "256m";
|
|
@@ -94,7 +97,11 @@ export declare class OciRuntime {
|
|
|
94
97
|
*/
|
|
95
98
|
status(signal?: AbortSignal): Promise<OciStatus>;
|
|
96
99
|
/** The reason a run cannot start (`undefined` = ready), checked before any job is created. */
|
|
97
|
-
unavailable(signal?: AbortSignal
|
|
100
|
+
unavailable(signal?: AbortSignal,
|
|
101
|
+
/** `auto` chooses the runtime itself, so the messages must not talk about `analysis.runtime: "oci"`. */
|
|
102
|
+
options?: {
|
|
103
|
+
auto?: boolean;
|
|
104
|
+
}): Promise<string | undefined>;
|
|
98
105
|
/**
|
|
99
106
|
* Runs the job's script in a container. Cancelling or timing out `kills` the container by name
|
|
100
107
|
* (killing the client process does not always stop it) and no `alisio-*` container is left.
|
|
@@ -105,12 +112,14 @@ export declare class OciRuntime {
|
|
|
105
112
|
signal: AbortSignal;
|
|
106
113
|
timeoutMs: number;
|
|
107
114
|
maxBytes: number;
|
|
108
|
-
onData?: (chunk: string) => void;
|
|
115
|
+
onData?: (chunk: string, stream: "stdout" | "stderr") => void;
|
|
109
116
|
logFiles: {
|
|
110
117
|
stdout: string;
|
|
111
118
|
stderr: string;
|
|
112
119
|
};
|
|
113
120
|
maxLogBytes: number;
|
|
121
|
+
/** Extra variables for the script (Forja: the frame nonce). */
|
|
122
|
+
env?: Record<string, string>;
|
|
114
123
|
}): Promise<ProcessResult & {
|
|
115
124
|
containerName: string;
|
|
116
125
|
}>;
|
package/dist/analysis/oci.js
CHANGED
|
@@ -19,6 +19,7 @@ export const CONTAINER = {
|
|
|
19
19
|
input: "/job/input",
|
|
20
20
|
work: "/job/work",
|
|
21
21
|
out: "/job/out",
|
|
22
|
+
scratch: "/job/scratch",
|
|
22
23
|
};
|
|
23
24
|
export const PIDS_LIMIT = 256;
|
|
24
25
|
export const TMPFS_SIZE = "256m";
|
|
@@ -69,7 +70,10 @@ export function ociRunArgs(input) {
|
|
|
69
70
|
else if (!host.rootless && host.uid !== 0)
|
|
70
71
|
args.push("--user", `${host.uid}:${host.gid}`);
|
|
71
72
|
}
|
|
72
|
-
args.push("-v", mount(input.mounts.script, CONTAINER.script, "ro"), "-v", mount(input.mounts.input, CONTAINER.input, "ro"), "-v", mount(input.mounts.work, CONTAINER.work, "rw"), "-v", mount(input.mounts.staging, CONTAINER.out, "rw")
|
|
73
|
+
args.push("-v", mount(input.mounts.script, CONTAINER.script, "ro"), "-v", mount(input.mounts.input, CONTAINER.input, "ro"), "-v", mount(input.mounts.work, CONTAINER.work, "rw"), "-v", mount(input.mounts.staging, CONTAINER.out, "rw"));
|
|
74
|
+
if (input.mounts.scratch)
|
|
75
|
+
args.push("-v", mount(input.mounts.scratch, CONTAINER.scratch, "ro"));
|
|
76
|
+
args.push("--tmpfs", `/tmp:size=${TMPFS_SIZE}`);
|
|
73
77
|
const env = {
|
|
74
78
|
ALISIO_INPUT_DIR: CONTAINER.input,
|
|
75
79
|
ALISIO_OUTPUT_DIR: CONTAINER.out,
|
|
@@ -83,6 +87,7 @@ export function ociRunArgs(input) {
|
|
|
83
87
|
PYTHONUNBUFFERED: "1",
|
|
84
88
|
PYTHONNOUSERSITE: "1",
|
|
85
89
|
PYTHONIOENCODING: "utf-8",
|
|
90
|
+
...(input.mounts.scratch ? { ALISIO_SCRATCH_DIR: CONTAINER.scratch } : {}),
|
|
86
91
|
...input.env,
|
|
87
92
|
};
|
|
88
93
|
for (const [key, value] of Object.entries(env))
|
|
@@ -191,16 +196,26 @@ export class OciRuntime {
|
|
|
191
196
|
}
|
|
192
197
|
}
|
|
193
198
|
/** The reason a run cannot start (`undefined` = ready), checked before any job is created. */
|
|
194
|
-
async unavailable(signal
|
|
199
|
+
async unavailable(signal,
|
|
200
|
+
/** `auto` chooses the runtime itself, so the messages must not talk about `analysis.runtime: "oci"`. */
|
|
201
|
+
options = {}) {
|
|
195
202
|
const { image } = this.settings;
|
|
203
|
+
if (!image && options.auto)
|
|
204
|
+
return "no container image is set (analysis.oci.image); `alisio analysis setup --oci --image <name>` prints the value";
|
|
196
205
|
if (!image)
|
|
197
206
|
return ('analysis.runtime is "oci" but analysis.oci.image is not set. Put a digest-pinned image ' +
|
|
198
207
|
"(name@sha256:…) in your user configuration; `alisio analysis setup --oci --image <name>` prints the value.");
|
|
199
208
|
if (!OCI_IMAGE_PATTERN.test(image))
|
|
200
209
|
return "analysis.oci.image must be pinned by digest";
|
|
201
210
|
const status = await this.status(signal);
|
|
202
|
-
if (!status.ok)
|
|
203
|
-
|
|
211
|
+
if (!status.ok) {
|
|
212
|
+
const alternative = status.alternative
|
|
213
|
+
? `; ${status.alternative} is installed (set analysis.oci.engine to "${status.alternative}")`
|
|
214
|
+
: "";
|
|
215
|
+
return options.auto
|
|
216
|
+
? `${status.reason}${alternative}`
|
|
217
|
+
: `${status.reason}${alternative}. The container runtime is optional: remove analysis.runtime "oci" to use managed Python.`;
|
|
218
|
+
}
|
|
204
219
|
return undefined;
|
|
205
220
|
}
|
|
206
221
|
/**
|
|
@@ -221,6 +236,7 @@ export class OciRuntime {
|
|
|
221
236
|
executionId: input.executionId,
|
|
222
237
|
mounts: input.mounts,
|
|
223
238
|
host: { ...host, rootless: status.ok ? status.rootless : false },
|
|
239
|
+
...(input.env ? { env: input.env } : {}),
|
|
224
240
|
});
|
|
225
241
|
const name = containerName(input.executionId);
|
|
226
242
|
const stop = AbortSignal.any([input.signal, AbortSignal.timeout(input.timeoutMs)]);
|