]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-124621: Emscripten: Support pyrepl in browser (GH-136931) (GH-136988)
authorŁukasz Langa <lukasz@langa.pl>
Tue, 22 Jul 2025 13:04:11 +0000 (15:04 +0200)
committerGitHub <noreply@github.com>
Tue, 22 Jul 2025 13:04:11 +0000 (15:04 +0200)
Basic support for pyrepl in Emscripten. Limitations:
* requires JSPI
* no signal handling implemented

As followup work, it would be nice to implement a webworker variant
for when JSPI is not available and proper signal handling.

Because it requires JSPI, it doesn't work in Safari. Firefox requires
setting an experimental flag. All the Chromiums have full support since
May. Until we make it work without JSPI, let's keep the original web_example
around.
(cherry picked from commit c933a6bb329bb97bc7e448388dad1b74f7ca4baa)

Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Co-authored-by: Éric <merwok@netwok.org>
13 files changed:
Lib/_pyrepl/trace.py
Lib/test/test_fcntl.py
Makefile.pre.in
Misc/NEWS.d/next/Library/2025-07-21-16-10-24.gh-issue-124621.wyoWc1.rst [new file with mode: 0644]
Modules/clinic/posixmodule.c.h
Modules/posixmodule.c
Python/emscripten_syscalls.c
Tools/wasm/emscripten/config.site-wasm32-emscripten
Tools/wasm/emscripten/wasm_assets.py [moved from Tools/wasm/emscripten/web_example/wasm_assets.py with 98% similarity]
Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html [new file with mode: 0644]
Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs [new file with mode: 0644]
configure
configure.ac

index a8eb2433cd3ccee302d123bb86960f460dea20c3..943ee12f964b29627efc0b1a6629b9fa0504d7a6 100644 (file)
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import os
+import sys
 
 # types
 if False:
@@ -12,10 +13,22 @@ if trace_filename := os.environ.get("PYREPL_TRACE"):
     trace_file = open(trace_filename, "a")
 
 
-def trace(line: str, *k: object, **kw: object) -> None:
-    if trace_file is None:
-        return
-    if k or kw:
-        line = line.format(*k, **kw)
-    trace_file.write(line + "\n")
-    trace_file.flush()
+
+if sys.platform == "emscripten":
+    from posix import _emscripten_log
+
+    def trace(line: str, *k: object, **kw: object) -> None:
+        if "PYREPL_TRACE" not in os.environ:
+            return
+        if k or kw:
+            line = line.format(*k, **kw)
+        _emscripten_log(line)
+
+else:
+    def trace(line: str, *k: object, **kw: object) -> None:
+        if trace_file is None:
+            return
+        if k or kw:
+            line = line.format(*k, **kw)
+        trace_file.write(line + "\n")
+        trace_file.flush()
index 0570257c5230d5102d6a1bded9ff4af34c18b9bc..9f1e2e250fe854ada3c9751c76e9032959711327 100644 (file)
@@ -8,7 +8,7 @@ import struct
 import sys
 import unittest
 from test.support import (
-    cpython_only, get_pagesize, is_apple, requires_subprocess, verbose
+    cpython_only, get_pagesize, is_apple, requires_subprocess, verbose, is_emscripten
 )
 from test.support.import_helper import import_module
 from test.support.os_helper import TESTFN, unlink, make_bad_fd
@@ -211,6 +211,7 @@ class TestFcntl(unittest.TestCase):
     @unittest.skipUnless(
         hasattr(fcntl, "F_SETPIPE_SZ") and hasattr(fcntl, "F_GETPIPE_SZ"),
         "F_SETPIPE_SZ and F_GETPIPE_SZ are not available on all platforms.")
+    @unittest.skipIf(is_emscripten, "Emscripten pipefs doesn't support these")
     def test_fcntl_f_pipesize(self):
         test_pipe_r, test_pipe_w = os.pipe()
         try:
index fae5e384d3245f29b786f7dd29eb84b99c42e695..01e10d1ab209aedaf26d99c233ce90bee42dc5fb 100644 (file)
@@ -804,7 +804,7 @@ build_wasm: check-clean-src $(BUILDPYTHON) platform sharedmods \
                python-config checksharedmods
 
 .PHONY: build_emscripten
