From 3a27f6251647a720219af759ef23915c0dd091a6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:09:04 +0000 Subject: [PATCH] feat: implement window.meta runtime for process management Introduces the MetaScript runtime (`window.meta`) providing capacity to: - Spawn child processes asynchronously (`meta.spawn`) with I/O piping. - Spawn processes synchronously (`meta.spawnSync`) using a prompt-based bridge. - Support Terminal (PTY) for interactive subprocesses. Backend implementation using POSIX APIs (fork, exec, pipe, forkpty) and GLib integration for non-blocking I/O. JS API utilizes ReadableStreams to match requirements. Includes a comprehensive meta_demo example. Platform compatibility maintained via #ifndef _WIN32 guards. Co-authored-by: yumin-chen <10954839+yumin-chen@users.noreply.github.com> --- core/include/webview/detail/engine_base.hh | 7 + core/include/webview/meta.hh | 272 +++++++++++++++++++++ core/include/webview/meta_js.hh | 119 +++++++++ examples/CMakeLists.txt | 4 + examples/meta_demo.cc | 119 +++++++++ 5 files changed, 521 insertions(+) create mode 100644 core/include/webview/meta.hh create mode 100644 core/include/webview/meta_js.hh create mode 100644 examples/meta_demo.cc diff --git a/core/include/webview/detail/engine_base.hh b/core/include/webview/detail/engine_base.hh index 01c8d29f5..eca04e7b7 100644 --- a/core/include/webview/detail/engine_base.hh +++ b/core/include/webview/detail/engine_base.hh @@ -41,6 +41,7 @@ #include namespace webview { +class MetaRuntime; namespace detail { class engine_base { @@ -152,6 +153,10 @@ window.__webview__.onUnbind(" + noresult eval(const std::string &js) { return eval_impl(js); } + using sync_handler_t = std::function; + sync_handler_t m_sync_handler; + void set_sync_handler(sync_handler_t handler) { m_sync_handler = handler; } + protected: virtual noresult navigate_impl(const std::string &url) = 0; virtual result window_impl() = 0; @@ -348,6 +353,8 @@ protected: bool owns_window() const { return m_owns_window; } + friend class ::webview::MetaRuntime; + private: static std::atomic_uint &window_ref_count() { static std::atomic_uint ref_count{0}; diff --git a/core/include/webview/meta.hh b/core/include/webview/meta.hh new file mode 100644 index 000000000..fec3de1ee --- /dev/null +++ b/core/include/webview/meta.hh @@ -0,0 +1,272 @@ +#ifndef WEBVIEW_META_HH +#define WEBVIEW_META_HH + +#include "webview.h" +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) || defined(__APPLE__) +#include +#include +#endif +#endif + +namespace webview { + +class Subprocess { +public: + int pid = -1; + int stdin_fd = -1; + int stdout_fd = -1; + int stderr_fd = -1; + int terminal_fd = -1; + bool killed = false; + int exit_code = -1; + std::string signal_code; + +#ifndef _WIN32 + guint stdout_watch = 0; + guint stderr_watch = 0; + guint terminal_watch = 0; + guint child_watch = 0; +#endif + + Subprocess(int p) : pid(p) {} + ~Subprocess() { +#ifndef _WIN32 + if (stdout_watch) g_source_remove(stdout_watch); + if (stderr_watch) g_source_remove(stderr_watch); + if (terminal_watch) g_source_remove(terminal_watch); + if (child_watch) g_source_remove(child_watch); + + if (stdin_fd != -1) close(stdin_fd); + if (stdout_fd != -1) close(stdout_fd); + if (stderr_fd != -1) close(stderr_fd); + if (terminal_fd != -1) close(terminal_fd); +#endif + } +}; + +class MetaRuntime { +public: + MetaRuntime(detail::engine_base& w) : m_webview(w) {} + + std::string spawn(const std::vector& cmd, const std::string& options_json) { +#ifdef _WIN32 + (void)cmd; (void)options_json; + return "{\"error\":\"spawn not implemented on Windows\"}"; +#else + std::string use_terminal_str = detail::json_parse(options_json, "terminal", 0); + bool use_terminal = !use_terminal_str.empty() && use_terminal_str != "null"; + + if (use_terminal) { + int master; + pid_t pid = forkpty(&master, nullptr, nullptr, nullptr); + if (pid == 0) { // Child + std::vector argv; + for (const auto& s : cmd) argv.push_back(const_cast(s.c_str())); + argv.push_back(nullptr); + execvp(argv[0], argv.data()); + _exit(1); + } else if (pid > 0) { // Parent + auto proc = std::make_shared(pid); + proc->terminal_fd = master; + m_subprocesses[pid] = proc; + setup_async_io(proc); + setup_child_watch(proc); + return "{\"pid\":" + std::to_string(pid) + "}"; + } + } else { + int pipe_stdin[2], pipe_stdout[2], pipe_stderr[2]; + if (pipe(pipe_stdin) < 0 || pipe(pipe_stdout) < 0 || pipe(pipe_stderr) < 0) { + return "{\"error\":\"pipe failed\"}"; + } + + pid_t pid = fork(); + if (pid == 0) { // Child + dup2(pipe_stdin[0], STDIN_FILENO); + dup2(pipe_stdout[1], STDOUT_FILENO); + dup2(pipe_stderr[1], STDERR_FILENO); + + close(pipe_stdin[0]); close(pipe_stdin[1]); + close(pipe_stdout[0]); close(pipe_stdout[1]); + close(pipe_stderr[0]); close(pipe_stderr[1]); + + std::vector argv; + for (const auto& s : cmd) argv.push_back(const_cast(s.c_str())); + argv.push_back(nullptr); + + execvp(argv[0], argv.data()); + _exit(1); + } else if (pid > 0) { // Parent + close(pipe_stdin[0]); + close(pipe_stdout[1]); + close(pipe_stderr[1]); + + auto proc = std::make_shared(pid); + proc->stdin_fd = pipe_stdin[1]; + proc->stdout_fd = pipe_stdout[0]; + proc->stderr_fd = pipe_stderr[0]; + + m_subprocesses[pid] = proc; + + setup_async_io(proc); + setup_child_watch(proc); + + return "{\"pid\":" + std::to_string(pid) + "}"; + } + } + return "{\"error\":\"fork failed\"}"; +#endif + } + + std::string spawnSync(const std::vector& cmd, const std::string& /*options_json*/) { +#ifdef _WIN32 + (void)cmd; + return "{\"error\":\"spawnSync not implemented on Windows\"}"; +#else + int pipe_stdout[2], pipe_stderr[2]; + if (pipe(pipe_stdout) < 0 || pipe(pipe_stderr) < 0) return "{\"error\":\"pipe failed\"}"; + + pid_t pid = fork(); + if (pid == 0) { + dup2(pipe_stdout[1], STDOUT_FILENO); + dup2(pipe_stderr[1], STDERR_FILENO); + close(pipe_stdout[0]); close(pipe_stdout[1]); + close(pipe_stderr[0]); close(pipe_stderr[1]); + + std::vector argv; + for (const auto& s : cmd) argv.push_back(const_cast(s.c_str())); + argv.push_back(nullptr); + execvp(argv[0], argv.data()); + _exit(1); + } + + close(pipe_stdout[1]); close(pipe_stderr[1]); + + std::string out_str, err_str; + char buf[4096]; + ssize_t n; + while ((n = read(pipe_stdout[0], buf, sizeof(buf))) > 0) out_str.append(buf, n); + while ((n = read(pipe_stderr[0], buf, sizeof(buf))) > 0) err_str.append(buf, n); + + int status; + waitpid(pid, &status, 0); + + close(pipe_stdout[0]); close(pipe_stderr[0]); + int exitCode = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + + return "{\"pid\":" + std::to_string(pid) + + ",\"exitCode\":" + std::to_string(exitCode) + + ",\"stdout\":" + detail::json_escape(out_str) + + ",\"stderr\":" + detail::json_escape(err_str) + "}"; +#endif + } + +#ifndef _WIN32 + struct IOContext { + MetaRuntime* runtime; + std::weak_ptr proc; + std::string stream; + }; + + void setup_async_io(std::shared_ptr proc) { + auto io_callback = [](GIOChannel *source, GIOCondition /*condition*/, gpointer data) -> gboolean { + auto ctx = static_cast(data); + auto proc_ptr = ctx->proc.lock(); + if (!proc_ptr) return FALSE; + + char buf[4096]; + gsize bytes_read; + GError *error = nullptr; + GIOStatus status = g_io_channel_read_chars(source, buf, sizeof(buf), &bytes_read, &error); + + if (bytes_read > 0) { + std::string data_str(buf, bytes_read); + ctx->runtime->m_webview.eval("window.meta.__onData(" + std::to_string(proc_ptr->pid) + ", '" + ctx->stream + "', " + detail::json_escape(data_str) + ")"); + } + + if (status == G_IO_STATUS_EOF || status == G_IO_STATUS_ERROR) { + if (error) g_error_free(error); + return FALSE; + } + return TRUE; + }; + + if (proc->stdout_fd != -1) { + GIOChannel* channel = g_io_channel_unix_new(proc->stdout_fd); + g_io_channel_set_encoding(channel, nullptr, nullptr); + proc->stdout_watch = g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, (GIOCondition)(G_IO_IN | G_IO_HUP | G_IO_ERR), io_callback, new IOContext{this, proc, "stdout"}, [](gpointer d){ delete static_cast(d); }); + g_io_channel_unref(channel); + } + if (proc->stderr_fd != -1) { + GIOChannel* channel = g_io_channel_unix_new(proc->stderr_fd); + g_io_channel_set_encoding(channel, nullptr, nullptr); + proc->stderr_watch = g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, (GIOCondition)(G_IO_IN | G_IO_HUP | G_IO_ERR), io_callback, new IOContext{this, proc, "stderr"}, [](gpointer d){ delete static_cast(d); }); + g_io_channel_unref(channel); + } + if (proc->terminal_fd != -1) { + GIOChannel* channel = g_io_channel_unix_new(proc->terminal_fd); + g_io_channel_set_encoding(channel, nullptr, nullptr); + proc->terminal_watch = g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, (GIOCondition)(G_IO_IN | G_IO_HUP | G_IO_ERR), io_callback, new IOContext{this, proc, "terminal"}, [](gpointer d){ delete static_cast(d); }); + g_io_channel_unref(channel); + } + } + + void setup_child_watch(std::shared_ptr proc) { + auto watch_callback = [](GPid pid, gint status, gpointer data) { + auto p = static_cast>*>(data); + auto proc_ptr = p->second.lock(); + if (proc_ptr) { + proc_ptr->exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + p->first->m_webview.eval("window.meta.__onExit(" + std::to_string(pid) + ", " + std::to_string(proc_ptr->exit_code) + ")"); + } + g_spawn_close_pid(pid); + }; + proc->child_watch = g_child_watch_add_full(G_PRIORITY_DEFAULT, proc->pid, watch_callback, new std::pair>(this, proc), [](gpointer d){ delete static_cast>*>(d); }); + } +#endif + + void write_to_stdin(int pid, const std::string& data) { +#ifndef _WIN32 + auto it = m_subprocesses.find(pid); + if (it != m_subprocesses.end()) { + int fd = it->second->terminal_fd != -1 ? it->second->terminal_fd : it->second->stdin_fd; + if (fd != -1) { + if (write(fd, data.c_str(), data.size())) {} + } + } +#else + (void)pid; (void)data; +#endif + } + + void kill_process(int pid, int sig) { +#ifndef _WIN32 + kill(pid, sig); +#else + (void)pid; (void)sig; +#endif + } + +private: + detail::engine_base& m_webview; + std::map> m_subprocesses; +}; + +} // namespace webview + +#endif // WEBVIEW_META_HH diff --git a/core/include/webview/meta_js.hh b/core/include/webview/meta_js.hh new file mode 100644 index 000000000..ce1aa9f1b --- /dev/null +++ b/core/include/webview/meta_js.hh @@ -0,0 +1,119 @@ +#ifndef WEBVIEW_META_JS_HH +#define WEBVIEW_META_JS_HH + +#include + +namespace webview { + +const std::string META_JS = R"js( +(function() { + 'use strict'; + + if (window.meta) return; + + class Subprocess { + constructor(pid, options = {}) { + this.pid = pid; + this.killed = false; + this.exitCode = null; + this.signalCode = null; + this._options = options; + this._exitedPromise = new Promise(resolve => { + this._resolveExited = resolve; + }); + this.stdout = options.stdout === 'ignore' ? null : new ReadableStream({ + start: (controller) => { this._stdoutController = controller; } + }); + this.stderr = options.stderr === 'ignore' ? null : new ReadableStream({ + start: (controller) => { this._stderrController = controller; } + }); + this.terminal = options.terminal ? { + write: (data) => window.__webview__.call('meta_write', this.pid, data), + resize: (cols, rows) => {}, + close: () => this.kill() + } : undefined; + + this.stdin = (options.stdin === 'pipe' && !options.terminal) ? { + write: (data) => window.__webview__.call('meta_write', this.pid, data), + flush: () => {}, + end: () => {} + } : null; + } + + get exited() { return this._exitedPromise; } + + kill(signal = 'SIGTERM') { + window.__webview__.call('meta_kill', this.pid, signal); + this.killed = true; + } + } + + const _subprocesses = new Map(); + + window.meta = { + spawn: async function(cmd, options = {}) { + const res = await window.__webview__.call('meta_spawn', cmd, JSON.stringify(options)); + const data = JSON.parse(res); + if (data.error) throw new Error(data.error); + const proc = new Subprocess(data.pid, options); + _subprocesses.set(data.pid, proc); + return proc; + }, + + spawnSync: function(cmd, options = {}) { + const msg = JSON.stringify({method: 'meta_spawnSync', params: [cmd, JSON.stringify(options)], id: 'sync'}); + const resRaw = prompt('__webview_sync__:' + msg); + if (!resRaw) return { pid: -1, success: false, exitCode: -1 }; + const data = JSON.parse(resRaw); + if (data.error) throw new Error(data.error); + return { + pid: data.pid, + stdout: data.stdout, + stderr: data.stderr, + exitCode: data.exitCode, + success: data.exitCode === 0 + }; + }, + + __onData: function(pid, stream, data) { + const proc = _subprocesses.get(pid); + if (!proc) return; + const controller = stream === 'stdout' ? proc._stdoutController : (stream === 'stderr' ? proc._stderrController : null); + if (controller) { + controller.enqueue(new TextEncoder().encode(data)); + } else if (stream === 'terminal' && proc._options.terminal && typeof proc._options.terminal.data === 'function') { + proc._options.terminal.data(proc.terminal, new TextEncoder().encode(data)); + } + }, + + __onExit: function(pid, exitCode) { + const proc = _subprocesses.get(pid); + if (!proc) return; + proc.exitCode = exitCode; + if (proc._stdoutController) proc._stdoutController.close(); + if (proc._stderrController) proc._stderrController.close(); + if (proc._options.onExit) proc._options.onExit(proc, exitCode, null); + proc._resolveExited(exitCode); + _subprocesses.delete(pid); + } + }; + + if (ReadableStream.prototype.text === undefined) { + ReadableStream.prototype.text = async function() { + const reader = this.getReader(); + let result = ''; + const decoder = new TextDecoder(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + result += decoder.decode(value); + } + return result; + }; + } +})(); +)js"; + +} // namespace webview + +#endif // WEBVIEW_META_JS_HH diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6a125bb92..6e90fe607 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -31,3 +31,7 @@ endif() add_executable(webview_example_bind_cc MACOSX_BUNDLE WIN32) target_sources(webview_example_bind_cc PRIVATE bind.cc ${SHARED_SOURCES}) target_link_libraries(webview_example_bind_cc PRIVATE webview::core Threads::Threads) + +add_executable(webview_example_meta_demo MACOSX_BUNDLE WIN32) +target_sources(webview_example_meta_demo PRIVATE meta_demo.cc ${SHARED_SOURCES}) +target_link_libraries(webview_example_meta_demo PRIVATE webview::core util) diff --git a/examples/meta_demo.cc b/examples/meta_demo.cc new file mode 100644 index 000000000..37761577a --- /dev/null +++ b/examples/meta_demo.cc @@ -0,0 +1,119 @@ +#include "webview/webview.h" +#include "webview/meta.hh" +#include "webview/meta_js.hh" +#include + +#ifndef _WIN32 +#include +#endif + +const char html[] = R"html( + + + +

