diff --git a/.gitignore b/.gitignore index 936cdaffe..090396d26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ # Build artifacts /build +node_modules/ +temp_app.cpp +test_meta_json +test_spawn diff --git a/bun.lock b/bun.lock new file mode 100644 index 000000000..040996f66 --- /dev/null +++ b/bun.lock @@ -0,0 +1,25 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "/app", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + } +} diff --git a/core/include/webview/alloy.hh b/core/include/webview/alloy.hh new file mode 100644 index 000000000..57f6686df --- /dev/null +++ b/core/include/webview/alloy.hh @@ -0,0 +1,320 @@ +#ifndef WEBVIEW_ALLOY_HH +#define WEBVIEW_ALLOY_HH + +#include "macros.h" +#include "types.hh" +#include "errors.hh" +#include "json.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__APPLE__) +#include +#define environ (*_NSGetEnviron()) +#include +#elif defined(__linux__) +#include +#include +#else +extern char **environ; +#endif +#endif + +namespace webview { +namespace detail { + +inline std::string base64_encode(const std::string &in) { + std::string out; + int val = 0, valb = -6; + for (unsigned char c : in) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(val >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) out.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[((val << 8) >> (valb + 8)) & 0x3F]); + while (out.size() % 4) out.push_back('='); + return out; +} + +struct terminal_options { + int cols = 80; + int rows = 24; + std::string name = "xterm-256color"; +}; + +struct spawn_options { + std::string cwd; + std::map env; + bool has_ipc = false; + terminal_options terminal; + bool use_terminal = false; + std::string stdin_type = "none"; + std::string stdout_type = "pipe"; + std::string stderr_type = "inherit"; + std::string serialization = "advanced"; + int timeout = 0; +}; + +struct sync_result { + int exit_code = -1; + std::string stdout_data; + std::string stderr_data; + std::string usage_json = "{}"; +}; + +#ifdef _WIN32 +class win32_subprocess { +public: + win32_subprocess(HANDLE hProcess, HANDLE hThread, HANDLE hStdin, HANDLE hStdout, HANDLE hStderr) + : m_process(hProcess), m_thread(hThread), m_stdin(hStdin), m_stdout(hStdout), m_stderr(hStderr) {} + ~win32_subprocess() { + if (m_stdin != INVALID_HANDLE_VALUE) CloseHandle(m_stdin); + if (m_stdout != INVALID_HANDLE_VALUE) CloseHandle(m_stdout); + if (m_stderr != INVALID_HANDLE_VALUE) CloseHandle(m_stderr); + if (m_thread != INVALID_HANDLE_VALUE) CloseHandle(m_thread); + if (m_process != INVALID_HANDLE_VALUE) CloseHandle(m_process); + } + void kill(int sig) { if (m_process != INVALID_HANDLE_VALUE) { TerminateProcess(m_process, (UINT)sig); m_killed = true; } } + void stdin_write(const std::string& data) { if (m_stdin != INVALID_HANDLE_VALUE) { DWORD written; WriteFile(m_stdin, data.data(), (DWORD)data.size(), &written, NULL); } } + void stdin_close() { if (m_stdin != INVALID_HANDLE_VALUE) { CloseHandle(m_stdin); m_stdin = INVALID_HANDLE_VALUE; } } + HANDLE get_process() const { return m_process; } + HANDLE get_stdout() const { return m_stdout; } + HANDLE get_stderr() const { return m_stderr; } + bool is_killed() const { return m_killed; } + int get_pid() const { return (int)GetProcessId(m_process); } +private: + HANDLE m_process; HANDLE m_thread; HANDLE m_stdin; HANDLE m_stdout; HANDLE m_stderr; std::atomic m_killed{false}; +}; +#else +class posix_subprocess { +public: + posix_subprocess(pid_t pid, int stdin_fd, int stdout_fd, int stderr_fd, int ipc_fd = -1) + : m_pid(pid), m_stdin(stdin_fd), m_stdout(stdout_fd), m_stderr(stderr_fd), m_ipc_fd(ipc_fd) {} + ~posix_subprocess() { + if (m_stdin != -1) close(m_stdin); if (m_stdout != -1) close(m_stdout); if (m_stderr != -1) close(m_stderr); if (m_ipc_fd != -1) close(m_ipc_fd); + } + void kill(int sig) { if (m_pid > 0) { ::kill(m_pid, sig); m_killed = true; } } + void stdin_write(const std::string& data) { if (m_stdin != -1) { write(m_stdin, data.data(), data.size()); } } + void stdin_close() { if (m_stdin != -1) { close(m_stdin); m_stdin = -1; } } + void ipc_send(const std::string& message) { if (m_ipc_fd != -1) { write(m_ipc_fd, message.data(), message.size()); write(m_ipc_fd, "\n", 1); } } + void ipc_disconnect() { if (m_ipc_fd != -1) { close(m_ipc_fd); m_ipc_fd = -1; } } + void resize(int cols, int rows) { +#if defined(__APPLE__) || defined(__linux__) + struct winsize ws; ws.ws_col = (unsigned short)cols; ws.ws_row = (unsigned short)rows; + ioctl(m_stdin, TIOCSWINSZ, &ws); +#endif + } + pid_t get_pid() const { return m_pid; } + bool is_killed() const { return m_killed; } + int get_stdout() const { return m_stdout; } + int get_stderr() const { return m_stderr; } + int get_ipc_fd() const { return m_ipc_fd; } +private: + pid_t m_pid; int m_stdin; int m_stdout; int m_stderr; int m_ipc_fd; std::atomic m_killed{false}; +}; +#endif + +class alloy_runtime { +public: + using dispatcher_t = std::function)>; + alloy_runtime(dispatcher_t dispatcher, std::function evaluator) + : m_dispatcher(dispatcher), m_evaluator(evaluator) {} + + int spawn(const std::string& id, const std::vector& cmd, const spawn_options& opts) { + if (cmd.empty()) return 0; +#ifdef _WIN32 + SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; + HANDLE hStdinR = INVALID_HANDLE_VALUE, hStdinW = INVALID_HANDLE_VALUE, hStdoutR = INVALID_HANDLE_VALUE, hStdoutW = INVALID_HANDLE_VALUE, hStderrR = INVALID_HANDLE_VALUE, hStderrW = INVALID_HANDLE_VALUE; + if (opts.stdin_type == "pipe") CreatePipe(&hStdinR, &hStdinW, &sa, 0); + if (opts.stdout_type == "pipe") CreatePipe(&hStdoutR, &hStdoutW, &sa, 0); + if (opts.stderr_type == "pipe") CreatePipe(&hStderrR, &hStderrW, &sa, 0); + STARTUPINFOA si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); + si.hStdInput = (hStdinR != INVALID_HANDLE_VALUE) ? hStdinR : GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = (hStdoutW != INVALID_HANDLE_VALUE) ? hStdoutW : GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = (hStderrW != INVALID_HANDLE_VALUE) ? hStderrW : GetStdHandle(STD_ERROR_HANDLE); + si.dwFlags |= STARTF_USESTDHANDLES; + std::string cl = ""; for (const auto& s : cmd) { if (!cl.empty()) cl += " "; cl += "\"" + s + "\""; } + if (CreateProcessA(NULL, (LPSTR)cl.c_str(), NULL, NULL, TRUE, 0, NULL, opts.cwd.empty() ? NULL : opts.cwd.c_str(), &si, &pi)) { + if (hStdinR != INVALID_HANDLE_VALUE) CloseHandle(hStdinR); if (hStdoutW != INVALID_HANDLE_VALUE) CloseHandle(hStdoutW); if (hStderrW != INVALID_HANDLE_VALUE) CloseHandle(hStderrW); + auto proc = std::make_shared(pi.hProcess, pi.hThread, hStdinW, hStdoutR, hStderrR); + { std::lock_guard lock(m_mutex); m_subprocesses[id] = proc; } + start_monitoring(id, proc); return proc->get_pid(); + } + return 0; +#else + int stdin_p[2] = {-1,-1}, stdout_p[2] = {-1,-1}, stderr_p[2] = {-1,-1}, ipc_p[2] = {-1,-1}, master = -1; + if (opts.use_terminal) { +#if defined(__APPLE__) || defined(__linux__) + struct winsize ws; ws.ws_col = (unsigned short)opts.terminal.cols; ws.ws_row = (unsigned short)opts.terminal.rows; + pid_t pid = forkpty(&master, nullptr, nullptr, &ws); + if (pid == 0) { + if (!opts.cwd.empty()) chdir(opts.cwd.c_str()); + std::vector av; for (const auto& s : cmd) av.push_back(const_cast(s.c_str())); av.push_back(nullptr); + execvp(av[0], av.data()); exit(1); + } else if (pid > 0) { + auto proc = std::make_shared(pid, master, master, -1); + { std::lock_guard lock(m_mutex); m_subprocesses[id] = proc; } + start_monitoring(id, proc); return (int)pid; + } +#endif + } + posix_spawn_file_actions_t acts; posix_spawn_file_actions_init(&acts); + if (opts.stdin_type == "pipe") { pipe(stdin_p); posix_spawn_file_actions_adddup2(&acts, stdin_p[0], STDIN_FILENO); posix_spawn_file_actions_addclose(&acts, stdin_p[1]); } + if (opts.stdout_type == "pipe") { pipe(stdout_p); posix_spawn_file_actions_adddup2(&acts, stdout_p[1], STDOUT_FILENO); posix_spawn_file_actions_addclose(&acts, stdout_p[0]); } + if (opts.stderr_type == "pipe") { pipe(stderr_p); posix_spawn_file_actions_adddup2(&acts, stderr_p[1], STDERR_FILENO); posix_spawn_file_actions_addclose(&acts, stderr_p[0]); } + if (opts.has_ipc) { pipe(ipc_p); posix_spawn_file_actions_adddup2(&acts, ipc_p[0], 3); } + std::vector av; for (const auto& s : cmd) av.push_back(const_cast(s.c_str())); av.push_back(nullptr); + pid_t pid; int st = posix_spawn(&pid, av[0], &acts, nullptr, av.data(), environ); + posix_spawn_file_actions_destroy(&acts); + if (stdin_p[0] != -1) close(stdin_p[0]); if (stdout_p[1] != -1) close(stdout_p[1]); if (stderr_p[1] != -1) close(stderr_p[1]); if (ipc_p[0] != -1) close(ipc_p[0]); + if (st != 0) { + if (stdin_p[1] != -1) close(stdin_p[1]); if (stdout_p[0] != -1) close(stdout_p[0]); if (stderr_p[0] != -1) close(stderr_p[0]); if (ipc_p[1] != -1) close(ipc_p[1]); + return 0; + } + auto proc = std::make_shared(pid, stdin_p[1], stdout_p[0], stderr_p[0], ipc_p[1]); + { std::lock_guard lock(m_mutex); m_subprocesses[id] = proc; } + start_monitoring(id, proc); return (int)pid; +#endif + } + + sync_result spawn_sync(const std::vector& cmd, const spawn_options& opts) { + sync_result res; +#ifdef _WIN32 + SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; + HANDLE hOutR, hOutW, hErrR, hErrW; + CreatePipe(&hOutR, &hOutW, &sa, 0); SetHandleInformation(hOutR, HANDLE_FLAG_INHERIT, 0); + CreatePipe(&hErrR, &hErrW, &sa, 0); SetHandleInformation(hErrR, HANDLE_FLAG_INHERIT, 0); + STARTUPINFOA si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.hStdOutput = hOutW; si.hStdError = hErrW; si.dwFlags |= STARTF_USESTDHANDLES; + std::string cl = ""; for (const auto& s : cmd) { if (!cl.empty()) cl += " "; cl += "\"" + s + "\""; } + if (CreateProcessA(NULL, (LPSTR)cl.c_str(), NULL, NULL, TRUE, 0, NULL, opts.cwd.empty() ? NULL : opts.cwd.c_str(), &si, &pi)) { + CloseHandle(hOutW); CloseHandle(hErrW); + auto r_all = [](HANDLE h) { std::string d; char b[4096]; DWORD n; while (ReadFile(h, b, sizeof(b), &n, NULL) && n > 0) d.append(b, n); CloseHandle(h); return d; }; + std::thread t1([&] { res.stdout_data = r_all(hOutR); }); std::thread t2([&] { res.stderr_data = r_all(hErrR); }); + WaitForSingleObject(pi.hProcess, INFINITE); DWORD ec; GetExitCodeProcess(pi.hProcess, &ec); res.exit_code = (int)ec; + t1.join(); t2.join(); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + } +#else + int so[2], se[2]; pipe(so); pipe(se); + pid_t pid = fork(); + if (pid == 0) { + if (!opts.cwd.empty()) chdir(opts.cwd.c_str()); + dup2(so[1], STDOUT_FILENO); dup2(se[1], STDERR_FILENO); close(so[0]); close(se[0]); + std::vector av; for (const auto& s : cmd) av.push_back(const_cast(s.c_str())); av.push_back(nullptr); + execvp(av[0], av.data()); exit(1); + } else if (pid > 0) { + close(so[1]); close(se[1]); + auto r_all = [](int fd) { std::string d; char b[4096]; ssize_t n; while ((n = read(fd, b, sizeof(b))) > 0) d.append(b, n); close(fd); return d; }; + std::thread t1([&] { res.stdout_data = r_all(so[0]); }); std::thread t2([&] { res.stderr_data = r_all(se[0]); }); + int st; struct rusage usage; wait4(pid, &st, 0, &usage); + res.exit_code = WIFEXITED(st) ? WEXITSTATUS(st) : -1; + t1.join(); t2.join(); long mr = usage.ru_maxrss; +#ifdef __APPLE__ + mr /= 1024; +#endif + res.usage_json = "{\"maxRSS\":" + std::to_string(mr) + "}"; + } +#endif + return res; + } + + void kill(const std::string& id, int sig) { std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->kill(sig); } + void stdin_write(const std::string& id, const std::string& data) { std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->stdin_write(data); } + void stdin_close(const std::string& id) { std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->stdin_close(); } + void ipc_send(const std::string& id, const std::string& msg) { +#ifndef _WIN32 + std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->ipc_send(msg); +#endif + } + void ipc_disconnect(const std::string& id) { +#ifndef _WIN32 + std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->ipc_disconnect(); +#endif + } + void terminal_resize(const std::string& id, int cols, int rows) { +#ifndef _WIN32 + std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->resize(cols, rows); +#endif + } + +private: + dispatcher_t m_dispatcher; std::function m_evaluator; std::mutex m_mutex; +#ifdef _WIN32 + std::map> m_subprocesses; + void start_monitoring(const std::string& id, std::shared_ptr proc) { + if (proc->get_stdout() != INVALID_HANDLE_VALUE) { + std::thread([this, id, proc]() { + char b[4096]; DWORD n; + while (ReadFile(proc->get_stdout(), b, sizeof(b), &n, NULL) && n > 0) { + std::string d(b, n); m_dispatcher([this, id, d]() { m_evaluator("window.alloy.__on_data(" + json_escape(id) + ", 'stdout', " + json_escape(base64_encode(d)) + ", true)"); return noresult{}; }); + } + }).detach(); + } + std::thread([this, id, proc]() { + WaitForSingleObject(proc->get_process(), INFINITE); DWORD ec; GetExitCodeProcess(proc->get_process(), &ec); + m_dispatcher([this, id, ec]() { m_evaluator("window.alloy.__on_exit(" + json_escape(id) + ", " + std::to_string(ec) + ", {})"); { std::lock_guard lock(m_mutex); m_subprocesses.erase(id); } return noresult{}; }); + }).detach(); + } +#else + std::map> m_subprocesses; + void start_monitoring(const std::string& id, std::shared_ptr proc) { + auto create_reader = [this, id](int fd, const std::string& stream) { + if (fd == -1) return; + std::thread([this, id, fd, stream]() { + char b[4096]; ssize_t n; + while ((n = read(fd, b, sizeof(b))) > 0) { + std::string d(b, n); m_dispatcher([this, id, stream, d]() { + m_evaluator("window.alloy.__on_data(" + json_escape(id) + ", " + json_escape(stream) + ", " + json_escape(base64_encode(d)) + ", true)"); + return noresult{}; + }); + } + }).detach(); + }; + create_reader(proc->get_stdout(), "stdout"); + create_reader(proc->get_stderr(), "stderr"); + create_reader(proc->get_ipc_fd(), "ipc"); + std::thread([this, id, proc]() { + int st; struct rusage usage; wait4(proc->get_pid(), &st, 0, &usage); + int ec = WIFEXITED(st) ? WEXITSTATUS(st) : -1; + m_dispatcher([this, id, ec, usage]() { + long mr = usage.ru_maxrss; +#ifdef __APPLE__ + mr /= 1024; +#endif + std::string uj = "{\"maxRSS\":" + std::to_string(mr) + "}"; + m_evaluator("window.alloy.__on_exit(" + json_escape(id) + ", " + std::to_string(ec) + ", " + uj + ")"); + { std::lock_guard lock(m_mutex); m_subprocesses.erase(id); } + return noresult{}; + }); + }).detach(); + } +#endif +}; + +} // namespace detail +} // namespace webview + +#endif // WEBVIEW_ALLOY_HH diff --git a/core/include/webview/alloy_bindings.hh b/core/include/webview/alloy_bindings.hh new file mode 100644 index 000000000..739f5e718 --- /dev/null +++ b/core/include/webview/alloy_bindings.hh @@ -0,0 +1,108 @@ +#ifndef WEBVIEW_ALLOY_BINDINGS_HH +#define WEBVIEW_ALLOY_BINDINGS_HH + +#include "alloy.hh" +#include "json.hh" +#include +#include + +namespace webview { +namespace detail { + +inline std::vector parse_alloy_json_array(const std::string& json) { + std::vector res; + for (int i = 0; ; ++i) { + auto val = json_parse(json, "", i); + if (val.empty()) { + const char* v; size_t vsz; + if (json_parse_c(json.c_str(), json.length(), nullptr, i, &v, &vsz) != 0) break; + res.push_back(val); + } else res.push_back(val); + } + return res; +} + +inline void setup_alloy_bindings(alloy_runtime& runtime, std::function, void*)> bind_fn) { + bind_fn("__alloy_spawn", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + auto cmd_json = json_parse(req, "", 1); + auto opts_json = json_parse(req, "", 2); + std::vector cmd = parse_alloy_json_array(cmd_json); + spawn_options opts; + opts.stdin_type = json_parse(opts_json, "stdin", -1); + if (opts.stdin_type.empty()) opts.stdin_type = "none"; + opts.stdout_type = json_parse(opts_json, "stdout", -1); + if (opts.stdout_type.empty()) opts.stdout_type = "pipe"; + opts.stderr_type = json_parse(opts_json, "stderr", -1); + if (opts.stderr_type.empty()) opts.stderr_type = "inherit"; + opts.cwd = json_parse(opts_json, "cwd", -1); + opts.has_ipc = !json_parse(opts_json, "ipc", -1).empty(); + auto terminal_json = json_parse(opts_json, "terminal", -1); + if (!terminal_json.empty()) { + opts.use_terminal = true; + auto cols_str = json_parse(terminal_json, "cols", -1); + if (!cols_str.empty()) opts.terminal.cols = std::stoi(cols_str); + auto rows_str = json_parse(terminal_json, "rows", -1); + if (!rows_str.empty()) opts.terminal.rows = std::stoi(rows_str); + } + int pid = runtime.spawn(proc_id, cmd, opts); + return "{\"pid\":" + std::to_string(pid) + "}"; + }, nullptr); + + bind_fn("__alloy_spawnSync", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto cmd_json = json_parse(req, "", 0); + auto opts_json = json_parse(req, "", 1); + std::vector cmd = parse_alloy_json_array(cmd_json); + spawn_options opts; + opts.cwd = json_parse(opts_json, "cwd", -1); + auto res = runtime.spawn_sync(cmd, opts); + return "{\"exitCode\":" + std::to_string(res.exit_code) + ",\"stdout\":" + json_escape(base64_encode(res.stdout_data)) + ",\"stderr\":" + json_escape(base64_encode(res.stderr_data)) + ",\"resourceUsage\":" + res.usage_json + ",\"success\":" + (res.exit_code == 0 ? "true" : "false") + ",\"isBase64\":true}"; + }, nullptr); + + bind_fn("__alloy_kill", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + auto signal_str = json_parse(req, "", 1); + int sig = 15; if (signal_str == "SIGKILL" || signal_str == "9") sig = 9; + runtime.kill(proc_id, sig); + }, nullptr); + + bind_fn("__alloy_stdin_write", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + auto data = json_parse(req, "", 1); + runtime.stdin_write(proc_id, data); + }, nullptr); + + bind_fn("__alloy_stdin_close", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + runtime.stdin_close(proc_id); + }, nullptr); + + bind_fn("__alloy_ipc_send", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + auto message = json_parse(req, "", 1); + runtime.ipc_send(proc_id, message); + }, nullptr); + + bind_fn("__alloy_ipc_disconnect", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + runtime.ipc_disconnect(proc_id); + }, nullptr); + + bind_fn("__alloy_terminal_write", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto term_id = json_parse(req, "", 0); + auto data = json_parse(req, "", 1); + runtime.stdin_write(term_id, data); + }, nullptr); + + bind_fn("__alloy_terminal_resize", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto term_id = json_parse(req, "", 0); + int cols = std::stoi(json_parse(req, "", 1)); + int rows = std::stoi(json_parse(req, "", 2)); + runtime.terminal_resize(term_id, cols, rows); + }, nullptr); +} + +} // namespace detail +} // namespace webview + +#endif // WEBVIEW_ALLOY_BINDINGS_HH diff --git a/core/include/webview/alloy_js.hh b/core/include/webview/alloy_js.hh new file mode 100644 index 000000000..e8aaa0edc --- /dev/null +++ b/core/include/webview/alloy_js.hh @@ -0,0 +1,219 @@ +#ifndef WEBVIEW_ALLOY_JS_HH +#define WEBVIEW_ALLOY_JS_HH + +namespace webview { +namespace detail { + +const char* alloy_bootstrap_js = R"js( +(function() { + 'use strict'; + + function generateId() { + var crypto = window.crypto || window.msCrypto; + var bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + return Array.prototype.slice.call(bytes).map(function(n) { + var s = n.toString(16); + return ((s.length % 2) == 1 ? '0' : '') + s; + }).join(''); + } + + const _promises = {}; + + function callNative(method, ...params) { + const id = generateId(); + const promise = new Promise((resolve, reject) => { + _promises[id] = { resolve, reject }; + }); + window.__webview__.post(JSON.stringify({ + id: id, + method: method, + params: params + })); + return promise; + } + + const _originalOnReply = window.__webview__.onReply; + window.__webview__.onReply = function(id, status, result) { + if (_promises[id]) { + if (status === 0) _promises[id].resolve(result); + else _promises[id].reject(result); + delete _promises[id]; + } else if (_originalOnReply) { + _originalOnReply.apply(this, arguments); + } + }; + + class Terminal { + constructor(id, options) { + this.id = id; + this.options = options; + this.closed = false; + } + write(data) { callNative('__alloy_terminal_write', this.id, data); } + resize(cols, rows) { callNative('__alloy_terminal_resize', this.id, cols, rows); } + setRawMode(enabled) { /* TODO if needed */ } + close() { /* TODO if needed */ this.closed = true; } + ref() {} + unref() {} + } + + class Subprocess { + constructor(id, options) { + this.id = id; + this.options = options; + this.pid = -1; + this.exitCode = null; + this.signalCode = null; + this.killed = false; + this._resourceUsage = null; + this.exited = new Promise(resolve => { this._exited_resolve = resolve; }); + + if (options.terminal) { + this.terminal = new Terminal(id, options.terminal); + } else { + if (options.stdout !== 'inherit' && options.stdout !== 'ignore') { + this.stdout = new ReadableStream({ start: (c) => { this._stdout_controller = c; } }); + } + if (options.stderr === 'pipe') { + this.stderr = new ReadableStream({ start: (c) => { this._stderr_controller = c; } }); + } + + if (options.stdin instanceof ReadableStream) { + this._pipeStdin(options.stdin); + } else if (options.stdin === 'pipe') { + this.stdin = { + write: (data) => { callNative('__alloy_stdin_write', this.id, data); }, + end: () => { callNative('__alloy_stdin_close', this.id); }, + flush: () => {} + }; + } + } + } + + async _pipeStdin(stream) { + const reader = stream.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + await callNative('__alloy_stdin_write', this.id, value); + } + } finally { + await callNative('__alloy_stdin_close', this.id); + } + } + + kill(signal = 'SIGTERM') { callNative('__alloy_kill', this.id, signal); this.killed = true; } + send(message) { callNative('__alloy_ipc_send', this.id, JSON.stringify(message)); } + disconnect() { callNative('__alloy_ipc_disconnect', this.id); } + resourceUsage() { return this._resourceUsage; } + unref() {} + ref() {} + } + + const _subprocesses = new Map(); + + window.alloy = { + spawn: function(cmd, options = {}) { + let actualCmd = Array.isArray(cmd) ? cmd : (cmd.cmd || [cmd]); + let opts = Object.assign({ stdout: 'pipe', stderr: 'inherit', stdin: 'none', serialization: 'advanced' }, + typeof cmd === 'object' && !Array.isArray(cmd) ? cmd : options); + + const procId = generateId(); + const proc = new Subprocess(procId, opts); + _subprocesses.set(procId, proc); + + let nativeStdin = opts.stdin; + if (opts.stdin instanceof ReadableStream) nativeStdin = 'pipe'; + + callNative('__alloy_spawn', procId, actualCmd, Object.assign({}, opts, { stdin: nativeStdin })).then(result => { + if (result) { + const parsed = JSON.parse(result); + if (parsed.pid) proc.pid = parsed.pid; + } + }); + return proc; + }, + + spawnSync: function(cmd, options = {}) { + let actualCmd = Array.isArray(cmd) ? cmd : (cmd.cmd || [cmd]); + let opts = Object.assign({}, typeof cmd === 'object' && !Array.isArray(cmd) ? cmd : options); + return callNative('__alloy_spawnSync', actualCmd, opts).then(res => { + const parsed = JSON.parse(res); + if (parsed.isBase64) { + const dec = new TextDecoder(); + if (parsed.stdout) { + const bin = atob(parsed.stdout); + const b = new Uint8Array(bin.length); + for(let i=0; i #include @@ -313,7 +316,17 @@ protected: dispatch([=] { context.call(id, args); }); } - virtual void on_window_created() { inc_window_count(); } + virtual void on_window_created() { + inc_window_count(); + m_alloy_runtime = std::unique_ptr(new alloy_runtime( + [this](std::function f) { return dispatch(f); }, + [this](const std::string &js) { eval(js); })); + setup_alloy_bindings(*m_alloy_runtime, + [this](const std::string &name, binding_t fn, void *arg) { + bind(name, fn, arg); + }); + init(alloy_bootstrap_js); + } virtual void on_window_destroyed(bool skip_termination = false) { if (dec_window_count() <= 0) { @@ -365,6 +378,7 @@ private: } std::map bindings; + std::unique_ptr m_alloy_runtime; user_script *m_bind_script{}; std::list m_user_scripts; diff --git a/core/tests/src/unit_tests.cc b/core/tests/src/unit_tests.cc index 8375255e0..2bfe98762 100644 --- a/core/tests/src/unit_tests.cc +++ b/core/tests/src/unit_tests.cc @@ -186,3 +186,12 @@ TEST_CASE("Ensure that narrow/wide string conversion works on Windows") { REQUIRE(narrow_string(std::wstring(2, L'\0')) == std::string(2, '\0')); } #endif + +TEST_CASE("MetaScript: Basic JSON mapping for spawn options") { + using webview::detail::json_parse; + + std::string opts_json = R"({"stdin":"pipe", "stdout":"pipe", "stderr":"inherit"})"; + REQUIRE(json_parse(opts_json, "stdin", -1) == "pipe"); + REQUIRE(json_parse(opts_json, "stdout", -1) == "pipe"); + REQUIRE(json_parse(opts_json, "stderr", -1) == "inherit"); +} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6a125bb92..d40abba8e 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_alloy_demo MACOSX_BUNDLE WIN32) +target_sources(webview_example_alloy_demo PRIVATE alloy_demo.cc ${SHARED_SOURCES}) +target_link_libraries(webview_example_alloy_demo PRIVATE webview::core Threads::Threads) diff --git a/examples/alloy_demo.cc b/examples/alloy_demo.cc new file mode 100644 index 000000000..2cd966a8a --- /dev/null +++ b/examples/alloy_demo.cc @@ -0,0 +1,53 @@ +#include "webview/webview.h" +#include +#include + +const char html[] = R"html( + + + +