-build_emscripten: build_wasm web_example
+build_emscripten: build_wasm web_example web_example_pyrepl_jspi
 
 # Check that the source is clean when building out of source.
 .PHONY: check-clean-src
@@ -1095,26 +1095,28 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)
 
 # wasm32-emscripten browser web example
 
-WEBEX_DIR=$(srcdir)/Tools/wasm/emscripten/web_example/
+EMSCRIPTEN_DIR=$(srcdir)/Tools/wasm/emscripten
+WEBEX_DIR=$(EMSCRIPTEN_DIR)/web_example/
+
+ZIP_STDLIB=python$(VERSION)$(ABI_THREAD).zip
+$(ZIP_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
+           $(EMSCRIPTEN_DIR)/wasm_assets.py \
+           Makefile pybuilddir.txt Modules/Setup.local
+       $(PYTHON_FOR_BUILD) $(EMSCRIPTEN_DIR)/wasm_assets.py \
+           --buildroot . --prefix $(prefix) -o $@
+
 web_example/index.html: $(WEBEX_DIR)/index.html
        @mkdir -p web_example
        @cp $< $@
 
-web_example/python.worker.mjs: $(WEBEX_DIR)/python.worker.mjs
+web_example/server.py: $(WEBEX_DIR)/server.py
        @mkdir -p web_example
        @cp $< $@
 
-web_example/server.py: $(WEBEX_DIR)/server.py
+web_example/$(ZIP_STDLIB): $(ZIP_STDLIB)
        @mkdir -p web_example
        @cp $< $@
 
-WEB_STDLIB=web_example/python$(VERSION)$(ABI_THREAD).zip
-$(WEB_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
-           $(WEBEX_DIR)/wasm_assets.py \
-           Makefile pybuilddir.txt Modules/Setup.local
-       $(PYTHON_FOR_BUILD) $(WEBEX_DIR)/wasm_assets.py \
-           --buildroot . --prefix $(prefix) -o $@
-
 web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
        @if test $(HOST_GNU_TYPE) != 'wasm32-unknown-emscripten' ; then \
                echo "Can only build web_example when target is Emscripten" ;\
@@ -1124,7 +1126,35 @@ web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
        cp python.wasm web_example/python.wasm
 
 .PHONY: web_example
-web_example: web_example/python.mjs web_example/python.worker.mjs web_example/index.html web_example/server.py $(WEB_STDLIB)
+web_example: web_example/python.mjs web_example/index.html web_example/server.py web_example/$(ZIP_STDLIB)
+
+WEBEX2=web_example_pyrepl_jspi
+WEBEX2_DIR=$(EMSCRIPTEN_DIR)/$(WEBEX2)/
+
+$(WEBEX2)/python.mjs $(WEBEX2)/python.wasm: $(BUILDPYTHON)
+       @if test $(HOST_GNU_TYPE) != 'wasm32-unknown-emscripten' ; then \
+               echo "Can only build web_example when target is Emscripten" ;\
+               exit 1 ;\
+       fi
+       @mkdir -p $(WEBEX2)
+       @cp python.mjs $(WEBEX2)/python.mjs
+       @cp python.wasm $(WEBEX2)/python.wasm
+
+$(WEBEX2)/index.html: $(WEBEX2_DIR)/index.html
+       @mkdir -p $(WEBEX2)
+       @cp $< $@
+
+$(WEBEX2)/src.mjs: $(WEBEX2_DIR)/src.mjs
+       @mkdir -p $(WEBEX2)
+       @cp $< $@
+
+$(WEBEX2)/$(ZIP_STDLIB): $(ZIP_STDLIB)
+       @mkdir -p $(WEBEX2)
+       @cp $< $@
+
+.PHONY: web_example_pyrepl_jspi
+web_example_pyrepl_jspi: $(WEBEX2)/python.mjs $(WEBEX2)/index.html $(WEBEX2)/src.mjs $(WEBEX2)/$(ZIP_STDLIB)
+
 
 ############################################################################
 # Header files
diff --git a/Misc/NEWS.d/next/Library/2025-07-21-16-10-24.gh-issue-124621.wyoWc1.rst b/Misc/NEWS.d/next/Library/2025-07-21-16-10-24.gh-issue-124621.wyoWc1.rst
new file mode 100644 (file)
index 0000000..3404918
--- /dev/null
@@ -0,0 +1 @@
+pyrepl now works in Emscripten.
index 65d3aff18d7b42ced4025b59c01f9aafb6e60431..87a17935507b9ca56cfff21c8e4eef304ed28fe6 100644 (file)
@@ -12727,6 +12727,80 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored))
 
 #endif /* defined(__EMSCRIPTEN__) */
 