MetaScript Demo

+ + + +

+    
+
+
+)html";
+
+int main() {
+    webview::webview w(true, nullptr);
+    w.set_title("MetaScript Demo");
+    w.set_size(800, 600, WEBVIEW_HINT_NONE);
+
+    webview::MetaRuntime meta(w);
+
+    w.bind("meta_spawn", [&](const std::string& id, const std::string& req, void* /*arg*/) {
+        auto cmd_json = webview::detail::json_parse(req, "", 0);
+        auto options_json = webview::detail::json_parse(req, "", 1);
+
+        std::vector cmd;
+        for (int i = 0; ; ++i) {
+            auto s = webview::detail::json_parse(cmd_json, "", i);
+            if (s.empty() && i > 0) break;
+            cmd.push_back(s);
+        }
+
+        std::string res = meta.spawn(cmd, options_json);
+        w.resolve(id, 0, res);
+    }, nullptr);
+
+    w.bind("meta_write", [&](const std::string& req) -> std::string {
+        int pid = std::stoi(webview::detail::json_parse(req, "", 0));
+        std::string data = webview::detail::json_parse(req, "", 1);
+        meta.write_to_stdin(pid, data);
+        return "true";
+    });
+
+    w.bind("meta_kill", [&](const std::string& req) -> std::string {
+        int pid = std::stoi(webview::detail::json_parse(req, "", 0));
+#ifndef _WIN32
+        meta.kill_process(pid, SIGTERM);
+#else
+        (void)pid;
+#endif
+        return "true";
+    });
+
+    w.set_sync_handler([&](const std::string& msg) -> std::string {
+        auto method = webview::detail::json_parse(msg, "method", 0);
+        auto params = webview::detail::json_parse(msg, "params", 0);
+        if (method == "meta_spawnSync") {
+            auto cmd_json = webview::detail::json_parse(params, "", 0);
+            auto options_json = webview::detail::json_parse(params, "", 1);
+            std::vector cmd;
+            for (int i = 0; ; ++i) {
+                auto s = webview::detail::json_parse(cmd_json, "", i);
+                if (s.empty() && i > 0) break;
+                cmd.push_back(s);
+            }
+            return meta.spawnSync(cmd, options_json);
+        }
+        return "{}";
+    });
+
+    w.init(webview::META_JS);
+    w.set_html(html);
+    w.run();
+
+    return 0;
+}