sandboxedjs 0.2.11 → 0.2.13
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/README.md +21 -9
- package/assets/logo.png +0 -0
- package/bin/sandboxedjs-egress.mjs +25 -10
- package/dist/index.cjs +87 -3
- package/dist/index.js +87 -3
- package/dist/python/python.data +140 -141
- package/dist/python/python.js +1 -1
- package/dist/python/python.wasm +0 -0
- package/dist/python/runtime.json +3 -3
- package/dist/python-worker.js +9 -0
- package/dist/service-worker.js +3 -2
- package/docs/agent/COMMANDS.md +85 -0
- package/docs/agent/DECISION-TREE.md +84 -0
- package/docs/agent/INVARIANTS.md +40 -0
- package/docs/agent/LAUNCH-PROMPT.md +37 -0
- package/docs/agent/LOOP.md +84 -0
- package/docs/agent/README.md +77 -0
- package/docs/agent/ROADMAP.md +37 -0
- package/docs/agent/STATE.md +93 -0
- package/docs/agent/tasks/00-verify-inherited-work.md +36 -0
- package/docs/agent/tasks/01-authoritative-metadata.md +34 -0
- package/docs/agent/tasks/02-native-dependencies.md +35 -0
- package/docs/agent/tasks/03-reproducible-inputs.md +27 -0
- package/docs/agent/tasks/04-build-frontends.md +27 -0
- package/docs/agent/tasks/05-registry-integration.md +27 -0
- package/docs/agent/tasks/06-package-cohorts.md +42 -0
- package/docs/agent/tasks/07-build-on-miss-boundary.md +31 -0
- package/docs/browser-runtime-architecture.md +132 -0
- package/docs/compatibility-implementation-plan.md +98 -0
- package/docs/developer-tool-packs.md +134 -0
- package/docs/frontend-automation.md +49 -0
- package/docs/fullstack-deployment.md +163 -0
- package/docs/handoff.md +275 -0
- package/docs/original-x64.md +39 -0
- package/docs/platform-hardening.md +49 -0
- package/docs/python/abi.md +97 -0
- package/docs/python/architecture.md +94 -0
- package/docs/python/baseline-inventory.md +54 -0
- package/docs/python/build-on-miss.md +198 -0
- package/docs/python/compatibility.md +206 -0
- package/docs/python/cross-build.md +354 -0
- package/docs/python/extensions.md +282 -0
- package/docs/python/release-gates.md +46 -0
- package/docs/python/virtual-sockets-plan.md +331 -0
- package/docs/runtime-lifecycle-fixes.md +39 -0
- package/docs/server-previews.md +268 -0
- package/docs/virtual-browser.md +120 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="assets/logo.png" alt="SandboxedJS" width="140" />
|
|
3
|
+
<img src="https://cdn.jsdelivr.net/npm/sandboxedjs@latest/assets/logo.png" alt="SandboxedJS" width="140" />
|
|
4
4
|
|
|
5
5
|
# SandboxedJS
|
|
6
6
|
|
|
@@ -117,7 +117,9 @@ terminal.start();
|
|
|
117
117
|
This is the case most people arrive for: a Vite frontend and a Python backend, in one container, in
|
|
118
118
|
a tab — the frontend calling `http://localhost:8000`, the backend calling a real API.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
The frontend-to-backend hop and the backend-to-internet hop have different requirements.
|
|
121
|
+
See the [full-stack deployment guide](docs/fullstack-deployment.md) for production asset staging,
|
|
122
|
+
Cloudflare Pages, Vercel, Node/serverless lifetimes, and troubleshooting.
|
|
121
123
|
|
|
122
124
|
### 1. Frontend → backend: nothing to configure
|
|
123
125
|
|
|
@@ -149,10 +151,14 @@ await box.exec("npm install", { cwd: "/app/frontend", timeoutMs: 600_000 });
|
|
|
149
151
|
|
|
150
152
|
box.spawn("fastapi run", { cwd: "/app/backend" });
|
|
151
153
|
box.spawn("npm run dev", { cwd: "/app/frontend" });
|
|
152
|
-
await box.waitForPort(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
if (!(await box.waitForPort(8000, { timeoutMs: 60_000 })) ||
|
|
155
|
+
!(await box.waitForPort(3000, { timeoutMs: 60_000 }))) {
|
|
156
|
+
throw new Error("Check backend and frontend startup logs");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const preview = await createPreview(box);
|
|
160
|
+
if (!preview) throw new Error("Preview requires a working service worker");
|
|
161
|
+
iframe.src = preview.urlFor(3000);
|
|
156
162
|
```
|
|
157
163
|
|
|
158
164
|
It works between preview pages and from another tab of the same browser while the owner page is
|
|
@@ -172,13 +178,14 @@ browser's rule about _pages_, not a container limit, and the only way through it
|
|
|
172
178
|
somewhere a page is not. So add one to the project you already deploy:
|
|
173
179
|
|
|
174
180
|
```bash
|
|
175
|
-
npx sandboxedjs-egress init --allow ollama.com,api.openai.com
|
|
181
|
+
npx sandboxedjs-egress init --target cloudflare --allow ollama.com,api.openai.com
|
|
176
182
|
```
|
|
177
183
|
|
|
178
184
|
That writes a single function file at the path your host serves — Cloudflare Pages, Vercel and
|
|
179
185
|
Netlify are detected, `--target` names one. Nothing else changes: a container in a browser probes
|
|
180
|
-
its own origin for that proxy before giving up
|
|
181
|
-
|
|
186
|
+
its own origin for that proxy before giving up. The guest project needs no proxy setting;
|
|
187
|
+
the host must deploy the generated function. For a separate relay, set `network.proxy`.
|
|
188
|
+
A host serving only static files cannot relay APIs that reject browser requests.
|
|
182
189
|
|
|
183
190
|
| Where you are | What to do |
|
|
184
191
|
| --------------------------------- | ----------------------------------------------------------------------------------- |
|
|
@@ -249,6 +256,11 @@ from source where a wheel does not exist; browsers have no compiler, so they ask
|
|
|
249
256
|
`npx sandboxedjs-build-wheels 4180`. See [docs/python/build-on-miss.md](docs/python/build-on-miss.md)
|
|
250
257
|
and [compatibility](docs/python/compatibility.md).
|
|
251
258
|
|
|
259
|
+
**Python thread offloads work in the bundled runtime:** `asyncio.to_thread`,
|
|
260
|
+
`run_in_executor`, and synchronous FastAPI routes use real worker threads.
|
|
261
|
+
For browser deployments, update `python-worker.js` and the entire `python/`
|
|
262
|
+
directory together. See [threading architecture and limits](docs/browser-runtime-architecture.md#python-thread-offloads).
|
|
263
|
+
|
|
252
264
|
**What does not work in a browser:** `copyIn()` / `copyOut()` and `expose()` (they need a real
|
|
253
265
|
filesystem and a real socket), and the CLI. They fail only if you call them.
|
|
254
266
|
|
package/assets/logo.png
ADDED
|
Binary file
|
|
@@ -45,7 +45,7 @@ const TARGETS = {
|
|
|
45
45
|
${allow}
|
|
46
46
|
/* Cloudflare Pages serves this file at /__sandboxedjs__/egress, which is where
|
|
47
47
|
* a container in this site's pages looks for its way out. */
|
|
48
|
-
export const onRequest = ({ request }) => handleEgressRequest(request, options);
|
|
48
|
+
export const onRequest = ({ request }: { request: Request }) => handleEgressRequest(request, options);
|
|
49
49
|
`,
|
|
50
50
|
},
|
|
51
51
|
vercel: {
|
|
@@ -57,7 +57,7 @@ export const config = { runtime: "edge" };
|
|
|
57
57
|
|
|
58
58
|
/* Vercel serves this file at /api/__sandboxedjs__/egress, which is one of the
|
|
59
59
|
* paths a container in this site's pages probes for its way out. */
|
|
60
|
-
export default (request) => handleEgressRequest(request, options);
|
|
60
|
+
export default (request: Request) => handleEgressRequest(request, options);
|
|
61
61
|
`,
|
|
62
62
|
},
|
|
63
63
|
netlify: {
|
|
@@ -67,7 +67,7 @@ export default (request) => handleEgressRequest(request, options);
|
|
|
67
67
|
${allow}
|
|
68
68
|
export const config = { path: "/.netlify/functions/sandboxedjs-egress" };
|
|
69
69
|
|
|
70
|
-
export default (request) => handleEgressRequest(request, options);
|
|
70
|
+
export default (request: Request) => handleEgressRequest(request, options);
|
|
71
71
|
`,
|
|
72
72
|
},
|
|
73
73
|
};
|
|
@@ -90,13 +90,28 @@ async function detectTarget(root) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async function init(argv) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
let directory;
|
|
94
|
+
let targetName;
|
|
95
|
+
let allowed = null;
|
|
96
|
+
for (let index = 0; index < argv.length; index++) {
|
|
97
|
+
const argument = argv[index];
|
|
98
|
+
if (argument === "--target" || argument === "--allow") {
|
|
99
|
+
const value = argv[++index];
|
|
100
|
+
if (!value || value.startsWith("--")) {
|
|
101
|
+
console.error(`sandboxedjs-egress init: ${argument} requires a value`);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (argument === "--target") targetName = value;
|
|
106
|
+
else allowed = value.split(",").map((host) => host.trim()).filter(Boolean);
|
|
107
|
+
} else if (argument.startsWith("-") || directory !== undefined) {
|
|
108
|
+
console.error(`sandboxedjs-egress init: unexpected argument ${argument}`);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
} else directory = argument;
|
|
112
|
+
}
|
|
113
|
+
const root = directory ?? process.cwd();
|
|
114
|
+
const name = targetName ?? await detectTarget(root);
|
|
100
115
|
if (!name || !TARGETS[name]) {
|
|
101
116
|
console.error(
|
|
102
117
|
name
|
package/dist/index.cjs
CHANGED
|
@@ -20027,7 +20027,7 @@ var CAPABILITIES = ["files", "descriptors", "pipes", "readiness", "time", "ident
|
|
|
20027
20027
|
var OP_NAMES = Object.fromEntries(
|
|
20028
20028
|
Object.entries(Op).map(([name, code]) => [code, name])
|
|
20029
20029
|
);
|
|
20030
|
-
Object.fromEntries(
|
|
20030
|
+
var ERRNO_NAMES = Object.fromEntries(
|
|
20031
20031
|
Object.entries(Errno).map(([name, code]) => [code, name])
|
|
20032
20032
|
);
|
|
20033
20033
|
|
|
@@ -21103,7 +21103,7 @@ var wheels_default = {
|
|
|
21103
21103
|
|
|
21104
21104
|
// package.json
|
|
21105
21105
|
var package_default = {
|
|
21106
|
-
version: "0.2.
|
|
21106
|
+
version: "0.2.13"};
|
|
21107
21107
|
|
|
21108
21108
|
// src/python/extension-abi.ts
|
|
21109
21109
|
var EXTENSION_ABI = {
|
|
@@ -21863,6 +21863,59 @@ var FileService = class {
|
|
|
21863
21863
|
init_signals();
|
|
21864
21864
|
init_binary();
|
|
21865
21865
|
|
|
21866
|
+
// src/python/thread-transport.ts
|
|
21867
|
+
init_binary();
|
|
21868
|
+
async function createThreadTransport(generation, handle) {
|
|
21869
|
+
const crypto2 = globalThis.crypto ?? (await nodeBuiltin("crypto")).webcrypto;
|
|
21870
|
+
const name = `sbx-python-${crypto2.randomUUID()}`;
|
|
21871
|
+
const channel = new BroadcastChannel(name);
|
|
21872
|
+
const pending = /* @__PURE__ */ new Set();
|
|
21873
|
+
let closed = false;
|
|
21874
|
+
let memory;
|
|
21875
|
+
let memoryReady;
|
|
21876
|
+
const ready = new Promise((resolve3) => {
|
|
21877
|
+
memoryReady = resolve3;
|
|
21878
|
+
});
|
|
21879
|
+
channel.onmessage = async ({ data }) => {
|
|
21880
|
+
if (closed || !(data?.request instanceof Uint8Array) || data.request.length < 16 || !Number.isSafeInteger(data.offset) || data.offset < 0 || data.offset % 4 || !Number.isSafeInteger(data.capacity) || data.capacity < 84) return;
|
|
21881
|
+
await ready;
|
|
21882
|
+
if (closed || !memory || data.offset + data.capacity > memory.buffer.byteLength) return;
|
|
21883
|
+
const control = new Int32Array(memory.buffer, data.offset, 1);
|
|
21884
|
+
const output = new Uint8Array(memory.buffer, data.offset, data.capacity);
|
|
21885
|
+
pending.add(control);
|
|
21886
|
+
try {
|
|
21887
|
+
const request = data.request.slice();
|
|
21888
|
+
new DataView(request.buffer).setUint32(8, generation, true);
|
|
21889
|
+
const response = await handle(request);
|
|
21890
|
+
if (closed) return;
|
|
21891
|
+
if (response.length > output.length - 64) throw new Error("thread response exceeds capacity");
|
|
21892
|
+
output.set(response, 64);
|
|
21893
|
+
const status = new DataView(response.buffer, response.byteOffset).getInt32(12, true);
|
|
21894
|
+
if (status < 0) output.set(new TextEncoder().encode(ERRNO_NAMES[-status] ?? "EIO"), 4);
|
|
21895
|
+
Atomics.store(control, 0, response.length);
|
|
21896
|
+
} catch {
|
|
21897
|
+
Atomics.store(control, 0, -1);
|
|
21898
|
+
} finally {
|
|
21899
|
+
pending.delete(control);
|
|
21900
|
+
Atomics.notify(control, 0);
|
|
21901
|
+
}
|
|
21902
|
+
};
|
|
21903
|
+
return { name, setMemory(value) {
|
|
21904
|
+
if (!(value.buffer instanceof SharedArrayBuffer)) throw new Error("Python thread memory must be shared");
|
|
21905
|
+
memory = value;
|
|
21906
|
+
memoryReady();
|
|
21907
|
+
}, close() {
|
|
21908
|
+
closed = true;
|
|
21909
|
+
memoryReady();
|
|
21910
|
+
channel.close();
|
|
21911
|
+
for (const control of pending) {
|
|
21912
|
+
Atomics.store(control, 0, -1);
|
|
21913
|
+
Atomics.notify(control, 0);
|
|
21914
|
+
}
|
|
21915
|
+
pending.clear();
|
|
21916
|
+
} };
|
|
21917
|
+
}
|
|
21918
|
+
|
|
21866
21919
|
// src/python/protocol.ts
|
|
21867
21920
|
var ProtocolError = class extends Error {
|
|
21868
21921
|
code = "ERR_SBX_ABI_PROTOCOL";
|
|
@@ -23109,6 +23162,7 @@ async function startPythonProcess(options) {
|
|
|
23109
23162
|
sockets: options.sockets,
|
|
23110
23163
|
processes: options.processes
|
|
23111
23164
|
});
|
|
23165
|
+
const threads = await createThreadTransport(generation, handle);
|
|
23112
23166
|
const buffers = createHostTransportBuffers();
|
|
23113
23167
|
let fault = null;
|
|
23114
23168
|
const server = new HostCallServer(buffers, handle, (error) => {
|
|
@@ -23127,8 +23181,10 @@ async function startPythonProcess(options) {
|
|
|
23127
23181
|
const cleanup = () => {
|
|
23128
23182
|
if (done) return;
|
|
23129
23183
|
done = true;
|
|
23184
|
+
controller.abort();
|
|
23130
23185
|
options.signal.removeEventListener("abort", abort);
|
|
23131
23186
|
server.close();
|
|
23187
|
+
threads.close();
|
|
23132
23188
|
table.closeAll();
|
|
23133
23189
|
try {
|
|
23134
23190
|
worker?.terminate();
|
|
@@ -23143,6 +23199,10 @@ async function startPythonProcess(options) {
|
|
|
23143
23199
|
}
|
|
23144
23200
|
worker.onMessage((message) => {
|
|
23145
23201
|
const detail = message;
|
|
23202
|
+
if (detail?.type === "thread-memory" && detail.memory) {
|
|
23203
|
+
threads.setMemory(detail.memory);
|
|
23204
|
+
return;
|
|
23205
|
+
}
|
|
23146
23206
|
if (detail?.type === "wake") {
|
|
23147
23207
|
void server.pump();
|
|
23148
23208
|
return;
|
|
@@ -23175,6 +23235,7 @@ async function startPythonProcess(options) {
|
|
|
23175
23235
|
type: "start",
|
|
23176
23236
|
buffers,
|
|
23177
23237
|
generation,
|
|
23238
|
+
threadChannel: threads.name,
|
|
23178
23239
|
moduleUrl: options.manifest.artifacts.moduleUrl,
|
|
23179
23240
|
argv: options.argv,
|
|
23180
23241
|
env: options.env,
|
|
@@ -25301,7 +25362,30 @@ if _egress:
|
|
|
25301
25362
|
sys.meta_path.insert(0, _SbxHttpxHook())
|
|
25302
25363
|
|
|
25303
25364
|
|
|
25304
|
-
if os.environ.get("
|
|
25365
|
+
if os.environ.get("SBX_THREAD_HOST_CALLS") == "1":
|
|
25366
|
+
import selectors as _selectors
|
|
25367
|
+
import threading as _threading
|
|
25368
|
+
import time as _time
|
|
25369
|
+
|
|
25370
|
+
class _SbxThreadSelector(_selectors.SelectSelector):
|
|
25371
|
+
"""Let Emscripten service filesystem calls proxied by Python threads.
|
|
25372
|
+
|
|
25373
|
+
Native socket calls have an independent channel per request. Filesystem
|
|
25374
|
+
calls still use the main runtime thread, so it must periodically leave
|
|
25375
|
+
its host poll and yield whenever other Python threads are alive. Keep
|
|
25376
|
+
the standard asyncio executor, context propagation and cancellation.
|
|
25377
|
+
"""
|
|
25378
|
+
def select(self, timeout=None):
|
|
25379
|
+
threaded = _threading.active_count() > 1
|
|
25380
|
+
if threaded:
|
|
25381
|
+
timeout = 0.01 if timeout is None else min(max(timeout, 0), 0.01)
|
|
25382
|
+
ready = super().select(timeout)
|
|
25383
|
+
if threaded:
|
|
25384
|
+
_time.sleep(0.001)
|
|
25385
|
+
return ready
|
|
25386
|
+
|
|
25387
|
+
_selectors.DefaultSelector = _SbxThreadSelector
|
|
25388
|
+
elif os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
|
|
25305
25389
|
import asyncio
|
|
25306
25390
|
|
|
25307
25391
|
async def _to_thread(func, /, *args, **kwargs):
|
package/dist/index.js
CHANGED
|
@@ -20010,7 +20010,7 @@ var CAPABILITIES = ["files", "descriptors", "pipes", "readiness", "time", "ident
|
|
|
20010
20010
|
var OP_NAMES = Object.fromEntries(
|
|
20011
20011
|
Object.entries(Op).map(([name, code]) => [code, name])
|
|
20012
20012
|
);
|
|
20013
|
-
Object.fromEntries(
|
|
20013
|
+
var ERRNO_NAMES = Object.fromEntries(
|
|
20014
20014
|
Object.entries(Errno).map(([name, code]) => [code, name])
|
|
20015
20015
|
);
|
|
20016
20016
|
|
|
@@ -21086,7 +21086,7 @@ var wheels_default = {
|
|
|
21086
21086
|
|
|
21087
21087
|
// package.json
|
|
21088
21088
|
var package_default = {
|
|
21089
|
-
version: "0.2.
|
|
21089
|
+
version: "0.2.13"};
|
|
21090
21090
|
|
|
21091
21091
|
// src/python/extension-abi.ts
|
|
21092
21092
|
var EXTENSION_ABI = {
|
|
@@ -21846,6 +21846,59 @@ var FileService = class {
|
|
|
21846
21846
|
init_signals();
|
|
21847
21847
|
init_binary();
|
|
21848
21848
|
|
|
21849
|
+
// src/python/thread-transport.ts
|
|
21850
|
+
init_binary();
|
|
21851
|
+
async function createThreadTransport(generation, handle) {
|
|
21852
|
+
const crypto2 = globalThis.crypto ?? (await nodeBuiltin("crypto")).webcrypto;
|
|
21853
|
+
const name = `sbx-python-${crypto2.randomUUID()}`;
|
|
21854
|
+
const channel = new BroadcastChannel(name);
|
|
21855
|
+
const pending = /* @__PURE__ */ new Set();
|
|
21856
|
+
let closed = false;
|
|
21857
|
+
let memory;
|
|
21858
|
+
let memoryReady;
|
|
21859
|
+
const ready = new Promise((resolve3) => {
|
|
21860
|
+
memoryReady = resolve3;
|
|
21861
|
+
});
|
|
21862
|
+
channel.onmessage = async ({ data }) => {
|
|
21863
|
+
if (closed || !(data?.request instanceof Uint8Array) || data.request.length < 16 || !Number.isSafeInteger(data.offset) || data.offset < 0 || data.offset % 4 || !Number.isSafeInteger(data.capacity) || data.capacity < 84) return;
|
|
21864
|
+
await ready;
|
|
21865
|
+
if (closed || !memory || data.offset + data.capacity > memory.buffer.byteLength) return;
|
|
21866
|
+
const control = new Int32Array(memory.buffer, data.offset, 1);
|
|
21867
|
+
const output = new Uint8Array(memory.buffer, data.offset, data.capacity);
|
|
21868
|
+
pending.add(control);
|
|
21869
|
+
try {
|
|
21870
|
+
const request = data.request.slice();
|
|
21871
|
+
new DataView(request.buffer).setUint32(8, generation, true);
|
|
21872
|
+
const response = await handle(request);
|
|
21873
|
+
if (closed) return;
|
|
21874
|
+
if (response.length > output.length - 64) throw new Error("thread response exceeds capacity");
|
|
21875
|
+
output.set(response, 64);
|
|
21876
|
+
const status = new DataView(response.buffer, response.byteOffset).getInt32(12, true);
|
|
21877
|
+
if (status < 0) output.set(new TextEncoder().encode(ERRNO_NAMES[-status] ?? "EIO"), 4);
|
|
21878
|
+
Atomics.store(control, 0, response.length);
|
|
21879
|
+
} catch {
|
|
21880
|
+
Atomics.store(control, 0, -1);
|
|
21881
|
+
} finally {
|
|
21882
|
+
pending.delete(control);
|
|
21883
|
+
Atomics.notify(control, 0);
|
|
21884
|
+
}
|
|
21885
|
+
};
|
|
21886
|
+
return { name, setMemory(value) {
|
|
21887
|
+
if (!(value.buffer instanceof SharedArrayBuffer)) throw new Error("Python thread memory must be shared");
|
|
21888
|
+
memory = value;
|
|
21889
|
+
memoryReady();
|
|
21890
|
+
}, close() {
|
|
21891
|
+
closed = true;
|
|
21892
|
+
memoryReady();
|
|
21893
|
+
channel.close();
|
|
21894
|
+
for (const control of pending) {
|
|
21895
|
+
Atomics.store(control, 0, -1);
|
|
21896
|
+
Atomics.notify(control, 0);
|
|
21897
|
+
}
|
|
21898
|
+
pending.clear();
|
|
21899
|
+
} };
|
|
21900
|
+
}
|
|
21901
|
+
|
|
21849
21902
|
// src/python/protocol.ts
|
|
21850
21903
|
var ProtocolError = class extends Error {
|
|
21851
21904
|
code = "ERR_SBX_ABI_PROTOCOL";
|
|
@@ -23092,6 +23145,7 @@ async function startPythonProcess(options) {
|
|
|
23092
23145
|
sockets: options.sockets,
|
|
23093
23146
|
processes: options.processes
|
|
23094
23147
|
});
|
|
23148
|
+
const threads = await createThreadTransport(generation, handle);
|
|
23095
23149
|
const buffers = createHostTransportBuffers();
|
|
23096
23150
|
let fault = null;
|
|
23097
23151
|
const server = new HostCallServer(buffers, handle, (error) => {
|
|
@@ -23110,8 +23164,10 @@ async function startPythonProcess(options) {
|
|
|
23110
23164
|
const cleanup = () => {
|
|
23111
23165
|
if (done) return;
|
|
23112
23166
|
done = true;
|
|
23167
|
+
controller.abort();
|
|
23113
23168
|
options.signal.removeEventListener("abort", abort);
|
|
23114
23169
|
server.close();
|
|
23170
|
+
threads.close();
|
|
23115
23171
|
table.closeAll();
|
|
23116
23172
|
try {
|
|
23117
23173
|
worker?.terminate();
|
|
@@ -23126,6 +23182,10 @@ async function startPythonProcess(options) {
|
|
|
23126
23182
|
}
|
|
23127
23183
|
worker.onMessage((message) => {
|
|
23128
23184
|
const detail = message;
|
|
23185
|
+
if (detail?.type === "thread-memory" && detail.memory) {
|
|
23186
|
+
threads.setMemory(detail.memory);
|
|
23187
|
+
return;
|
|
23188
|
+
}
|
|
23129
23189
|
if (detail?.type === "wake") {
|
|
23130
23190
|
void server.pump();
|
|
23131
23191
|
return;
|
|
@@ -23158,6 +23218,7 @@ async function startPythonProcess(options) {
|
|
|
23158
23218
|
type: "start",
|
|
23159
23219
|
buffers,
|
|
23160
23220
|
generation,
|
|
23221
|
+
threadChannel: threads.name,
|
|
23161
23222
|
moduleUrl: options.manifest.artifacts.moduleUrl,
|
|
23162
23223
|
argv: options.argv,
|
|
23163
23224
|
env: options.env,
|
|
@@ -25284,7 +25345,30 @@ if _egress:
|
|
|
25284
25345
|
sys.meta_path.insert(0, _SbxHttpxHook())
|
|
25285
25346
|
|
|
25286
25347
|
|
|
25287
|
-
if os.environ.get("
|
|
25348
|
+
if os.environ.get("SBX_THREAD_HOST_CALLS") == "1":
|
|
25349
|
+
import selectors as _selectors
|
|
25350
|
+
import threading as _threading
|
|
25351
|
+
import time as _time
|
|
25352
|
+
|
|
25353
|
+
class _SbxThreadSelector(_selectors.SelectSelector):
|
|
25354
|
+
"""Let Emscripten service filesystem calls proxied by Python threads.
|
|
25355
|
+
|
|
25356
|
+
Native socket calls have an independent channel per request. Filesystem
|
|
25357
|
+
calls still use the main runtime thread, so it must periodically leave
|
|
25358
|
+
its host poll and yield whenever other Python threads are alive. Keep
|
|
25359
|
+
the standard asyncio executor, context propagation and cancellation.
|
|
25360
|
+
"""
|
|
25361
|
+
def select(self, timeout=None):
|
|
25362
|
+
threaded = _threading.active_count() > 1
|
|
25363
|
+
if threaded:
|
|
25364
|
+
timeout = 0.01 if timeout is None else min(max(timeout, 0), 0.01)
|
|
25365
|
+
ready = super().select(timeout)
|
|
25366
|
+
if threaded:
|
|
25367
|
+
_time.sleep(0.001)
|
|
25368
|
+
return ready
|
|
25369
|
+
|
|
25370
|
+
_selectors.DefaultSelector = _SbxThreadSelector
|
|
25371
|
+
elif os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
|
|
25288
25372
|
import asyncio
|
|
25289
25373
|
|
|
25290
25374
|
async def _to_thread(func, /, *args, **kwargs):
|