+#if defined(__EMSCRIPTEN__)
+
+PyDoc_STRVAR(os__emscripten_log__doc__,
+"_emscripten_log($module, /, arg)\n"
+"--\n"
+"\n"
+"Log something to the JS console. Emscripten only.");
+
+#define OS__EMSCRIPTEN_LOG_METHODDEF    \
+    {"_emscripten_log", _PyCFunction_CAST(os__emscripten_log), METH_FASTCALL|METH_KEYWORDS, os__emscripten_log__doc__},
+
+static PyObject *
+os__emscripten_log_impl(PyObject *module, const char *arg);
+
+static PyObject *
+os__emscripten_log(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 1
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { &_Py_ID(arg), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"arg", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "_emscripten_log",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    const char *arg;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!PyUnicode_Check(args[0])) {
+        _PyArg_BadArgument("_emscripten_log", "argument 'arg'", "str", args[0]);
+        goto exit;
+    }
+    Py_ssize_t arg_length;
+    arg = PyUnicode_AsUTF8AndSize(args[0], &arg_length);
+    if (arg == NULL) {
+        goto exit;
+    }
+    if (strlen(arg) != (size_t)arg_length) {
+        PyErr_SetString(PyExc_ValueError, "embedded null character");
+        goto exit;
+    }
+    return_value = os__emscripten_log_impl(module, arg);
+
+exit:
+    return return_value;
+}
+
+#endif /* defined(__EMSCRIPTEN__) */
+
 #ifndef OS_TTYNAME_METHODDEF
     #define OS_TTYNAME_METHODDEF
 #endif /* !defined(OS_TTYNAME_METHODDEF) */
@@ -13398,4 +13472,8 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored))
 #ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF
     #define OS__EMSCRIPTEN_DEBUGGER_METHODDEF
 #endif /* !defined(OS__EMSCRIPTEN_DEBUGGER_METHODDEF) */
-/*[clinic end generated code: output=c693071966d11548 input=a9049054013a1b77]*/
+
+#ifndef OS__EMSCRIPTEN_LOG_METHODDEF
+    #define OS__EMSCRIPTEN_LOG_METHODDEF
+#endif /* !defined(OS__EMSCRIPTEN_LOG_METHODDEF) */
+/*[clinic end generated code: output=9e5f9b9ce732a534 input=a9049054013a1b77]*/
index b3fe9953264ae32f0f2a761cf836339f6f2a570d..904c2f55bbccca949940de379c4084de1479c263 100644 (file)
@@ -16953,6 +16953,25 @@ os__emscripten_debugger_impl(PyObject *module)
     emscripten_debugger();
     Py_RETURN_NONE;
 }
+
+EM_JS(void, emscripten_log_impl_js, (const char* arg), {
+    console.warn(UTF8ToString(arg));
+});
+
+/*[clinic input]
+os._emscripten_log
+   arg: str
+
+Log something to the JS console. Emscripten only.
+[clinic start generated code]*/
+
+static PyObject *
+os__emscripten_log_impl(PyObject *module, const char *arg)
+/*[clinic end generated code: output=9749e5e293c42784 input=350aa1f70bc1e905]*/
+{
+    emscripten_log_impl_js(arg);
+    Py_RETURN_NONE;
+}
 #endif /* __EMSCRIPTEN__ */
 
 
@@ -17172,6 +17191,7 @@ static PyMethodDef posix_methods[] = {
     OS__IS_INPUTHOOK_INSTALLED_METHODDEF
     OS__CREATE_ENVIRON_METHODDEF
     OS__EMSCRIPTEN_DEBUGGER_METHODDEF
+    OS__EMSCRIPTEN_LOG_METHODDEF
     {NULL,              NULL}            /* Sentinel */
 };
 
index d3eedad30e36393e58190e0d2fc20f786b0fc65b..b1236e6b123fc94b45f128640ad86b18601f2fb4 100644 (file)
@@ -1,4 +1,5 @@
 #include "emscripten.h"
