sandboxedjs 0.1.32 → 0.1.34
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/index.cjs +703 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +703 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17907,20 +17907,620 @@ def _install():
|
|
|
17907
17907
|
|
|
17908
17908
|
_install()
|
|
17909
17909
|
`;
|
|
17910
|
+
var PROCESS_PY = `
|
|
17911
|
+
import io, os, shlex, subprocess, sys
|
|
17912
|
+
import _sbx_host as _host
|
|
17913
|
+
from pyodide.ffi import can_run_sync, run_sync
|
|
17914
|
+
|
|
17915
|
+
_PIPE = subprocess.PIPE
|
|
17916
|
+
_DEVNULL = subprocess.DEVNULL
|
|
17917
|
+
_STDOUT = subprocess.STDOUT
|
|
17918
|
+
|
|
17919
|
+
|
|
17920
|
+
def _argv_for(args, shell):
|
|
17921
|
+
if shell:
|
|
17922
|
+
line = args if isinstance(args, str) else " ".join(str(a) for a in args)
|
|
17923
|
+
return ["sh", "-c", line]
|
|
17924
|
+
if isinstance(args, (str, bytes)):
|
|
17925
|
+
return shlex.split(args if isinstance(args, str) else args.decode())
|
|
17926
|
+
return [str(a) for a in args]
|
|
17927
|
+
|
|
17928
|
+
|
|
17929
|
+
def _require_blocking(what):
|
|
17930
|
+
if not can_run_sync():
|
|
17931
|
+
raise OSError(
|
|
17932
|
+
"cannot " + what + " on this host: WebAssembly stack switching "
|
|
17933
|
+
"(JSPI) is unavailable."
|
|
17934
|
+
)
|
|
17935
|
+
|
|
17936
|
+
|
|
17937
|
+
def _to_bytes_result(value):
|
|
17938
|
+
if value is None:
|
|
17939
|
+
return b""
|
|
17940
|
+
to_bytes = getattr(value, "to_bytes", None)
|
|
17941
|
+
return to_bytes() if to_bytes is not None else bytes(value.to_py())
|
|
17942
|
+
|
|
17943
|
+
|
|
17944
|
+
class _ChildReader(io.RawIOBase):
|
|
17945
|
+
"""One of a running child's output streams.
|
|
17946
|
+
|
|
17947
|
+
Reads suspend only for as long as the child takes to produce the next
|
|
17948
|
+
bytes, so \`for line in p.stdout\` follows a child that is still running
|
|
17949
|
+
instead of waiting for it to exit.
|
|
17950
|
+
"""
|
|
17951
|
+
|
|
17952
|
+
def __init__(self, child_id, which):
|
|
17953
|
+
self._id = child_id
|
|
17954
|
+
self._which = which
|
|
17955
|
+
|
|
17956
|
+
def readable(self):
|
|
17957
|
+
return True
|
|
17958
|
+
|
|
17959
|
+
def readinto(self, target):
|
|
17960
|
+
want = len(target)
|
|
17961
|
+
if want == 0:
|
|
17962
|
+
return 0
|
|
17963
|
+
_require_blocking("read from a child process")
|
|
17964
|
+
data = _to_bytes_result(run_sync(_host.proc_read(self._id, self._which, want)))
|
|
17965
|
+
target[: len(data)] = data
|
|
17966
|
+
return len(data)
|
|
17967
|
+
|
|
17968
|
+
|
|
17969
|
+
class _ChildWriter(io.RawIOBase):
|
|
17970
|
+
"""A running child's standard input."""
|
|
17971
|
+
|
|
17972
|
+
def __init__(self, child_id):
|
|
17973
|
+
self._id = child_id
|
|
17974
|
+
|
|
17975
|
+
def writable(self):
|
|
17976
|
+
return True
|
|
17977
|
+
|
|
17978
|
+
def write(self, data):
|
|
17979
|
+
if isinstance(data, str):
|
|
17980
|
+
text = data
|
|
17981
|
+
else:
|
|
17982
|
+
text = bytes(data).decode("utf-8", "replace")
|
|
17983
|
+
_host.proc_write(self._id, text)
|
|
17984
|
+
return len(data)
|
|
17985
|
+
|
|
17986
|
+
def close(self):
|
|
17987
|
+
if not self.closed:
|
|
17988
|
+
_host.proc_close_stdin(self._id)
|
|
17989
|
+
super().close()
|
|
17990
|
+
|
|
17991
|
+
|
|
17992
|
+
class SbxPopen:
|
|
17993
|
+
"""A child process, run through the container's kernel.
|
|
17994
|
+
|
|
17995
|
+
The child is a real, live process: it starts when \`Popen\` is constructed
|
|
17996
|
+
and runs on the host's event loop while Python carries on. The parent only
|
|
17997
|
+
suspends when it actually reads, waits, or communicates \u2014 which is what
|
|
17998
|
+
makes streaming from a long-running child work rather than deadlocking on
|
|
17999
|
+
a single interpreter stack.
|
|
18000
|
+
"""
|
|
18001
|
+
|
|
18002
|
+
def __init__(self, args, bufsize=-1, executable=None, stdin=None, stdout=None,
|
|
18003
|
+
stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None,
|
|
18004
|
+
env=None, universal_newlines=None, startupinfo=None, creationflags=0,
|
|
18005
|
+
restore_signals=True, start_new_session=False, pass_fds=(), *,
|
|
18006
|
+
text=None, encoding=None, errors=None, **kwargs):
|
|
18007
|
+
self.args = args
|
|
18008
|
+
self.returncode = None
|
|
18009
|
+
self._text = bool(text or encoding or errors or universal_newlines)
|
|
18010
|
+
self._encoding = encoding or "utf-8"
|
|
18011
|
+
self._errors = errors or "replace"
|
|
18012
|
+
self._merge_err = stderr == _STDOUT
|
|
18013
|
+
self._closed = False
|
|
18014
|
+
|
|
18015
|
+
argv = _argv_for(args, shell)
|
|
18016
|
+
env_map = None if env is None else {str(k): str(v) for k, v in dict(env).items()}
|
|
18017
|
+
self._id = _host.proc_start(
|
|
18018
|
+
argv, None if cwd is None else str(cwd), env_map, self._merge_err
|
|
18019
|
+
)
|
|
18020
|
+
self.pid = int(_host.proc_pid(self._id))
|
|
18021
|
+
|
|
18022
|
+
self.stdin = self._wrap_writer() if stdin == _PIPE else None
|
|
18023
|
+
self.stdout = self._wrap_reader(1) if stdout == _PIPE else None
|
|
18024
|
+
# When merged there is only one stream, and it is stream 1.
|
|
18025
|
+
self.stderr = self._wrap_reader(2) if stderr == _PIPE and not self._merge_err else None
|
|
18026
|
+
|
|
18027
|
+
# Output nobody captured belongs on the parent's own streams, or a
|
|
18028
|
+
# program's diagnostics would vanish rather than being seen.
|
|
18029
|
+
self._drain_out = stdout is None
|
|
18030
|
+
self._drain_err = stderr is None and not self._merge_err
|
|
18031
|
+
|
|
18032
|
+
def _wrap_reader(self, which):
|
|
18033
|
+
raw = io.BufferedReader(_ChildReader(self._id, which))
|
|
18034
|
+
if self._text:
|
|
18035
|
+
return io.TextIOWrapper(raw, encoding=self._encoding, errors=self._errors)
|
|
18036
|
+
return raw
|
|
18037
|
+
|
|
18038
|
+
def _wrap_writer(self):
|
|
18039
|
+
raw = io.BufferedWriter(_ChildWriter(self._id))
|
|
18040
|
+
if self._text:
|
|
18041
|
+
return io.TextIOWrapper(raw, encoding=self._encoding, errors=self._errors)
|
|
18042
|
+
return raw
|
|
18043
|
+
|
|
18044
|
+
def _read_all(self, which):
|
|
18045
|
+
_require_blocking("read from a child process")
|
|
18046
|
+
parts = []
|
|
18047
|
+
while True:
|
|
18048
|
+
chunk = _to_bytes_result(run_sync(_host.proc_read(self._id, which, 65536)))
|
|
18049
|
+
if not chunk:
|
|
18050
|
+
break
|
|
18051
|
+
parts.append(chunk)
|
|
18052
|
+
return b"".join(parts)
|
|
18053
|
+
|
|
18054
|
+
def _decode(self, raw):
|
|
18055
|
+
return raw.decode(self._encoding, self._errors) if self._text else raw
|
|
18056
|
+
|
|
18057
|
+
def communicate(self, input=None, timeout=None):
|
|
18058
|
+
if input is not None and self.stdin is not None:
|
|
18059
|
+
if isinstance(input, bytes):
|
|
18060
|
+
input = input.decode(self._encoding, "replace")
|
|
18061
|
+
self.stdin.write(input)
|
|
18062
|
+
if self.stdin is not None:
|
|
18063
|
+
try:
|
|
18064
|
+
self.stdin.close()
|
|
18065
|
+
except Exception:
|
|
18066
|
+
pass
|
|
18067
|
+
|
|
18068
|
+
out = self.stdout.read() if self.stdout is not None else None
|
|
18069
|
+
err = self.stderr.read() if self.stderr is not None else None
|
|
18070
|
+
self.wait()
|
|
18071
|
+
self._flush_uncaptured()
|
|
18072
|
+
return out, err
|
|
18073
|
+
|
|
18074
|
+
def _flush_uncaptured(self):
|
|
18075
|
+
if self._drain_out:
|
|
18076
|
+
text = self._decode_raw(self._read_all(1))
|
|
18077
|
+
if text:
|
|
18078
|
+
sys.stdout.write(text)
|
|
18079
|
+
if self._drain_err:
|
|
18080
|
+
text = self._decode_raw(self._read_all(2))
|
|
18081
|
+
if text:
|
|
18082
|
+
sys.stderr.write(text)
|
|
18083
|
+
self._drain_out = False
|
|
18084
|
+
self._drain_err = False
|
|
18085
|
+
|
|
18086
|
+
def _decode_raw(self, raw):
|
|
18087
|
+
return raw.decode(self._encoding, "replace")
|
|
18088
|
+
|
|
18089
|
+
def poll(self):
|
|
18090
|
+
code = _host.proc_poll(self._id)
|
|
18091
|
+
if code is not None:
|
|
18092
|
+
self.returncode = int(code)
|
|
18093
|
+
return self.returncode
|
|
18094
|
+
|
|
18095
|
+
def wait(self, timeout=None):
|
|
18096
|
+
_require_blocking("wait for a child process")
|
|
18097
|
+
self.returncode = int(run_sync(_host.proc_wait(self._id)))
|
|
18098
|
+
return self.returncode
|
|
18099
|
+
|
|
18100
|
+
def kill(self):
|
|
18101
|
+
self.send_signal("SIGKILL")
|
|
18102
|
+
|
|
18103
|
+
def terminate(self):
|
|
18104
|
+
self.send_signal("SIGTERM")
|
|
18105
|
+
|
|
18106
|
+
def send_signal(self, sig):
|
|
18107
|
+
_host.proc_kill(self._id, sig if isinstance(sig, str) else "SIGTERM")
|
|
18108
|
+
|
|
18109
|
+
def __del__(self):
|
|
18110
|
+
try:
|
|
18111
|
+
_host.proc_release(self._id)
|
|
18112
|
+
except Exception:
|
|
18113
|
+
pass
|
|
18114
|
+
|
|
18115
|
+
def __enter__(self):
|
|
18116
|
+
return self
|
|
18117
|
+
|
|
18118
|
+
def __exit__(self, *exc):
|
|
18119
|
+
if self.stdin is not None:
|
|
18120
|
+
try:
|
|
18121
|
+
self.stdin.close()
|
|
18122
|
+
except Exception:
|
|
18123
|
+
pass
|
|
18124
|
+
self.wait()
|
|
18125
|
+
self._flush_uncaptured()
|
|
18126
|
+
for stream in (self.stdout, self.stderr):
|
|
18127
|
+
if stream is not None:
|
|
18128
|
+
try:
|
|
18129
|
+
stream.close()
|
|
18130
|
+
except Exception:
|
|
18131
|
+
pass
|
|
18132
|
+
# The host holds the process and its pipes until it is told the child
|
|
18133
|
+
# is finished with; a loop spawning children would otherwise keep every
|
|
18134
|
+
# one of them alive. Safe here only because the child has been waited
|
|
18135
|
+
# for and everything anyone wanted has been read.
|
|
18136
|
+
_host.proc_release(self._id)
|
|
18137
|
+
return False
|
|
18138
|
+
|
|
18139
|
+
|
|
18140
|
+
def _system(command):
|
|
18141
|
+
with SbxPopen(command, shell=True) as child:
|
|
18142
|
+
pass
|
|
18143
|
+
# os.system returns a wait status, not an exit code.
|
|
18144
|
+
return (child.returncode or 0) << 8
|
|
18145
|
+
|
|
18146
|
+
|
|
18147
|
+
def _popen(command, mode="r", buffering=-1):
|
|
18148
|
+
if "w" in mode:
|
|
18149
|
+
raise OSError("os.popen with mode 'w' is not supported in this container")
|
|
18150
|
+
child = SbxPopen(command, shell=True, stdout=_PIPE, text=True)
|
|
18151
|
+
stream = child.stdout
|
|
18152
|
+
inner = stream.close
|
|
18153
|
+
|
|
18154
|
+
def close():
|
|
18155
|
+
inner()
|
|
18156
|
+
code = child.wait()
|
|
18157
|
+
return None if code == 0 else code << 8
|
|
18158
|
+
|
|
18159
|
+
stream.close = close
|
|
18160
|
+
return stream
|
|
18161
|
+
|
|
18162
|
+
|
|
18163
|
+
def _install_processes():
|
|
18164
|
+
subprocess.Popen = SbxPopen
|
|
18165
|
+
os.system = _system
|
|
18166
|
+
os.popen = _popen
|
|
18167
|
+
|
|
18168
|
+
|
|
18169
|
+
_install_processes()
|
|
18170
|
+
`;
|
|
18171
|
+
var ASYNCIO_PY = `
|
|
18172
|
+
import asyncio
|
|
18173
|
+
from pyodide.ffi import can_run_sync, run_sync
|
|
18174
|
+
|
|
18175
|
+
_orig_run = asyncio.run
|
|
18176
|
+
|
|
18177
|
+
|
|
18178
|
+
def _run(main, *, debug=None, loop_factory=None):
|
|
18179
|
+
if not asyncio.iscoroutine(main) and not asyncio.isfuture(main):
|
|
18180
|
+
raise ValueError("a coroutine was expected, got {!r}".format(main))
|
|
18181
|
+
if not can_run_sync():
|
|
18182
|
+
# No stack switching: the original at least raises a comprehensible
|
|
18183
|
+
# error rather than this shim inventing a new one.
|
|
18184
|
+
return _orig_run(main, debug=debug, loop_factory=loop_factory)
|
|
18185
|
+
return run_sync(main)
|
|
18186
|
+
|
|
18187
|
+
|
|
18188
|
+
def _run_until_complete(self, future):
|
|
18189
|
+
if not can_run_sync():
|
|
18190
|
+
raise RuntimeError(
|
|
18191
|
+
"cannot wait for a coroutine on this host: WebAssembly stack "
|
|
18192
|
+
"switching (JSPI) is unavailable."
|
|
18193
|
+
)
|
|
18194
|
+
return run_sync(future)
|
|
18195
|
+
|
|
18196
|
+
|
|
18197
|
+
async def _create_server(self, *args, **kwargs):
|
|
18198
|
+
# Pyodide's loop raises a bare NotImplementedError from deep inside
|
|
18199
|
+
# asyncio, which tells a reader nothing about why their server will not
|
|
18200
|
+
# start. Naming the limit is the least this can do until the container can
|
|
18201
|
+
# route connections to Python.
|
|
18202
|
+
raise NotImplementedError(
|
|
18203
|
+
"this container cannot yet accept network connections from Python: "
|
|
18204
|
+
"asyncio's create_server is not implemented on Pyodide's event loop, "
|
|
18205
|
+
"so an ASGI/WSGI server such as uvicorn will install and import but "
|
|
18206
|
+
"not bind a port. Outbound requests do work."
|
|
18207
|
+
)
|
|
18208
|
+
|
|
18209
|
+
|
|
18210
|
+
def _install_asyncio():
|
|
18211
|
+
asyncio.run = _run
|
|
18212
|
+
asyncio.runners.run = _run
|
|
18213
|
+
loop_type = type(asyncio.get_event_loop())
|
|
18214
|
+
loop_type.run_until_complete = _run_until_complete
|
|
18215
|
+
loop_type.create_server = _create_server
|
|
18216
|
+
|
|
18217
|
+
|
|
18218
|
+
_install_asyncio()
|
|
18219
|
+
`;
|
|
18220
|
+
var NETWORK_PY = `
|
|
18221
|
+
import builtins, email.message, io, sys, urllib.error, urllib.request, urllib.response
|
|
18222
|
+
import _sbx_host as _host
|
|
18223
|
+
from pyodide.ffi import can_run_sync, run_sync
|
|
18224
|
+
|
|
18225
|
+
|
|
18226
|
+
def _fetch(request):
|
|
18227
|
+
if not can_run_sync():
|
|
18228
|
+
raise urllib.error.URLError(
|
|
18229
|
+
"cannot make a request on this host: WebAssembly stack switching "
|
|
18230
|
+
"(JSPI) is unavailable."
|
|
18231
|
+
)
|
|
18232
|
+
body = request.data
|
|
18233
|
+
if isinstance(body, str):
|
|
18234
|
+
body = body.encode()
|
|
18235
|
+
try:
|
|
18236
|
+
result = run_sync(
|
|
18237
|
+
_host.http(
|
|
18238
|
+
request.full_url,
|
|
18239
|
+
request.get_method(),
|
|
18240
|
+
[[k, v] for k, v in request.header_items()],
|
|
18241
|
+
body,
|
|
18242
|
+
)
|
|
18243
|
+
)
|
|
18244
|
+
except Exception as error:
|
|
18245
|
+
raise urllib.error.URLError(str(error)) from None
|
|
18246
|
+
|
|
18247
|
+
headers = email.message.Message()
|
|
18248
|
+
for pair in result.headers.to_py():
|
|
18249
|
+
headers[str(pair[0])] = str(pair[1])
|
|
18250
|
+
payload = result.body.to_bytes()
|
|
18251
|
+
response = urllib.response.addinfourl(
|
|
18252
|
+
io.BytesIO(payload), headers, str(result.url), int(result.status)
|
|
18253
|
+
)
|
|
18254
|
+
response.msg = str(result.statusText)
|
|
18255
|
+
return response
|
|
18256
|
+
|
|
18257
|
+
|
|
18258
|
+
class _SbxHTTPHandler(urllib.request.HTTPHandler):
|
|
18259
|
+
"""Both schemes, from one handler.
|
|
18260
|
+
|
|
18261
|
+
Pyodide builds urllib without \`ssl\`, so \`HTTPSHandler\` does not exist to
|
|
18262
|
+
subclass \u2014 and https is the scheme most callers actually want. Serving
|
|
18263
|
+
both from a subclass of the one handler that does exist keeps
|
|
18264
|
+
\`build_opener\` treating this as a replacement rather than stacking it
|
|
18265
|
+
alongside the original.
|
|
18266
|
+
"""
|
|
18267
|
+
|
|
18268
|
+
def http_open(self, request):
|
|
18269
|
+
return _fetch(request)
|
|
18270
|
+
|
|
18271
|
+
def https_open(self, request):
|
|
18272
|
+
return _fetch(request)
|
|
18273
|
+
|
|
18274
|
+
https_request = urllib.request.HTTPHandler.do_request_
|
|
18275
|
+
|
|
18276
|
+
|
|
18277
|
+
def _raw_fetch(url, method, headers, body):
|
|
18278
|
+
"""One request, through the container's network path."""
|
|
18279
|
+
if not can_run_sync():
|
|
18280
|
+
raise urllib.error.URLError(
|
|
18281
|
+
"cannot make a request on this host: WebAssembly stack switching "
|
|
18282
|
+
"(JSPI) is unavailable."
|
|
18283
|
+
)
|
|
18284
|
+
if isinstance(body, str):
|
|
18285
|
+
body = body.encode()
|
|
18286
|
+
result = run_sync(
|
|
18287
|
+
_host.http(url, method, [[str(k), str(v)] for k, v in dict(headers).items()], body)
|
|
18288
|
+
)
|
|
18289
|
+
return {
|
|
18290
|
+
"status": int(result.status),
|
|
18291
|
+
"reason": str(result.statusText),
|
|
18292
|
+
"headers": [(str(p[0]), str(p[1])) for p in result.headers.to_py()],
|
|
18293
|
+
"body": result.body.to_bytes(),
|
|
18294
|
+
"url": str(result.url),
|
|
18295
|
+
}
|
|
18296
|
+
|
|
18297
|
+
|
|
18298
|
+
# \u2500\u2500 Routing third-party HTTP stacks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
18299
|
+
#
|
|
18300
|
+
# A library that brings its own transport does not go through urllib, and in
|
|
18301
|
+
# Pyodide several of them reach JavaScript's fetch directly \u2014 which leaves the
|
|
18302
|
+
# container's network policy behind entirely. They cannot all be patched at
|
|
18303
|
+
# boot either, because pip installs them later.
|
|
18304
|
+
#
|
|
18305
|
+
# So: adapters are registered by module name and applied the moment that module
|
|
18306
|
+
# is first imported, whenever that happens. Supporting another stack is a small
|
|
18307
|
+
# function registered here, not a change to the machinery.
|
|
18308
|
+
|
|
18309
|
+
_ADAPTERS = {}
|
|
18310
|
+
|
|
18311
|
+
|
|
18312
|
+
def _register_adapter(name):
|
|
18313
|
+
def decorate(fn):
|
|
18314
|
+
_ADAPTERS[name] = fn
|
|
18315
|
+
return fn
|
|
18316
|
+
|
|
18317
|
+
return decorate
|
|
18318
|
+
|
|
18319
|
+
|
|
18320
|
+
def _apply_adapter(name):
|
|
18321
|
+
fn = _ADAPTERS.pop(name, None)
|
|
18322
|
+
if fn is None:
|
|
18323
|
+
return
|
|
18324
|
+
try:
|
|
18325
|
+
fn()
|
|
18326
|
+
except Exception:
|
|
18327
|
+
# A stack we cannot adapt must not stop the import that triggered it.
|
|
18328
|
+
pass
|
|
18329
|
+
|
|
18330
|
+
|
|
18331
|
+
def _install_import_hook():
|
|
18332
|
+
real_import = builtins.__import__
|
|
18333
|
+
|
|
18334
|
+
def hooked(name, globals=None, locals=None, fromlist=(), level=0):
|
|
18335
|
+
module = real_import(name, globals, locals, fromlist, level)
|
|
18336
|
+
root = name.split(".")[0] if level == 0 else None
|
|
18337
|
+
if root in _ADAPTERS:
|
|
18338
|
+
_apply_adapter(root)
|
|
18339
|
+
return module
|
|
18340
|
+
|
|
18341
|
+
builtins.__import__ = hooked
|
|
18342
|
+
|
|
18343
|
+
|
|
18344
|
+
@_register_adapter("requests")
|
|
18345
|
+
def _adapt_requests():
|
|
18346
|
+
"""Replace the transport, not the API.
|
|
18347
|
+
|
|
18348
|
+
\`HTTPAdapter.send\` is the single seam every requests call passes through,
|
|
18349
|
+
below sessions, redirects, cookies and retries and above the urllib3
|
|
18350
|
+
transport that would otherwise reach the network on its own. Replacing it
|
|
18351
|
+
leaves everything callers actually use intact.
|
|
18352
|
+
"""
|
|
18353
|
+
import requests
|
|
18354
|
+
from requests.adapters import HTTPAdapter
|
|
18355
|
+
from requests.structures import CaseInsensitiveDict
|
|
18356
|
+
|
|
18357
|
+
native_send = HTTPAdapter.send
|
|
18358
|
+
|
|
18359
|
+
def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
|
|
18360
|
+
if not can_run_sync():
|
|
18361
|
+
# Without stack switching this transport cannot run at all. Falling
|
|
18362
|
+
# back to the library's own is better than breaking requests on
|
|
18363
|
+
# such a host \u2014 but the container's network policy is not the part
|
|
18364
|
+
# that degrades, so a forbidden host is still refused.
|
|
18365
|
+
if not _host.policy_allows(request.url):
|
|
18366
|
+
raise requests.exceptions.ConnectionError(
|
|
18367
|
+
"outbound network access is disabled for this container "
|
|
18368
|
+
"(enable with network: { allowOutbound: true })"
|
|
18369
|
+
)
|
|
18370
|
+
return native_send(self, request, stream, timeout, verify, cert, proxies)
|
|
18371
|
+
try:
|
|
18372
|
+
result = _raw_fetch(request.url, request.method, request.headers, request.body)
|
|
18373
|
+
except Exception as error:
|
|
18374
|
+
raise requests.exceptions.ConnectionError(str(error)) from None
|
|
18375
|
+
response = requests.Response()
|
|
18376
|
+
response.status_code = result["status"]
|
|
18377
|
+
response.reason = result["reason"]
|
|
18378
|
+
response.headers = CaseInsensitiveDict(result["headers"])
|
|
18379
|
+
response.url = result["url"]
|
|
18380
|
+
response.request = request
|
|
18381
|
+
response.raw = io.BytesIO(result["body"])
|
|
18382
|
+
response.encoding = requests.utils.get_encoding_from_headers(response.headers)
|
|
18383
|
+
return response
|
|
18384
|
+
|
|
18385
|
+
HTTPAdapter.send = send
|
|
18386
|
+
|
|
18387
|
+
|
|
18388
|
+
def _install_network():
|
|
18389
|
+
urllib.request.HTTPHandler = _SbxHTTPHandler
|
|
18390
|
+
urllib.request.HTTPSHandler = _SbxHTTPHandler
|
|
18391
|
+
urllib.request.install_opener(urllib.request.build_opener(_SbxHTTPHandler))
|
|
18392
|
+
_install_import_hook()
|
|
18393
|
+
for already_imported in [n for n in _ADAPTERS if n in sys.modules]:
|
|
18394
|
+
_apply_adapter(already_imported)
|
|
18395
|
+
|
|
18396
|
+
|
|
18397
|
+
_install_network()
|
|
18398
|
+
`;
|
|
18399
|
+
async function httpForPython(slot, url, method, headers, body) {
|
|
18400
|
+
const current = slot.current;
|
|
18401
|
+
if (!current) throw new Error("no program is bound");
|
|
18402
|
+
const result = await performRequest(current.ctx, new URL(url), {
|
|
18403
|
+
method,
|
|
18404
|
+
headers: Object.fromEntries(headers),
|
|
18405
|
+
...body ? { body } : {}
|
|
18406
|
+
});
|
|
18407
|
+
return {
|
|
18408
|
+
status: result.status,
|
|
18409
|
+
statusText: result.statusText,
|
|
18410
|
+
headers: Object.entries(result.headers),
|
|
18411
|
+
body: result.body,
|
|
18412
|
+
url: result.url
|
|
18413
|
+
};
|
|
18414
|
+
}
|
|
18415
|
+
async function resuming(py, slot, work) {
|
|
18416
|
+
const parent = slot.current;
|
|
18417
|
+
try {
|
|
18418
|
+
return await work();
|
|
18419
|
+
} finally {
|
|
18420
|
+
if (parent && slot.current !== parent) bindProgram(py, parent);
|
|
18421
|
+
}
|
|
18422
|
+
}
|
|
17910
18423
|
function installSyscallBridge(py) {
|
|
17911
|
-
const slot = py.
|
|
18424
|
+
const slot = py.__sbxSlot ??= { current: null };
|
|
18425
|
+
const children = /* @__PURE__ */ new Map();
|
|
18426
|
+
let lastChildId = 0;
|
|
17912
18427
|
py.registerJsModule("_sbx_host", {
|
|
17913
18428
|
/* Not `?.` and `??`: `buffered` returns undefined to mean "you will have
|
|
17914
18429
|
* to wait", and a nullish default would quietly turn that into end-of-file —
|
|
17915
18430
|
* which is exactly the silent EOF this bridge exists to remove. */
|
|
17916
|
-
buffered: (size) => slot.
|
|
17917
|
-
read: (size) => slot.
|
|
17918
|
-
isatty: () => slot.
|
|
18431
|
+
buffered: (size) => slot.current ? slot.current.stdin.buffered(size) : EMPTY,
|
|
18432
|
+
read: (size) => slot.current ? slot.current.stdin.read(size) : Promise.resolve(EMPTY),
|
|
18433
|
+
isatty: () => slot.current?.stdin.isatty() ?? false,
|
|
18434
|
+
proc_start: (argv, cwd, env2, merge) => {
|
|
18435
|
+
const current = slot.current;
|
|
18436
|
+
if (!current) throw new Error("no program is bound");
|
|
18437
|
+
const { ctx } = current;
|
|
18438
|
+
const stdin = new Pipe();
|
|
18439
|
+
stdin.interactive = true;
|
|
18440
|
+
const stdout = new Pipe();
|
|
18441
|
+
const stderr = merge ? stdout : new Pipe();
|
|
18442
|
+
const proc = ctx.kernel.spawn(argv, {
|
|
18443
|
+
cwd: cwd ?? ctx.cwd,
|
|
18444
|
+
env: env2 ?? ctx.env,
|
|
18445
|
+
cred: ctx.cred,
|
|
18446
|
+
stdin,
|
|
18447
|
+
stdout,
|
|
18448
|
+
stderr
|
|
18449
|
+
});
|
|
18450
|
+
void proc.wait().then(() => {
|
|
18451
|
+
stdout.end();
|
|
18452
|
+
if (stderr !== stdout) stderr.end();
|
|
18453
|
+
});
|
|
18454
|
+
const id = ++lastChildId;
|
|
18455
|
+
children.set(id, { proc, stdin, stdout, stderr });
|
|
18456
|
+
return id;
|
|
18457
|
+
},
|
|
18458
|
+
proc_read: (id, which2, size) => {
|
|
18459
|
+
const child = children.get(id);
|
|
18460
|
+
if (!child) return Promise.resolve(EMPTY);
|
|
18461
|
+
const pipe = which2 === 2 ? child.stderr : child.stdout;
|
|
18462
|
+
return resuming(py, slot, async () => await pipe.read(size) ?? EMPTY);
|
|
18463
|
+
},
|
|
18464
|
+
proc_write: (id, text2) => {
|
|
18465
|
+
children.get(id)?.stdin.write(text2);
|
|
18466
|
+
},
|
|
18467
|
+
proc_close_stdin: (id) => {
|
|
18468
|
+
children.get(id)?.stdin.end();
|
|
18469
|
+
},
|
|
18470
|
+
/* Non-blocking on purpose: `poll()` must be able to say "still running". */
|
|
18471
|
+
proc_poll: (id) => children.get(id)?.proc.exitCode ?? void 0,
|
|
18472
|
+
proc_pid: (id) => children.get(id)?.proc.pid ?? 0,
|
|
18473
|
+
proc_wait: (id) => {
|
|
18474
|
+
const child = children.get(id);
|
|
18475
|
+
if (!child) return Promise.resolve(0);
|
|
18476
|
+
return resuming(py, slot, () => child.proc.wait());
|
|
18477
|
+
},
|
|
18478
|
+
proc_kill: (id, signal) => {
|
|
18479
|
+
const child = children.get(id);
|
|
18480
|
+
if (!child) return;
|
|
18481
|
+
const current = slot.current;
|
|
18482
|
+
current?.ctx.kernel.procs.signal(child.proc.pid, signal);
|
|
18483
|
+
},
|
|
18484
|
+
proc_release: (id) => {
|
|
18485
|
+
children.delete(id);
|
|
18486
|
+
},
|
|
18487
|
+
http: (url, method, headers, body) => resuming(py, slot, () => httpForPython(slot, url, method, headers, body)),
|
|
18488
|
+
/* So Python can enforce the container's policy even on a host where it
|
|
18489
|
+
* cannot route the request itself. */
|
|
18490
|
+
policy_allows: (url) => {
|
|
18491
|
+
const current = slot.current;
|
|
18492
|
+
if (!current) return false;
|
|
18493
|
+
const net = current.ctx.kernel.net;
|
|
18494
|
+
try {
|
|
18495
|
+
return net.isLocal(new URL(url).hostname) || net.outboundAllowed(url);
|
|
18496
|
+
} catch {
|
|
18497
|
+
return false;
|
|
18498
|
+
}
|
|
18499
|
+
}
|
|
17919
18500
|
});
|
|
17920
18501
|
py.runPython(BRIDGE_PY);
|
|
18502
|
+
py.runPython(PROCESS_PY);
|
|
18503
|
+
py.runPython(NETWORK_PY);
|
|
18504
|
+
py.runPython(ASYNCIO_PY);
|
|
17921
18505
|
}
|
|
17922
|
-
function
|
|
17923
|
-
py.
|
|
18506
|
+
function bindProgram(py, binding) {
|
|
18507
|
+
py.__sbxSlot.current = binding;
|
|
18508
|
+
const { ctx, stdin } = binding;
|
|
18509
|
+
const decoder8 = new TextDecoder();
|
|
18510
|
+
py.setStdout({
|
|
18511
|
+
write: (buffer) => (ctx.write(decoder8.decode(buffer)), buffer.length)
|
|
18512
|
+
});
|
|
18513
|
+
py.setStderr({
|
|
18514
|
+
write: (buffer) => (ctx.stderr.write(decoder8.decode(buffer)), buffer.length)
|
|
18515
|
+
});
|
|
18516
|
+
py.setStdin({
|
|
18517
|
+
read: (buffer) => {
|
|
18518
|
+
const ready = stdin.buffered(buffer.length);
|
|
18519
|
+
if (!ready || ready.length === 0) return 0;
|
|
18520
|
+
buffer.set(ready);
|
|
18521
|
+
return ready.length;
|
|
18522
|
+
}
|
|
18523
|
+
});
|
|
17924
18524
|
}
|
|
17925
18525
|
|
|
17926
18526
|
// src/runtime/cpython.ts
|
|
@@ -18111,30 +18711,9 @@ async function runCPythonProgram(ctx, source, argv, scriptDir = null) {
|
|
|
18111
18711
|
});
|
|
18112
18712
|
} catch {
|
|
18113
18713
|
}
|
|
18114
|
-
const decoder8 = new TextDecoder();
|
|
18115
|
-
py.setStdout({
|
|
18116
|
-
write: (buffer) => {
|
|
18117
|
-
ctx.write(decoder8.decode(buffer));
|
|
18118
|
-
return buffer.length;
|
|
18119
|
-
}
|
|
18120
|
-
});
|
|
18121
|
-
py.setStderr({
|
|
18122
|
-
write: (buffer) => {
|
|
18123
|
-
ctx.stderr.write(decoder8.decode(buffer));
|
|
18124
|
-
return buffer.length;
|
|
18125
|
-
}
|
|
18126
|
-
});
|
|
18127
18714
|
const stdinHost = createStdinHost(ctx);
|
|
18128
18715
|
await stdinHost.prime();
|
|
18129
|
-
|
|
18130
|
-
py.setStdin({
|
|
18131
|
-
read: (buffer) => {
|
|
18132
|
-
const ready = stdinHost.buffered(buffer.length);
|
|
18133
|
-
if (!ready || ready.length === 0) return 0;
|
|
18134
|
-
buffer.set(ready);
|
|
18135
|
-
return ready.length;
|
|
18136
|
-
}
|
|
18137
|
-
});
|
|
18716
|
+
bindProgram(py, { ctx, stdin: stdinHost });
|
|
18138
18717
|
try {
|
|
18139
18718
|
bootstrap(py, ctx, argv, scriptDir);
|
|
18140
18719
|
} catch (error) {
|
|
@@ -18173,9 +18752,7 @@ async function runCPythonRepl(ctx) {
|
|
|
18173
18752
|
}
|
|
18174
18753
|
mountContainerDirs(py, ctx);
|
|
18175
18754
|
bootstrap(py, ctx, [""], null);
|
|
18176
|
-
|
|
18177
|
-
py.setStdout({ write: (data) => (ctx.write(decoder8.decode(data)), data.length) });
|
|
18178
|
-
py.setStderr({ write: (data) => (ctx.stderr.write(decoder8.decode(data)), data.length) });
|
|
18755
|
+
bindProgram(py, { ctx, stdin: createStdinHost(ctx) });
|
|
18179
18756
|
ctx.line(`Python ${String(py.runPython("import sys; sys.version.split()[0]"))} (Pyodide)`);
|
|
18180
18757
|
ctx.line('Type "help()" for more information.');
|
|
18181
18758
|
let source = "";
|
|
@@ -18198,6 +18775,67 @@ async function runCPythonRepl(ctx) {
|
|
|
18198
18775
|
}
|
|
18199
18776
|
return 0;
|
|
18200
18777
|
}
|
|
18778
|
+
var PIP_PY = `
|
|
18779
|
+
import json, re
|
|
18780
|
+
|
|
18781
|
+
import micropip
|
|
18782
|
+
|
|
18783
|
+
_MISSING = re.compile(r"Can't find a pure Python 3 wheel for '([^']+)'")
|
|
18784
|
+
_EXTRAS = re.compile(r"^\\s*([A-Za-z0-9._-]+)\\s*\\[([^\\]]+)\\](.*)$")
|
|
18785
|
+
|
|
18786
|
+
|
|
18787
|
+
def _name_of(spec):
|
|
18788
|
+
return re.split(r"[\\[<>=!~;\\s]", spec.strip(), 1)[0]
|
|
18789
|
+
|
|
18790
|
+
|
|
18791
|
+
def _installed_version(name):
|
|
18792
|
+
try:
|
|
18793
|
+
found = micropip.list()[name.replace("_", "-").lower()]
|
|
18794
|
+
return getattr(found, "version", None)
|
|
18795
|
+
except Exception:
|
|
18796
|
+
return None
|
|
18797
|
+
|
|
18798
|
+
|
|
18799
|
+
async def _install_one(spec):
|
|
18800
|
+
"""Install one requirement, retrying without extras if they are impossible."""
|
|
18801
|
+
attempt = spec
|
|
18802
|
+
dropped = None
|
|
18803
|
+
while True:
|
|
18804
|
+
try:
|
|
18805
|
+
await micropip.install(attempt)
|
|
18806
|
+
except ValueError as error:
|
|
18807
|
+
message = str(error)
|
|
18808
|
+
missing = _MISSING.findall(message)
|
|
18809
|
+
extras = _EXTRAS.match(attempt)
|
|
18810
|
+
if missing and extras and dropped is None:
|
|
18811
|
+
dropped = [e.strip() for e in extras.group(2).split(",")]
|
|
18812
|
+
attempt = extras.group(1) + extras.group(3)
|
|
18813
|
+
continue
|
|
18814
|
+
return {
|
|
18815
|
+
"spec": spec, "ok": False, "missing": missing,
|
|
18816
|
+
"reason": message.strip().split("\\n")[0],
|
|
18817
|
+
}
|
|
18818
|
+
except Exception as error:
|
|
18819
|
+
return {"spec": spec, "ok": False, "missing": [], "reason": str(error).strip().split("\\n")[0]}
|
|
18820
|
+
|
|
18821
|
+
name = _name_of(attempt)
|
|
18822
|
+
return {
|
|
18823
|
+
"spec": spec, "ok": True, "name": name,
|
|
18824
|
+
"version": _installed_version(name), "dropped": dropped,
|
|
18825
|
+
}
|
|
18826
|
+
|
|
18827
|
+
|
|
18828
|
+
async def __sbx_pip(specs_json):
|
|
18829
|
+
results = []
|
|
18830
|
+
for spec in json.loads(specs_json):
|
|
18831
|
+
try:
|
|
18832
|
+
results.append(await _install_one(spec))
|
|
18833
|
+
except Exception as error:
|
|
18834
|
+
results.append({"spec": spec, "ok": False, "missing": [], "reason": str(error)})
|
|
18835
|
+
return json.dumps(results)
|
|
18836
|
+
|
|
18837
|
+
__sbx_pip
|
|
18838
|
+
`;
|
|
18201
18839
|
var micropip = defineCommand({
|
|
18202
18840
|
name: "micropip",
|
|
18203
18841
|
path: "/usr/bin/micropip",
|
|
@@ -18235,6 +18873,12 @@ var micropip = defineCommand({
|
|
|
18235
18873
|
ctx.stderr.write("pip: no packages specified\n");
|
|
18236
18874
|
return 1;
|
|
18237
18875
|
}
|
|
18876
|
+
if (!ctx.kernel.net.outboundAllowed("https://pypi.org")) {
|
|
18877
|
+
ctx.stderr.write(
|
|
18878
|
+
"pip: outbound network access is disabled for this container (enable with network: { allowOutbound: true })\n"
|
|
18879
|
+
);
|
|
18880
|
+
return 1;
|
|
18881
|
+
}
|
|
18238
18882
|
let py;
|
|
18239
18883
|
try {
|
|
18240
18884
|
py = await interpreterFor(ctx);
|
|
@@ -18242,22 +18886,42 @@ var micropip = defineCommand({
|
|
|
18242
18886
|
ctx.stderr.write("pip: Pyodide is unavailable in this host\n");
|
|
18243
18887
|
return 127;
|
|
18244
18888
|
}
|
|
18889
|
+
let outcomes;
|
|
18245
18890
|
try {
|
|
18246
18891
|
await py.loadPackage("micropip");
|
|
18247
|
-
const
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
await micropipModule.install(name);
|
|
18251
|
-
ctx.line(`Successfully installed ${name}`);
|
|
18252
|
-
}
|
|
18253
|
-
return 0;
|
|
18892
|
+
for (const name of packages) ctx.line(`Collecting ${name}`);
|
|
18893
|
+
const install2 = py.runPython(PIP_PY);
|
|
18894
|
+
outcomes = JSON.parse(String(await install2(JSON.stringify(packages))));
|
|
18254
18895
|
} catch (error) {
|
|
18255
18896
|
ctx.stderr.write(
|
|
18256
|
-
`
|
|
18897
|
+
`pip: installation failed: ${error instanceof Error ? error.message : String(error)}
|
|
18257
18898
|
`
|
|
18258
18899
|
);
|
|
18259
18900
|
return 1;
|
|
18260
18901
|
}
|
|
18902
|
+
const installed2 = [];
|
|
18903
|
+
for (const outcome of outcomes) {
|
|
18904
|
+
if (outcome.ok) {
|
|
18905
|
+
installed2.push(
|
|
18906
|
+
outcome.version ? `${outcome.name}-${outcome.version}` : String(outcome.name)
|
|
18907
|
+
);
|
|
18908
|
+
if (outcome.dropped?.length) {
|
|
18909
|
+
ctx.stderr.write(
|
|
18910
|
+
` WARNING: ${outcome.name}: the ${outcome.dropped.map((extra) => `'${extra}'`).join(", ")} extra needs native code with no WebAssembly build; installed ${outcome.name} without it
|
|
18911
|
+
`
|
|
18912
|
+
);
|
|
18913
|
+
}
|
|
18914
|
+
continue;
|
|
18915
|
+
}
|
|
18916
|
+
const missing = outcome.missing ?? [];
|
|
18917
|
+
ctx.stderr.write(
|
|
18918
|
+
missing.length > 0 ? `ERROR: could not install ${outcome.spec}: no WebAssembly build exists for ${missing.map((name) => name.split(/[<>=!~;\s]/)[0]).join(", ")}
|
|
18919
|
+
` : `ERROR: could not install ${outcome.spec}: ${outcome.reason ?? "unknown error"}
|
|
18920
|
+
`
|
|
18921
|
+
);
|
|
18922
|
+
}
|
|
18923
|
+
if (installed2.length > 0) ctx.line(`Successfully installed ${installed2.join(" ")}`);
|
|
18924
|
+
return outcomes.every((outcome) => outcome.ok) ? 0 : 1;
|
|
18261
18925
|
}
|
|
18262
18926
|
});
|
|
18263
18927
|
|