gwchq-textjam 0.2.28 → 0.2.30
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/assets/{PyodideWorkerdeabb0560f66f0f43f49.js → PyodideWorkerb1409749c37aedc47dc8.js}
RENAMED
|
@@ -88,7 +88,7 @@ const PyodideWorker = () => {
|
|
|
88
88
|
assets.pyodideBaseUrl = `${packageApiUrl}/pyodide.js`;
|
|
89
89
|
importScripts(toAbsoluteFromOrigin(assets.pyodideBaseUrl));
|
|
90
90
|
|
|
91
|
-
initialisePyodide();
|
|
91
|
+
initialisePyodide(data);
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const supportsAllFeatures = typeof SharedArrayBuffer !== "undefined";
|
|
@@ -115,8 +115,24 @@ const PyodideWorker = () => {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
let pyodide, pyodidePromise, stdinBuffer, interruptBuffer, stopped;
|
|
118
|
+
let currentRunId = null;
|
|
119
|
+
let stdinStrategy = "sab";
|
|
120
|
+
let supportsJspi = false;
|
|
121
|
+
let stdinFallbackConfig = null;
|
|
122
|
+
const STDIN_CANCELLED_RESPONSE_CODE = 499;
|
|
123
|
+
const STDIN_TIMEOUT_RESPONSE_CODE = 408;
|
|
124
|
+
const STDIN_ABORTED_RESPONSE_CODE = 410;
|
|
125
|
+
const STDIN_ROUTE_NOT_FOUND_RESPONSE_CODE = 404;
|
|
126
|
+
// HTTP 204 No Content from the stdin Service Worker signals end-of-file —
|
|
127
|
+
// distinct from a cancellation/interrupt so input() raises EOFError and the
|
|
128
|
+
// user's `try/except EOFError` blocks keep working under the sync-XHR fallback.
|
|
129
|
+
const STDIN_EOF_RESPONSE_CODE = 204;
|
|
118
130
|
/** When true, input() uses postMessage + run_sync(getInputAsync) instead of setStdin (requires JSPI). */
|
|
119
131
|
let useMessageStdin = false;
|
|
132
|
+
/** When true, input() uses sync XHR via the Service Worker fallback. */
|
|
133
|
+
let useSyncXhrStdin = false;
|
|
134
|
+
/** True when current input request was cancelled from outside the worker. */
|
|
135
|
+
let stdinCancelled = false;
|
|
120
136
|
/** Used when SharedArrayBuffer is unavailable: resolve for the pending input() call. */
|
|
121
137
|
let pendingStdinResolve = null;
|
|
122
138
|
// Until Pyodide is fully initialised, keep stdout/stderr in the dev console only.
|
|
@@ -167,6 +183,8 @@ const PyodideWorker = () => {
|
|
|
167
183
|
break;
|
|
168
184
|
}
|
|
169
185
|
case "runPython":
|
|
186
|
+
currentRunId = data.runId || null;
|
|
187
|
+
stdinCancelled = false;
|
|
170
188
|
runPython(data.python, data.userModuleNames);
|
|
171
189
|
break;
|
|
172
190
|
case "stopPython": {
|
|
@@ -179,6 +197,7 @@ const PyodideWorker = () => {
|
|
|
179
197
|
pendingStdinResolve(null);
|
|
180
198
|
pendingStdinResolve = null;
|
|
181
199
|
}
|
|
200
|
+
stdinCancelled = true;
|
|
182
201
|
break;
|
|
183
202
|
}
|
|
184
203
|
default: {
|
|
@@ -195,6 +214,19 @@ const PyodideWorker = () => {
|
|
|
195
214
|
const runPython = async (python, userModuleNames) => {
|
|
196
215
|
stopped = false;
|
|
197
216
|
|
|
217
|
+
if (stdinStrategy === "unavailable") {
|
|
218
|
+
postMessage({
|
|
219
|
+
method: "handleError",
|
|
220
|
+
file: "main.py",
|
|
221
|
+
line: "",
|
|
222
|
+
mistake: "",
|
|
223
|
+
type: "RuntimeError",
|
|
224
|
+
info: "Python input fallback is unavailable. Please enable and activate the stdin Service Worker, then reload the page.",
|
|
225
|
+
});
|
|
226
|
+
await clearPyodideData(userModuleNames);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
198
230
|
// When stdin uses postMessage (no SharedArrayBuffer), run_sync() in input() requires
|
|
199
231
|
// runPythonAsync so that JSPI stack switching can suspend until the main thread sends stdinResponse.
|
|
200
232
|
const runUserCode = useMessageStdin
|
|
@@ -206,8 +238,37 @@ const PyodideWorker = () => {
|
|
|
206
238
|
await runUserCode();
|
|
207
239
|
});
|
|
208
240
|
} catch (error) {
|
|
241
|
+
const isStdinControlError =
|
|
242
|
+
error?.message === "PYODIDE_STDIN_CANCELLED" ||
|
|
243
|
+
error?.message === "PYODIDE_STDIN_TIMEOUT" ||
|
|
244
|
+
error?.message === "PYODIDE_STDIN_ABORTED";
|
|
245
|
+
|
|
246
|
+
if (stdinCancelled || isStdinControlError) {
|
|
247
|
+
postMessage({
|
|
248
|
+
method: "handleError",
|
|
249
|
+
file: "main.py",
|
|
250
|
+
line: "",
|
|
251
|
+
mistake: "",
|
|
252
|
+
type: "KeyboardInterrupt",
|
|
253
|
+
info: "Execution interrupted",
|
|
254
|
+
});
|
|
255
|
+
await clearPyodideData(userModuleNames);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
209
259
|
if (!(error instanceof pyodide.ffi.PythonError)) {
|
|
210
|
-
|
|
260
|
+
postMessage({
|
|
261
|
+
method: "handleError",
|
|
262
|
+
file: "main.py",
|
|
263
|
+
line: "",
|
|
264
|
+
mistake: "",
|
|
265
|
+
type: "RuntimeError",
|
|
266
|
+
info:
|
|
267
|
+
error?.message ||
|
|
268
|
+
"Python execution failed while reading stdin fallback input.",
|
|
269
|
+
});
|
|
270
|
+
await clearPyodideData(userModuleNames);
|
|
271
|
+
return;
|
|
211
272
|
}
|
|
212
273
|
const parsed = parsePythonError(error);
|
|
213
274
|
// Stop resolves stdin with EOF so input() raises EOFError; show as interrupt, not error.
|
|
@@ -239,7 +300,14 @@ const PyodideWorker = () => {
|
|
|
239
300
|
// Suppress internal loader output (e.g. "Loading pyodide-http") from the
|
|
240
301
|
// user console while resolving imports and loading packages.
|
|
241
302
|
suppressInternalStdStreams = true;
|
|
242
|
-
|
|
303
|
+
|
|
304
|
+
// `find_imports` returns a PyProxy whose .toJs() copies the contents but
|
|
305
|
+
// leaves the underlying Python object alive. Destroy it explicitly so JS
|
|
306
|
+
// GC can't try to finalise it later from a context where the GIL isn't
|
|
307
|
+
// held (which would surface as a NoGilError).
|
|
308
|
+
const importsProxy = pyodide._api.pyodide_code.find_imports(python);
|
|
309
|
+
const imports = importsProxy.toJs();
|
|
310
|
+
importsProxy.destroy();
|
|
243
311
|
|
|
244
312
|
await pyodide.runPythonAsync(`
|
|
245
313
|
import builtins
|
|
@@ -247,6 +315,18 @@ const PyodideWorker = () => {
|
|
|
247
315
|
builtins.open = builtins._original_open
|
|
248
316
|
`);
|
|
249
317
|
|
|
318
|
+
// Load packages sequentially rather than in Promise.all. Each
|
|
319
|
+
// `loadDependency` does multiple `await` hops (vendored .before(),
|
|
320
|
+
// loadPackage, micropip.install, …). When several run concurrently their
|
|
321
|
+
// await points interleave and Pyodide ends up servicing multiple in-flight
|
|
322
|
+
// package loads — that interleaving is the trigger for the NoGilError seen
|
|
323
|
+
// when complex projects load matplotlib's dependency tree. Sequential
|
|
324
|
+
// loading costs a few ms on cold start but matches how `loadPackage`
|
|
325
|
+
// already serialises its own work internally.
|
|
326
|
+
// for (const name of imports) {
|
|
327
|
+
// checkIfStopped();
|
|
328
|
+
// await loadDependency(name);
|
|
329
|
+
// }
|
|
250
330
|
await Promise.all(imports.map((name) => loadDependency(name)));
|
|
251
331
|
|
|
252
332
|
checkIfStopped();
|
|
@@ -358,9 +438,13 @@ const PyodideWorker = () => {
|
|
|
358
438
|
}
|
|
359
439
|
|
|
360
440
|
// If the import is for a module built into Python then do nothing.
|
|
441
|
+
// The PyProxy is only used as a presence check, so destroy it immediately
|
|
442
|
+
// — leaking it leaves a Python ref for JS GC to finalise later, which
|
|
443
|
+
// raises NoGilError when GC runs outside a GIL-held context.
|
|
361
444
|
try {
|
|
362
445
|
const pythonModule = pyodide.pyimport(pkgName);
|
|
363
446
|
if (pythonModule) {
|
|
447
|
+
pythonModule.destroy();
|
|
364
448
|
return;
|
|
365
449
|
}
|
|
366
450
|
} catch (_) {}
|
|
@@ -372,6 +456,7 @@ const PyodideWorker = () => {
|
|
|
372
456
|
|
|
373
457
|
const pyodidePackage = pyodide.pyimport(pkgName);
|
|
374
458
|
if (pyodidePackage) {
|
|
459
|
+
pyodidePackage.destroy();
|
|
375
460
|
return;
|
|
376
461
|
}
|
|
377
462
|
} catch (_) {}
|
|
@@ -451,6 +536,10 @@ const PyodideWorker = () => {
|
|
|
451
536
|
pyodidePackage = pyodide.pyimport("matplotlib");
|
|
452
537
|
} catch (_) {}
|
|
453
538
|
if (pyodidePackage) {
|
|
539
|
+
// Destroy the throwaway proxy before running follow-up Python — the
|
|
540
|
+
// proxy is only used as a "did matplotlib actually load?" gate, and
|
|
541
|
+
// leaking it would hand a Python ref to JS GC.
|
|
542
|
+
pyodidePackage.destroy();
|
|
454
543
|
pyodide.runPython(`
|
|
455
544
|
import matplotlib.pyplot as plt
|
|
456
545
|
import io
|
|
@@ -601,6 +690,62 @@ const PyodideWorker = () => {
|
|
|
601
690
|
},
|
|
602
691
|
};
|
|
603
692
|
|
|
693
|
+
const readInputViaSyncXhr = (runId) => {
|
|
694
|
+
const requestId = crypto.randomUUID();
|
|
695
|
+
const requestUrl = new URL(
|
|
696
|
+
stdinFallbackConfig.endpointPath,
|
|
697
|
+
// eslint-disable-next-line no-restricted-globals
|
|
698
|
+
self.location.origin,
|
|
699
|
+
);
|
|
700
|
+
requestUrl.searchParams.set("runId", runId || "");
|
|
701
|
+
requestUrl.searchParams.set("requestId", requestId);
|
|
702
|
+
requestUrl.searchParams.set("clientId", stdinFallbackConfig.clientId);
|
|
703
|
+
|
|
704
|
+
const xhr = new XMLHttpRequest();
|
|
705
|
+
xhr.open("GET", requestUrl.toString(), false);
|
|
706
|
+
xhr.setRequestHeader("X-Pyodide-Stdin-Request", "true");
|
|
707
|
+
xhr.setRequestHeader("Cache-Control", "no-store");
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
xhr.send(null);
|
|
711
|
+
} catch (_) {
|
|
712
|
+
throw new Error("Failed to read Python input via stdin fallback");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (xhr.status === STDIN_CANCELLED_RESPONSE_CODE) {
|
|
716
|
+
stdinCancelled = true;
|
|
717
|
+
throw new Error("PYODIDE_STDIN_CANCELLED");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (xhr.status === STDIN_TIMEOUT_RESPONSE_CODE) {
|
|
721
|
+
throw new Error("PYODIDE_STDIN_TIMEOUT");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (xhr.status === STDIN_ABORTED_RESPONSE_CODE) {
|
|
725
|
+
throw new Error("PYODIDE_STDIN_ABORTED");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// EOF: orchestrator signalled Ctrl+D / no more input. Returning null tells
|
|
729
|
+
// Pyodide's line-based stdin handler to raise EOFError at the input() call
|
|
730
|
+
// site, rather than escalating to a KeyboardInterrupt that tears down the run.
|
|
731
|
+
if (xhr.status === STDIN_EOF_RESPONSE_CODE) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (xhr.status !== 200) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
`Python input request failed with status ${xhr.status}: ${xhr.responseText}`,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
let content = xhr.responseText ?? "";
|
|
742
|
+
if (!content.endsWith("\n")) {
|
|
743
|
+
content += "\n";
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return content;
|
|
747
|
+
};
|
|
748
|
+
|
|
604
749
|
const clearPyodideData = async (userModuleNames) => {
|
|
605
750
|
postMessage({ method: "handleLoading" });
|
|
606
751
|
try {
|
|
@@ -627,10 +772,17 @@ const PyodideWorker = () => {
|
|
|
627
772
|
console.error("Error while clearing Pyodide data:", error);
|
|
628
773
|
}
|
|
629
774
|
console.log("clearPyodideData done");
|
|
630
|
-
postMessage({
|
|
775
|
+
postMessage({
|
|
776
|
+
method: "handleLoaded",
|
|
777
|
+
stdinBuffer,
|
|
778
|
+
interruptBuffer,
|
|
779
|
+
stdinStrategy,
|
|
780
|
+
supportsJspi,
|
|
781
|
+
stdinFallbackEnabled: Boolean(stdinFallbackConfig?.enabled),
|
|
782
|
+
});
|
|
631
783
|
};
|
|
632
784
|
|
|
633
|
-
const initialisePyodide = async () => {
|
|
785
|
+
const initialisePyodide = async (data) => {
|
|
634
786
|
postMessage({ method: "handleLoading" });
|
|
635
787
|
|
|
636
788
|
pyodidePromise = loadPyodide({
|
|
@@ -656,11 +808,30 @@ const PyodideWorker = () => {
|
|
|
656
808
|
|
|
657
809
|
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
658
810
|
|
|
811
|
+
supportsJspi =
|
|
812
|
+
typeof WebAssembly?.Suspending === "function" &&
|
|
813
|
+
typeof WebAssembly?.promising === "function";
|
|
814
|
+
|
|
815
|
+
stdinFallbackConfig = {
|
|
816
|
+
enabled: Boolean(data?.stdinFallback?.enabled),
|
|
817
|
+
endpointPath: data?.stdinFallback?.endpointPath || "/pyodide-stdin",
|
|
818
|
+
clientId: data?.stdinFallback?.clientId || "",
|
|
819
|
+
};
|
|
820
|
+
|
|
659
821
|
// When SharedArrayBuffer is unavailable, always use the postMessage-based
|
|
660
822
|
// stdin path. JSPI / run_sync will raise at runtime if the environment
|
|
661
823
|
// cannot stack-switch, but in JSPI-capable browsers this enables
|
|
662
824
|
// interactive input() without COOP/COEP.
|
|
663
|
-
useMessageStdin = !supportsAllFeatures;
|
|
825
|
+
useMessageStdin = !supportsAllFeatures && supportsJspi;
|
|
826
|
+
useSyncXhrStdin =
|
|
827
|
+
!supportsAllFeatures && !supportsJspi && stdinFallbackConfig.enabled;
|
|
828
|
+
stdinStrategy = supportsAllFeatures
|
|
829
|
+
? "sab"
|
|
830
|
+
: useMessageStdin
|
|
831
|
+
? "jspi"
|
|
832
|
+
: useSyncXhrStdin
|
|
833
|
+
? "sync-xhr"
|
|
834
|
+
: "unavailable";
|
|
664
835
|
pyodide.globals.set("__stdin_via_message__", useMessageStdin);
|
|
665
836
|
|
|
666
837
|
await pyodide.runPythonAsync(`
|
|
@@ -703,13 +874,25 @@ const PyodideWorker = () => {
|
|
|
703
874
|
interruptBuffer =
|
|
704
875
|
interruptBuffer || new Uint8Array(new SharedArrayBuffer(1));
|
|
705
876
|
pyodide.setInterruptBuffer(interruptBuffer);
|
|
877
|
+
} else if (useSyncXhrStdin) {
|
|
878
|
+
pyodide.setStdin({
|
|
879
|
+
stdin: () => readInputViaSyncXhr(currentRunId),
|
|
880
|
+
isatty: true,
|
|
881
|
+
});
|
|
706
882
|
}
|
|
707
883
|
|
|
708
884
|
// From this point on, anything written to stdout / stderr is considered
|
|
709
885
|
// user-visible and will be forwarded to the UI.
|
|
710
886
|
userStdStreamsEnabled = true;
|
|
711
887
|
|
|
712
|
-
postMessage({
|
|
888
|
+
postMessage({
|
|
889
|
+
method: "handleLoaded",
|
|
890
|
+
stdinBuffer,
|
|
891
|
+
interruptBuffer,
|
|
892
|
+
stdinStrategy,
|
|
893
|
+
supportsJspi,
|
|
894
|
+
stdinFallbackEnabled: stdinFallbackConfig.enabled,
|
|
895
|
+
});
|
|
713
896
|
};
|
|
714
897
|
|
|
715
898
|
const readFromStdin = (bufferToWrite) => {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-globals */
|
|
2
|
+
|
|
3
|
+
const DEBUG_STDIN_SW = false;
|
|
4
|
+
const STDIN_ENDPOINT_PATH = "/pyodide-stdin";
|
|
5
|
+
const DEFAULT_INPUT_TIMEOUT_MS = 120000;
|
|
6
|
+
const CANCELLED_STATUS = 499;
|
|
7
|
+
const TIMEOUT_STATUS = 408;
|
|
8
|
+
const ABORTED_STATUS = 410;
|
|
9
|
+
// HTTP 204 = clean EOF (Ctrl+D, persistent stdinClosed). Distinguished from
|
|
10
|
+
// 499 cancel so the Pyodide worker can raise EOFError instead of KeyboardInterrupt.
|
|
11
|
+
const EOF_STATUS = 204;
|
|
12
|
+
|
|
13
|
+
/** @type {Map<string, { resolve: (response: Response) => void, timeoutId: number, runId: string, requestId: string, clientId: string }>} */
|
|
14
|
+
const pendingRequests = new Map();
|
|
15
|
+
/** @type {Map<string, string>} */
|
|
16
|
+
const tabClientToWindowClient = new Map();
|
|
17
|
+
|
|
18
|
+
const log = (...args) => {
|
|
19
|
+
if (!DEBUG_STDIN_SW) return;
|
|
20
|
+
console.log("[pyodide-stdin-sw]", ...args);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const pendingKey = (clientId, runId, requestId) =>
|
|
24
|
+
[clientId || "", runId || "", requestId || ""].join(":");
|
|
25
|
+
|
|
26
|
+
const noStoreHeaders = {
|
|
27
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
28
|
+
"Cache-Control": "no-store",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const responseWith = (status, body) =>
|
|
32
|
+
new Response(body, {
|
|
33
|
+
status,
|
|
34
|
+
headers: noStoreHeaders,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const finishPending = (key, status, body) => {
|
|
38
|
+
const pending = pendingRequests.get(key);
|
|
39
|
+
if (!pending) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clearTimeout(pending.timeoutId);
|
|
44
|
+
pending.resolve(responseWith(status, body));
|
|
45
|
+
pendingRequests.delete(key);
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const findPendingEntries = (clientId, runId, requestId) => {
|
|
50
|
+
const matches = [];
|
|
51
|
+
for (const [key, pending] of pendingRequests.entries()) {
|
|
52
|
+
if (clientId && pending.clientId !== clientId) continue;
|
|
53
|
+
if (runId && pending.runId !== runId) continue;
|
|
54
|
+
if (requestId && pending.requestId !== requestId) continue;
|
|
55
|
+
matches.push([key, pending]);
|
|
56
|
+
}
|
|
57
|
+
return matches;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
self.addEventListener("install", (event) => {
|
|
61
|
+
log("install");
|
|
62
|
+
event.waitUntil(self.skipWaiting());
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
self.addEventListener("activate", (event) => {
|
|
66
|
+
log("activate");
|
|
67
|
+
event.waitUntil(self.clients.claim());
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
self.addEventListener("fetch", (event) => {
|
|
71
|
+
const requestUrl = new URL(event.request.url);
|
|
72
|
+
if (
|
|
73
|
+
event.request.method !== "GET" ||
|
|
74
|
+
requestUrl.pathname !== STDIN_ENDPOINT_PATH
|
|
75
|
+
) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
event.respondWith(handleStdinFetch(event, requestUrl));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const handleStdinFetch = async (event, requestUrl) => {
|
|
83
|
+
const runId = requestUrl.searchParams.get("runId") || "";
|
|
84
|
+
const requestId = requestUrl.searchParams.get("requestId") || "";
|
|
85
|
+
const tabClientId = requestUrl.searchParams.get("clientId") || "";
|
|
86
|
+
const timeoutMsRaw = Number.parseInt(
|
|
87
|
+
requestUrl.searchParams.get("timeoutMs") || "",
|
|
88
|
+
10,
|
|
89
|
+
);
|
|
90
|
+
const timeoutMs =
|
|
91
|
+
Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0
|
|
92
|
+
? timeoutMsRaw
|
|
93
|
+
: DEFAULT_INPUT_TIMEOUT_MS;
|
|
94
|
+
|
|
95
|
+
const fetchClientId = event.clientId || "";
|
|
96
|
+
const clientId = tabClientId || fetchClientId;
|
|
97
|
+
|
|
98
|
+
if (!runId || !requestId || !clientId) {
|
|
99
|
+
return responseWith(400, "Invalid stdin request identifiers");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const key = pendingKey(clientId, runId, requestId);
|
|
103
|
+
if (pendingRequests.has(key)) {
|
|
104
|
+
return responseWith(409, "Duplicate stdin request");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
log("stdin request pending", { clientId, runId, requestId });
|
|
108
|
+
|
|
109
|
+
const responsePromise = new Promise((resolve) => {
|
|
110
|
+
const timeoutId = self.setTimeout(() => {
|
|
111
|
+
log("stdin request timeout", { clientId, runId, requestId });
|
|
112
|
+
finishPending(key, TIMEOUT_STATUS, "stdin request timed out");
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
|
|
115
|
+
pendingRequests.set(key, {
|
|
116
|
+
resolve,
|
|
117
|
+
timeoutId,
|
|
118
|
+
runId,
|
|
119
|
+
requestId,
|
|
120
|
+
clientId,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const registeredWindowClientId = tabClientToWindowClient.get(clientId) || "";
|
|
125
|
+
let targetClient = null;
|
|
126
|
+
|
|
127
|
+
if (registeredWindowClientId) {
|
|
128
|
+
targetClient = await self.clients.get(registeredWindowClientId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!targetClient && fetchClientId) {
|
|
132
|
+
const fetchClient = await self.clients.get(fetchClientId);
|
|
133
|
+
if (fetchClient?.type === "window") {
|
|
134
|
+
targetClient = fetchClient;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const message = {
|
|
139
|
+
type: "PYODIDE_STDIN_REQUEST",
|
|
140
|
+
clientId,
|
|
141
|
+
runId,
|
|
142
|
+
requestId,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (targetClient) {
|
|
146
|
+
targetClient.postMessage(message);
|
|
147
|
+
return responsePromise;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const matchedClients = await self.clients.matchAll({
|
|
151
|
+
includeUncontrolled: true,
|
|
152
|
+
});
|
|
153
|
+
const windowClients = matchedClients.filter(
|
|
154
|
+
(client) => client.type === "window",
|
|
155
|
+
);
|
|
156
|
+
if (!windowClients.length) {
|
|
157
|
+
finishPending(key, ABORTED_STATUS, "stdin client unavailable");
|
|
158
|
+
return responseWith(ABORTED_STATUS, "stdin client unavailable");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const client of windowClients) {
|
|
162
|
+
client.postMessage(message);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return responsePromise;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
self.addEventListener("message", (event) => {
|
|
169
|
+
const data = event.data;
|
|
170
|
+
if (!data || typeof data !== "object") {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const eventClientId =
|
|
175
|
+
event.source && "id" in event.source ? event.source.id : "";
|
|
176
|
+
const clientId = data.clientId || eventClientId || "";
|
|
177
|
+
|
|
178
|
+
if (data.type === "PYODIDE_STDIN_REGISTER") {
|
|
179
|
+
if (data.clientId && eventClientId) {
|
|
180
|
+
tabClientToWindowClient.set(String(data.clientId), String(eventClientId));
|
|
181
|
+
log("registered stdin tab client", {
|
|
182
|
+
clientId: String(data.clientId),
|
|
183
|
+
windowClientId: String(eventClientId),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (data.type === "PYODIDE_STDIN_RESPONSE") {
|
|
190
|
+
const key = pendingKey(clientId, data.runId, data.requestId);
|
|
191
|
+
const submitted = finishPending(key, 200, String(data.value ?? ""));
|
|
192
|
+
if (!submitted) {
|
|
193
|
+
log("stdin response for unknown request", {
|
|
194
|
+
clientId,
|
|
195
|
+
runId: data.runId,
|
|
196
|
+
requestId: data.requestId,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (data.type === "PYODIDE_STDIN_EOF") {
|
|
203
|
+
// EOF is targeted at a specific (runId, requestId) just like a normal response.
|
|
204
|
+
// The worker maps HTTP 204 → null, which Pyodide's line-based stdin treats as EOF.
|
|
205
|
+
const key = pendingKey(clientId, data.runId, data.requestId);
|
|
206
|
+
const submitted = finishPending(key, EOF_STATUS, "");
|
|
207
|
+
if (!submitted) {
|
|
208
|
+
log("stdin EOF for unknown request", {
|
|
209
|
+
clientId,
|
|
210
|
+
runId: data.runId,
|
|
211
|
+
requestId: data.requestId,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (data.type === "PYODIDE_STDIN_CANCEL") {
|
|
218
|
+
const matches = findPendingEntries(clientId, data.runId, data.requestId);
|
|
219
|
+
for (const [key] of matches) {
|
|
220
|
+
finishPending(key, CANCELLED_STATUS, "stdin request cancelled");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!matches.length) {
|
|
224
|
+
log("stdin cancel for unknown request", {
|
|
225
|
+
clientId,
|
|
226
|
+
runId: data.runId,
|
|
227
|
+
requestId: data.requestId,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|