+#include "stdio.h"
 
 // If we're running in node, report the UID of the user in the native system as
 // the UID of the user. Since the nodefs will report the uid correctly, if we
@@ -40,7 +41,7 @@ int __syscall_umask(int mask) {
 
 #include <wasi/api.h>
 #include <errno.h>
-#undef errno
+#include <fcntl.h>
 
 // Variant of EM_JS that does C preprocessor substitution on the body
 #define EM_JS_MACROS(ret, func_name, args, body...)                            \
@@ -100,7 +101,7 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
         return;
     }
     const origResolveGlobalSymbol = resolveGlobalSymbol;
-    if (!Module.onExit && globalThis?.process?.exit) {
+    if (ENVIRONMENT_IS_NODE && !Module.onExit) {
         Module.onExit = (code) => process.exit(code);
     }
     // * wrap the main symbol with WebAssembly.promising,
@@ -115,7 +116,7 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
             orig.sym = (...args) => {
                 (async () => {
                     const ret = await main(...args);
-                    process?.exit?.(ret);
+                    Module.onExit?.(ret);
                 })();
                 _emscripten_exit_with_live_runtime();
             };
@@ -185,7 +186,7 @@ EM_JS_MACROS(__externref_t, __maybe_fd_read_async, (
             if (e.name !== 'ErrnoError') {
                 throw e;
             }
-            return e.errno;
+            return e["errno"];
         }
     })();
 };
