UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

2 lines • 33.2 kB
export default "import base64\nimport errno\nimport glob\nimport importlib\nimport io\nimport json\nimport logging\nimport operator\nimport os\nimport shutil\nimport subprocess\nimport sys\nimport traceback\nimport types\nimport warnings\nfrom contextlib import contextmanager\n\nimport _mirage_exit\nimport _mirage_process\nimport _mirage_stdio\nimport _mirage_xattr\nfrom pyodide.ffi import to_js\n\n_process_active = False\n_inherited_inputs = {}\n_process_stdio = None\n_exit_status = None\n# The most of the run's input one read asks the host for, and the most of\n# its output one write hands over: the bridge's own buffer.\nSTDIO_CHUNK = 64 * 1024\n\nlogger = logging.getLogger(__name__)\n_FLAG_FIELDS = tuple(\n name\n for name, field in vars(type(sys.flags)).items()\n if isinstance(field, types.MemberDescriptorType)\n)\n_FLAG_INDEX = {name: index for index, name in enumerate(_FLAG_FIELDS)}\n\n\nclass ProgramExit(BaseException):\n \"\"\"``os._exit`` inside a run.\n\n Emscripten's exit unwinds the C stack past every Python frame, so a\n run that let it through would never restore what it swapped in. A run\n swaps ``os._exit`` for :func:`program_exit` and ends with the code\n this carries.\n\n Python cannot raise an exception a guest is unable to catch, so what\n a real exit guarantees is kept another way: once a guest calls it,\n nothing it writes reaches the mount or its output, and the run ends\n with this code whatever the guest does after.\n\n Args:\n code (int): the exit status.\n \"\"\"\n\n def __init__(self, code):\n super().__init__(code)\n self.code = code\n\n\ndef program_exit(code):\n \"\"\"End the run with ``code``, as ``os._exit`` ends a process.\n\n Args:\n code (int): the exit status.\n \"\"\"\n global _exit_status\n _exit_status = operator.index(code)\n _mirage_exit.seal()\n raise ProgramExit(_exit_status)\n\n\ndef _process_call(op, **params):\n global _process_active\n result = json.loads(_mirage_process.run(json.dumps(dict(op=op, **params))))\n if \"error\" in result:\n raise OSError(\n getattr(errno, result[\"code\"], errno.EIO), result[\"error\"]\n )\n for pid, count in result.get(\"consumed\", {}).items():\n inherited = _inherited_inputs.get(int(pid))\n if inherited is not None:\n stream, offset = inherited\n if not stream.closed:\n stream.skip_to(offset + count)\n if op == \"spawn\":\n _process_active = True\n elif op == \"finish\":\n _process_active = False\n _inherited_inputs.clear()\n for stream in (\"stdout\", \"stderr\"):\n data = base64.b64decode(result.get(\"inherited_\" + stream, \"\"))\n if data:\n target = _process_stdio[1 if stream == \"stdout\" else 2]\n target.buffer.write(data)\n return result\n\n\nclass ProcessPipe(io.RawIOBase):\n def __init__(self, pid, stream):\n super().__init__()\n self.pid = pid\n self.stream = stream\n self.pending = bytearray()\n self.eof = False\n\n def readable(self):\n return self.stream != \"stdin\"\n\n def writable(self):\n return self.stream == \"stdin\"\n\n def readinto(self, target):\n self._checkClosed()\n if not self.readable():\n raise io.UnsupportedOperation(\"not readable\")\n if not target:\n return 0\n if not self.pending and not self.eof:\n result = _process_call(\"read\", pid=self.pid, stream=self.stream)\n self.pending.extend(base64.b64decode(result[\"data\"]))\n self.eof = not self.pending\n size = min(len(target), len(self.pending))\n target[:size] = self.pending[:size]\n del self.pending[:size]\n return size\n\n def write(self, data):\n self._checkClosed()\n if not self.writable():\n raise io.UnsupportedOperation(\"not writable\")\n data = bytes(data)\n _process_call(\n \"write\", pid=self.pid, data=base64.b64encode(data).decode(\"ascii\")\n )\n return len(data)\n\n def close(self):\n if not self.closed and _process_active:\n if self.writable():\n _process_call(\"close\", pid=self.pid)\n else:\n _process_call(\"close\", pid=self.pid, stream=self.stream)\n super().close()\n\n\nclass MiragePopen:\n \"\"\"The subprocess protocol backed by invocation-owned Mirage processes.\"\"\"\n\n def __init__(\n self,\n args,\n bufsize=-1,\n executable=None,\n stdin=None,\n stdout=None,\n stderr=None,\n preexec_fn=None,\n close_fds=True,\n shell=False,\n cwd=None,\n env=None,\n universal_newlines=None,\n startupinfo=None,\n creationflags=0,\n restore_signals=True,\n start_new_session=False,\n pass_fds=(),\n *,\n user=None,\n group=None,\n extra_groups=None,\n encoding=None,\n errors=None,\n text=None,\n umask=-1,\n pipesize=-1,\n process_group=None,\n ):\n if (\n preexec_fn is not None\n or not close_fds\n or startupinfo is not None\n or creationflags\n or not restore_signals\n or start_new_session\n or pass_fds\n or user is not None\n or group is not None\n or extra_groups is not None\n or umask != -1\n or pipesize != -1\n or process_group not in (None, -1)\n ):\n raise NotImplementedError(\n \"native process attributes and descriptors are unsupported\"\n )\n if not isinstance(bufsize, int):\n raise TypeError(\"bufsize must be an integer\")\n if (\n text is not None\n and universal_newlines is not None\n and bool(text) != bool(universal_newlines)\n ):\n raise subprocess.SubprocessError(\n \"Cannot disambiguate when both text and universal_newlines \"\n \"are supplied but different. Pass one or the other.\"\n )\n if stdin not in (None, subprocess.PIPE, subprocess.DEVNULL):\n raise NotImplementedError(\n \"stdin supports inheritance, PIPE and DEVNULL\"\n )\n if stdout not in (None, subprocess.PIPE, subprocess.DEVNULL):\n raise NotImplementedError(\n \"stdout supports inheritance, PIPE and DEVNULL\"\n )\n if stderr not in (\n None,\n subprocess.PIPE,\n subprocess.DEVNULL,\n subprocess.STDOUT,\n ):\n raise NotImplementedError(\n \"stderr supports inheritance, PIPE, DEVNULL and STDOUT\"\n )\n self.args = args\n if isinstance(args, (str, bytes, os.PathLike)):\n argv = [os.fsdecode(args)]\n else:\n argv = [os.fsdecode(arg) for arg in args]\n if not argv:\n raise ValueError(\"args must not be empty\")\n if shell:\n argv = [\n os.fsdecode(executable) if executable else \"/usr/bin/sh\",\n \"-c\",\n *argv,\n ]\n elif executable is not None:\n raise NotImplementedError(\n \"separate executable and argv[0] are unsupported\"\n )\n if any(\"\\0\" in arg for arg in argv):\n raise ValueError(\"embedded null byte\")\n environment = {\n os.fsdecode(k): os.fsdecode(v)\n for k, v in (os.environ if env is None else env).items()\n }\n if any(\n \"=\" in k or \"\\0\" in k or \"\\0\" in v for k, v in environment.items()\n ):\n raise ValueError(\"illegal environment variable\")\n directory = (\n os.getcwd() if cwd is None else os.path.abspath(os.fsdecode(cwd))\n )\n if not os.path.isdir(directory):\n if os.path.exists(directory):\n raise NotADirectoryError(\n errno.ENOTDIR, \"Not a directory\", directory\n )\n raise FileNotFoundError(\n errno.ENOENT, \"No such file or directory\", directory\n )\n self.encoding, self.errors = encoding or \"utf-8\", errors or \"strict\"\n self.text_mode = bool(text or universal_newlines or encoding or errors)\n self.returncode = None\n self._communication_started = False\n self._output = None\n self._raw_pipes = {}\n self._input_mode = stdin\n self._output_mode = stdout\n self._error_mode = stderr\n inherited = b\"\"\n source = None\n if stdin is None:\n source = _process_stdio[0].buffer\n offset = source.taken\n inherited = source.rest()\n result = _process_call(\n \"spawn\",\n argv=argv,\n cwd=directory,\n env=environment,\n stdin=stdin or 0,\n stdout=stdout or 0,\n stderr=stderr or 0,\n data=base64.b64encode(inherited).decode(\"ascii\"),\n )\n self.pid = result[\"pid\"]\n self._released = False\n if source is not None and inherited:\n _inherited_inputs[self.pid] = (source, offset)\n self.stdin = (\n self._pipe(\"stdin\", bufsize) if stdin == subprocess.PIPE else None\n )\n self.stdout = (\n self._pipe(\"stdout\", bufsize)\n if stdout == subprocess.PIPE\n else None\n )\n self.stderr = (\n self._pipe(\"stderr\", bufsize)\n if stderr == subprocess.PIPE\n else None\n )\n\n def _pipe(self, name, bufsize):\n raw = ProcessPipe(self.pid, name)\n self._raw_pipes[name] = raw\n stream = raw\n if bufsize != 0:\n size = io.DEFAULT_BUFFER_SIZE if bufsize < 2 else bufsize\n stream = (\n io.BufferedWriter(raw, size)\n if name == \"stdin\"\n else io.BufferedReader(raw, size)\n )\n if self.text_mode:\n stream = io.TextIOWrapper(\n stream,\n encoding=self.encoding,\n errors=self.errors,\n write_through=True,\n line_buffering=bufsize == 1,\n )\n return stream\n\n def poll(self):\n if self.returncode is not None:\n return self.returncode\n self.returncode = _process_call(\"poll\", pid=self.pid)[\"returncode\"]\n return self.returncode\n\n def wait(self, timeout=None):\n if self.returncode is not None:\n return self.returncode\n result = _process_call(\"wait\", pid=self.pid, timeout=timeout)\n self.returncode = result[\"returncode\"]\n if result[\"timeout\"]:\n raise subprocess.TimeoutExpired(self.args, timeout)\n return self.returncode\n\n def communicate(self, input=None, timeout=None):\n if self._communication_started and input is not None:\n raise ValueError(\"Cannot send input after starting communication\")\n if self._output is not None:\n return self._output\n if input is not None and self.stdin is None:\n raise ValueError(\"Cannot send input when stdin is not PIPE\")\n params = dict(pid=self.pid, timeout=timeout)\n if not self._communication_started:\n if self.stdin is not None and not self.stdin.closed:\n try:\n self.stdin.flush()\n except BrokenPipeError:\n pass\n if input is not None:\n raw = (\n input.encode(self.encoding, self.errors)\n if self.text_mode\n else memoryview(input).tobytes()\n )\n params[\"data\"] = base64.b64encode(raw).decode(\"ascii\")\n self._communication_started = True\n result = _process_call(\"communicate\", **params)\n out = (\n base64.b64decode(result[\"stdout\"])\n if self.stdout is not None\n else None\n )\n err = (\n base64.b64decode(result[\"stderr\"])\n if self.stderr is not None\n else None\n )\n self.returncode = result[\"returncode\"]\n if result[\"timeout\"]:\n raise subprocess.TimeoutExpired(\n self.args, timeout, output=out, stderr=err\n )\n outputs = []\n for name, data in ((\"stdout\", out), (\"stderr\", err)):\n if data is None:\n outputs.append(None)\n else:\n raw = self._raw_pipes[name]\n raw.pending.extend(data)\n raw.eof = True\n outputs.append(getattr(self, name).read())\n for stream in (self.stdin, self.stdout, self.stderr):\n if stream is not None and not stream.closed:\n stream.close()\n self._output = tuple(outputs)\n self._release()\n return self._output\n\n def send_signal(self, signal):\n if signal not in (9, 15):\n raise NotImplementedError(\"only termination signals are supported\")\n if self.returncode is None:\n _process_call(\"kill\", pid=self.pid, signal=int(signal))\n\n def terminate(self):\n self.send_signal(15)\n\n def kill(self):\n self.send_signal(9)\n\n def _release(self):\n if not self._released:\n _process_call(\"release\", pid=self.pid)\n _inherited_inputs.pop(self.pid, None)\n self._released = True\n\n def __enter__(self):\n return self\n\n def __exit__(self, *args):\n try:\n for stream in (self.stdout, self.stderr, self.stdin):\n if stream is not None and not stream.closed:\n stream.close()\n finally:\n self.wait()\n self._release()\n\n\ndef process_which(cmd, mode=os.F_OK | os.X_OK, path=None):\n if mode != os.F_OK | os.X_OK:\n raise NotImplementedError(\"which supports executable lookup only\")\n result = _process_call(\n \"resolve\",\n argv=[os.fsdecode(cmd)],\n cwd=os.getcwd(),\n env={\n \"PATH\": os.fsdecode(path)\n if path is not None\n else os.environ.get(\"PATH\", \"/usr/bin\")\n },\n )\n found = result[\"path\"]\n return (\n os.fsencode(found)\n if isinstance(cmd, bytes) and found is not None\n else found\n )\n\n\nsubprocess.Popen = MiragePopen\nshutil.which = process_which\n\n\nclass PipeInput(io.BufferedIOBase):\n \"\"\"The run's input, as a pipe hands it over: the host sends each\n piece as the guest reads it, so a writer that never ends holds back\n only a read that waits on it. It is the buffer itself, so what the\n guest has read is exactly ``taken``, and a child that inherits the\n input gets every byte after it.\"\"\"\n\n def __init__(self):\n super().__init__()\n self.held = bytearray()\n self.taken = 0\n self.ended = False\n\n def readable(self):\n return True\n\n def _pull(self):\n # Numbers, not a buffer: Pyodide's buffer conversion slices with\n # signed wasm32 addresses, so above 2 GiB it reads the wrong bytes.\n data = bytes(_mirage_stdio.read(STDIO_CHUNK).to_py())\n if data:\n self.held.extend(data)\n else:\n self.ended = True\n\n def _take(self, size):\n data = bytes(self.held[:size])\n del self.held[:size]\n self.taken += len(data)\n return data\n\n def read(self, size=-1):\n self._checkClosed()\n if size is None or size < 0:\n while not self.ended:\n self._pull()\n return self._take(len(self.held))\n while len(self.held) < size and not self.ended:\n self._pull()\n return self._take(size)\n\n def read1(self, size=-1):\n self._checkClosed()\n if size == 0:\n return b\"\"\n if not self.held and not self.ended:\n self._pull()\n return self._take(len(self.held) if size is None or size < 0 else size)\n\n def peek(self, size=0):\n self._checkClosed()\n if not self.held and not self.ended:\n self._pull()\n return bytes(self.held)\n\n def readinto(self, target):\n # Any writable buffer, by its bytes, as BufferedReader takes one.\n view = memoryview(target).cast(\"B\")\n data = self.read(len(view))\n view[: len(data)] = data\n return len(data)\n\n def readinto1(self, target):\n view = memoryview(target).cast(\"B\")\n data = self.read1(len(view))\n view[: len(data)] = data\n return len(data)\n\n def rest(self):\n \"\"\"The input left, read to its end and kept for the guest's own\n reads.\"\"\"\n while not self.ended:\n self._pull()\n return bytes(self.held)\n\n def skip_to(self, taken):\n \"\"\"Drop what a child read, up to ``taken`` bytes into the input.\n\n Args:\n taken (int): how far into the input the child has read.\n \"\"\"\n drop = min(len(self.held), max(0, taken - self.taken))\n del self.held[:drop]\n self.taken += drop\n\n\nclass OutputCapture(io.RawIOBase):\n \"\"\"One of the run's output streams, each write handed to the host as\n it is made. As CPython's to a pipe, stdout is written a block at a\n time and stderr a line at a time; text goes to the block at once, so\n it keeps its order with what the guest writes to ``buffer``. Under\n ``-u`` the text sits on the stream itself, as CPython's does, so\n every write goes out as it is made.\n\n Args:\n fd (int): 1 for stdout, 2 for stderr.\n unbuffered (bool): the run's ``-u``.\n \"\"\"\n\n def __init__(self, fd, unbuffered=False):\n super().__init__()\n self.fd = fd\n self.text = io.TextIOWrapper(\n self if unbuffered else io.BufferedWriter(self),\n encoding=\"utf-8\",\n errors=\"replace\",\n write_through=True,\n line_buffering=fd == 2,\n )\n\n def writable(self):\n return True\n\n def write(self, data):\n self._checkClosed()\n # A count of bytes, whatever the buffer's item size.\n payload = bytes(data)\n self._send(payload)\n return len(payload)\n\n def _send(self, data):\n if _exit_status is not None:\n return\n for start in range(0, len(data), STDIO_CHUNK):\n # Numbers, not a buffer, for the reason PipeInput reads them.\n piece = to_js(list(data[start : start + STDIO_CHUNK]))\n if _mirage_stdio.write(self.fd, piece) < 0:\n raise BrokenPipeError(errno.EPIPE, os.strerror(errno.EPIPE))\n\n def diagnostic(self, text):\n # Host diagnostics remain available even after the guest closes stderr.\n if self.text.buffer is not None and not self.text.closed:\n self.text.flush()\n self._send(text.encode(\"utf-8\", errors=\"replace\"))\n\n def finish(self):\n \"\"\"Flush what the guest left buffered, as CPython's exit does.\"\"\"\n if self.text.buffer is not None and not self.text.closed:\n self.text.flush()\n\n def __exit__(self, *exc):\n try:\n # A guest may reconfigure buffering, close, or detach the wrapper.\n if self.text.buffer is not None and not self.text.closed:\n self.text.close()\n except BrokenPipeError as error:\n # Its reader went away; what the run's exit had to say about it\n # was said by finish.\n logger.debug(\"guest stream %d closed: %r\", self.fd, error)\n finally:\n self.close()\n\n\nclass InvocationFlags(tuple):\n \"\"\"Read-only sys.flags view for switches implemented by a warm guest.\"\"\"\n\n def __new__(cls, original, **overrides):\n fields = _FLAG_FIELDS[: original.n_sequence_fields]\n values = (\n overrides.get(name, getattr(original, name)) for name in fields\n )\n instance = super().__new__(cls, values)\n object.__setattr__(instance, \"_original\", original)\n return instance\n\n def __getattr__(self, name):\n index = _FLAG_INDEX.get(name)\n if index is not None and index < len(self):\n return self[index]\n return getattr(self._original, name)\n\n def __setattr__(self, name, value):\n raise AttributeError(\"readonly attribute\")\n\n def __delattr__(self, name):\n raise AttributeError(\"readonly attribute\")\n\n def __dir__(self):\n return sorted(set(super().__dir__()) | set(dir(self._original)))\n\n def __repr__(self):\n fields = _FLAG_FIELDS[: self.n_sequence_fields]\n values = \", \".join(\n f\"{name}={getattr(self, name)!r}\" for name in fields\n )\n return f\"sys.flags({values})\"\n\n\n@contextmanager\ndef interpreter_state(flags, filename, script, stderr):\n \"\"\"Apply and restore the interpreter switches this guest implements.\n\n Args:\n flags (dict): Parsed initialization switches.\n filename (str | None): The program's display path.\n script (bool): Whether the program was read from a script.\n stderr (OutputCapture): Destination for invalid warning options.\n \"\"\"\n saved_flags = sys.flags\n saved_path = sys.path\n saved_entries = saved_path[:]\n saved_dwb = sys.dont_write_bytecode\n saved_xop = sys._xoptions\n saved_options = saved_xop.copy()\n saved_filters = warnings.filters\n saved_filter_entries = saved_filters[:]\n optimize = min(int(flags.get(\"O\") or 0), 2)\n loader = importlib._bootstrap_external.SourceLoader\n saved_source_to_code = loader.source_to_code\n\n # importlib compiles an imported module at the level the interpreter\n # started with, and names its cached bytecode after sys.flags, so -O\n # has to reach the compile as well for the two to agree.\n def source_to_code(self, data, path, *, _optimize=-1):\n return saved_source_to_code(\n self,\n data,\n path,\n _optimize=optimize if _optimize == -1 else _optimize,\n )\n\n try:\n sys.dont_write_bytecode = bool(flags.get(\"B\") or saved_dwb)\n sys.flags = InvocationFlags(\n saved_flags,\n optimize=optimize,\n dont_write_bytecode=int(sys.dont_write_bytecode),\n safe_path=bool(flags.get(\"P\")),\n )\n for xopt in flags.get(\"X\") or []:\n name, _, value = str(xopt).partition(\"=\")\n sys._xoptions[name] = value if value else True\n for spec in flags.get(\"W\") or []:\n try:\n warnings._setoption(str(spec))\n except warnings._OptionError as error:\n stderr.diagnostic(f\"Invalid -W option ignored: {error}\\n\")\n if \"\" in sys.path:\n sys.path.remove(\"\")\n if not sys.flags.safe_path:\n sys.path.insert(\n 0,\n os.path.dirname(os.path.realpath(filename)) if script else \"\",\n )\n if optimize:\n loader.source_to_code = source_to_code\n yield optimize\n finally:\n loader.source_to_code = saved_source_to_code\n sys.flags = saved_flags\n sys.path = saved_path\n saved_path[:] = saved_entries\n sys.dont_write_bytecode = saved_dwb\n sys._xoptions = saved_xop\n saved_xop.clear()\n saved_xop.update(saved_options)\n warnings.filters = saved_filters\n saved_filters[:] = saved_filter_entries\n warnings._filters_mutated()\n\n\ndef run(request, arm_interrupt, disarm_interrupt):\n global _process_stdio, _process_active, _exit_status\n _exit_status = None\n user_code = request[\"code\"]\n init_flags = request[\"flags\"]\n argv = request[\"argv\"]\n cwd = request[\"cwd\"]\n filename = request[\"filename\"]\n script = request[\"script\"]\n script_cli = request[\"script_cli\"]\n merged_env = request[\"env\"]\n piped = request[\"piped\"]\n saved_getcwd = os.getcwd\n saved_cwd = saved_getcwd()\n saved_chdir = os.chdir\n saved_exit = os._exit\n saved_env = dict(os.environ)\n saved_stdin = sys.stdin\n saved_stdout = sys.stdout\n saved_stderr = sys.stderr\n saved_executable = sys.executable\n saved_argv = sys.argv\n had_main = \"__main__\" in sys.modules\n saved_main = sys.modules.get(\"__main__\")\n\n flags = dict(init_flags) if init_flags is not None else {}\n unbuffered = bool(flags.get(\"u\"))\n\n out_bytes = OutputCapture(1, unbuffered)\n err_bytes = OutputCapture(2, unbuffered)\n out_text = out_bytes.text\n err_text = err_bytes.text\n\n stdin_pipe = PipeInput()\n stdin_text = io.TextIOWrapper(\n stdin_pipe, encoding=\"utf-8\", errors=\"replace\"\n )\n\n with out_bytes, err_bytes:\n exit_code = 0\n try:\n sys.executable = \"/usr/bin/python3\"\n os.environ.clear()\n os.environ.update(merged_env)\n with interpreter_state(\n flags, filename, script, err_bytes\n ) as optimize:\n sys.stdin = stdin_text\n sys.stdout = out_text\n sys.stderr = err_text\n _process_stdio = (stdin_text, out_text, err_text)\n sys.argv = list(argv)\n main_module = types.ModuleType(\"__main__\")\n user_globals = main_module.__dict__\n user_globals[\"__annotations__\"] = {}\n # What CPython's file adapter binds, for a script and for\n # stdin.\n if filename is not None:\n user_globals[\"__file__\"] = filename\n user_globals[\"__cached__\"] = None\n sys.modules[\"__main__\"] = main_module\n if script_cli:\n user_globals.update(\n argv=list(argv),\n stdin=stdin_pipe.rest() if piped else None,\n )\n os._exit = program_exit\n try:\n try:\n arm_interrupt()\n if cwd != \"\":\n saved_chdir(cwd)\n exec(\n compile(\n user_code,\n filename or \"<string>\",\n \"exec\",\n optimize=optimize,\n ),\n user_globals,\n )\n finally:\n disarm_interrupt()\n except SystemExit as e:\n code = e.code\n if code is None:\n exit_code = 0\n elif isinstance(code, bool):\n exit_code = int(code)\n elif isinstance(code, int):\n exit_code = code\n else:\n err_bytes.diagnostic(str(code) + \"\\n\")\n exit_code = 1\n except ProgramExit as e:\n exit_code = e.code\n except BaseException as e:\n err_bytes.diagnostic(\n \"\".join(\n traceback.format_exception(\n type(e), e, e.__traceback__.tb_next\n )\n )\n )\n exit_code = 1\n finally:\n if had_main:\n sys.modules[\"__main__\"] = saved_main\n else:\n sys.modules.pop(\"__main__\", None)\n os.environ.clear()\n os.environ.update(saved_env)\n if _process_active:\n try:\n _process_call(\"finish\")\n except BaseException:\n err_bytes.diagnostic(traceback.format_exc())\n exit_code = 1\n _process_active = False\n _inherited_inputs.clear()\n _process_stdio = None\n sys.stdin = saved_stdin\n sys.stdout = saved_stdout\n sys.stderr = saved_stderr\n sys.executable = saved_executable\n sys.argv = saved_argv\n os.chdir = saved_chdir\n os.getcwd = saved_getcwd\n os._exit = saved_exit\n saved_chdir(saved_cwd)\n # CPython flushes stdout as it exits, and a flush that fails is\n # reported and makes the exit status 120, whatever the program's.\n if _exit_status is None:\n try:\n out_bytes.finish()\n except BrokenPipeError as exc:\n when = \"while\" if sys.version_info >= (3, 14) else \"on\"\n err_bytes.diagnostic(\n f\"Exception ignored {when} flushing sys.stdout:\\n\"\n + \"\".join(traceback.format_exception_only(exc))\n )\n exit_code = 120\n\n if _exit_status is not None:\n exit_code = _exit_status\n _exit_status = None\n return exit_code\n\n\ndef seed_sys_path(paths):\n misses = []\n expanded = []\n for path in paths:\n if any(c in path for c in \"*?[\"):\n hits = sorted(glob.glob(path))\n if not hits:\n misses.append(path)\n expanded.extend(hits)\n else:\n expanded.append(path)\n sys.path[:0] = [path for path in expanded if path not in sys.path]\n importlib.invalidate_caches()\n return misses\n\n\n# Emscripten builds os without the extended-attribute family CPython has\n# on linux, so a guest asking a mounted path for its attributes got\n# AttributeError. The host\n# registers _mirage_xattr, which answers from the dispatcher, and\n# each condition it reports is raised as the errno linux would raise.\nXATTR_ERRNO = {\n \"NO_XATTR\": errno.ENODATA,\n \"ENOENT\": errno.ENOENT,\n \"ENOTDIR\": errno.ENOTDIR,\n \"EEXIST\": errno.EEXIST,\n \"EACCES\": errno.EACCES,\n \"EPERM\": errno.EPERM,\n \"EROFS\": errno.EROFS,\n \"EINVAL\": errno.EINVAL,\n \"ELOOP\": errno.ELOOP,\n \"ENOTSUP\": errno.ENOTSUP,\n}\n\n\ndef xattr_call(\n op, path, attribute=None, value=None, flags=0, follow_symlinks=True\n):\n if isinstance(path, int):\n raise OSError(errno.ENOTSUP, os.strerror(errno.ENOTSUP))\n target = os.path.abspath(os.fsdecode(path))\n name = None if attribute is None else os.fsdecode(attribute)\n payload = (\n None\n if value is None\n else base64.b64encode(bytes(value)).decode(\"ascii\")\n )\n answer = json.loads(\n _mirage_xattr.call(\n op,\n target,\n name,\n payload,\n bool(flags & 1),\n bool(flags & 2),\n not follow_symlinks,\n )\n )\n code = answer.get(\"code\")\n if code is not None:\n number = XATTR_ERRNO.get(code, errno.EIO)\n raise OSError(number, os.strerror(number), target)\n return answer.get(\"value\")\n\n\ndef getxattr(path, attribute, *, follow_symlinks=True):\n found = xattr_call(\n \"getxattr\", path, attribute, follow_symlinks=follow_symlinks\n )\n return base64.b64decode(found)\n\n\ndef listxattr(path=None, *, follow_symlinks=True):\n return list(\n xattr_call(\n \"listxattr\",\n \".\" if path is None else path,\n follow_symlinks=follow_symlinks,\n )\n )\n\n\ndef setxattr(path, attribute, value, flags=0, *, follow_symlinks=True):\n xattr_call(\"setxattr\", path, attribute, value, flags, follow_symlinks)\n\n\ndef removexattr(path, attribute, *, follow_symlinks=True):\n xattr_call(\"removexattr\", path, attribute, follow_symlinks=follow_symlinks)\n\n\ndef install_xattrs():\n os.getxattr = getxattr\n os.listxattr = listxattr\n os.setxattr = setxattr\n os.removexattr = removexattr\n os.XATTR_CREATE = 1\n os.XATTR_REPLACE = 2\n os.XATTR_SIZE_MAX = 65536\n\n\ninstall_xattrs()\n"; //# sourceMappingURL=pyodide.js.map