@qvac/harness 0.6.0 → 0.7.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/CHANGELOG.md +91 -1
- package/README.md +32 -27
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.js +2 -2
- package/dist-lib/lib/capabilities/preview-model-source.d.ts +0 -1
- package/dist-lib/lib/connect.js +5 -2
- package/dist-lib/lib/contract-types.d.ts +1 -0
- package/dist-lib/lib/docx/index.d.ts +1 -1
- package/dist-lib/lib/docx/index.js +1 -1
- package/dist-lib/lib/hardware/capability-probe.js +9 -4
- package/dist-lib/lib/hooks/built-in/sandbox.js +0 -5
- package/dist-lib/lib/models/model-catalog-constants.d.ts +7 -0
- package/dist-lib/lib/models/model-catalog-constants.js +17 -0
- package/dist-lib/lib/models/model-catalog.d.ts +14 -6
- package/dist-lib/lib/models/model-catalog.js +14 -6
- package/dist-lib/lib/models/model-list.d.ts +2 -0
- package/dist-lib/lib/models/model-list.js +9 -4
- package/dist-lib/lib/models/model-task.d.ts +3 -0
- package/dist-lib/lib/models/model-task.js +14 -0
- package/dist-lib/lib/models/music-catalog.d.ts +2 -0
- package/dist-lib/lib/models/music-catalog.js +9 -0
- package/dist-lib/lib/office/index.d.ts +1 -1
- package/dist-lib/lib/office/index.js +1 -1
- package/dist-lib/lib/office/python.js +1 -2
- package/dist-lib/lib/tools/exec/adapters/python/deadline-state.d.ts +10 -0
- package/dist-lib/lib/tools/exec/adapters/python/deadline-state.js +5 -0
- package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +10 -17
- package/dist-lib/lib/tools/exec/adapters/python/loader.js +15 -11
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +1 -15
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +1 -0
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +13 -2
- package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +3 -3
- package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -14
- package/dist-lib/lib/tools/exec/adapters/python/worker/active.d.ts +19 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +72 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +0 -1
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +8 -76
- package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +2 -126
- package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.d.ts +1 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.js +21 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/serve.d.ts +2 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +90 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.d.ts → spawn.d.ts} +0 -1
- package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.js → spawn.js} +0 -8
- package/dist-lib/lib/tools/exec/bare-runtime.d.ts +2 -0
- package/dist-lib/lib/tools/exec/bare-runtime.js +10 -0
- package/dist-lib/lib/tools/exec.d.ts +0 -2
- package/dist-lib/lib/tools/exec.js +6 -11
- package/dist-lib/schema/harness.js +2 -1
- package/dist-lib/spec/hrpc/messages.js +6 -5
- package/dist-lib/spec/hyperschema/index.js +6 -5
- package/dist-lib/spec/hyperschema/schema.json +6 -1
- package/dist-lib/spec/hyperschema/types.d.ts +2 -1
- package/package.json +39 -20
- package/dist/harness.bundle +0 -42313
- package/dist/harness.cjs +0 -18
- package/dist/harness.d.ts +0 -8
- package/dist/prebuilds-manifest.json +0 -372
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.d.ts +0 -3
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.js +0 -38
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const RESEND_MS = 100;
|
|
7
|
-
const SOURCE = `
|
|
8
|
-
const state = new Int32Array(data.state)
|
|
9
|
-
const interrupt = new Uint8Array(data.interrupt)
|
|
10
|
-
Atomics.wait(state, 0, ${ARMED}, data.ms)
|
|
11
|
-
if (Atomics.compareExchange(state, 0, ${ARMED}, ${FIRED}) === ${ARMED}) {
|
|
12
|
-
do Atomics.store(interrupt, 0, ${SIGINT})
|
|
13
|
-
while (Atomics.wait(state, 0, ${FIRED}, ${RESEND_MS}) === 'timed-out')
|
|
14
|
-
}
|
|
15
|
-
`;
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { Worker } from 'worker_threads';
|
|
3
|
+
import { errorMessage } from '../../../../guards.js';
|
|
4
|
+
import { hlog } from '../../../../log.js';
|
|
5
|
+
import { DISARMED, FIRED } from './deadline-state.js';
|
|
16
6
|
export function startInterruptTimer(ms) {
|
|
17
7
|
const state = new Int32Array(new SharedArrayBuffer(4));
|
|
18
8
|
const interrupt = new Uint8Array(new SharedArrayBuffer(1));
|
|
19
|
-
const
|
|
9
|
+
const workerData = { state: state.buffer, interrupt: interrupt.buffer, ms };
|
|
10
|
+
const entry = fileURLToPath(import.meta.resolve('#pyodide-interrupt-deadline'));
|
|
11
|
+
new Worker(entry, { workerData })
|
|
12
|
+
.on('error', (error) => hlog('python: interrupt deadline failed', { err: errorMessage(error) }))
|
|
13
|
+
.unref();
|
|
20
14
|
return {
|
|
21
15
|
interrupt,
|
|
22
16
|
disarm() {
|
|
23
17
|
const fired = Atomics.exchange(state, 0, DISARMED) === FIRED;
|
|
24
18
|
Atomics.notify(state, 0);
|
|
25
|
-
reap();
|
|
26
19
|
return fired;
|
|
27
20
|
}
|
|
28
21
|
};
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
import b4a from 'b4a';
|
|
10
2
|
import { constants as fsConstants } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
11
4
|
import process from 'process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
12
6
|
import { createGuardedFetch } from './guarded-fetch.js';
|
|
13
7
|
import { seedPackageCache } from './wheels.js';
|
|
14
8
|
class B4aTextDecoder {
|
|
@@ -95,15 +89,25 @@ export function pyodideLoadOptions() {
|
|
|
95
89
|
}
|
|
96
90
|
return { ...silentStdio, packageCacheDir: cacheDir };
|
|
97
91
|
}
|
|
92
|
+
function pyodideIndexURL() {
|
|
93
|
+
import.meta.asset('pyodide/python_stdlib.zip');
|
|
94
|
+
import.meta.asset('pyodide/pyodide-lock.json');
|
|
95
|
+
return path.dirname(fileURLToPath(import.meta.asset('pyodide/pyodide.asm.wasm'))) + '/';
|
|
96
|
+
}
|
|
98
97
|
export const defaultLoad = async () => {
|
|
99
98
|
if (typeof globalThis.Bare !== 'undefined') {
|
|
100
99
|
await shapeBareGlobals();
|
|
101
100
|
}
|
|
102
|
-
const
|
|
103
|
-
const {
|
|
101
|
+
const { loadPyodide } = await import('pyodide');
|
|
102
|
+
const { default: createPyodideModule } = await import('pyodide/pyodide.asm.mjs');
|
|
104
103
|
const options = pyodideLoadOptions();
|
|
105
104
|
if (options.packageCacheDir) {
|
|
106
105
|
seedPackageCache(options.packageCacheDir);
|
|
107
106
|
}
|
|
108
|
-
|
|
107
|
+
const indexURL = pyodideIndexURL();
|
|
108
|
+
return (await loadPyodide({
|
|
109
|
+
...options,
|
|
110
|
+
indexURL,
|
|
111
|
+
createPyodideModule
|
|
112
|
+
}));
|
|
109
113
|
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { AbortController } from 'bare-abort-controller';
|
|
2
|
-
import process from 'process';
|
|
3
2
|
import ScopeLock from 'scope-lock';
|
|
4
3
|
import { errorMessage } from '../../../../guards.js';
|
|
5
4
|
import { AbortedError, raceAbort } from '../../../../helpers/race-abort.js';
|
|
6
5
|
import { hlog } from '../../../../log.js';
|
|
7
6
|
import { MAX_OUTPUT_CHARS } from '../../runner.js';
|
|
8
7
|
import { startInterruptTimer } from './interrupt-timer.js';
|
|
9
|
-
import {
|
|
8
|
+
import { pyodideLoadOptions } from './loader.js';
|
|
10
9
|
import { isPyodideFatal, performRun } from './run-session.js';
|
|
11
10
|
import { runPythonInWorker } from './worker/client.js';
|
|
12
11
|
export { pyodideLoadOptions };
|
|
@@ -98,11 +97,6 @@ async function runInProcess(load, spec, options) {
|
|
|
98
97
|
stop.abort(undefined);
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
|
-
function workerAvailable() {
|
|
102
|
-
if (process.env.QVAC_PYODIDE_WORKER_ENTRY)
|
|
103
|
-
return true;
|
|
104
|
-
return !import.meta.url.includes('.bundle/');
|
|
105
|
-
}
|
|
106
100
|
export async function runPython(code, options = {}) {
|
|
107
101
|
if (options.signal?.aborted)
|
|
108
102
|
throw new AbortedError();
|
|
@@ -126,14 +120,6 @@ export async function runPython(code, options = {}) {
|
|
|
126
120
|
askConnect: options.askConnect
|
|
127
121
|
});
|
|
128
122
|
}
|
|
129
|
-
if (!workerAvailable()) {
|
|
130
|
-
hlog('python: worker entry unavailable in this bundle; running in-process');
|
|
131
|
-
return runInProcess(defaultLoad, spec, {
|
|
132
|
-
timeoutMs,
|
|
133
|
-
signal: options.signal,
|
|
134
|
-
askConnect: options.askConnect
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
123
|
await runLock.lock();
|
|
138
124
|
try {
|
|
139
125
|
if (options.signal?.aborted) {
|
|
@@ -12,6 +12,7 @@ export declare function pinned(spec: string): string;
|
|
|
12
12
|
export declare function pipMessage(targets: readonly string[] | null, { sandbox }?: {
|
|
13
13
|
sandbox?: boolean | undefined;
|
|
14
14
|
}): string;
|
|
15
|
+
export declare function sandboxInstallError(command: string, declared: readonly string[] | undefined): string | null;
|
|
15
16
|
export interface PipFromSourceHintOptions {
|
|
16
17
|
readonly packages?: readonly string[] | undefined;
|
|
17
18
|
readonly sandbox?: boolean;
|
|
@@ -242,6 +242,16 @@ export function pipMessage(targets, { sandbox = false } = {}) {
|
|
|
242
242
|
const declare = specs ? `packages: ${JSON.stringify(specs)}` : 'the PyPI names in packages';
|
|
243
243
|
return `pip is not available. Call exec with language "python", ${declare} and the Python source in command. Installing and running are the same one call.`;
|
|
244
244
|
}
|
|
245
|
+
export function sandboxInstallError(command, declared) {
|
|
246
|
+
const install = pipInstallTargets(command)?.map(pinned) ?? null;
|
|
247
|
+
const packages = [...new Set([...(declared ?? []), ...(install ?? [])])];
|
|
248
|
+
if (packages.length === 0 || planVendoredInstall(packages)) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return install
|
|
252
|
+
? pipMessage(install, { sandbox: true })
|
|
253
|
+
: 'sandbox mode: installing packages reaches the network';
|
|
254
|
+
}
|
|
245
255
|
const PIP_CALL = /\b(?:subprocess\.\w+|os\.(?:system|popen)|Popen|check_(?:output|call))\s*\(/;
|
|
246
256
|
const STRING_LITERAL = /"([^"\n]*)"|'([^'\n]*)'/g;
|
|
247
257
|
function pipLineTargets(line) {
|
|
@@ -1279,8 +1289,9 @@ export function createPythonAdapter(disabled, loadPyodide, network = false) {
|
|
|
1279
1289
|
const packages = install
|
|
1280
1290
|
? [...new Set([...(prepared.packages ?? []), ...install])]
|
|
1281
1291
|
: prepared.packages;
|
|
1282
|
-
|
|
1283
|
-
|
|
1292
|
+
const refusal = sandbox ? sandboxInstallError(command, prepared.packages) : null;
|
|
1293
|
+
if (refusal) {
|
|
1294
|
+
return fail(refusal);
|
|
1284
1295
|
}
|
|
1285
1296
|
const result = await runPython(install ? 'pass' : source, {
|
|
1286
1297
|
timeoutMs: options.timeoutMs,
|
|
@@ -28,8 +28,8 @@ interface WheelFs {
|
|
|
28
28
|
exists: boolean;
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
export declare function wheelsDir(): string
|
|
32
|
-
export declare function vendoredManifest(dir?: string
|
|
31
|
+
export declare function wheelsDir(): string;
|
|
32
|
+
export declare function vendoredManifest(dir?: string): WheelManifest | null;
|
|
33
33
|
export declare function normalizeDistribution(name: string): string;
|
|
34
34
|
export interface VendoredOptions {
|
|
35
35
|
readonly dir?: string | undefined;
|
|
@@ -38,5 +38,5 @@ export interface VendoredOptions {
|
|
|
38
38
|
export declare function vendoredSpec(distribution: string, opts?: VendoredOptions): string | null;
|
|
39
39
|
export declare function planVendoredInstall(specs: readonly string[], opts?: VendoredOptions): VendoredInstall | null;
|
|
40
40
|
export declare function stageVendoredWheels(target: WheelFs, install: VendoredInstall): string[];
|
|
41
|
-
export declare function seedPackageCache(cacheDir: string, dir?: string
|
|
41
|
+
export declare function seedPackageCache(cacheDir: string, dir?: string): void;
|
|
42
42
|
export {};
|
|
@@ -16,20 +16,9 @@ const ManifestSchema = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
const STAGE_DIR = '/qvac-wheels';
|
|
18
18
|
export function wheelsDir() {
|
|
19
|
-
|
|
20
|
-
if (fromHost) {
|
|
21
|
-
return fromHost;
|
|
22
|
-
}
|
|
23
|
-
const root = new URL('../../../../../', import.meta.url);
|
|
24
|
-
const source = new URL(root.pathname.endsWith('/dist-lib/') ? '../wheels' : 'wheels', root);
|
|
25
|
-
if (source.href.includes('.bundle/')) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
return fileURLToPath(source.href);
|
|
19
|
+
return process.env.QVAC_PYTHON_WHEELS_DIR || fileURLToPath(import.meta.asset('#pyodide-wheels'));
|
|
29
20
|
}
|
|
30
21
|
export function vendoredManifest(dir = wheelsDir()) {
|
|
31
|
-
if (!dir)
|
|
32
|
-
return null;
|
|
33
22
|
const file = `${dir}/manifest.json`;
|
|
34
23
|
if (!fs.existsSync(file)) {
|
|
35
24
|
hlog('python: no vendored wheels', { dir });
|
|
@@ -70,7 +59,7 @@ export function vendoredSpec(distribution, opts = {}) {
|
|
|
70
59
|
export function planVendoredInstall(specs, opts = {}) {
|
|
71
60
|
const dir = opts.dir ?? wheelsDir();
|
|
72
61
|
const manifest = opts.manifest === undefined ? vendoredManifest(dir) : opts.manifest;
|
|
73
|
-
if (!manifest ||
|
|
62
|
+
if (!manifest || specs.length === 0) {
|
|
74
63
|
return null;
|
|
75
64
|
}
|
|
76
65
|
const wheels = new Set();
|
|
@@ -108,7 +97,7 @@ export function stageVendoredWheels(target, install) {
|
|
|
108
97
|
}
|
|
109
98
|
export function seedPackageCache(cacheDir, dir = wheelsDir()) {
|
|
110
99
|
const manifest = vendoredManifest(dir);
|
|
111
|
-
if (!manifest
|
|
100
|
+
if (!manifest) {
|
|
112
101
|
return;
|
|
113
102
|
}
|
|
114
103
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type WorkerMessage } from './protocol.ts';
|
|
2
|
+
import type { WorkerLink } from './spawn.ts';
|
|
3
|
+
export interface ActiveWorker {
|
|
4
|
+
readonly link: WorkerLink;
|
|
5
|
+
readonly view: Uint8Array;
|
|
6
|
+
readonly ready: Promise<void>;
|
|
7
|
+
nextId: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function activeWorker(boot: () => ActiveWorker): ActiveWorker;
|
|
10
|
+
export declare function forget(worker: ActiveWorker): void;
|
|
11
|
+
export declare function destroy(worker: ActiveWorker): void;
|
|
12
|
+
export declare function readMessage(link: WorkerLink): Promise<WorkerMessage | null>;
|
|
13
|
+
export declare function drainAndReap(worker: ActiveWorker): Promise<void>;
|
|
14
|
+
export interface Grace {
|
|
15
|
+
expired: Promise<'grace'>;
|
|
16
|
+
clear: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function grace(ms: number): Grace;
|
|
19
|
+
export declare function closePythonWorker(): Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { hlog } from '../../../../../log.js';
|
|
2
|
+
import { asWorkerMessage } from './protocol.js';
|
|
3
|
+
const SHUTDOWN_GRACE_MS = 3_000;
|
|
4
|
+
let active = null;
|
|
5
|
+
export function activeWorker(boot) {
|
|
6
|
+
return (active ??= boot());
|
|
7
|
+
}
|
|
8
|
+
export function forget(worker) {
|
|
9
|
+
if (active === worker) {
|
|
10
|
+
active = null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function destroy(worker) {
|
|
14
|
+
forget(worker);
|
|
15
|
+
worker.link.kill();
|
|
16
|
+
}
|
|
17
|
+
export async function readMessage(link) {
|
|
18
|
+
for (;;) {
|
|
19
|
+
const message = asWorkerMessage(await link.read());
|
|
20
|
+
if (message?.type !== 'log') {
|
|
21
|
+
return message;
|
|
22
|
+
}
|
|
23
|
+
hlog(`python worker: ${message.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function drainAndReap(worker) {
|
|
27
|
+
forget(worker);
|
|
28
|
+
const closed = readMessage(worker.link).then(() => 'message', () => 'closed');
|
|
29
|
+
const timer = grace(SHUTDOWN_GRACE_MS);
|
|
30
|
+
try {
|
|
31
|
+
const settled = await Promise.race([closed, timer.expired]);
|
|
32
|
+
if (settled === 'closed') {
|
|
33
|
+
worker.link.reap();
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
worker.link.kill();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
timer.clear();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function grace(ms) {
|
|
44
|
+
let timer;
|
|
45
|
+
const expired = new Promise((resolve) => {
|
|
46
|
+
timer = setTimeout(() => resolve('grace'), ms);
|
|
47
|
+
});
|
|
48
|
+
return { expired, clear: () => clearTimeout(timer) };
|
|
49
|
+
}
|
|
50
|
+
export async function closePythonWorker() {
|
|
51
|
+
const worker = active;
|
|
52
|
+
if (!worker) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
active = null;
|
|
56
|
+
const timer = grace(SHUTDOWN_GRACE_MS);
|
|
57
|
+
try {
|
|
58
|
+
const booted = await Promise.race([worker.ready.then(() => 'ready'), timer.expired]);
|
|
59
|
+
if (booted === 'grace') {
|
|
60
|
+
worker.link.kill();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await worker.link.write({ type: 'close' });
|
|
64
|
+
await drainAndReap(worker);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
worker.link.kill();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
timer.clear();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -6,4 +6,3 @@ export interface RunSpecOptions {
|
|
|
6
6
|
readonly askConnect?: ((host: string) => Promise<boolean>) | undefined;
|
|
7
7
|
}
|
|
8
8
|
export declare function runPythonInWorker(spec: RunSpec, opts: RunSpecOptions): Promise<PythonRunResult>;
|
|
9
|
-
export declare function closePythonWorker(): Promise<void>;
|
|
@@ -1,30 +1,19 @@
|
|
|
1
|
-
import { spawnPythonWorker } from '#pyodide-worker-spawn';
|
|
2
1
|
import { AbortController } from 'bare-abort-controller';
|
|
3
2
|
import process from 'process';
|
|
4
3
|
import { fileURLToPath } from 'url';
|
|
5
4
|
import { AbortedError, raceAbort } from '../../../../../helpers/race-abort.js';
|
|
6
5
|
import { harnessLogEnabled, hlog } from '../../../../../log.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import { RESEND_MS, SIGINT } from '../deadline-state.js';
|
|
7
|
+
import { fromWireResult } from './protocol.js';
|
|
8
|
+
import { activeWorker, destroy, drainAndReap, forget, grace, readMessage } from './active.js';
|
|
9
|
+
import { spawnPythonWorker } from './spawn.js';
|
|
10
10
|
function interruptGraceMs() {
|
|
11
11
|
const ms = Number(process.env.QVAC_PYODIDE_INTERRUPT_GRACE_MS);
|
|
12
12
|
return ms > 0 ? ms : 30_000;
|
|
13
13
|
}
|
|
14
|
-
const SHUTDOWN_GRACE_MS = 3_000;
|
|
15
|
-
let active = null;
|
|
16
14
|
function workerEntry() {
|
|
17
|
-
return (process.env.QVAC_PYODIDE_WORKER_ENTRY
|
|
18
|
-
fileURLToPath(
|
|
19
|
-
}
|
|
20
|
-
async function readMessage(link) {
|
|
21
|
-
for (;;) {
|
|
22
|
-
const message = asWorkerMessage(await link.read());
|
|
23
|
-
if (message?.type !== 'log') {
|
|
24
|
-
return message;
|
|
25
|
-
}
|
|
26
|
-
hlog(`python worker: ${message.message}`);
|
|
27
|
-
}
|
|
15
|
+
return (process.env.QVAC_PYODIDE_WORKER_ENTRY ||
|
|
16
|
+
fileURLToPath(import.meta.resolve('#pyodide-worker-entry')));
|
|
28
17
|
}
|
|
29
18
|
async function awaitReady(link) {
|
|
30
19
|
const message = await readMessage(link);
|
|
@@ -47,38 +36,11 @@ function boot() {
|
|
|
47
36
|
nextId: 1
|
|
48
37
|
};
|
|
49
38
|
worker.ready.catch(() => {
|
|
50
|
-
|
|
51
|
-
active = null;
|
|
52
|
-
}
|
|
39
|
+
forget(worker);
|
|
53
40
|
link.kill();
|
|
54
41
|
});
|
|
55
42
|
return worker;
|
|
56
43
|
}
|
|
57
|
-
function destroy(worker) {
|
|
58
|
-
if (active === worker) {
|
|
59
|
-
active = null;
|
|
60
|
-
}
|
|
61
|
-
worker.link.kill();
|
|
62
|
-
}
|
|
63
|
-
async function drainAndReap(worker) {
|
|
64
|
-
if (active === worker) {
|
|
65
|
-
active = null;
|
|
66
|
-
}
|
|
67
|
-
const closed = readMessage(worker.link).then(() => 'message', () => 'closed');
|
|
68
|
-
const timer = grace(SHUTDOWN_GRACE_MS);
|
|
69
|
-
try {
|
|
70
|
-
const settled = await Promise.race([closed, timer.expired]);
|
|
71
|
-
if (settled === 'closed') {
|
|
72
|
-
worker.link.reap();
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
worker.link.kill();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
timer.clear();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
44
|
async function resultFor(worker, id, askConnect) {
|
|
83
45
|
let message = await readMessage(worker.link);
|
|
84
46
|
while (message?.type === 'connect-ask') {
|
|
@@ -97,13 +59,6 @@ async function resultFor(worker, id, askConnect) {
|
|
|
97
59
|
}
|
|
98
60
|
throw new Error('python worker sent an unexpected message');
|
|
99
61
|
}
|
|
100
|
-
function grace(ms) {
|
|
101
|
-
let timer;
|
|
102
|
-
const expired = new Promise((resolve) => {
|
|
103
|
-
timer = setTimeout(() => resolve('grace'), ms);
|
|
104
|
-
});
|
|
105
|
-
return { expired, clear: () => clearTimeout(timer) };
|
|
106
|
-
}
|
|
107
62
|
async function interrupt(worker, outcome) {
|
|
108
63
|
Atomics.store(worker.view, 0, SIGINT);
|
|
109
64
|
const resend = setInterval(() => Atomics.store(worker.view, 0, SIGINT), RESEND_MS);
|
|
@@ -128,7 +83,7 @@ async function interrupt(worker, outcome) {
|
|
|
128
83
|
}
|
|
129
84
|
}
|
|
130
85
|
export async function runPythonInWorker(spec, opts) {
|
|
131
|
-
const worker = (
|
|
86
|
+
const worker = activeWorker(boot);
|
|
132
87
|
worker.link.ref();
|
|
133
88
|
try {
|
|
134
89
|
const id = worker.nextId++;
|
|
@@ -190,27 +145,4 @@ export async function runPythonInWorker(spec, opts) {
|
|
|
190
145
|
worker.link.unref();
|
|
191
146
|
}
|
|
192
147
|
}
|
|
193
|
-
export async function closePythonWorker() {
|
|
194
|
-
const worker = active;
|
|
195
|
-
if (!worker) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
active = null;
|
|
199
|
-
const timer = grace(SHUTDOWN_GRACE_MS);
|
|
200
|
-
try {
|
|
201
|
-
const booted = await Promise.race([worker.ready.then(() => 'ready'), timer.expired]);
|
|
202
|
-
if (booted === 'grace') {
|
|
203
|
-
worker.link.kill();
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
await worker.link.write({ type: 'close' });
|
|
207
|
-
await drainAndReap(worker);
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
worker.link.kill();
|
|
211
|
-
}
|
|
212
|
-
finally {
|
|
213
|
-
timer.clear();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
148
|
function noop() { }
|
|
@@ -1,127 +1,3 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import process from 'process';
|
|
10
|
-
import { errorMessage, isRecord } from '../../../../../guards.js';
|
|
11
|
-
import { setHarnessLog, setHarnessLogSink } from '../../../../../log.js';
|
|
12
1
|
import { defaultLoad } from '../loader.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
function narrowThreadData(data) {
|
|
16
|
-
if (isRecord(data) && 'sab' in data && 'handle' in data) {
|
|
17
|
-
const sab = data.sab;
|
|
18
|
-
const log = 'log' in data && data.log === true;
|
|
19
|
-
if (sab instanceof SharedArrayBuffer) {
|
|
20
|
-
return { handle: data.handle, sab, log };
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
throw new Error('python worker started without { handle, sab } thread data');
|
|
24
|
-
}
|
|
25
|
-
async function openBarePort() {
|
|
26
|
-
const { default: Channel } = await import('bare-channel');
|
|
27
|
-
const self = Bare.Thread.self;
|
|
28
|
-
if (!self) {
|
|
29
|
-
throw new Error('python worker entry loaded on the main thread');
|
|
30
|
-
}
|
|
31
|
-
const { handle, sab, log } = narrowThreadData(self.data);
|
|
32
|
-
const port = Channel.from(handle).connect();
|
|
33
|
-
return {
|
|
34
|
-
port: {
|
|
35
|
-
read: () => port.read(),
|
|
36
|
-
write: (message) => void port.write(message),
|
|
37
|
-
close: () => void port.close()
|
|
38
|
-
},
|
|
39
|
-
handle,
|
|
40
|
-
sab,
|
|
41
|
-
log
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
async function openNodePort() {
|
|
45
|
-
const { parentPort, workerData } = await import('worker_threads');
|
|
46
|
-
if (!parentPort) {
|
|
47
|
-
throw new Error('python worker entry loaded on the main thread');
|
|
48
|
-
}
|
|
49
|
-
const data = narrowThreadData({ handle: parentPort, ...workerData });
|
|
50
|
-
const queued = [];
|
|
51
|
-
const waiting = [];
|
|
52
|
-
parentPort.on('message', (value) => {
|
|
53
|
-
const next = waiting.shift();
|
|
54
|
-
if (next) {
|
|
55
|
-
next(value);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
queued.push(value);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
return {
|
|
62
|
-
port: {
|
|
63
|
-
read() {
|
|
64
|
-
if (queued.length > 0) {
|
|
65
|
-
return Promise.resolve(queued.shift());
|
|
66
|
-
}
|
|
67
|
-
return new Promise((resolve) => waiting.push(resolve));
|
|
68
|
-
},
|
|
69
|
-
write: (message) => parentPort.postMessage(message),
|
|
70
|
-
close: () => parentPort.close()
|
|
71
|
-
},
|
|
72
|
-
...data
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
async function loadInterpreter() {
|
|
76
|
-
const testLoader = process.env.QVAC_PYODIDE_TEST_LOADER;
|
|
77
|
-
if (!testLoader) {
|
|
78
|
-
return defaultLoad;
|
|
79
|
-
}
|
|
80
|
-
const loaded = await import(__rewriteRelativeImportExtension(testLoader));
|
|
81
|
-
if (isRecord(loaded) && 'default' in loaded && typeof loaded.default === 'function') {
|
|
82
|
-
return loaded.default;
|
|
83
|
-
}
|
|
84
|
-
throw new Error(`QVAC_PYODIDE_TEST_LOADER module has no default export: ${testLoader}`);
|
|
85
|
-
}
|
|
86
|
-
const { port, sab, log } = typeof globalThis.Bare !== 'undefined' ? await openBarePort() : await openNodePort();
|
|
87
|
-
setHarnessLog(log);
|
|
88
|
-
setHarnessLogSink((message) => port.write({ type: 'log', message }));
|
|
89
|
-
try {
|
|
90
|
-
const load = await loadInterpreter();
|
|
91
|
-
const py = await load();
|
|
92
|
-
py.setInterruptBuffer(new Uint8Array(sab));
|
|
93
|
-
port.write({ type: 'ready' });
|
|
94
|
-
for (;;) {
|
|
95
|
-
const message = asHostMessage(await port.read());
|
|
96
|
-
if (!message || message.type === 'close') {
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
if (message.type === 'connect-answer') {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
let closing = false;
|
|
103
|
-
const askConnect = async (host) => {
|
|
104
|
-
if (closing)
|
|
105
|
-
return false;
|
|
106
|
-
port.write({ type: 'connect-ask', host });
|
|
107
|
-
const answer = asHostMessage(await port.read());
|
|
108
|
-
if (answer?.type === 'connect-answer') {
|
|
109
|
-
return answer.allowed;
|
|
110
|
-
}
|
|
111
|
-
closing = true;
|
|
112
|
-
return false;
|
|
113
|
-
};
|
|
114
|
-
const { result, evict } = await performRun(py, message.spec, { askConnect });
|
|
115
|
-
port.write({ type: 'result', id: message.id, result: toWireResult(result), evict });
|
|
116
|
-
if (evict || closing) {
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
const message = errorMessage(error);
|
|
123
|
-
port.write({ type: 'boot-error', message });
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
port.close();
|
|
127
|
-
}
|
|
2
|
+
import { serve } from './serve.js';
|
|
3
|
+
await serve(defaultLoad);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { workerData } from 'worker_threads';
|
|
2
|
+
import { isRecord } from '../../../../../guards.js';
|
|
3
|
+
import { ARMED, FIRED, RESEND_MS, SIGINT } from '../deadline-state.js';
|
|
4
|
+
function narrowDeadlineData(data) {
|
|
5
|
+
if (isRecord(data)) {
|
|
6
|
+
const { state, interrupt, ms } = data;
|
|
7
|
+
if (state instanceof SharedArrayBuffer && interrupt instanceof SharedArrayBuffer) {
|
|
8
|
+
return { state, interrupt, ms: Number(ms) };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
throw new Error('interrupt deadline started without { state, interrupt } worker data');
|
|
12
|
+
}
|
|
13
|
+
const data = narrowDeadlineData(workerData);
|
|
14
|
+
const state = new Int32Array(data.state);
|
|
15
|
+
const interrupt = new Uint8Array(data.interrupt);
|
|
16
|
+
Atomics.wait(state, 0, ARMED, data.ms);
|
|
17
|
+
if (Atomics.compareExchange(state, 0, ARMED, FIRED) === ARMED) {
|
|
18
|
+
do {
|
|
19
|
+
Atomics.store(interrupt, 0, SIGINT);
|
|
20
|
+
} while (Atomics.wait(state, 0, FIRED, RESEND_MS) === 'timed-out');
|
|
21
|
+
}
|