AlloyScript Demo

+ +

+    
+
+
+)html";
+
+#ifdef _WIN32
+int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/,
+                   LPSTR /*lpCmdLine*/, int /*nCmdShow*/) {
+#else
+int main() {
+#endif
+  try {
+    webview::webview w(true, nullptr);
+    w.set_title("AlloyScript Demo");
+    w.set_size(800, 600, WEBVIEW_HINT_NONE);
+    w.set_html(html);
+    w.run();
+  } catch (const webview::exception &e) {
+    std::cerr << e.what() << '\n';
+    return 1;
+  }
+  return 0;
+}
diff --git a/index.ts b/index.ts
new file mode 100644
index 000000000..f67b2c645
--- /dev/null
+++ b/index.ts
@@ -0,0 +1 @@
+console.log("Hello via Bun!");
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..2d62c2b8c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,11 @@
+{
+  "name": "@alloyscript/runtime",
+  "module": "index.ts",
+  "type": "module",
+  "devDependencies": {
+    "@types/bun": "latest"
+  },
+  "peerDependencies": {
+    "typescript": "^5"
+  }
+}
diff --git a/scripts/build_alloy.ts b/scripts/build_alloy.ts
new file mode 100644
index 000000000..db4d18545
--- /dev/null
+++ b/scripts/build_alloy.ts
@@ -0,0 +1,43 @@
+import { build } from "bun";
+import { writeFileSync } from "node:fs";
+
+async function buildAlloy(sourceFile: string, outputFile: string) {
+    console.log(`Building ${sourceFile}...`);
+
+    const result = await build({
+        entrypoints: [sourceFile],
+        minify: true,
+        target: "browser",
+    });
+
+    if (!result.success) {
+        console.error("Build failed:", result.logs);
+        process.exit(1);
+    }
+
+    const transpiledJs = await result.outputs[0].text();
+
+    const cppTemplate = `
+#include "webview/webview.h"
+#include 
+
+int main() {
+    webview::webview w(false, nullptr);
+    w.set_title("AlloyScript App");
+    w.set_size(1024, 768, WEBVIEW_HINT_NONE);
+    std::string js = R"js(${transpiledJs})js";
+    w.init(js);
+    w.set_html("
"); + w.run(); + return 0; +} +`; + + const tempCpp = "temp_app.cpp"; + writeFileSync(tempCpp, cppTemplate); + console.log(`Embedded source into ${tempCpp}. Use CMake to build the final binary.`); +} + +const source = process.argv[2] || "index.ts"; +const output = process.argv[3] || "app"; +buildAlloy(source, output); diff --git a/tests/spawn.test.ts b/tests/spawn.test.ts new file mode 100644 index 000000000..f3a8757ee --- /dev/null +++ b/tests/spawn.test.ts @@ -0,0 +1,34 @@ +import { expect, test, describe } from "bun:test"; + +describe("Spawn Bridge", () => { + test("alloy.spawn exists", () => { + expect((window as any).alloy.spawn).toBeDefined(); + }); + + test("should spawn a process and read stdout", async () => { + const proc = (window as any).alloy.spawn(["echo", "hello"]); + const text = await proc.stdout.text(); + expect(text.trim()).toBe("hello"); + const exitCode = await proc.exited; + expect(exitCode).toBe(0); + expect(proc.pid).toBeGreaterThan(0); + }); + + test("should handle spawnSync", async () => { + const result = await (window as any).alloy.spawnSync(["echo", "sync"]); + const dec = new TextDecoder(); + expect(dec.decode(result.stdout).trim()).toBe("sync"); + expect(result.exitCode).toBe(0); + }); + + test("should pipe stdin", async () => { + const proc = (window as any).alloy.spawn(["cat"], { + stdin: "pipe", + stdout: "pipe" + }); + proc.stdin.write("alloy"); + proc.stdin.end(); + const text = await proc.stdout.text(); + expect(text.trim()).toBe("alloy"); + }); +}); diff --git a/tests/sqlite.test.ts b/tests/sqlite.test.ts new file mode 100644 index 000000000..00b40e117 --- /dev/null +++ b/tests/sqlite.test.ts @@ -0,0 +1,13 @@ +import { expect, test, describe } from "bun:test"; + +describe("SQLite Bridge", () => { + test("should open a database and execute queries", async () => { + const db = new (window as any).alloy.sqlite.Database(":memory:"); + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); + db.run("INSERT INTO users (name) VALUES (?)", ["Alice"]); + const row = db.query("SELECT * FROM users WHERE name = ?").get("Alice"); + expect(row).toBeDefined(); + expect(row.name).toBe("Alice"); + db.close(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..bfa0fead5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}