Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# Build artifacts
/build
node_modules/
temp_app.cpp
test_meta_json
test_spawn
25 changes: 25 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

320 changes: 320 additions & 0 deletions core/include/webview/alloy.hh

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions core/include/webview/alloy_bindings.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#ifndef WEBVIEW_ALLOY_BINDINGS_HH
#define WEBVIEW_ALLOY_BINDINGS_HH

#include "alloy.hh"
#include "json.hh"
#include <string>
#include <vector>

namespace webview {
namespace detail {

inline std::vector<std::string> parse_alloy_json_array(const std::string& json) {
std::vector<std::string> 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(const std::string&, std::function<void(const std::string&, const std::string&, void*)>, 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<std::string> 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<std::string> 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
219 changes: 219 additions & 0 deletions core/include/webview/alloy_js.hh
Original file line number Diff line number Diff line change
@@ -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<bin.length; i++) b[i] = bin.charCodeAt(i);
parsed.stdout = b;
}
if (parsed.stderr) {
const bin = atob(parsed.stderr);
const b = new Uint8Array(bin.length);
for(let i=0; i<bin.length; i++) b[i] = bin.charCodeAt(i);
parsed.stderr = b;
}
}
return parsed;
});
},

__on_data: function(id, stream, data, isBase64) {
const proc = _subprocesses.get(id);
if (!proc) return;

let finalData = data;
if (isBase64) {
const bin = atob(data);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
finalData = bytes;
}

if (stream === 'ipc') {
if (proc.options.ipc) {
const str = new TextDecoder().decode(finalData);
try { proc.options.ipc(JSON.parse(str), proc); } catch(e) { proc.options.ipc(str, proc); }
}
} else if (proc.terminal) {
if (proc.options.terminal.data) proc.options.terminal.data(proc.terminal, finalData);
} else {
const c = stream === 'stdout' ? proc._stdout_controller : proc._stderr_controller;
if (c) c.enqueue(finalData);
}
},

__on_exit: function(id, exitCode, usage) {
const proc = _subprocesses.get(id);
if (!proc) return;
proc.exitCode = exitCode;
proc._resourceUsage = usage;
if (proc._stdout_controller) proc._stdout_controller.close();
if (proc._stderr_controller) proc._stderr_controller.close();
proc._exited_resolve(exitCode);
if (proc.options.onExit) proc.options.onExit(proc, exitCode, null);
}
};

if (!ReadableStream.prototype.text) {
ReadableStream.prototype.text = async function() {
const reader = this.getReader();
let res = '', dec = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res += dec.decode(value);
}
return res;
};
}
window.alloy.Terminal = Terminal;
})();
)js";

} // namespace detail
} // namespace webview

#endif // WEBVIEW_ALLOY_JS_HH
16 changes: 15 additions & 1 deletion core/include/webview/detail/engine_base.hh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
#include "../types.hh"
#include "json.hh"
#include "user_script.hh"
#include "../alloy.hh"
#include "../alloy_js.hh"
#include "../alloy_bindings.hh"

#include <atomic>
#include <functional>
Expand Down Expand Up @@ -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<alloy_runtime>(new alloy_runtime(
[this](std::function<void()> 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) {
Expand Down Expand Up @@ -365,6 +378,7 @@ private:
}

std::map<std::string, binding_ctx_t> bindings;
std::unique_ptr<alloy_runtime> m_alloy_runtime;
user_script *m_bind_script{};
std::list<user_script> m_user_scripts;

Expand Down
Loading