@@ -199,16 +200,16 @@ __wasi_errno_t __wasi_fd_read_orig(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
 
 // Take a promise that resolves to __wasi_errno_t and suspend until it resolves,
 // get the output.
-EM_JS(__wasi_errno_t, __block_for_errno, (__externref_t p), {
+EM_JS(int, __block_for_int, (__externref_t p), {
     return p;
 }
 if (WebAssembly.Suspending) {
-    __block_for_errno = new WebAssembly.Suspending(__block_for_errno);
+    __block_for_int = new WebAssembly.Suspending(__block_for_int);
 }
 )
 
 // Replacement for fd_read syscall. Call __maybe_fd_read_async. If it returned
-// null, delegate back to __wasi_fd_read_orig. Otherwise, use __block_for_errno
+// null, delegate back to __wasi_fd_read_orig. Otherwise, use __block_for_int
 // to get the result.
 __wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
                               size_t iovs_len, __wasi_size_t *nread) {
@@ -216,6 +217,103 @@ __wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
   if (__builtin_wasm_ref_is_null_extern(p)) {
     return __wasi_fd_read_orig(fd, iovs, iovs_len, nread);
   }
-  __wasi_errno_t res = __block_for_errno(p);
-  return res;
+  return __block_for_int(p);
+}
+
+#include <poll.h>
+#define POLLFD_FD 0
+#define POLLFD_EVENTS 4
+#define POLLFD_REVENTS 6
+#define POLLFD_SIZE 8
+_Static_assert(offsetof(struct pollfd, fd) == 0, "Unepxected pollfd struct layout");
+_Static_assert(offsetof(struct pollfd, events) == 4, "Unepxected pollfd struct layout");
+_Static_assert(offsetof(struct pollfd, revents) == 6, "Unepxected pollfd struct layout");
+_Static_assert(sizeof(struct pollfd) == 8, "Unepxected pollfd struct layout");
+
+EM_JS_MACROS(__externref_t, __maybe_poll_async, (intptr_t fds, int nfds, int timeout), {
+    if (!WebAssembly.promising) {
+        return null;
+    }
+    return (async function() {
+        try {
+            var nonzero = 0;
+            var promises = [];
+            for (var i = 0; i < nfds; i++) {
+                var pollfd = fds + POLLFD_SIZE * i;
+                var fd = HEAP32[(pollfd + POLLFD_FD)/4];
+                var events = HEAP16[(pollfd + POLLFD_EVENTS)/2];
+                var mask = POLLNVAL;
+                var stream = FS.getStream(fd);
+                if (stream) {
+                    mask = POLLIN | POLLOUT;
+                    if (stream.stream_ops.pollAsync) {
+                        promises.push(stream.stream_ops.pollAsync(stream, timeout).then((mask) => {
+                            mask &= events | POLLERR | POLLHUP;
+                            HEAP16[(pollfd + POLLFD_REVENTS)/2] = mask;
+                            if (mask) {
+                                nonzero ++;
+                            }
+                        }));
+                    } else if (stream.stream_ops.poll) {
+                        var mask = stream.stream_ops.poll(stream, timeout);
+                        mask &= events | POLLERR | POLLHUP;
+                        HEAP16[(pollfd + POLLFD_REVENTS)/2] = mask;
+                        if (mask) {
+                            nonzero ++;
+                        }
+                    }
+                }
+            }
+            await Promise.all(promises);
+            return nonzero;
+        } catch(e) {
+            if (e?.name !== "ErrnoError") throw e;
+            return -e["errno"];
+        }
+    })();
+});
+
+// Bind original poll syscall to syscall_poll_orig().
+int syscall_poll_orig(intptr_t fds, int nfds, int timeout)
+    __attribute__((__import_module__("env"),
+                   __import_name__("__syscall_poll"), __warn_unused_result__));
+
+int __syscall_poll(intptr_t fds, int nfds, int timeout) {
+    __externref_t p = __maybe_poll_async(fds, nfds, timeout);
+    if (__builtin_wasm_ref_is_null_extern(p)) {
+        return syscall_poll_orig(fds, nfds, timeout);
+    }
+    return __block_for_int(p);
+}
+
+#include <sys/ioctl.h>
+
+int syscall_ioctl_orig(int fd, int request, void* varargs)
+    __attribute__((__import_module__("env"),
+                   __import_name__("__syscall_ioctl"), __warn_unused_result__));
+
+int __syscall_ioctl(int fd, int request, void* varargs) {
+    if (request == FIOCLEX || request == FIONCLEX) {
+        return 0;
+    }
+    if (request == FIONBIO) {
+        // Implement FIONBIO via fcntl.
+        // TODO: Upstream this.
+        int flags = fcntl(fd, F_GETFL, 0);
+        int nonblock = **((int**)varargs);
+        if (flags < 0) {
+            return -errno;
+        }
+        if (nonblock) {
+            flags |= O_NONBLOCK;
+        } else {
+            flags &= (~O_NONBLOCK);
+        }
+        int res = fcntl(fd, F_SETFL, flags);
+        if (res < 0) {
+            return -errno;
+        }
+        return res;
+    }
+    return syscall_ioctl_orig(fd, request, varargs);
 }
index 8c3a338dacb2dc970badf847869b10abeb6a3658..9f98e3f3c3bb1f7607e42b82253246b01afaaea3 100644 (file)
@@ -69,7 +69,6 @@ ac_cv_func_posix_fallocate=no
 
 # Syscalls that resulted in a segfault
 ac_cv_func_utimensat=no
-ac_cv_header_sys_ioctl_h=no
 
 # sockets are supported, but only AF_INET / AF_INET6 in non-blocking mode.
 # Disable AF_UNIX and AF_PACKET support, see socketmodule.h.
similarity index 98%
rename from Tools/wasm/emscripten/web_example/wasm_assets.py
rename to Tools/wasm/emscripten/wasm_assets.py
index deeb9229a4412bcabec6ff0c01bfdc7bfb695c78..b08e7ce1114a4a375d58b0c0f71183466464165f 100755 (executable)
@@ -18,7 +18,7 @@ import zipfile
 from typing import Dict
 
 # source directory
-SRCDIR = pathlib.Path(__file__).parents[4].absolute()
+SRCDIR = pathlib.Path(__file__).parents[3].absolute()
 SRCDIR_LIB = SRCDIR / "Lib"
 
 
@@ -84,7 +84,6 @@ OMIT_MODULE_FILES = {
     "_json": ["json/"],
     "_multiprocessing": ["concurrent/futures/process.py", "multiprocessing/"],
     "pyexpat": ["xml/", "xmlrpc/"],
-    "readline": ["rlcompleter.py"],
     "_sqlite3": ["sqlite3/"],
     "_ssl": ["ssl.py"],
     "_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"],
diff --git a/Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html b/Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html
new file mode 100644 (file)
index 0000000..1f72bd2
--- /dev/null
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+    <head>
+        <link
+            rel="stylesheet"
+            href="https://unpkg.com/xterm@4.18.0/css/xterm.css"
+        />
+        <style>
+             body {
+                 background-color: #300a24;
+             }
+
+             .xterm-dom-renderer-owner-1 .xterm-fg-3 {
+                color: #c4a000 !important;
+            }
+
+            .xterm-dom-renderer-owner-1 .xterm-fg-6 {
+                color: #2aa1b3 !important;
+            }
+
+            .xterm-dom-renderer-owner-1 .xterm-fg-12 {
+                color: #1054a6 !important;
+            }
+
+            .xterm-dom-renderer-owner-1 .xterm-fg-13 {
+                color: #a347ba !important;
+            }
+        </style>
+    </head>
+    <body>
+        <div id="terminal"></div>
+        <script type="module" src="src.mjs"></script>
+    </body>
+</html>
diff --git a/Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs b/Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs
new file mode 100644 (file)
index 0000000..5642372
--- /dev/null
@@ -0,0 +1,194 @@
+// Much of this is adapted from here:
+// https://github.com/mame/xterm-pty/blob/main/emscripten-pty.js
+// Thanks to xterm-pty for making this possible!
+
+import createEmscriptenModule from "./python.mjs";
+import { openpty } from "https://unpkg.com/xterm-pty/index.mjs";
+import "https://unpkg.com/@xterm/xterm/lib/xterm.js";
+
+var term = new Terminal();
+term.open(document.getElementById("terminal"));
+const { master, slave: PTY } = openpty();
+term.loadAddon(master);
+globalThis.PTY = PTY;
+
+async function setupStdlib(Module) {
+  const versionInt = Module.HEAPU32[Module._Py_Version >>> 2];
+  const major = (versionInt >>> 24) & 0xff;
+  const minor = (versionInt >>> 16) & 0xff;
+  // Prevent complaints about not finding exec-prefix by making a lib-dynload directory
+  Module.FS.mkdirTree(`/lib/python${major}.${minor}/lib-dynload/`);
+  const resp = await fetch(`python${major}.${minor}.zip`);
+  const stdlibBuffer = await resp.arrayBuffer();
+  Module.FS.writeFile(
+    `/lib/python${major}${minor}.zip`,
+    new Uint8Array(stdlibBuffer),
+    { canOwn: true },
+  );
+}
+
+const tty_ops = {
+  ioctl_tcgets: () => {
+    const termios = PTY.ioctl("TCGETS");
+    const data = {
+      c_iflag: termios.iflag,
+      c_oflag: termios.oflag,
+      c_cflag: termios.cflag,
+      c_lflag: termios.lflag,
+      c_cc: termios.cc,
+    };
+    return data;
+  },
+
+  ioctl_tcsets: (_tty, _optional_actions, data) => {
+    PTY.ioctl("TCSETS", {
+      iflag: data.c_iflag,
+      oflag: data.c_oflag,
+      cflag: data.c_cflag,
+      lflag: data.c_lflag,
+      cc: data.c_cc,
+    });
+    return 0;
+  },
+
+  ioctl_tiocgwinsz: () => PTY.ioctl("TIOCGWINSZ").reverse(),
+
+  get_char: () => {
+    throw new Error("Should not happen");
+  },
+  put_char: () => {
+    throw new Error("Should not happen");
+  },
+
+  fsync: () => {},
+};
+
+const POLLIN = 1;
+const POLLOUT = 4;
+
+const waitResult = {
+  READY: 0,
+  SIGNAL: 1,
+  TIMEOUT: 2,
+};
+
+function onReadable() {
+  var handle;
+  var promise = new Promise((resolve) => {
+    handle = PTY.onReadable(() => resolve(waitResult.READY));
+  });
+  return [promise, handle];
+}
+
+function onSignal() {
+  // TODO: signal handling
+  var handle = { dispose() {} };
+  var promise = new Promise((resolve) => {});
+  return [promise, handle];
+}
+
+function onTimeout(timeout) {
+  var id;
+  var promise = new Promise((resolve) => {
+    if (timeout > 0) {
+      id = setTimeout(resolve, timeout, waitResult.TIMEOUT);
+    }
+  });
+  var handle = {
+    dispose() {
+      if (id) {
+        clearTimeout(id);
+      }
+    },
+  };
+  return [promise, handle];
+}
+
+async function waitForReadable(timeout) {
+  let p1, p2, p3;
+  let h1, h2, h3;
+  try {
+    [p1, h1] = onReadable();
+    [p2, h2] = onTimeout(timeout);
+    [p3, h3] = onSignal();
+    return await Promise.race([p1, p2, p3]);
+  } finally {
+    h1.dispose();
+    h2.dispose();
+    h3.dispose();
+  }
+}
+
+const FIONREAD = 0x541b;
+
+const tty_stream_ops = {
+  async readAsync(stream, buffer, offset, length, pos /* ignored */) {
+    let readBytes = PTY.read(length);
+    if (length && !readBytes.length) {
+      const status = await waitForReadable(-1);
+      if (status === waitResult.READY) {
+        readBytes = PTY.read(length);
+      } else {
+        throw new Error("Not implemented");
+      }
+    }
+    buffer.set(readBytes, offset);
+    return readBytes.length;
+  },
+
+  write: (stream, buffer, offset, length) => {
+    // Note: default `buffer` is for some reason `HEAP8` (signed), while we want unsigned `HEAPU8`.
+    buffer = new Uint8Array(
+      buffer.buffer,
+      buffer.byteOffset,
+      buffer.byteLength,
+    );
+    const toWrite = Array.from(buffer.subarray(offset, offset + length));
+    PTY.write(toWrite);
+    return length;
+  },
+
+  async pollAsync(stream, timeout) {
+    if (!PTY.readable && timeout) {
+      await waitForReadable(timeout);
+    }
+    return (PTY.readable ? POLLIN : 0) | (PTY.writable ? POLLOUT : 0);
+  },
+  ioctl(stream, request, varargs) {
+    if (request === FIONREAD) {
+      const res = PTY.fromLdiscToUpperBuffer.length;
+      Module.HEAPU32[varargs / 4] = res;
+      return 0;
+    }
+    throw new Error("Unimplemented ioctl request");
+  },
+};
+
+async function setupStdio(Module) {
+  Object.assign(Module.TTY.default_tty_ops, tty_ops);
+  Object.assign(Module.TTY.stream_ops, tty_stream_ops);
+}
+
+const emscriptenSettings = {
+  async preRun(Module) {
+    Module.addRunDependency("pre-run");
+    Module.ENV.TERM = "xterm-256color";
+    // Uncomment next line to turn on tracing (messages go to browser console).
+    // Module.ENV.PYREPL_TRACE = "1";
+
+    // Leak module so we can try to show traceback if we crash on startup
+    globalThis.Module = Module;
+    await Promise.all([setupStdlib(Module), setupStdio(Module)]);
+    Module.removeRunDependency("pre-run");
+  },
+};
+
+try {
+  await createEmscriptenModule(emscriptenSettings);
+} catch (e) {
+  // Show JavaScript exception and traceback
+  console.warn(e);
+  // Show Python exception and traceback
+  Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
+  process.exit(1);
+}
index 946cd471f64d327d5339e80b90a536a72ff69c30..345822a6c6df763f8770868474166708245239a3 100755 (executable)
--- a/configure
+++ b/configure
@@ -9603,7 +9603,7 @@ fi
         as_fn_append LDFLAGS_NODIST " -sWASM_BIGINT"
 
         as_fn_append LINKFORSHARED " -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"
-    as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32"
+    as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY"
     as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"
     as_fn_append LINKFORSHARED " -sSTACK_SIZE=5MB"
         as_fn_append LINKFORSHARED " -sTEXTDECODER=2"
@@ -31196,9 +31196,7 @@ case $ac_sys_system in #(
 
 
 
-    py_cv_module_fcntl=n/a
     py_cv_module_readline=n/a
-    py_cv_module_termios=n/a
     py_cv_module_=n/a
 
    ;; #(
index 6b15beb050c1af0d9f0808a3ea98d304844b35a3..d6059471771871c4fb6085285440051faf57c317 100644 (file)
@@ -2335,7 +2335,7 @@ AS_CASE([$ac_sys_system],
 
     dnl Include file system support
     AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"])
-    AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32"])
+    AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY"])
     AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"])
     AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"])
     dnl Avoid bugs in JS fallback string decoding path
@@ -7778,9 +7778,7 @@ AS_CASE([$ac_sys_system],
     )
     dnl fcntl, readline, and termios are not particularly useful in browsers.
     PY_STDLIB_MOD_SET_NA(
-      [fcntl],
       [readline],
-      [termios],
     )
   ],
   [WASI], [