@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +91 -1
  2. package/README.md +32 -27
  3. package/dist-lib/index.d.ts +1 -0
  4. package/dist-lib/index.js +2 -2
  5. package/dist-lib/lib/capabilities/preview-model-source.d.ts +0 -1
  6. package/dist-lib/lib/connect.js +5 -2
  7. package/dist-lib/lib/contract-types.d.ts +1 -0
  8. package/dist-lib/lib/docx/index.d.ts +1 -1
  9. package/dist-lib/lib/docx/index.js +1 -1
  10. package/dist-lib/lib/hardware/capability-probe.js +9 -4
  11. package/dist-lib/lib/hooks/built-in/sandbox.js +0 -5
  12. package/dist-lib/lib/models/model-catalog-constants.d.ts +7 -0
  13. package/dist-lib/lib/models/model-catalog-constants.js +17 -0
  14. package/dist-lib/lib/models/model-catalog.d.ts +14 -6
  15. package/dist-lib/lib/models/model-catalog.js +14 -6
  16. package/dist-lib/lib/models/model-list.d.ts +2 -0
  17. package/dist-lib/lib/models/model-list.js +9 -4
  18. package/dist-lib/lib/models/model-task.d.ts +3 -0
  19. package/dist-lib/lib/models/model-task.js +14 -0
  20. package/dist-lib/lib/models/music-catalog.d.ts +2 -0
  21. package/dist-lib/lib/models/music-catalog.js +9 -0
  22. package/dist-lib/lib/office/index.d.ts +1 -1
  23. package/dist-lib/lib/office/index.js +1 -1
  24. package/dist-lib/lib/office/python.js +1 -2
  25. package/dist-lib/lib/tools/exec/adapters/python/deadline-state.d.ts +10 -0
  26. package/dist-lib/lib/tools/exec/adapters/python/deadline-state.js +5 -0
  27. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +10 -17
  28. package/dist-lib/lib/tools/exec/adapters/python/loader.js +15 -11
  29. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +1 -15
  30. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +1 -0
  31. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +13 -2
  32. package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +3 -3
  33. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -14
  34. package/dist-lib/lib/tools/exec/adapters/python/worker/active.d.ts +19 -0
  35. package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +72 -0
  36. package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +0 -1
  37. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +8 -76
  38. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +2 -126
  39. package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.d.ts +1 -0
  40. package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.js +21 -0
  41. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.d.ts +2 -0
  42. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +90 -0
  43. package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.d.ts → spawn.d.ts} +0 -1
  44. package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.js → spawn.js} +0 -8
  45. package/dist-lib/lib/tools/exec/bare-runtime.d.ts +2 -0
  46. package/dist-lib/lib/tools/exec/bare-runtime.js +10 -0
  47. package/dist-lib/lib/tools/exec.d.ts +0 -2
  48. package/dist-lib/lib/tools/exec.js +6 -11
  49. package/dist-lib/schema/harness.js +2 -1
  50. package/dist-lib/spec/hrpc/messages.js +6 -5
  51. package/dist-lib/spec/hyperschema/index.js +6 -5
  52. package/dist-lib/spec/hyperschema/schema.json +6 -1
  53. package/dist-lib/spec/hyperschema/types.d.ts +2 -1
  54. package/package.json +39 -20
  55. package/dist/harness.bundle +0 -42313
  56. package/dist/harness.cjs +0 -18
  57. package/dist/harness.d.ts +0 -8
  58. package/dist/prebuilds-manifest.json +0 -372
  59. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.d.ts +0 -3
  60. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.js +0 -38
@@ -0,0 +1,5 @@
1
+ export const ARMED = 0;
2
+ export const DISARMED = 1;
3
+ export const FIRED = 2;
4
+ export const SIGINT = 2;
5
+ export const RESEND_MS = 100;
@@ -1,28 +1,21 @@
1
- import { spawnInlineThread } from '#pyodide-worker-spawn';
2
- const ARMED = 0;
3
- const DISARMED = 1;
4
- const FIRED = 2;
5
- const SIGINT = 2;
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 reap = spawnInlineThread(SOURCE, { state: state.buffer, interrupt: interrupt.buffer, ms });
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 moduleName = 'pyodide';
103
- const { loadPyodide } = await import(__rewriteRelativeImportExtension(moduleName));
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
- return (await loadPyodide(options));
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 { defaultLoad, pyodideLoadOptions } from './loader.js';
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
- if (sandbox && install && !planVendoredInstall(packages ?? [])) {
1283
- return fail(pipMessage(install, { sandbox: true }));
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 | undefined;
32
- export declare function vendoredManifest(dir?: string | undefined): WheelManifest | null;
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 | undefined): void;
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
- const fromHost = process.env.QVAC_PYTHON_WHEELS_DIR;
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 || !dir || specs.length === 0) {
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 || !dir) {
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 { asWorkerMessage, fromWireResult } from './protocol.js';
8
- const SIGINT = 2;
9
- const RESEND_MS = 100;
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(new URL(import.meta.url.endsWith('.ts') ? './entry.ts' : './entry.js', import.meta.url).href));
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
- if (active === worker) {
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 = (active ??= boot());
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 { performRun } from '../run-session.js';
14
- import { asHostMessage, toWireResult } from './protocol.js';
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,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
+ }
@@ -0,0 +1,2 @@
1
+ import { type LoadPyodideFn } from '../run-session.ts';
2
+ export declare function serve(load: LoadPyodideFn): Promise<void>;