2752 lines
108 KiB
C++
2752 lines
108 KiB
C++
// W3 — production wasm workspace runtime + membrane (WASM-PROPOSAL §6, §7).
|
||
//
|
||
// One workspace per request: a fresh Wasmtime store holding the core module
|
||
// instance (production uce_lib carve-out, owns memory/allocator/DValue) plus unit
|
||
// PIC side modules loaded lazily — including mid-request via the
|
||
// uce_host_component_resolve hostcall, which is how component()/unit_render()
|
||
// inside the guest trigger dynamic loading.
|
||
//
|
||
// Designed for amalgamation-include (like uce_lib.cpp): the including TU must
|
||
// provide String/DValue/ucb_encode/ucb_decode (types.cpp + dvalue.cpp) and
|
||
// wasm_trace.h. W4 includes this from the FastCGI worker build; the W3 CLI
|
||
// driver (w3_driver.cpp) is the first consumer.
|
||
//
|
||
// Loader rules carried from the spike phases, now enforced as policy:
|
||
// - import discipline: units may import only env / GOT.mem / GOT.func
|
||
// (no WASI — the core is zero-WASI for UCE's own calls, and units get
|
||
// everything from the core); units defining allocator symbols are rejected
|
||
// - GOT.mem of a PIC module's data exports are __memory_base-relative
|
||
// offsets; the loader adds the owning module's base (Phase 0 erratum)
|
||
// - GOT.func resolves host-side: the target function's funcref is placed
|
||
// into the shared table and the slot index becomes the GOT value
|
||
// - export name collisions: core wins, then first-loading unit wins
|
||
// (mirrors native dlopen global-symbol interposition for identical
|
||
// vague-linkage template instantiations)
|
||
// - uce.abi stamp must match the core ABI version; fail loudly, never guess
|
||
|
||
#include <wasmtime.hh>
|
||
|
||
#include <chrono>
|
||
#include <cmath>
|
||
#include <cstring>
|
||
#include <ctime>
|
||
#include <fstream>
|
||
#include <filesystem>
|
||
#include <map>
|
||
#include <set>
|
||
#include <cstdio>
|
||
#include <memory>
|
||
#include <optional>
|
||
#include <string>
|
||
#include <arpa/inet.h>
|
||
#include <netinet/in.h>
|
||
#include <sys/socket.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/file.h>
|
||
#include <fcntl.h>
|
||
#include <limits.h>
|
||
#include <unistd.h>
|
||
#include <sys/time.h>
|
||
#include <vector>
|
||
#include <algorithm>
|
||
#include <dirent.h>
|
||
#include <cerrno>
|
||
#include <sys/wait.h>
|
||
#include <signal.h>
|
||
#include <poll.h>
|
||
|
||
struct WasmDylinkInfo
|
||
{
|
||
u32 mem_size = 0;
|
||
u32 mem_align = 0;
|
||
u32 table_size = 0;
|
||
u32 table_align = 0;
|
||
bool found = false;
|
||
};
|
||
|
||
struct WasmAbiInfo
|
||
{
|
||
u32 version = 0;
|
||
String toolchain;
|
||
bool found = false;
|
||
};
|
||
|
||
struct WasmUnitModule
|
||
{
|
||
String source_path; // absolute .uce path
|
||
String wasm_path; // artifact in the unit cache
|
||
time_t mtime = 0;
|
||
WasmDylinkInfo dylink;
|
||
WasmAbiInfo abi;
|
||
std::optional<wasmtime::Module> module;
|
||
};
|
||
|
||
struct WasmWorkerConfig
|
||
{
|
||
String core_wasm_path = "bin/wasm/core.wasm";
|
||
String site_root; // absolute
|
||
String cache_root = "/tmp/uce/work";
|
||
std::vector<String> write_roots; // absolute prefixes the write membrane allows
|
||
int64_t memory_limit = 512ll * 1024 * 1024;
|
||
u32 table_headroom = 4096;
|
||
u64 epoch_deadline_ticks = 200; // ticker period × ticks = CPU budget
|
||
bool verbose = false;
|
||
// uce_host_* names (bare, without the "uce_host_" prefix) the sysadmin has
|
||
// disabled via UCE_HOSTCALL_BLOCKLIST. A blocked hostcall resolves to a trap
|
||
// stub at workspace birth (see make_host_import); empty = feature off.
|
||
std::set<String> hostcall_blocklist;
|
||
};
|
||
|
||
struct WasmResponse
|
||
{
|
||
bool ok = false;
|
||
bool handler_present = true; // false → unit has no handler for the requested kind (404)
|
||
String body;
|
||
DValue meta; // status / headers / cookies / session
|
||
String error; // collapsed trace or loader error when !ok
|
||
u64 workspace_birth_us = 0;
|
||
u64 component_resolve_count = 0;
|
||
u64 component_resolve_total_us = 0;
|
||
};
|
||
|
||
static u64 wasm_file_lock_timeout_ms()
|
||
{
|
||
const char* raw = getenv("UCE_FILE_LOCK_TIMEOUT_MS");
|
||
if(!raw || !*raw)
|
||
return(2000);
|
||
char* end = 0;
|
||
unsigned long long parsed = strtoull(raw, &end, 10);
|
||
return(end == raw ? 2000 : (u64)parsed);
|
||
}
|
||
|
||
static u64 wasm_monotonic_ms()
|
||
{
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return((u64)ts.tv_sec * 1000ull + (u64)ts.tv_nsec / 1000000ull);
|
||
}
|
||
|
||
static int wasm_open_locked_file(const String& file_name, int flags, int lock_type, bool truncate_after_lock)
|
||
{
|
||
int fd = open(file_name.c_str(), flags, 0644);
|
||
if(fd < 0)
|
||
return(-1);
|
||
fcntl(fd, F_SETFD, FD_CLOEXEC);
|
||
u64 timeout = wasm_file_lock_timeout_ms();
|
||
u64 deadline = wasm_monotonic_ms() + timeout;
|
||
while(true)
|
||
{
|
||
if(flock(fd, lock_type | LOCK_NB) == 0)
|
||
{
|
||
if(truncate_after_lock && ftruncate(fd, 0) != 0)
|
||
{
|
||
flock(fd, LOCK_UN);
|
||
close(fd);
|
||
return(-1);
|
||
}
|
||
return(fd);
|
||
}
|
||
if(errno != EWOULDBLOCK && errno != EAGAIN && errno != EINTR)
|
||
{
|
||
close(fd);
|
||
return(-1);
|
||
}
|
||
if(timeout == 0 || wasm_monotonic_ms() >= deadline)
|
||
{
|
||
fprintf(stderr, "[wasm] file lock timeout after %llums: %s\n", (unsigned long long)timeout, file_name.c_str());
|
||
close(fd);
|
||
return(-1);
|
||
}
|
||
usleep(10000);
|
||
}
|
||
}
|
||
|
||
static bool wasm_fd_write_all(int fd, const char* data, size_t remaining, u64* written_out)
|
||
{
|
||
u64 total = 0;
|
||
while(remaining > 0)
|
||
{
|
||
ssize_t n = write(fd, data, remaining);
|
||
if(n < 0)
|
||
{
|
||
if(errno == EINTR)
|
||
continue;
|
||
return(false);
|
||
}
|
||
if(n == 0)
|
||
return(false);
|
||
data += n;
|
||
remaining -= (size_t)n;
|
||
total += (u64)n;
|
||
}
|
||
if(written_out)
|
||
*written_out = total;
|
||
return(true);
|
||
}
|
||
|
||
|
||
|
||
// ---- file-backed async job registry + bounded process execution ----------
|
||
|
||
static String uce_job_root()
|
||
{
|
||
const char* env = getenv("UCE_JOB_ROOT");
|
||
String root = (env && *env) ? String(env) : String("/run/uce/jobs");
|
||
std::error_code ec;
|
||
std::filesystem::create_directories(root, ec);
|
||
if(ec)
|
||
{
|
||
root = "/tmp/uce/jobs";
|
||
std::filesystem::create_directories(root, ec);
|
||
}
|
||
return(root);
|
||
}
|
||
|
||
static String uce_job_path(u64 id) { return(uce_job_root() + "/" + std::to_string(id)); }
|
||
static String uce_read_text(const String& path) { std::ifstream in(path, std::ios::binary); if(!in) return(""); std::ostringstream ss; ss << in.rdbuf(); return(ss.str()); }
|
||
static void uce_write_text(const String& path, const String& data) { std::ofstream out(path, std::ios::binary|std::ios::trunc); out.write(data.data(), (std::streamsize)data.size()); }
|
||
|
||
static u64 uce_job_new(const String& kind)
|
||
{
|
||
String root = uce_job_root();
|
||
std::error_code ec;
|
||
std::filesystem::create_directories(root, ec);
|
||
u64 seed = ((u64)time(0) << 32) ^ ((u64)getpid() << 16) ^ (u64)rand();
|
||
for(int i = 0; i < 100; i++)
|
||
{
|
||
u64 id = seed ^ (wasm_monotonic_ms() + (u64)i * 0x9e3779b97f4a7c15ull);
|
||
String dir = root + "/" + std::to_string(id);
|
||
if(mkdir(dir.c_str(), 0700) == 0)
|
||
{
|
||
uce_write_text(dir + "/kind", kind);
|
||
uce_write_text(dir + "/created", std::to_string((u64)time(0)));
|
||
uce_write_text(dir + "/state", "pending");
|
||
return(id);
|
||
}
|
||
}
|
||
return(0);
|
||
}
|
||
|
||
static void uce_job_reap()
|
||
{
|
||
String root = uce_job_root();
|
||
u64 now = (u64)time(0);
|
||
u64 ttl = 3600;
|
||
if(const char* raw = getenv("UCE_JOB_TTL_SECONDS")) { char* e=0; unsigned long long v=strtoull(raw,&e,10); if(e!=raw && v>0) ttl=(u64)v; }
|
||
std::error_code ec;
|
||
for(auto& e : std::filesystem::directory_iterator(root, ec))
|
||
{
|
||
if(!e.is_directory()) continue;
|
||
u64 created = strtoull(uce_read_text(e.path().string()+"/created").c_str(), 0, 10);
|
||
if(created > 0 && now > created + ttl)
|
||
std::filesystem::remove_all(e.path(), ec);
|
||
}
|
||
}
|
||
|
||
static DValue uce_process_exec(String cmd, String input, StringMap env, u64 timeout_ms)
|
||
{
|
||
DValue r;
|
||
r["exit_code"] = (f64)-1;
|
||
r["stdout"] = "";
|
||
r["stderr"] = "";
|
||
r["timed_out"].set_bool(false);
|
||
if(timeout_ms == 0) timeout_ms = 5000;
|
||
int inpipe[2], outpipe[2], errpipe[2];
|
||
if(pipe(inpipe) || pipe(outpipe) || pipe(errpipe)) { r["stderr"]="pipe failed"; return(r); }
|
||
pid_t pid = fork();
|
||
if(pid == 0)
|
||
{
|
||
dup2(inpipe[0], 0); dup2(outpipe[1], 1); dup2(errpipe[1], 2);
|
||
close(inpipe[0]); close(inpipe[1]); close(outpipe[0]); close(outpipe[1]); close(errpipe[0]); close(errpipe[1]);
|
||
for(auto& kv : env) setenv(kv.first.c_str(), kv.second.c_str(), 1);
|
||
execl("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0);
|
||
_exit(127);
|
||
}
|
||
close(inpipe[0]); close(outpipe[1]); close(errpipe[1]);
|
||
fcntl(outpipe[0], F_SETFL, fcntl(outpipe[0], F_GETFL, 0) | O_NONBLOCK);
|
||
fcntl(errpipe[0], F_SETFL, fcntl(errpipe[0], F_GETFL, 0) | O_NONBLOCK);
|
||
size_t input_off = 0; bool in_open = true, out_open = true, err_open = true; int status = 0; bool exited = false;
|
||
u64 deadline = wasm_monotonic_ms() + timeout_ms;
|
||
while(out_open || err_open || !exited)
|
||
{
|
||
if(!exited)
|
||
{
|
||
pid_t w = waitpid(pid, &status, WNOHANG);
|
||
if(w == pid) exited = true;
|
||
}
|
||
if(in_open)
|
||
{
|
||
if(input_off < input.size()) { ssize_t n=write(inpipe[1], input.data()+input_off, input.size()-input_off); if(n>0) input_off += (size_t)n; else if(n<0 && errno!=EINTR) { close(inpipe[1]); in_open=false; } }
|
||
else { close(inpipe[1]); in_open=false; }
|
||
}
|
||
char buf[4096];
|
||
ssize_t n;
|
||
while((n=read(outpipe[0], buf, sizeof(buf))) > 0) r["stdout"] = r["stdout"].to_string() + String(buf, n);
|
||
if(n == 0 && out_open) { close(outpipe[0]); out_open=false; }
|
||
while((n=read(errpipe[0], buf, sizeof(buf))) > 0) r["stderr"] = r["stderr"].to_string() + String(buf, n);
|
||
if(n == 0 && err_open) { close(errpipe[0]); err_open=false; }
|
||
if(!exited && wasm_monotonic_ms() >= deadline)
|
||
{
|
||
r["timed_out"].set_bool(true);
|
||
kill(pid, SIGKILL);
|
||
waitpid(pid, &status, 0);
|
||
exited = true;
|
||
}
|
||
if((out_open || err_open || !exited)) usleep(10000);
|
||
}
|
||
if(WIFEXITED(status)) { r["exit_code"] = (f64)WEXITSTATUS(status); r["timed_out"].set_bool(false); }
|
||
else if(WIFSIGNALED(status)) r["exit_code"] = (f64)(128 + WTERMSIG(status));
|
||
return(r);
|
||
}
|
||
|
||
static DValue uce_shell_exec_spec(const DValue& spec)
|
||
{
|
||
return(uce_process_exec(spec.key("cmd") ? spec.key("cmd")->to_string() : String(""), spec.key("stdin") ? spec.key("stdin")->to_string() : String(""), spec.key("env") ? spec.key("env")->to_stringmap() : StringMap(), spec.key("timeout_ms") ? spec.key("timeout_ms")->to_u64(5000) : 5000));
|
||
}
|
||
|
||
static void uce_job_finish(u64 id, DValue result, String final_state="done")
|
||
{
|
||
String dir = uce_job_path(id);
|
||
uce_write_text(dir + "/result.tmp", ucb_encode(result));
|
||
rename((dir + "/result.tmp").c_str(), (dir + "/result").c_str());
|
||
uce_write_text(dir + "/state", final_state);
|
||
}
|
||
|
||
static u64 uce_shell_spawn_spec(const DValue& spec)
|
||
{
|
||
uce_job_reap();
|
||
u64 id = uce_job_new("shell");
|
||
if(!id) return(0);
|
||
pid_t pid = fork();
|
||
if(pid == 0)
|
||
{
|
||
setsid();
|
||
uce_write_text(uce_job_path(id) + "/worker_pid", std::to_string((long long)getpid()));
|
||
uce_write_text(uce_job_path(id) + "/state", "running");
|
||
DValue result = uce_shell_exec_spec(spec);
|
||
uce_job_finish(id, result, "done");
|
||
_exit(0);
|
||
}
|
||
if(pid < 0) { DValue r; r["error"]="fork failed"; uce_job_finish(id,r,"failed"); return(id); }
|
||
uce_write_text(uce_job_path(id) + "/worker_pid", std::to_string((long long)pid));
|
||
uce_write_text(uce_job_path(id) + "/state", "running");
|
||
return(id);
|
||
}
|
||
|
||
|
||
static DValue uce_exec_argv_capture(std::vector<String> argv, String input, u64 timeout_ms)
|
||
{
|
||
DValue r; r["exit_code"]=(f64)-1; r["stdout"]=""; r["stderr"]=""; r["timed_out"].set_bool(false);
|
||
if(argv.empty()) { r["stderr"]="empty argv"; return(r); }
|
||
if(timeout_ms == 0) timeout_ms = 5000;
|
||
int inpipe[2], outpipe[2], errpipe[2];
|
||
if(pipe(inpipe)||pipe(outpipe)||pipe(errpipe)) { r["stderr"]="pipe failed"; return(r); }
|
||
pid_t pid=fork();
|
||
if(pid==0)
|
||
{
|
||
dup2(inpipe[0],0); dup2(outpipe[1],1); dup2(errpipe[1],2);
|
||
close(inpipe[0]); close(inpipe[1]); close(outpipe[0]); close(outpipe[1]); close(errpipe[0]); close(errpipe[1]);
|
||
std::vector<char*> args; for(auto& a: argv) args.push_back((char*)a.c_str()); args.push_back(0);
|
||
execvp(args[0], args.data()); _exit(127);
|
||
}
|
||
close(inpipe[0]); close(outpipe[1]); close(errpipe[1]);
|
||
fcntl(outpipe[0], F_SETFL, fcntl(outpipe[0], F_GETFL, 0)|O_NONBLOCK); fcntl(errpipe[0], F_SETFL, fcntl(errpipe[0], F_GETFL, 0)|O_NONBLOCK);
|
||
size_t input_off=0; bool in_open=true,out_open=true,err_open=true,exited=false; int status=0; u64 deadline=wasm_monotonic_ms()+timeout_ms;
|
||
while(out_open || err_open || !exited)
|
||
{
|
||
if(!exited) { pid_t w=waitpid(pid,&status,WNOHANG); if(w==pid) exited=true; }
|
||
if(in_open) { if(input_off<input.size()) { ssize_t n=write(inpipe[1], input.data()+input_off, input.size()-input_off); if(n>0) input_off+=(size_t)n; else if(n<0 && errno!=EINTR) { close(inpipe[1]); in_open=false; } } else { close(inpipe[1]); in_open=false; } }
|
||
char buf[4096]; ssize_t n; while((n=read(outpipe[0],buf,sizeof(buf)))>0) r["stdout"] = r["stdout"].to_string()+String(buf,n); if(n==0&&out_open){close(outpipe[0]);out_open=false;}
|
||
while((n=read(errpipe[0],buf,sizeof(buf)))>0) r["stderr"] = r["stderr"].to_string()+String(buf,n); if(n==0&&err_open){close(errpipe[0]);err_open=false;}
|
||
if(!exited && wasm_monotonic_ms() >= deadline) { r["timed_out"].set_bool(true); kill(pid,SIGKILL); waitpid(pid,&status,0); exited=true; }
|
||
if(out_open || err_open || !exited) usleep(10000);
|
||
}
|
||
if(WIFEXITED(status)) { r["exit_code"]=(f64)WEXITSTATUS(status); r["timed_out"].set_bool(false); } else if(WIFSIGNALED(status)) r["exit_code"]=(f64)(128+WTERMSIG(status));
|
||
return(r);
|
||
}
|
||
|
||
static bool uce_header_name_safe(String name)
|
||
{
|
||
if(name=="") return(false);
|
||
for(unsigned char c: name) if(!(isalnum(c)||c=='-'||c=='_')) return(false);
|
||
return(true);
|
||
}
|
||
|
||
static DValue uce_http_request_value(const DValue& req)
|
||
{
|
||
DValue r; r["status"]=(f64)0; r["headers"].set_array(); r["body"]=""; r["error"]="";
|
||
const DValue* method_value = req.key("method");
|
||
const DValue* url_value = req.key("url");
|
||
const DValue* timeout_value = req.key("timeout_ms");
|
||
const DValue* follow_redirects_value = req.key("follow_redirects");
|
||
const DValue* headers_value = req.key("headers");
|
||
const DValue* body_value = req.key("body");
|
||
String method = method_value ? to_upper(method_value->to_string()) : String("GET");
|
||
String url = url_value ? url_value->to_string() : String("");
|
||
if(url=="" || url.find('\0')!=String::npos) { r["error"]="missing url"; return(r); }
|
||
if(method=="") method="GET";
|
||
u64 timeout_ms = timeout_value ? timeout_value->to_u64(5000) : 5000;
|
||
std::vector<String> argv = {"curl", "-sS", "--http1.0", "-X", method, "-D", "-", "-w", "\nUCE_HTTP_STATUS:%{http_code}", "--max-time", std::to_string(std::max<u64>(1, timeout_ms / 1000))};
|
||
if(follow_redirects_value && follow_redirects_value->to_bool()) argv.push_back("-L");
|
||
if(headers_value) headers_value->each([&](const DValue& v, String k){ if(uce_header_name_safe(k)) { argv.push_back("-H"); argv.push_back(k + ": " + replace(replace(v.to_string(), "\r", " "), "\n", " ")); } });
|
||
String body = body_value ? body_value->to_string() : String("");
|
||
if(body_value) { argv.push_back("--data-binary"); argv.push_back("@-"); }
|
||
argv.push_back(url);
|
||
if(access("/usr/bin/curl", X_OK)!=0 && access("/bin/curl", X_OK)!=0) { r["error"]="curl binary not found in runtime PATH"; return(r); }
|
||
DValue pr = uce_exec_argv_capture(argv, body, timeout_ms);
|
||
String out = pr["stdout"].to_string();
|
||
String marker="\nUCE_HTTP_STATUS:"; size_t mp=out.rfind(marker);
|
||
if(mp!=String::npos) { r["status"]=(f64)strtoull(out.c_str()+mp+marker.size(),0,10); out=out.substr(0,mp); }
|
||
else r["error"]="curl did not report status";
|
||
String sep="\r\n\r\n"; size_t hp=out.rfind(sep); size_t sep_len=4; if(hp==String::npos) { sep="\n\n"; hp=out.rfind(sep); sep_len=2; }
|
||
String hdrs = hp==String::npos ? String("") : out.substr(0,hp); r["body"] = hp==String::npos ? out : out.substr(hp+sep_len);
|
||
DValue headers;
|
||
for(String line: split(replace(hdrs,"\r",""), "\n")) { size_t c=line.find(':'); if(c!=String::npos) headers[trim(line.substr(0,c))] = trim(line.substr(c+1)); }
|
||
r["headers"] = headers;
|
||
if(pr["exit_code"].to_s64() != 0 && r["error"].to_string()=="") r["error"] = trim(pr["stderr"].to_string());
|
||
return(r);
|
||
}
|
||
|
||
static u64 uce_http_spawn_spec(const DValue& req)
|
||
{
|
||
uce_job_reap(); u64 id=uce_job_new("http"); if(!id) return(0);
|
||
pid_t pid=fork();
|
||
if(pid==0) { setsid(); uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)getpid())); uce_write_text(uce_job_path(id)+"/state", "running"); DValue result=uce_http_request_value(req); uce_job_finish(id,result,result["error"].to_string()==""?"done":"failed"); _exit(0); }
|
||
if(pid<0) { DValue r; r["error"]="fork failed"; uce_job_finish(id,r,"failed"); return(id); }
|
||
uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)pid)); uce_write_text(uce_job_path(id)+"/state", "running"); return(id);
|
||
}
|
||
|
||
static DValue uce_job_status_value(u64 id)
|
||
{
|
||
DValue r; String dir=uce_job_path(id); r["job_id"]=(f64)id;
|
||
if(id==0 || !std::filesystem::is_directory(dir)) { r["state"]="missing"; return(r); }
|
||
String state=trim(uce_read_text(dir+"/state")); if(state=="") state="pending"; r["state"]=state;
|
||
r["kind"]=trim(uce_read_text(dir+"/kind")); r["pid"]=(f64)strtoull(uce_read_text(dir+"/worker_pid").c_str(),0,10);
|
||
r["done"].set_bool(state=="done"||state=="failed"||state=="cancelled");
|
||
return(r);
|
||
}
|
||
|
||
static DValue uce_job_result_value(u64 id, u64 timeout_ms)
|
||
{
|
||
u64 deadline = wasm_monotonic_ms() + timeout_ms;
|
||
while(timeout_ms > 0 && wasm_monotonic_ms() < deadline)
|
||
{
|
||
DValue st = uce_job_status_value(id);
|
||
if(st["done"].to_bool()) break;
|
||
usleep(10000);
|
||
}
|
||
DValue r = uce_job_status_value(id);
|
||
String encoded = uce_read_text(uce_job_path(id)+"/result");
|
||
if(encoded != "") { DValue decoded; String err; if(ucb_decode(encoded, decoded, &err)) r["result"] = decoded; }
|
||
return(r);
|
||
}
|
||
|
||
static bool uce_job_cancel_value(u64 id)
|
||
{
|
||
DValue st = uce_job_status_value(id);
|
||
if(st["state"].to_string()=="missing") return(false);
|
||
String state = st["state"].to_string();
|
||
if(state == "done" || state == "failed" || state == "cancelled")
|
||
return(false);
|
||
pid_t pid = (pid_t)st["pid"].to_u64(0);
|
||
if(pid > 0) kill(-pid, SIGKILL);
|
||
DValue result; result["cancelled"].set_bool(true);
|
||
uce_job_finish(id, result, "cancelled");
|
||
return(true);
|
||
}
|
||
|
||
// ---- module byte parsing (hardened; carried from the phase 3 spike) -------
|
||
|
||
// included into both w3_driver.cpp and the native server TU (via backend.cpp);
|
||
// file-scope helpers are static so each TU gets its own copy with no clash
|
||
static bool wasm_read_uleb(const std::vector<u8>& buf, size_t& pos, size_t end, u64& out)
|
||
{
|
||
out = 0;
|
||
u32 shift = 0;
|
||
while(true)
|
||
{
|
||
if(pos >= end || shift >= 64)
|
||
return(false);
|
||
u8 byte = buf[pos++];
|
||
out |= ((u64)(byte & 0x7f)) << shift;
|
||
if((byte & 0x80) == 0)
|
||
return(true);
|
||
shift += 7;
|
||
}
|
||
}
|
||
|
||
static bool wasm_parse_sections(const std::vector<u8>& bytes, WasmDylinkInfo& dylink, WasmAbiInfo& abi, String& error)
|
||
{
|
||
if(bytes.size() < 8 || memcmp(bytes.data(), "\0asm", 4) != 0)
|
||
{
|
||
error = "not a wasm module";
|
||
return(false);
|
||
}
|
||
if(!(bytes[4] == 1 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0))
|
||
{
|
||
error = "unsupported wasm binary version";
|
||
return(false);
|
||
}
|
||
size_t pos = 8;
|
||
while(pos < bytes.size())
|
||
{
|
||
u8 section_id = bytes[pos++];
|
||
u64 size = 0;
|
||
if(!wasm_read_uleb(bytes, pos, bytes.size(), size) || size > bytes.size() - pos)
|
||
{
|
||
error = "malformed wasm section header";
|
||
return(false);
|
||
}
|
||
size_t end = pos + (size_t)size;
|
||
if(section_id == 0)
|
||
{
|
||
u64 name_len = 0;
|
||
size_t cursor = pos;
|
||
if(!wasm_read_uleb(bytes, cursor, end, name_len) || name_len > end - cursor)
|
||
{
|
||
error = "malformed custom section name";
|
||
return(false);
|
||
}
|
||
String name((const char*)bytes.data() + cursor, (size_t)name_len);
|
||
cursor += (size_t)name_len;
|
||
if(name == "dylink.0")
|
||
{
|
||
while(cursor < end)
|
||
{
|
||
u8 sub = bytes[cursor++];
|
||
u64 sub_len = 0;
|
||
if(!wasm_read_uleb(bytes, cursor, end, sub_len) || sub_len > end - cursor)
|
||
{
|
||
error = "malformed dylink.0 subsection";
|
||
return(false);
|
||
}
|
||
size_t sub_end = cursor + (size_t)sub_len;
|
||
if(sub == 1) // WASM_DYLINK_MEM_INFO
|
||
{
|
||
u64 v[4];
|
||
for(int i = 0; i < 4; i++)
|
||
if(!wasm_read_uleb(bytes, cursor, sub_end, v[i]))
|
||
{
|
||
error = "malformed dylink.0 mem_info";
|
||
return(false);
|
||
}
|
||
if(dylink.found)
|
||
{
|
||
error = "duplicate dylink.0 mem_info";
|
||
return(false);
|
||
}
|
||
if(v[1] >= 31 || v[3] >= 31)
|
||
{
|
||
error = "unsupported dylink alignment";
|
||
return(false);
|
||
}
|
||
dylink.mem_size = (u32)v[0];
|
||
dylink.mem_align = (u32)v[1];
|
||
dylink.table_size = (u32)v[2];
|
||
dylink.table_align = (u32)v[3];
|
||
dylink.found = true;
|
||
}
|
||
cursor = sub_end;
|
||
}
|
||
}
|
||
else if(name == "uce.abi")
|
||
{
|
||
String text((const char*)bytes.data() + cursor, end - cursor);
|
||
abi.found = true;
|
||
size_t line_start = 0;
|
||
while(line_start < text.size())
|
||
{
|
||
size_t line_end = text.find('\n', line_start);
|
||
if(line_end == String::npos)
|
||
line_end = text.size();
|
||
String line = text.substr(line_start, line_end - line_start);
|
||
if(line.rfind("unit_abi_version=", 0) == 0)
|
||
abi.version = (u32)strtoul(line.c_str() + 17, 0, 10);
|
||
if(line.rfind("toolchain=", 0) == 0)
|
||
abi.toolchain = line.substr(10);
|
||
line_start = line_end + 1;
|
||
}
|
||
}
|
||
}
|
||
pos = end;
|
||
}
|
||
return(true);
|
||
}
|
||
|
||
static bool wasm_read_file(const String& path, std::vector<u8>& out)
|
||
{
|
||
std::ifstream in(path, std::ios::binary);
|
||
if(!in)
|
||
return(false);
|
||
in.seekg(0, std::ios::end);
|
||
std::streamoff n = in.tellg();
|
||
if(n < 0)
|
||
return(false);
|
||
in.seekg(0, std::ios::beg);
|
||
out.resize((size_t)n);
|
||
if(n == 0)
|
||
return(true);
|
||
in.read((char*)out.data(), n);
|
||
return((bool)in);
|
||
}
|
||
|
||
// ---- worker (per process): engine + compiled module caches ----------------
|
||
|
||
class WasmWorker
|
||
{
|
||
public:
|
||
WasmWorkerConfig cfg;
|
||
|
||
explicit WasmWorker(WasmWorkerConfig config) : cfg(std::move(config)), engine(make_engine())
|
||
{
|
||
}
|
||
|
||
String init()
|
||
{
|
||
std::vector<u8> bytes;
|
||
if(!wasm_read_file(cfg.core_wasm_path, bytes))
|
||
return("cannot read core module: " + cfg.core_wasm_path);
|
||
WasmAbiInfo abi_ignored;
|
||
String parse_error;
|
||
if(!wasm_parse_sections(bytes, core_dylink, abi_ignored, parse_error))
|
||
return("core module: " + parse_error);
|
||
|
||
String core_cached_path = cached_wasm_path(cfg.core_wasm_path);
|
||
String compile_error;
|
||
auto compiled_or_cached = load_or_compile_cached_module(engine, core_cached_path, cfg.core_wasm_path, bytes, compile_error);
|
||
if(!compiled_or_cached)
|
||
return("core module compile failed: " + compile_error);
|
||
core_module.emplace(std::move(*compiled_or_cached));
|
||
return("");
|
||
}
|
||
|
||
wasmtime::Engine engine;
|
||
std::optional<wasmtime::Module> core_module;
|
||
WasmDylinkInfo core_dylink;
|
||
|
||
// unit artifact path in the W2 cache: cache_root + <abs source dir> + /<file>.wasm
|
||
String unit_wasm_path(const String& source_path) const
|
||
{
|
||
return(cfg.cache_root + source_path + ".wasm");
|
||
}
|
||
|
||
std::shared_ptr<WasmUnitModule> unit_module(const String& source_path, String& error)
|
||
{
|
||
String wasm_path = unit_wasm_path(source_path);
|
||
struct stat st;
|
||
if(stat(wasm_path.c_str(), &st) != 0)
|
||
{
|
||
error = "no wasm artifact for " + source_path + " (expected " + wasm_path + ")";
|
||
return(nullptr);
|
||
}
|
||
auto cached = module_cache.find(wasm_path);
|
||
if(cached != module_cache.end() && cached->second->mtime == st.st_mtime)
|
||
return(cached->second);
|
||
|
||
auto unit = std::make_shared<WasmUnitModule>();
|
||
unit->source_path = source_path;
|
||
unit->wasm_path = wasm_path;
|
||
unit->mtime = st.st_mtime;
|
||
std::vector<u8> bytes;
|
||
if(!wasm_read_file(wasm_path, bytes))
|
||
{
|
||
error = "cannot read " + wasm_path;
|
||
return(nullptr);
|
||
}
|
||
if(!wasm_parse_sections(bytes, unit->dylink, unit->abi, error))
|
||
{
|
||
error = wasm_path + ": " + error;
|
||
return(nullptr);
|
||
}
|
||
if(!unit->dylink.found)
|
||
{
|
||
error = wasm_path + ": missing dylink.0 mem_info (not a PIC side module)";
|
||
return(nullptr);
|
||
}
|
||
if(!unit->abi.found)
|
||
{
|
||
error = wasm_path + ": missing uce.abi stamp";
|
||
return(nullptr);
|
||
}
|
||
String unit_cached_path = cached_wasm_path(wasm_path);
|
||
String compile_error;
|
||
auto compiled_or_cached = load_or_compile_cached_module(engine, unit_cached_path, wasm_path, bytes, compile_error);
|
||
if(!compiled_or_cached)
|
||
{
|
||
error = wasm_path + ": compile failed: " + compile_error;
|
||
return(nullptr);
|
||
}
|
||
unit->module.emplace(std::move(*compiled_or_cached));
|
||
module_cache[wasm_path] = unit;
|
||
return(unit);
|
||
}
|
||
|
||
private:
|
||
static String cached_wasm_path(const String& wasm_path)
|
||
{
|
||
if(wasm_path.size() >= 5 && wasm_path.rfind(".wasm", wasm_path.size() - 5) == wasm_path.size() - 5)
|
||
return(wasm_path.substr(0, wasm_path.size() - 5) + ".cwasm");
|
||
return(wasm_path + ".cwasm");
|
||
}
|
||
|
||
static std::optional<wasmtime::Module> load_or_compile_cached_module(wasmtime::Engine& engine, const String& cached_path, const String& wasm_path,
|
||
std::vector<u8>& bytes, String& compile_error)
|
||
{
|
||
struct stat wasm_stat;
|
||
struct stat cached_stat;
|
||
if(stat(cached_path.c_str(), &cached_stat) == 0 && stat(wasm_path.c_str(), &wasm_stat) == 0)
|
||
{
|
||
if(cached_stat.st_mtime > wasm_stat.st_mtime)
|
||
{
|
||
auto deserialized = wasmtime::Module::deserialize_file(engine, cached_path);
|
||
if(deserialized)
|
||
return(deserialized.ok());
|
||
}
|
||
}
|
||
|
||
auto compiled = wasmtime::Module::compile(engine, bytes);
|
||
if(!compiled)
|
||
{
|
||
compile_error = String(compiled.err().message());
|
||
return(std::nullopt);
|
||
}
|
||
auto result = compiled.ok();
|
||
auto serialized = result.serialize();
|
||
if(serialized)
|
||
{
|
||
String tmp = cached_path + "." + std::to_string((long long)getpid()) + ".tmp";
|
||
auto data = serialized.ok();
|
||
{
|
||
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
||
if(out)
|
||
{
|
||
out.write((const char*)data.data(), (std::streamsize)data.size());
|
||
if(out)
|
||
{
|
||
out.flush();
|
||
out.close();
|
||
if(std::rename(tmp.c_str(), cached_path.c_str()) != 0)
|
||
(void)std::remove(tmp.c_str());
|
||
}
|
||
}
|
||
else
|
||
(void)std::remove(tmp.c_str());
|
||
}
|
||
}
|
||
return(result);
|
||
}
|
||
|
||
static wasmtime::Engine make_engine()
|
||
{
|
||
wasmtime::Config config;
|
||
config.epoch_interruption(true);
|
||
// CRITICAL: the host (linux_fastcgi.cpp) installs its own SIGSEGV/SIGILL/
|
||
// SIGBUS handlers per request (install_request_fault_handlers) with plain
|
||
// signal(), which clobbers Wasmtime's trap-handling handlers. With
|
||
// signals_based_traps on, a guest `unreachable`/OOB surfaces as a host
|
||
// signal that the native on_segfault catches and abort()s the worker —
|
||
// a 502 instead of a clean wasm trap. Disabling it makes Cranelift emit
|
||
// explicit trap checks: guest traps stay pure wasm traps returned as
|
||
// Result errors, never a host signal, so the two never collide.
|
||
config.signals_based_traps(false);
|
||
return(wasmtime::Engine(std::move(config)));
|
||
}
|
||
|
||
std::map<String, std::shared_ptr<WasmUnitModule>> module_cache;
|
||
};
|
||
|
||
// ---- workspace (per request) ----------------------------------------------
|
||
|
||
class WasmWorkspace
|
||
{
|
||
public:
|
||
WasmWorker& worker;
|
||
wasmtime::Store store;
|
||
u64 workspace_birth_us = 0;
|
||
u64 component_resolve_count = 0;
|
||
u64 component_resolve_total_us = 0;
|
||
|
||
struct FileHandle
|
||
{
|
||
int fd = -1;
|
||
bool writable = false;
|
||
};
|
||
std::vector<FileHandle> file_handles;
|
||
|
||
struct RequestPerfSnapshot
|
||
{
|
||
u64 worker_pid = 0;
|
||
u64 parent_pid = 0;
|
||
u64 request_count = 0;
|
||
f64 time_init = 0;
|
||
f64 time_start = 0;
|
||
bool active = false;
|
||
} request_perf;
|
||
|
||
explicit WasmWorkspace(WasmWorker& w) : worker(w), store(w.engine)
|
||
{
|
||
}
|
||
|
||
void set_perf_snapshot(u64 worker_pid, u64 parent_pid, u64 request_count, f64 time_init, f64 time_start)
|
||
{
|
||
request_perf.worker_pid = worker_pid;
|
||
request_perf.parent_pid = parent_pid;
|
||
request_perf.request_count = request_count;
|
||
request_perf.time_init = time_init;
|
||
request_perf.time_start = time_start;
|
||
request_perf.active = true;
|
||
}
|
||
|
||
#ifdef UCE_WASM_HOST_CONNECTORS
|
||
// Host-owned resource handle table (§3.1): connections opened by the guest
|
||
// live here and are closed when the workspace drops at request end. This is
|
||
// the wasm-side enforcement of request-scoped DB lifecycle; app code should
|
||
// never cache these opaque handles across requests.
|
||
std::vector<SQLite*> sqlite_handles;
|
||
std::vector<MySQL*> mysql_handles;
|
||
#endif
|
||
~WasmWorkspace()
|
||
{
|
||
for(auto& h : file_handles)
|
||
{
|
||
if(h.fd >= 0)
|
||
{
|
||
flock(h.fd, LOCK_UN);
|
||
close(h.fd);
|
||
h.fd = -1;
|
||
}
|
||
}
|
||
#ifdef UCE_WASM_HOST_CONNECTORS
|
||
for(auto* db : sqlite_handles)
|
||
if(db)
|
||
delete db; // ~SQLite disconnects
|
||
for(auto* db : mysql_handles)
|
||
if(db)
|
||
delete db; // ~MySQL disconnects
|
||
#endif
|
||
}
|
||
|
||
// The guest calls a sized hostcall twice (buf=0 to learn the length, then
|
||
// to fetch). For side-effecting ops (sqlite) re-executing on the fetch is
|
||
// wrong, so the result is staged on the first call (keyed on the exact
|
||
// input bytes) and replayed on the second without re-running the op.
|
||
String staged_hostcall_input;
|
||
String staged_hostcall_result;
|
||
String staged_socket_read_key;
|
||
String staged_socket_read_result;
|
||
String staged_memcache_key;
|
||
String staged_memcache_result;
|
||
bool hostcall_staged(const String& input, String& out)
|
||
{
|
||
if(!staged_hostcall_input.empty() && input == staged_hostcall_input)
|
||
{
|
||
out = staged_hostcall_result;
|
||
staged_hostcall_input = "";
|
||
staged_hostcall_result = "";
|
||
return(true);
|
||
}
|
||
return(false);
|
||
}
|
||
void hostcall_stage(const String& input, const String& out)
|
||
{
|
||
staged_hostcall_input = input;
|
||
staged_hostcall_result = out;
|
||
}
|
||
|
||
// resolve-kind values shared with the guest core (src/wasm/core.cpp)
|
||
// must match WasmResolveKind in src/wasm/core.cpp
|
||
|
||
String birth()
|
||
{
|
||
auto cx = ctx();
|
||
store.limiter(worker.cfg.memory_limit, -1, -1, -1, -1);
|
||
cx.set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
|
||
|
||
auto& module = *worker.core_module;
|
||
std::vector<wasmtime::Extern> imports;
|
||
for(auto import_type : module.imports())
|
||
{
|
||
String mod(import_type.module());
|
||
String name(import_type.name());
|
||
auto extern_type = wasmtime::ExternType::from_import(import_type);
|
||
if(auto* table_ref = std::get_if<wasmtime::TableType::Ref>(&extern_type))
|
||
{
|
||
if(name.rfind("__indirect_function_table", 0) != 0)
|
||
return("core imports unexpected table " + mod + "." + name);
|
||
u32 core_min = table_ref->min();
|
||
u32 total = core_min + worker.cfg.table_headroom;
|
||
wasmtime::TableType table_type(wasmtime::ValType::funcref(), total, total);
|
||
auto created = wasmtime::Table::create(cx, table_type, wasmtime::Val(std::optional<wasmtime::Func>()));
|
||
if(!created)
|
||
return("table create failed: " + String(created.err().message()));
|
||
table.emplace(created.ok());
|
||
table_next_free = core_min;
|
||
imports.push_back(*table);
|
||
continue;
|
||
}
|
||
auto* func_ref = std::get_if<wasmtime::FuncType::Ref>(&extern_type);
|
||
if(!func_ref)
|
||
return("core has unexpected non-func import " + mod + "." + name);
|
||
wasmtime::FuncType func_type(*func_ref);
|
||
imports.push_back(make_host_import(cx, mod, name, func_type));
|
||
}
|
||
if(!table)
|
||
return("core does not import __indirect_function_table — rebuild core with --import-table");
|
||
|
||
auto created = wasmtime::Instance::create(cx, module, imports);
|
||
if(!created)
|
||
return("core instantiation failed: " + trap_text(created.err()));
|
||
core.emplace(created.ok());
|
||
|
||
auto exported_memory = core->get(cx, "memory");
|
||
if(!exported_memory || !std::get_if<wasmtime::Memory>(&*exported_memory))
|
||
return("core does not export memory");
|
||
memory.emplace(std::get<wasmtime::Memory>(*exported_memory));
|
||
|
||
String error = call_core("_initialize", {}, 0);
|
||
if(error != "")
|
||
return(error);
|
||
int32_t rc = 0;
|
||
error = call_core("uce_wasm_core_init", {}, &rc);
|
||
if(error != "")
|
||
return(error);
|
||
if(rc != 0)
|
||
return("uce_wasm_core_init returned " + std::to_string(rc));
|
||
int32_t core_abi = 0;
|
||
error = call_core("uce_wasm_core_abi_version", {}, &core_abi);
|
||
if(error != "")
|
||
return(error);
|
||
abi_version = (u32)core_abi;
|
||
error = call_core("uce_wasm_request", {}, &request_ptr);
|
||
if(error != "")
|
||
return(error);
|
||
if(request_ptr == 0)
|
||
return("core returned null Request*");
|
||
return("");
|
||
}
|
||
|
||
String apply_context(const DValue& context_tree)
|
||
{
|
||
String encoded = ucb_encode(context_tree);
|
||
int32_t guest_ptr = 0;
|
||
String error = call_core("uce_alloc", { (int32_t)encoded.size() }, &guest_ptr);
|
||
if(error != "")
|
||
return(error);
|
||
if(guest_ptr == 0)
|
||
return("guest uce_alloc failed for context buffer");
|
||
error = guest_write((u32)guest_ptr, encoded);
|
||
if(error != "")
|
||
return(error);
|
||
int32_t rc = 0;
|
||
error = call_core("uce_wasm_apply_context", { guest_ptr, (int32_t)encoded.size() }, &rc);
|
||
if(error != "")
|
||
return(error);
|
||
call_core("uce_free", { guest_ptr }, 0);
|
||
if(rc != 0)
|
||
return("uce_wasm_apply_context returned " + std::to_string(rc));
|
||
return("");
|
||
}
|
||
|
||
// Invoke the entry unit through a named handler ("render"/"cli"/"websocket"/
|
||
// "serve_http:named"). The handler is just an export name — same resolve +
|
||
// ONCE + dispatch path as a component. handler_present, when provided,
|
||
// reports whether the unit exports it (caller maps a missing render to an
|
||
// empty body, a missing cli/serve handler to a 404).
|
||
String invoke_entry(const String& entry_source_path, const String& handler, bool* handler_present = 0)
|
||
{
|
||
entry_dir = dir_of(entry_source_path);
|
||
size_t unit_index = 0;
|
||
String error = load_unit(entry_source_path, unit_index);
|
||
if(error != "")
|
||
return(error);
|
||
bool present = (bool)unit_func(unit_index, handler_export_symbol(handler));
|
||
if(handler_present)
|
||
*handler_present = present;
|
||
if(!present)
|
||
return("");
|
||
// Invoke through the core: it resolves the same handler and runs ONCE +
|
||
// dispatch (unit is pre-loaded above; resolution is cached). The core
|
||
// entry takes two guest buffers — the unit path and the handler name.
|
||
auto entry = core_func("uce_wasm_invoke_entry");
|
||
if(!entry)
|
||
return("core does not export uce_wasm_invoke_entry");
|
||
int32_t path_ptr = 0, handler_ptr = 0;
|
||
error = call_core("uce_alloc", { (int32_t)entry_source_path.size() }, &path_ptr);
|
||
if(error != "" || path_ptr == 0)
|
||
return(error == "" ? String("guest uce_alloc failed for entry path") : error);
|
||
error = guest_write((u32)path_ptr, entry_source_path);
|
||
if(error == "")
|
||
{
|
||
error = call_core("uce_alloc", { (int32_t)handler.size() }, &handler_ptr);
|
||
if(error == "" && handler_ptr == 0)
|
||
error = "guest uce_alloc failed for handler";
|
||
}
|
||
if(error == "")
|
||
error = guest_write((u32)handler_ptr, handler);
|
||
if(error != "")
|
||
{
|
||
if(path_ptr) call_core("uce_free", { path_ptr }, 0);
|
||
if(handler_ptr) call_core("uce_free", { handler_ptr }, 0);
|
||
return(error);
|
||
}
|
||
auto result = entry->call(ctx(), { wasmtime::Val(path_ptr), wasmtime::Val((int32_t)entry_source_path.size()),
|
||
wasmtime::Val(handler_ptr), wasmtime::Val((int32_t)handler.size()) });
|
||
call_core("uce_free", { path_ptr }, 0);
|
||
call_core("uce_free", { handler_ptr }, 0);
|
||
if(!result)
|
||
return(trap_text(result.err()));
|
||
return("");
|
||
}
|
||
|
||
String collect(WasmResponse& response)
|
||
{
|
||
String error = call_core("uce_wasm_finish_output", {}, 0);
|
||
if(error != "")
|
||
return(error);
|
||
error = call_core("uce_wasm_finish_response_meta", {}, 0);
|
||
if(error != "")
|
||
return(error);
|
||
int32_t body_ptr = 0, body_len = 0, meta_ptr = 0, meta_len = 0;
|
||
if((error = call_core("uce_wasm_output_data", {}, &body_ptr)) != "") return(error);
|
||
if((error = call_core("uce_wasm_output_size", {}, &body_len)) != "") return(error);
|
||
if((error = call_core("uce_wasm_response_meta_data", {}, &meta_ptr)) != "") return(error);
|
||
if((error = call_core("uce_wasm_response_meta_size", {}, &meta_len)) != "") return(error);
|
||
error = guest_read((u32)body_ptr, (u32)body_len, response.body);
|
||
if(error != "")
|
||
return(error);
|
||
String meta_encoded;
|
||
error = guest_read((u32)meta_ptr, (u32)meta_len, meta_encoded);
|
||
if(error != "")
|
||
return(error);
|
||
String decode_error;
|
||
if(!ucb_decode(meta_encoded, response.meta, &decode_error))
|
||
return("response meta decode failed: " + decode_error);
|
||
return("");
|
||
}
|
||
|
||
private:
|
||
std::optional<wasmtime::Instance> core;
|
||
std::optional<wasmtime::Memory> memory;
|
||
std::optional<wasmtime::Table> table;
|
||
u32 table_next_free = 0;
|
||
u32 abi_version = 0;
|
||
int32_t request_ptr = 0;
|
||
String entry_dir;
|
||
|
||
struct LoadedUnit
|
||
{
|
||
std::shared_ptr<WasmUnitModule> mod;
|
||
std::optional<wasmtime::Instance> instance;
|
||
u32 memory_base = 0;
|
||
};
|
||
std::vector<LoadedUnit> units;
|
||
std::map<String, size_t> units_by_source;
|
||
std::map<String, u32> handler_slots; // source + ":" + symbol → table slot
|
||
std::vector<wasmtime::Func> host_funcs; // keep host imports alive
|
||
|
||
wasmtime::Store::Context ctx()
|
||
{
|
||
return(wasmtime::Store::Context(store));
|
||
}
|
||
|
||
static String dir_of(const String& path)
|
||
{
|
||
auto pos = path.find_last_of('/');
|
||
return(pos == String::npos ? String("") : path.substr(0, pos));
|
||
}
|
||
|
||
static String trap_text(const wasmtime::TrapError& error)
|
||
{
|
||
return(wasm_trace_collapse(String(error.message())));
|
||
}
|
||
|
||
// ---- guest memory access (pointer re-derived per call: it moves) ------
|
||
|
||
String guest_write(u32 ptr, const String& data)
|
||
{
|
||
auto span = memory->data(ctx());
|
||
if((size_t)ptr + data.size() > span.size())
|
||
return("guest write out of bounds");
|
||
memcpy(span.data() + ptr, data.data(), data.size());
|
||
return("");
|
||
}
|
||
|
||
String guest_read(u32 ptr, u32 len, String& out)
|
||
{
|
||
auto span = memory->data(ctx());
|
||
if((size_t)ptr + len > span.size())
|
||
return("guest read out of bounds");
|
||
out.assign((const char*)span.data() + ptr, len);
|
||
return("");
|
||
}
|
||
|
||
// ---- calls -------------------------------------------------------------
|
||
|
||
std::optional<wasmtime::Func> core_func(const String& name)
|
||
{
|
||
auto exported = core->get(ctx(), std::string_view(name));
|
||
if(!exported)
|
||
return(std::nullopt);
|
||
if(auto* func = std::get_if<wasmtime::Func>(&*exported))
|
||
return(*func);
|
||
return(std::nullopt);
|
||
}
|
||
|
||
std::optional<wasmtime::Func> unit_func(size_t unit_index, const String& name)
|
||
{
|
||
auto exported = units[unit_index].instance->get(ctx(), std::string_view(name));
|
||
if(!exported)
|
||
return(std::nullopt);
|
||
if(auto* func = std::get_if<wasmtime::Func>(&*exported))
|
||
return(*func);
|
||
return(std::nullopt);
|
||
}
|
||
|
||
String call_core(const String& name, std::vector<int32_t> argv, int32_t* result_out)
|
||
{
|
||
auto func = core_func(name);
|
||
if(!func)
|
||
return("core does not export " + name);
|
||
std::vector<wasmtime::Val> args;
|
||
for(auto value : argv)
|
||
args.push_back(wasmtime::Val(value));
|
||
auto result = func->call(ctx(), args);
|
||
if(!result)
|
||
return(trap_text(result.err()));
|
||
auto values = result.ok();
|
||
if(result_out)
|
||
*result_out = values.empty() ? 0 : values[0].i32();
|
||
return("");
|
||
}
|
||
|
||
// ---- symbol registry (core first, then units in load order) -----------
|
||
|
||
std::optional<wasmtime::Func> resolve_func(const String& name)
|
||
{
|
||
if(auto func = core_func(name))
|
||
return(func);
|
||
for(size_t i = 0; i < units.size(); i++)
|
||
if(auto func = unit_func(i, name))
|
||
return(func);
|
||
return(std::nullopt);
|
||
}
|
||
|
||
// data symbol: value of the exported i32 global + owning module's base
|
||
// (core is non-PIC → base 0; PIC units export __memory_base-relative)
|
||
bool resolve_data(const String& name, u32& address_out)
|
||
{
|
||
auto from_core = core->get(ctx(), std::string_view(name));
|
||
if(from_core)
|
||
if(auto* global = std::get_if<wasmtime::Global>(&*from_core))
|
||
{
|
||
address_out = (u32)global->get(ctx()).i32();
|
||
return(true);
|
||
}
|
||
for(size_t i = 0; i < units.size(); i++)
|
||
{
|
||
auto exported = units[i].instance->get(ctx(), std::string_view(name));
|
||
if(exported)
|
||
if(auto* global = std::get_if<wasmtime::Global>(&*exported))
|
||
{
|
||
address_out = units[i].memory_base + (u32)global->get(ctx()).i32();
|
||
return(true);
|
||
}
|
||
}
|
||
return(false);
|
||
}
|
||
|
||
String place_funcref(const wasmtime::Func& func, u32& slot_out)
|
||
{
|
||
auto cx = ctx();
|
||
if(table_next_free >= table->size(cx))
|
||
return("funcref table headroom exhausted");
|
||
auto set = table->set(cx, table_next_free, wasmtime::Val(std::optional<wasmtime::Func>(func)));
|
||
if(!set)
|
||
return("table set failed: " + String(set.err().message()));
|
||
slot_out = table_next_free++;
|
||
return("");
|
||
}
|
||
|
||
// ---- unit loading (the §6 sequence) ------------------------------------
|
||
|
||
String load_unit(const String& source_path, size_t& unit_index_out)
|
||
{
|
||
auto known = units_by_source.find(source_path);
|
||
if(known != units_by_source.end())
|
||
{
|
||
unit_index_out = known->second;
|
||
return("");
|
||
}
|
||
|
||
String error;
|
||
auto mod = worker.unit_module(source_path, error);
|
||
if(!mod)
|
||
return(error);
|
||
if(mod->abi.version != abi_version)
|
||
return(mod->wasm_path + ": uce.abi version " + std::to_string(mod->abi.version)
|
||
+ " does not match core ABI " + std::to_string(abi_version));
|
||
|
||
auto cx = ctx();
|
||
auto& module = *mod->module;
|
||
|
||
// base allocation
|
||
u32 align = 1u << mod->dylink.mem_align;
|
||
int32_t raw_base = 0;
|
||
error = call_core("malloc", { (int32_t)(mod->dylink.mem_size + align) }, &raw_base);
|
||
if(error != "")
|
||
return(error);
|
||
if(raw_base == 0)
|
||
return("core malloc failed for unit data segment");
|
||
u32 memory_base = ((u32)raw_base + (align - 1)) & ~(align - 1);
|
||
u32 table_base = table_next_free;
|
||
if(mod->dylink.table_size > table->size(cx) - table_next_free)
|
||
return("funcref table headroom exhausted by " + source_path);
|
||
table_next_free += mod->dylink.table_size;
|
||
|
||
// import resolution
|
||
std::vector<wasmtime::Extern> imports;
|
||
std::vector<std::pair<String, wasmtime::Global>> deferred_got;
|
||
for(auto import_type : module.imports())
|
||
{
|
||
String mod_name(import_type.module());
|
||
String name(import_type.name());
|
||
auto extern_type = wasmtime::ExternType::from_import(import_type);
|
||
bool is_func_import = std::get_if<wasmtime::FuncType::Ref>(&extern_type) != 0;
|
||
|
||
if(mod_name == "env" && name == "memory")
|
||
{
|
||
imports.push_back(*memory);
|
||
continue;
|
||
}
|
||
if(mod_name == "env" && name == "__indirect_function_table")
|
||
{
|
||
imports.push_back(*table);
|
||
continue;
|
||
}
|
||
if(mod_name == "env" && name == "__stack_pointer")
|
||
{
|
||
auto sp = core->get(cx, "__stack_pointer");
|
||
if(!sp || !std::get_if<wasmtime::Global>(&*sp))
|
||
return("core does not export __stack_pointer");
|
||
imports.push_back(std::get<wasmtime::Global>(*sp));
|
||
continue;
|
||
}
|
||
if(mod_name == "env" && (name == "__memory_base" || name == "__table_base"))
|
||
{
|
||
int32_t value = name == "__memory_base" ? (int32_t)memory_base : (int32_t)table_base;
|
||
wasmtime::GlobalType global_type(wasmtime::ValType::i32(), false);
|
||
auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val(value));
|
||
if(!global)
|
||
return("global create failed: " + String(global.err().message()));
|
||
imports.push_back(global.ok());
|
||
continue;
|
||
}
|
||
if(mod_name == "env" && is_func_import)
|
||
{
|
||
auto func = resolve_func(name);
|
||
if(!func)
|
||
return(source_path + ": unresolved import env." + wasm_trace_demangle(name));
|
||
imports.push_back(*func);
|
||
continue;
|
||
}
|
||
if(mod_name == "GOT.mem")
|
||
{
|
||
wasmtime::GlobalType global_type(wasmtime::ValType::i32(), true);
|
||
u32 address = 0;
|
||
if(resolve_data(name, address))
|
||
{
|
||
auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)address));
|
||
if(!global)
|
||
return("global create failed: " + String(global.err().message()));
|
||
imports.push_back(global.ok());
|
||
}
|
||
else
|
||
{
|
||
// provisional; self-resolved from the unit's own export
|
||
// (plus its memory base) after instantiation
|
||
auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)0));
|
||
if(!global)
|
||
return("global create failed: " + String(global.err().message()));
|
||
deferred_got.push_back({ name, global.ok() });
|
||
imports.push_back(deferred_got.back().second);
|
||
}
|
||
continue;
|
||
}
|
||
if(mod_name == "GOT.func")
|
||
{
|
||
auto func = resolve_func(name);
|
||
if(!func)
|
||
return(source_path + ": unresolved GOT.func." + wasm_trace_demangle(name));
|
||
u32 slot = 0;
|
||
error = place_funcref(*func, slot);
|
||
if(error != "")
|
||
return(error);
|
||
wasmtime::GlobalType global_type(wasmtime::ValType::i32(), true);
|
||
auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)slot));
|
||
if(!global)
|
||
return("global create failed: " + String(global.err().message()));
|
||
imports.push_back(global.ok());
|
||
continue;
|
||
}
|
||
return(source_path + ": import policy violation: " + mod_name + "." + name);
|
||
}
|
||
|
||
auto created = wasmtime::Instance::create(cx, module, imports);
|
||
if(!created)
|
||
return(source_path + ": instantiation failed: " + trap_text(created.err()));
|
||
|
||
LoadedUnit unit;
|
||
unit.mod = mod;
|
||
unit.instance.emplace(created.ok());
|
||
unit.memory_base = memory_base;
|
||
units.push_back(std::move(unit));
|
||
size_t unit_index = units.size() - 1;
|
||
units_by_source[source_path] = unit_index;
|
||
unit_index_out = unit_index;
|
||
|
||
// deferred GOT: the unit's own data exports are module-relative —
|
||
// add this unit's memory base (Phase 0 FINDINGS erratum)
|
||
for(auto& [name, got] : deferred_got)
|
||
{
|
||
auto own = units[unit_index].instance->get(cx, std::string_view(name));
|
||
if(!own || !std::get_if<wasmtime::Global>(&*own))
|
||
return(source_path + ": GOT.mem." + name + " defined neither by core nor by any unit");
|
||
u32 offset = (u32)std::get<wasmtime::Global>(*own).get(cx).i32();
|
||
auto set = got.set(cx, wasmtime::Val((int32_t)(memory_base + offset)));
|
||
if(!set)
|
||
return("GOT patch failed: " + String(set.err().message()));
|
||
}
|
||
|
||
// init sequence, then bind this unit's context to the request
|
||
if(auto relocs = unit_func(unit_index, "__wasm_apply_data_relocs"))
|
||
{
|
||
auto result = relocs->call(ctx(), {});
|
||
if(!result)
|
||
return(trap_text(result.err()));
|
||
}
|
||
if(auto ctors = unit_func(unit_index, "__wasm_call_ctors"))
|
||
{
|
||
auto result = ctors->call(ctx(), {});
|
||
if(!result)
|
||
return(trap_text(result.err()));
|
||
}
|
||
if(auto set_request = unit_func(unit_index, "__uce_set_current_request"))
|
||
{
|
||
auto result = set_request->call(ctx(), { wasmtime::Val(request_ptr) });
|
||
if(!result)
|
||
return(trap_text(result.err()));
|
||
}
|
||
if(worker.cfg.verbose)
|
||
fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n",
|
||
source_path.c_str(), memory_base, table_base);
|
||
// The epoch budget is a guest-CPU watchdog, but the ticker is wall-clock
|
||
// and host-side module compilation here (lazy, mid-render, possibly many
|
||
// units) burns it without the guest running. Reset the deadline after a
|
||
// load so the budget measures guest execution between membrane crossings,
|
||
// not our compile time. A genuine runaway loop makes no loads, so it
|
||
// still trips the deadline.
|
||
ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
|
||
return("");
|
||
}
|
||
|
||
// ---- component target resolution (host side of the membrane) ----------
|
||
|
||
static String normalize_component_path(String name)
|
||
{
|
||
// mirrors component_normalize_path in compiler.cpp / core.cpp
|
||
if(name.length() >= 4 && name.substr(name.length() - 4) == ".uce")
|
||
return(name);
|
||
return(name + ".uce");
|
||
}
|
||
|
||
static bool file_exists_host(const String& path)
|
||
{
|
||
struct stat st;
|
||
return(stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode));
|
||
}
|
||
|
||
static bool dir_exists_host(const String& path)
|
||
{
|
||
struct stat st;
|
||
return(stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
|
||
}
|
||
|
||
// Keep cwd host behavior local to this process but guard it with the same
|
||
// write-root policy we use for file writes (plus a single parity fallback).
|
||
String resolve_guest_cwd_set(const String& raw)
|
||
{
|
||
if(raw == "" || raw.find('\0') != String::npos)
|
||
return("");
|
||
|
||
String raw_target = raw;
|
||
if(raw.rfind("/", 0) != 0)
|
||
{
|
||
String cwd = ::cwd_get();
|
||
if(cwd == "")
|
||
return("");
|
||
raw_target = cwd + "/" + raw;
|
||
}
|
||
|
||
char resolved[PATH_MAX];
|
||
if(!realpath(raw_target.c_str(), resolved))
|
||
return("");
|
||
String resolved_target(resolved);
|
||
if(!dir_exists_host(resolved_target))
|
||
return("");
|
||
|
||
// Policy: allow only roots we already expose for writable filesystem access.
|
||
std::vector<String> roots;
|
||
roots.push_back(worker.cfg.site_root);
|
||
for(auto& root : worker.cfg.write_roots)
|
||
roots.push_back(root);
|
||
for(auto& root : roots)
|
||
{
|
||
if(root == "")
|
||
continue;
|
||
char root_real[PATH_MAX];
|
||
if(!realpath(root.c_str(), root_real))
|
||
continue;
|
||
String canonical_root(root_real);
|
||
if(resolved_target == canonical_root)
|
||
return(resolved_target);
|
||
if(canonical_root != "/" && resolved_target.rfind(canonical_root + "/", 0) == 0)
|
||
return(resolved_target);
|
||
}
|
||
|
||
// Parity/fallback: allow returning to the process start directory so
|
||
// legacy behavior is not silently broken for existing native/cached flows.
|
||
String start_directory = ::process_start_directory();
|
||
if(start_directory != "" && resolved_target == start_directory)
|
||
return(resolved_target);
|
||
return("");
|
||
}
|
||
|
||
String resolve_source_path(const String& file_name, const String& current_unit)
|
||
{
|
||
std::vector<String> bases;
|
||
if(file_name.rfind("/", 0) == 0)
|
||
bases.push_back(""); // absolute target
|
||
if(entry_dir != "")
|
||
bases.push_back(entry_dir + "/");
|
||
if(current_unit != "")
|
||
{
|
||
String current_dir = dir_of(current_unit);
|
||
if(current_dir != "" && current_dir != entry_dir)
|
||
bases.push_back(current_dir + "/");
|
||
}
|
||
bases.push_back(worker.cfg.site_root + "/");
|
||
|
||
for(auto& base : bases)
|
||
{
|
||
std::vector<String> candidates;
|
||
candidates.push_back(base + file_name);
|
||
candidates.push_back(base + normalize_component_path(file_name));
|
||
if(file_name.rfind("components/", 0) != 0 && base != "")
|
||
{
|
||
candidates.push_back(base + "components/" + file_name);
|
||
candidates.push_back(base + "components/" + normalize_component_path(file_name));
|
||
}
|
||
for(auto& candidate : candidates)
|
||
if(file_exists_host(candidate))
|
||
return(candidate);
|
||
}
|
||
return("");
|
||
}
|
||
|
||
// guest file access policy: only inside the site tree, resolved against
|
||
// the entry unit's directory first (the native cwd convention), then the
|
||
// site root; containment checked on the canonicalized path
|
||
String resolve_guest_file(const String& raw, const String& current_unit = "", bool allow_dir = false)
|
||
{
|
||
if(raw == "" || raw.find('\0') != String::npos)
|
||
return("");
|
||
std::vector<String> candidates;
|
||
if(raw.rfind("/", 0) == 0)
|
||
candidates.push_back(raw);
|
||
else
|
||
{
|
||
String current_dir = current_unit != "" ? dir_of(current_unit) : String("");
|
||
if(current_dir != "")
|
||
candidates.push_back(current_dir + "/" + raw);
|
||
if(entry_dir != "" && entry_dir != current_dir)
|
||
candidates.push_back(entry_dir + "/" + raw);
|
||
candidates.push_back(worker.cfg.site_root + "/" + raw);
|
||
}
|
||
// readable roots = the site tree plus the writable scratch dirs (a page
|
||
// can read back what it is allowed to write, e.g. /tmp), canonicalized.
|
||
std::vector<String> read_roots;
|
||
read_roots.push_back(worker.cfg.site_root);
|
||
for(auto& root : worker.cfg.write_roots)
|
||
read_roots.push_back(root);
|
||
std::vector<String> root_prefixes;
|
||
for(auto& root : read_roots)
|
||
{
|
||
char root_real[4096];
|
||
if(root != "" && realpath(root.c_str(), root_real))
|
||
root_prefixes.push_back(String(root_real));
|
||
}
|
||
for(auto& candidate : candidates)
|
||
{
|
||
char resolved[4096];
|
||
if(!realpath(candidate.c_str(), resolved))
|
||
continue;
|
||
String path(resolved);
|
||
bool allowed = false;
|
||
for(auto& prefix : root_prefixes)
|
||
if(path == prefix || path.rfind(prefix + "/", 0) == 0)
|
||
{
|
||
allowed = true;
|
||
break;
|
||
}
|
||
if(!allowed)
|
||
continue;
|
||
if(file_exists_host(path) || (allow_dir && dir_exists_host(path)))
|
||
return(path);
|
||
}
|
||
return("");
|
||
}
|
||
|
||
// write membrane policy: resolve the target (absolute, or relative to the
|
||
// current unit / site root) and allow it only if its parent directory
|
||
// canonicalizes under one of the configured write roots (site tree + the
|
||
// runtime scratch dirs). The file itself need not exist yet.
|
||
String resolve_guest_write(const String& raw, const String& current_unit)
|
||
{
|
||
if(raw == "" || raw.find('\0') != String::npos)
|
||
return("");
|
||
String target;
|
||
if(raw.rfind("/", 0) == 0)
|
||
target = raw;
|
||
else
|
||
{
|
||
String current_dir = current_unit != "" ? dir_of(current_unit) : String("");
|
||
target = (current_dir != "" ? current_dir : worker.cfg.site_root) + "/" + raw;
|
||
}
|
||
String parent = dir_of(target);
|
||
String base = parent.size() < target.size() ? target.substr(parent.size() + 1) : String("");
|
||
if(base == "" || base == "." || base == "..")
|
||
return("");
|
||
char parent_real[4096];
|
||
if(!realpath(parent.c_str(), parent_real))
|
||
return("");
|
||
String resolved_parent(parent_real);
|
||
for(auto& root : worker.cfg.write_roots)
|
||
{
|
||
char root_real[4096];
|
||
if(root == "" || !realpath(root.c_str(), root_real))
|
||
continue;
|
||
String root_prefix(root_real);
|
||
if(resolved_parent == root_prefix || resolved_parent.rfind(root_prefix + "/", 0) == 0)
|
||
return(resolved_parent + "/" + base);
|
||
}
|
||
return("");
|
||
}
|
||
|
||
static String sanitize_symbol_suffix(const String& raw)
|
||
{
|
||
// mirrors ascii_safe_name in functionlib.cpp
|
||
String result;
|
||
for(auto c : raw)
|
||
if(isalnum((unsigned char)c) || c == '_')
|
||
result.push_back(c);
|
||
return(result);
|
||
}
|
||
|
||
// __uce_<base>[_<suffix>] for a handler spec like "component:CARD" / "render".
|
||
static String handler_export_symbol(const String& handler)
|
||
{
|
||
String export_prefix = "export:";
|
||
if(handler.rfind(export_prefix, 0) == 0)
|
||
return(handler.substr(export_prefix.size()));
|
||
auto colon = handler.find(":");
|
||
String symbol = "__uce_" + (colon == String::npos ? handler : handler.substr(0, colon));
|
||
if(colon != String::npos)
|
||
symbol += "_" + sanitize_symbol_suffix(handler.substr(colon + 1));
|
||
return(symbol);
|
||
}
|
||
|
||
// hostcall body: uce_host_component_resolve(unit, handler, current) → slot.
|
||
// `handler` names the export ("render", "component:CARD", "cli",
|
||
// "serve_http:named", "once") or is "exists" (probe only, loads nothing).
|
||
int32_t component_resolve(const String& target, const String& handler, const String& current_unit, String& resolved_out)
|
||
{
|
||
auto probe_start = std::chrono::steady_clock::now();
|
||
auto record_probe = [&]() {
|
||
component_resolve_count += 1;
|
||
component_resolve_total_us += (u64)std::chrono::duration_cast<std::chrono::microseconds>(
|
||
std::chrono::steady_clock::now() - probe_start).count();
|
||
};
|
||
String file_name = target;
|
||
if(file_name == "" && current_unit != "")
|
||
file_name = current_unit;
|
||
if(file_name == "")
|
||
{
|
||
record_probe();
|
||
return(0);
|
||
}
|
||
|
||
String resolved = resolve_source_path(file_name, current_unit);
|
||
if(resolved == "")
|
||
{
|
||
record_probe();
|
||
return(0);
|
||
}
|
||
resolved_out = resolved;
|
||
if(handler == "exists")
|
||
{
|
||
record_probe();
|
||
return(1);
|
||
}
|
||
|
||
if(!file_exists_host(worker.unit_wasm_path(resolved)) || compiler_unit_needs_recompile(context, resolved, 0))
|
||
get_shared_unit(context, resolved);
|
||
|
||
size_t unit_index = 0;
|
||
String error = load_unit(resolved, unit_index);
|
||
if(error != "")
|
||
{
|
||
fprintf(stderr, "[wasm] component load failed: %s\n", error.c_str());
|
||
record_probe();
|
||
return(0);
|
||
}
|
||
String symbol = handler_export_symbol(handler);
|
||
String slot_key = resolved + ":" + symbol;
|
||
auto cached = handler_slots.find(slot_key);
|
||
if(cached != handler_slots.end())
|
||
{
|
||
record_probe();
|
||
return((int32_t)cached->second);
|
||
}
|
||
auto handler_fn = unit_func(unit_index, symbol);
|
||
if(!handler_fn)
|
||
{
|
||
// ONCE is optional per unit; a missing __uce_once is not an error
|
||
if(handler != "once")
|
||
fprintf(stderr, "[wasm] %s does not export %s\n", resolved.c_str(), symbol.c_str());
|
||
record_probe();
|
||
return(0);
|
||
}
|
||
u32 slot = 0;
|
||
error = place_funcref(*handler_fn, slot);
|
||
if(error != "")
|
||
{
|
||
fprintf(stderr, "[wasm] %s\n", error.c_str());
|
||
record_probe();
|
||
return(0);
|
||
}
|
||
handler_slots[slot_key] = slot;
|
||
record_probe();
|
||
return((int32_t)slot);
|
||
}
|
||
|
||
String run_task_callback(u64 callback_id)
|
||
{
|
||
auto runner = core_func("uce_wasm_task_run");
|
||
if(!runner)
|
||
return("core does not export uce_wasm_task_run");
|
||
auto result = runner->call(ctx(), { wasmtime::Val((int64_t)callback_id) });
|
||
if(!result)
|
||
return(trap_text(result.err()));
|
||
return("");
|
||
}
|
||
|
||
// ---- host imports for the core -----------------------------------------
|
||
|
||
wasmtime::Extern make_host_import(wasmtime::Store::Context cx, const String& mod, const String& name, const wasmtime::FuncType& func_type)
|
||
{
|
||
using namespace wasmtime;
|
||
WasmWorkspace* self = this;
|
||
|
||
auto add = [&](auto&& callback) -> Extern {
|
||
Func func(cx, func_type, callback);
|
||
host_funcs.push_back(func);
|
||
return(host_funcs.back());
|
||
};
|
||
|
||
// Hostcall blocklist (UCE_HOSTCALL_BLOCKLIST): a sysadmin-disabled hostcall
|
||
// resolves to a trap stub instead of its real implementation, so a unit
|
||
// invoking it fails at runtime into the configurable error page. The
|
||
// decision is made once per import at workspace birth — no per-call cost,
|
||
// and zero cost when nothing is blocked. A small core set stays exempt so
|
||
// the runtime itself cannot be bricked.
|
||
if(mod == "env" && !worker.cfg.hostcall_blocklist.empty() && name.rfind("uce_host_", 0) == 0)
|
||
{
|
||
static const std::set<String> non_blockable = { "component_resolve" };
|
||
String bare = name.substr(9);
|
||
if(worker.cfg.hostcall_blocklist.count(bare) && !non_blockable.count(bare))
|
||
{
|
||
std::string blocked(name);
|
||
return(add([blocked](Caller, Span<const Val>, Span<Val>) -> Result<std::monostate, Trap> {
|
||
return(Trap("UCE_POLICY_BLOCKED:" + blocked));
|
||
}));
|
||
}
|
||
}
|
||
|
||
if(mod == "env" && name == "uce_host_time")
|
||
return(add([](Caller, Span<const Val>, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
results[0] = Val((int64_t)::time(0));
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_time_precise")
|
||
return(add([](Caller, Span<const Val>, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
results[0] = Val(time_precise());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_request_perf")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
DValue response;
|
||
if(self->request_perf.active)
|
||
{
|
||
f64 now = time_precise();
|
||
response["worker_pid"] = (f64)self->request_perf.worker_pid;
|
||
response["parent_pid"] = (f64)self->request_perf.parent_pid;
|
||
response["request_count"] = (f64)self->request_perf.request_count;
|
||
if(self->request_perf.time_start > 0 && self->request_perf.time_init > 0)
|
||
response["accept_us"] = (f64)((self->request_perf.time_start - self->request_perf.time_init) * 1000000.0);
|
||
if(self->request_perf.time_start > 0)
|
||
response["running_us"] = (f64)((now - self->request_perf.time_start) * 1000000.0);
|
||
if(self->request_perf.time_init > 0)
|
||
response["total_us"] = (f64)((now - self->request_perf.time_init) * 1000000.0);
|
||
if(self->workspace_birth_us > 0)
|
||
response["workspace_birth_us"] = (f64)self->workspace_birth_us;
|
||
}
|
||
String encoded = ucb_encode(response);
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
if(buf != 0 && cap >= encoded.size())
|
||
self->hostcall_write(buf, encoded);
|
||
results[0] = Val((int32_t)encoded.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_env")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String key, value;
|
||
if(self->hostcall_read(args[0].i32(), args[1].i32(), key) == "")
|
||
if(const char* raw = getenv(key.c_str()))
|
||
value = raw;
|
||
u32 cap = (u32)args[3].i32();
|
||
if(value.size() && cap >= value.size())
|
||
self->hostcall_write(args[2].i32(), value);
|
||
results[0] = Val((int32_t)value.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_random")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
u32 len = (u32)args[1].i32();
|
||
String bytes(len, 0);
|
||
FILE* urandom = fopen("/dev/urandom", "rb");
|
||
if(urandom)
|
||
{
|
||
size_t got = fread(&bytes[0], 1, len, urandom);
|
||
fclose(urandom);
|
||
bytes.resize(got);
|
||
}
|
||
self->hostcall_write(args[0].i32(), bytes);
|
||
results[0] = Val((int32_t)bytes.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_sha256")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=sha256_native(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_sha256_hex")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=sha256_hex_native(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_hmac_sha256")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String key,in; self->hostcall_read(args[0].i32(), args[1].i32(), key); self->hostcall_read(args[2].i32(), args[3].i32(), in); String out=hmac_sha256_native(key,in); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_hmac_sha256_hex")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String key,in; self->hostcall_read(args[0].i32(), args[1].i32(), key); self->hostcall_read(args[2].i32(), args[3].i32(), in); String out=hmac_sha256_hex_native(key,in); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_base64_encode")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=base64_encode(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_base64_decode")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); bool ok=false; String out=base64_decode(in, ok); if(!ok) out=""; u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_crypto_equal")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String a,b; self->hostcall_read(args[0].i32(), args[1].i32(), a); self->hostcall_read(args[2].i32(), args[3].i32(), b); results[0]=Val((int32_t)(crypto_equal_native(a,b)?1:0)); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_log")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
|
||
String text;
|
||
self->hostcall_read(args[1].i32(), args[2].i32(), text);
|
||
fprintf(stderr, "[guest log %d] %.*s\n", args[0].i32(), (int)text.size(), text.data());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_shell_exec")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String cmd;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), cmd);
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
String out;
|
||
String stage_key = "shell:" + cmd;
|
||
if(!self->hostcall_staged(stage_key, out))
|
||
{
|
||
out = ::shell_exec(cmd);
|
||
if(buf == 0)
|
||
self->hostcall_stage(stage_key, out);
|
||
}
|
||
if(buf != 0 && cap >= out.size())
|
||
self->hostcall_write(buf, out);
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_http_request")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); String out; String stage_key="http:"+encoded;
|
||
if(!self->hostcall_staged(stage_key,out)) { DValue req,response; String err; if(ucb_decode(encoded,req,&err)) response=uce_http_request_value(req); else response["error"]="http_request decode failed: "+err; out=ucb_encode(response); if(buf==0) self->hostcall_stage(stage_key,out); }
|
||
if(buf&&cap>=out.size()) self->hostcall_write(buf,out); caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks); results[0]=Val((int32_t)out.size()); return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_http_request_async")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); DValue req; String err; u64 id=0; if(ucb_decode(encoded,req,&err)) id=uce_http_spawn_spec(req); results[0]=Val((int64_t)id); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_shell_exec_dv")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||
u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32();
|
||
String out; String stage_key="shell_dv:"+encoded;
|
||
if(!self->hostcall_staged(stage_key,out)) { DValue spec, response; String err; if(ucb_decode(encoded,spec,&err)) response=uce_shell_exec_spec(spec); else response["error"]="shell_exec spec decode failed: "+err; out=ucb_encode(response); if(buf==0) self->hostcall_stage(stage_key,out); }
|
||
if(buf&&cap>=out.size()) self->hostcall_write(buf,out);
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
results[0]=Val((int32_t)out.size()); return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_shell_spawn")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); DValue spec; String err; u64 id=0; if(ucb_decode(encoded,spec,&err)) id=uce_shell_spawn_spec(spec); results[0]=Val((int64_t)id); return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_job_status")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String out=ucb_encode(uce_job_status_value((u64)args[0].i64())); u32 cap=(u32)args[2].i32(); int32_t buf=args[1].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_job_result")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String out=ucb_encode(uce_job_result_value((u64)args[0].i64(), 100)); u32 cap=(u32)args[2].i32(); int32_t buf=args[1].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_job_await")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { u64 timeout=std::min<u64>((u64)args[1].i64(), 30000); String out=ucb_encode(uce_job_result_value((u64)args[0].i64(), timeout)); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_job_cancel")
|
||
return(add([](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { results[0]=Val((int32_t)(uce_job_cancel_value((u64)args[0].i64())?1:0)); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_path_real")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
String resolved = ::path_real(path);
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
if(buf != 0 && cap >= resolved.size())
|
||
self->hostcall_write(buf, resolved);
|
||
results[0] = Val((int32_t)resolved.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_path_is_within")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, root;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), root);
|
||
results[0] = Val(::path_is_within(path, root) ? (int32_t)1 : (int32_t)0);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_cwd_get")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String cwd = ::cwd_get();
|
||
u32 cap = (u32)args[1].i32();
|
||
int32_t buf = args[0].i32();
|
||
if(buf != 0 && cap >= cwd.size())
|
||
self->hostcall_write(buf, cwd);
|
||
results[0] = Val((int32_t)cwd.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_cwd_set")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
String resolved = self->resolve_guest_cwd_set(path);
|
||
results[0] = Val(::chdir(resolved.c_str()) == 0 ? (int32_t)1 : (int32_t)0);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_process_start_directory")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String cwd = ::process_start_directory();
|
||
u32 cap = (u32)args[1].i32();
|
||
int32_t buf = args[0].i32();
|
||
if(buf != 0 && cap >= cwd.size())
|
||
self->hostcall_write(buf, cwd);
|
||
results[0] = Val((int32_t)cwd.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_last_trap_trace")
|
||
return(add([](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
(void)args;
|
||
results[0] = Val((int32_t)0);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_exists")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
String resolved = self->resolve_guest_file(path, current);
|
||
if(self->worker.cfg.verbose)
|
||
fprintf(stderr, "[wasm] file_exists(%s, current=%s) -> %s\n", path.c_str(), current.c_str(), resolved.c_str());
|
||
results[0] = Val(resolved != "" ? (int32_t)1 : (int32_t)0);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_mkdir")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
String resolved = self->resolve_guest_write(path, current);
|
||
int ok = 0;
|
||
if(resolved != "")
|
||
ok = (::mkdir(resolved.c_str(), 0777) == 0 || errno == EEXIST) ? 1 : 0;
|
||
results[0] = Val((int32_t)ok);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_mtime")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
String resolved = self->resolve_guest_file(path, current);
|
||
int64_t mtime = 0;
|
||
struct stat st;
|
||
if(resolved != "" && stat(resolved.c_str(), &st) == 0)
|
||
mtime = (int64_t)st.st_mtime;
|
||
results[0] = Val((int64_t)mtime);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_read")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
String stage_key = "file_read:" + path + "\0" + current;
|
||
String content;
|
||
if(!self->hostcall_staged(stage_key, content))
|
||
{
|
||
String resolved = self->resolve_guest_file(path, current);
|
||
content = resolved == "" ? String("") : ::file_get_contents(resolved);
|
||
self->hostcall_stage(stage_key, content);
|
||
}
|
||
u32 cap = (u32)args[5].i32();
|
||
int32_t buf = args[4].i32();
|
||
// length-query convention: no copy unless the buffer fits
|
||
if(buf != 0 && cap >= content.size())
|
||
self->hostcall_write(buf, content);
|
||
results[0] = Val((int32_t)content.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_list")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
String resolved = self->resolve_guest_file(path, current, true /*allow_dir*/);
|
||
String listing;
|
||
if(resolved != "")
|
||
{
|
||
std::vector<String> names;
|
||
if(DIR* d = opendir(resolved.c_str()))
|
||
{
|
||
while(struct dirent* e = readdir(d))
|
||
{
|
||
String n = e->d_name;
|
||
if(n != "." && n != "..")
|
||
names.push_back(n);
|
||
}
|
||
closedir(d);
|
||
}
|
||
// match the native ls -1 convention: bare names, sorted
|
||
std::sort(names.begin(), names.end());
|
||
listing = join(names, "\n");
|
||
}
|
||
u32 cap = (u32)args[5].i32();
|
||
int32_t buf = args[4].i32();
|
||
// length-query convention: no copy unless the buffer fits
|
||
if(buf != 0 && cap >= listing.size())
|
||
self->hostcall_write(buf, listing);
|
||
results[0] = Val((int32_t)listing.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_write")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current, content;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
self->hostcall_read(args[4].i32(), args[5].i32(), content);
|
||
bool append = args[6].i32() != 0;
|
||
String resolved = self->resolve_guest_write(path, current);
|
||
bool ok = false;
|
||
if(resolved != "")
|
||
ok = append ? file_append(resolved, content) : file_put_contents(resolved, content);
|
||
else if(self->worker.cfg.verbose)
|
||
fprintf(stderr, "[wasm] file_write denied: %s\n", path.c_str());
|
||
results[0] = Val(ok ? (int32_t)1 : (int32_t)0);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_open")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current, mode;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
self->hostcall_read(args[4].i32(), args[5].i32(), mode);
|
||
String resolved;
|
||
int flags = O_RDONLY;
|
||
int lock_type = LOCK_SH;
|
||
bool writable = false;
|
||
bool truncate_after_lock = false;
|
||
if(mode == "r")
|
||
resolved = self->resolve_guest_file(path, current);
|
||
else if(mode == "w")
|
||
{
|
||
resolved = self->resolve_guest_write(path, current);
|
||
flags = O_RDWR | O_CREAT;
|
||
lock_type = LOCK_EX;
|
||
writable = true;
|
||
truncate_after_lock = true;
|
||
}
|
||
else if(mode == "a")
|
||
{
|
||
resolved = self->resolve_guest_write(path, current);
|
||
flags = O_RDWR | O_CREAT | O_APPEND;
|
||
lock_type = LOCK_EX;
|
||
writable = true;
|
||
}
|
||
else if(mode == "r+")
|
||
{
|
||
resolved = self->resolve_guest_write(path, current);
|
||
flags = O_RDWR;
|
||
lock_type = LOCK_EX;
|
||
writable = true;
|
||
}
|
||
uint64_t handle = 0;
|
||
if(resolved != "")
|
||
{
|
||
int fd = wasm_open_locked_file(resolved, flags, lock_type, truncate_after_lock);
|
||
if(fd >= 0)
|
||
{
|
||
if(mode == "a")
|
||
lseek(fd, 0, SEEK_END);
|
||
self->file_handles.push_back({fd, writable});
|
||
handle = self->file_handles.size();
|
||
}
|
||
}
|
||
results[0] = Val((int64_t)handle);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_handle_read")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
u64 handle = (u64)args[0].i64();
|
||
u64 len = (u64)args[1].i64();
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
String out;
|
||
String stage_key = "file_handle_read:" + std::to_string(handle) + ":" + std::to_string(len);
|
||
if(!self->hostcall_staged(stage_key, out))
|
||
{
|
||
if(handle >= 1 && handle <= self->file_handles.size())
|
||
{
|
||
int fd = self->file_handles[(size_t)handle - 1].fd;
|
||
if(fd >= 0 && len > 0)
|
||
{
|
||
out.resize((size_t)std::min<u64>(len, 16ull * 1024ull * 1024ull));
|
||
ssize_t n = read(fd, &out[0], out.size());
|
||
out.resize(n > 0 ? (size_t)n : 0);
|
||
}
|
||
}
|
||
if(buf == 0) self->hostcall_stage(stage_key, out);
|
||
}
|
||
if(buf != 0 && cap >= out.size()) self->hostcall_write(buf, out);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_handle_pread")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
u64 handle = (u64)args[0].i64();
|
||
u64 offset = (u64)args[1].i64();
|
||
u64 len = (u64)args[2].i64();
|
||
u32 cap = (u32)args[4].i32();
|
||
int32_t buf = args[3].i32();
|
||
String out;
|
||
String stage_key = "file_handle_pread:" + std::to_string(handle) + ":" + std::to_string(offset) + ":" + std::to_string(len);
|
||
if(!self->hostcall_staged(stage_key, out))
|
||
{
|
||
if(handle >= 1 && handle <= self->file_handles.size())
|
||
{
|
||
int fd = self->file_handles[(size_t)handle - 1].fd;
|
||
if(fd >= 0 && len > 0)
|
||
{
|
||
out.resize((size_t)std::min<u64>(len, 16ull * 1024ull * 1024ull));
|
||
ssize_t n = pread(fd, &out[0], out.size(), (off_t)offset);
|
||
out.resize(n > 0 ? (size_t)n : 0);
|
||
}
|
||
}
|
||
if(buf == 0) self->hostcall_stage(stage_key, out);
|
||
}
|
||
if(buf != 0 && cap >= out.size()) self->hostcall_write(buf, out);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_handle_write")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String data; self->hostcall_read(args[1].i32(), args[2].i32(), data);
|
||
u64 handle = (u64)args[0].i64(); u64 written = 0;
|
||
if(handle >= 1 && handle <= self->file_handles.size())
|
||
{
|
||
auto& h = self->file_handles[(size_t)handle - 1];
|
||
if(h.fd >= 0 && h.writable) wasm_fd_write_all(h.fd, data.data(), data.size(), &written);
|
||
}
|
||
results[0] = Val((int64_t)written);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_handle_pwrite")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String data; self->hostcall_read(args[2].i32(), args[3].i32(), data);
|
||
u64 handle = (u64)args[0].i64(); u64 offset = (u64)args[1].i64(); u64 written = 0;
|
||
if(handle >= 1 && handle <= self->file_handles.size())
|
||
{
|
||
auto& h = self->file_handles[(size_t)handle - 1];
|
||
if(h.fd >= 0 && h.writable)
|
||
{
|
||
while(written < data.size())
|
||
{
|
||
ssize_t n = pwrite(h.fd, data.data() + written, data.size() - written, (off_t)(offset + written));
|
||
if(n < 0 && errno == EINTR) continue;
|
||
if(n <= 0) break;
|
||
written += (u64)n;
|
||
}
|
||
}
|
||
}
|
||
results[0] = Val((int64_t)written);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_handle_seek")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
u64 handle = (u64)args[0].i64(); s64 pos = -1;
|
||
if(handle >= 1 && handle <= self->file_handles.size())
|
||
{
|
||
int fd = self->file_handles[(size_t)handle - 1].fd;
|
||
if(fd >= 0) pos = (s64)lseek(fd, (off_t)args[1].i64(), args[2].i32());
|
||
}
|
||
results[0] = Val((int64_t)pos);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_handle_tell")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
u64 handle = (u64)args[0].i64(); s64 pos = -1;
|
||
if(handle >= 1 && handle <= self->file_handles.size())
|
||
{
|
||
int fd = self->file_handles[(size_t)handle - 1].fd;
|
||
if(fd >= 0) pos = (s64)lseek(fd, 0, SEEK_CUR);
|
||
}
|
||
results[0] = Val((int64_t)pos);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_handle_close")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
|
||
u64 handle = (u64)args[0].i64();
|
||
if(handle >= 1 && handle <= self->file_handles.size())
|
||
{
|
||
auto& h = self->file_handles[(size_t)handle - 1];
|
||
if(h.fd >= 0) { flock(h.fd, LOCK_UN); close(h.fd); h.fd = -1; }
|
||
}
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_stat")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current; self->hostcall_read(args[0].i32(), args[1].i32(), path); self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
String resolved = self->resolve_guest_file(path, current, true); DValue r; struct stat st;
|
||
r["exists"].set_bool(resolved != "" && lstat(resolved.c_str(), &st) == 0);
|
||
if(r["exists"].to_bool()) { r["size"]=(f64)st.st_size; r["mtime"]=(f64)st.st_mtime; r["ctime"]=(f64)st.st_ctime; r["mode"]=(f64)(st.st_mode & 07777); r["is_dir"].set_bool(S_ISDIR(st.st_mode)); r["is_file"].set_bool(S_ISREG(st.st_mode)); r["is_symlink"].set_bool(S_ISLNK(st.st_mode)); }
|
||
else { r["size"]=(f64)0; r["mtime"]=(f64)0; r["ctime"]=(f64)0; r["mode"]=(f64)0; r["is_dir"].set_bool(false); r["is_file"].set_bool(false); r["is_symlink"].set_bool(false); }
|
||
String out = ucb_encode(r); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf && cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_dir_list")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String path, current; self->hostcall_read(args[0].i32(), args[1].i32(), path); self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
String resolved = self->resolve_guest_file(path, current, true); DValue list; list.set_array();
|
||
if(resolved != "") { std::vector<String> names; if(DIR* d=opendir(resolved.c_str())) { while(struct dirent* e=readdir(d)) { String n=e->d_name; if(n!="."&&n!="..") names.push_back(n); } closedir(d); } std::sort(names.begin(), names.end()); for(auto& n:names) { String p=resolved+"/"+n; struct stat st; DValue item; item["name"]=n; if(lstat(p.c_str(), &st)==0) { item["size"]=(f64)st.st_size; item["mtime"]=(f64)st.st_mtime; item["type"]=S_ISDIR(st.st_mode)?"dir":S_ISLNK(st.st_mode)?"symlink":S_ISREG(st.st_mode)?"file":"other"; } list.push(item); } }
|
||
String out=ucb_encode(list); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_file_rename")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String from,to,current; self->hostcall_read(args[0].i32(),args[1].i32(),from); self->hostcall_read(args[2].i32(),args[3].i32(),to); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rf=self->resolve_guest_write(from,current), rt=self->resolve_guest_write(to,current); results[0]=Val((int32_t)(rf!=""&&rt!=""&&rename(rf.c_str(),rt.c_str())==0)); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_file_copy")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String from,to,current; self->hostcall_read(args[0].i32(),args[1].i32(),from); self->hostcall_read(args[2].i32(),args[3].i32(),to); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rf=self->resolve_guest_file(from,current), rt=self->resolve_guest_write(to,current); bool ok=false; if(rf!=""&&rt!="") { std::ifstream in(rf, std::ios::binary); std::ofstream out(rt, std::ios::binary|std::ios::trunc); out<<in.rdbuf(); struct stat st; if(in&&out) { ok=true; if(stat(rf.c_str(),&st)==0) chmod(rt.c_str(), st.st_mode & 07777); } } results[0]=Val((int32_t)ok); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_file_truncate")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); results[0]=Val((int32_t)(r!=""&&truncate(r.c_str(),(off_t)args[4].i64())==0)); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_dir_remove")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); bool rec=args[4].i32()!=0; bool ok=false; if(r!="") { if(rec) ok=std::filesystem::remove_all(r)>0; else ok=::rmdir(r.c_str())==0; } results[0]=Val((int32_t)ok); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_file_temp")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String prefix,current; self->hostcall_read(args[0].i32(),args[1].i32(),prefix); self->hostcall_read(args[2].i32(),args[3].i32(),current); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); String out; String stage_key="file_temp:"+prefix+"\0"+current; if(!self->hostcall_staged(stage_key,out)) { if(prefix=="") prefix="/tmp/uce-temp"; String templ=self->resolve_guest_write(prefix+"XXXXXX",current); if(templ!="") { std::vector<char> t(templ.begin(), templ.end()); t.push_back(0); int fd=mkstemp(t.data()); if(fd>=0) { close(fd); out=t.data(); } } if(buf==0) self->hostcall_stage(stage_key,out); } if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_file_chmod")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); results[0]=Val((int32_t)(r!=""&&chmod(r.c_str(),(mode_t)args[4].i32())==0)); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_file_symlink")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String target,linkpath,current; self->hostcall_read(args[0].i32(),args[1].i32(),target); self->hostcall_read(args[2].i32(),args[3].i32(),linkpath); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rt=self->resolve_guest_file(target,current), rl=self->resolve_guest_write(linkpath,current); results[0]=Val((int32_t)(rt!=""&&rl!=""&&symlink(rt.c_str(),rl.c_str())==0)); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_file_fsync")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { u64 handle=(u64)args[0].i64(); bool ok=false; if(handle>=1&&handle<=self->file_handles.size()) { int fd=self->file_handles[(size_t)handle-1].fd; ok=fd>=0&&fsync(fd)==0; } results[0]=Val((int32_t)ok); return(std::monostate()); }));
|
||
if(mod == "env" && name == "uce_host_zip")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String encoded;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
String out;
|
||
String stage_key = "zip:" + encoded;
|
||
if(!self->hostcall_staged(stage_key, out))
|
||
{
|
||
DValue request, response;
|
||
String decode_error;
|
||
try
|
||
{
|
||
if(ucb_decode(encoded, request, &decode_error))
|
||
{
|
||
String op = request["op"].to_string();
|
||
if(op == "list")
|
||
{
|
||
String path = self->resolve_guest_file(request["path"].to_string());
|
||
if(path == "") throw std::runtime_error("zip_list: path is outside wasm file policy");
|
||
response["result"] = zip_list(path);
|
||
}
|
||
else if(op == "read")
|
||
{
|
||
String path = self->resolve_guest_file(request["path"].to_string());
|
||
if(path == "") throw std::runtime_error("zip_read: path is outside wasm file policy");
|
||
response["result"] = zip_read(path, request["entry"].to_string());
|
||
}
|
||
else if(op == "create")
|
||
{
|
||
String path = self->resolve_guest_write(request["path"].to_string(), "");
|
||
if(path == "") throw std::runtime_error("zip_create: path is outside wasm file policy");
|
||
DValue* entries = request.key("entries");
|
||
response["ok"].set_bool(zip_create(path, entries ? *entries : DValue()));
|
||
}
|
||
else if(op == "extract")
|
||
{
|
||
String path = self->resolve_guest_file(request["path"].to_string());
|
||
String destination = self->resolve_guest_write(request["destination"].to_string(), "");
|
||
if(path == "" || destination == "") throw std::runtime_error("zip_extract: path is outside wasm file policy");
|
||
response["ok"].set_bool(zip_extract(path, destination));
|
||
}
|
||
else if(op == "gz_compress")
|
||
response["result"] = gz_compress(request["src"].to_string());
|
||
else if(op == "gz_uncompress")
|
||
response["result"] = gz_uncompress(request["src"].to_string());
|
||
}
|
||
}
|
||
catch(const std::exception& e)
|
||
{
|
||
response["error"] = e.what();
|
||
}
|
||
out = ucb_encode(response);
|
||
if(buf == 0)
|
||
self->hostcall_stage(stage_key, out);
|
||
}
|
||
if(buf != 0 && cap >= out.size())
|
||
self->hostcall_write(buf, out);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_units")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String encoded;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
String out;
|
||
String stage_key = "units:" + encoded;
|
||
if(!self->hostcall_staged(stage_key, out))
|
||
{
|
||
DValue request, response;
|
||
String decode_error;
|
||
try
|
||
{
|
||
if(ucb_decode(encoded, request, &decode_error))
|
||
{
|
||
String op = request["op"].to_string();
|
||
if(op == "info")
|
||
response["result"] = unit_info(request["path"].to_string());
|
||
else if(op == "list")
|
||
{
|
||
StringList paths = units_list();
|
||
for(auto& path : paths)
|
||
{
|
||
DValue item;
|
||
item = path;
|
||
response["result"].push(item);
|
||
}
|
||
}
|
||
else if(op == "compile")
|
||
response["ok"].set_bool(unit_compile(request["path"].to_string()));
|
||
else if(op == "call")
|
||
{
|
||
DValue* param = request.key("param");
|
||
ob_start();
|
||
DValue* result = unit_call(request["file"].to_string(), request["function"].to_string(), param);
|
||
response["output"] = ob_get_close();
|
||
if(result)
|
||
response["result"] = *result;
|
||
}
|
||
}
|
||
}
|
||
catch(const std::exception& e)
|
||
{
|
||
response["error"] = e.what();
|
||
}
|
||
out = ucb_encode(response);
|
||
if(buf == 0)
|
||
self->hostcall_stage(stage_key, out);
|
||
}
|
||
if(buf != 0 && cap >= out.size())
|
||
self->hostcall_write(buf, out);
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
#ifdef UCE_WASM_HOST_CONNECTORS
|
||
if(mod == "env" && name == "uce_host_sqlite")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
// {op,handle,path,query,params} in → result out. The native
|
||
// connector runs host-side; connections live in the workspace
|
||
// handle table (handle = 1-based index).
|
||
String encoded;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
String out;
|
||
String stage_key = "sqlite:" + encoded;
|
||
// run the op once across the length-query + fetch pair
|
||
if(!self->hostcall_staged(stage_key, out))
|
||
{
|
||
DValue request, response;
|
||
String decode_error;
|
||
if(ucb_decode(encoded, request, &decode_error))
|
||
{
|
||
String op = request["op"].to_string();
|
||
if(op == "connect")
|
||
{
|
||
SQLite* db = new SQLite();
|
||
db->connect(request["path"].to_string());
|
||
u64 handle = 0;
|
||
if(db->connection)
|
||
{
|
||
self->sqlite_handles.push_back(db);
|
||
handle = self->sqlite_handles.size();
|
||
}
|
||
response["handle"] = (f64)handle;
|
||
response["error_code"] = (f64)db->error_code;
|
||
response["statement_info"] = db->error();
|
||
if(handle == 0)
|
||
delete db;
|
||
}
|
||
else
|
||
{
|
||
u64 handle = request["handle"].to_u64();
|
||
SQLite* db = (handle >= 1 && handle <= self->sqlite_handles.size())
|
||
? self->sqlite_handles[(size_t)handle - 1] : 0;
|
||
if(op == "query" && db)
|
||
{
|
||
StringMap params;
|
||
DValue* p = request.key("params");
|
||
if(p)
|
||
p->each([&](const DValue& value, String key) { params[key] = value.to_string(); });
|
||
response["result"] = db->query(request["query"].to_string(), params);
|
||
response["insert_id"] = (f64)db->insert_id;
|
||
response["affected"] = (f64)db->affected_rows;
|
||
response["error_code"] = (f64)db->error_code;
|
||
response["statement_info"] = db->error();
|
||
}
|
||
else if(op == "disconnect" && db)
|
||
{
|
||
delete db;
|
||
self->sqlite_handles[(size_t)handle - 1] = 0;
|
||
}
|
||
}
|
||
}
|
||
out = ucb_encode(response);
|
||
if(buf == 0)
|
||
self->hostcall_stage(stage_key, out);
|
||
}
|
||
if(buf != 0 && cap >= out.size())
|
||
self->hostcall_write(buf, out);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_memcache_command")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String command;
|
||
self->hostcall_read(args[1].i32(), args[2].i32(), command);
|
||
String key = "memcache:" + std::to_string((u64)args[0].i64()) + ":" + command;
|
||
u32 cap = (u32)args[4].i32();
|
||
int32_t buf = args[3].i32();
|
||
String out;
|
||
if(buf != 0 && self->staged_memcache_key == key)
|
||
{
|
||
out = self->staged_memcache_result;
|
||
self->staged_memcache_key = "";
|
||
self->staged_memcache_result = "";
|
||
}
|
||
else
|
||
{
|
||
::socket_write((u64)args[0].i64(), command + "\r\n");
|
||
out = ::socket_read((u64)args[0].i64());
|
||
if(buf == 0)
|
||
{
|
||
self->staged_memcache_key = key;
|
||
self->staged_memcache_result = out;
|
||
}
|
||
}
|
||
if(buf != 0 && cap >= out.size())
|
||
self->hostcall_write(buf, out);
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_mysql")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String encoded;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
String out;
|
||
String stage_key = "mysql:" + encoded;
|
||
if(!self->hostcall_staged(stage_key, out))
|
||
{
|
||
DValue request, response;
|
||
String decode_error;
|
||
if(ucb_decode(encoded, request, &decode_error))
|
||
{
|
||
String op = request["op"].to_string();
|
||
if(op == "connect")
|
||
{
|
||
MySQL* db = new MySQL();
|
||
bool ok = db->connect(request["host"].to_string(), request["username"].to_string(), request["password"].to_string());
|
||
u64 handle = 0;
|
||
if(ok && db->connection)
|
||
{
|
||
self->mysql_handles.push_back(db);
|
||
handle = self->mysql_handles.size();
|
||
}
|
||
response["handle"] = (f64)handle;
|
||
response["error_code"] = (f64)db->_preload_next_error_code;
|
||
response["statement_info"] = db->error();
|
||
if(handle == 0)
|
||
delete db;
|
||
}
|
||
else if(op == "escape")
|
||
{
|
||
String quote = request["quote_char"].to_string();
|
||
response["result"] = mysql_escape(request["raw"].to_string(), quote.size() ? quote[0] : 0);
|
||
}
|
||
else
|
||
{
|
||
u64 handle = request["handle"].to_u64();
|
||
MySQL* db = (handle >= 1 && handle <= self->mysql_handles.size())
|
||
? self->mysql_handles[(size_t)handle - 1] : 0;
|
||
if(op == "query" && db)
|
||
{
|
||
response["result"] = db->query(request["query"].to_string());
|
||
response["insert_id"] = (f64)db->insert_id;
|
||
response["affected"] = (f64)db->affected_rows;
|
||
response["error_code"] = (f64)db->_preload_next_error_code;
|
||
response["statement_info"] = db->error();
|
||
}
|
||
else if(op == "disconnect" && db)
|
||
{
|
||
delete db;
|
||
self->mysql_handles[(size_t)handle - 1] = 0;
|
||
}
|
||
}
|
||
}
|
||
out = ucb_encode(response);
|
||
if(buf == 0)
|
||
self->hostcall_stage(stage_key, out);
|
||
}
|
||
if(buf != 0 && cap >= out.size())
|
||
self->hostcall_write(buf, out);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
#endif
|
||
if(mod == "env" && name == "uce_host_file_unlink")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
|
||
String path, current;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||
String resolved = self->resolve_guest_write(path, current);
|
||
if(resolved != "")
|
||
::unlink(resolved.c_str());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_socket_connect")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String host;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), host);
|
||
int fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||
if(fd >= 0)
|
||
{
|
||
struct sockaddr_in addr = {0};
|
||
addr.sin_family = AF_INET;
|
||
addr.sin_port = htons((short)args[2].i32());
|
||
addr.sin_addr.s_addr = inet_addr(host.c_str());
|
||
if(::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
|
||
{
|
||
::close(fd);
|
||
fd = -1;
|
||
}
|
||
else if(fd == 0)
|
||
{
|
||
int moved = ::dup(fd);
|
||
::close(fd);
|
||
fd = moved;
|
||
}
|
||
if(fd > 0 && context)
|
||
context->resources.sockets.push_back(fd);
|
||
}
|
||
results[0] = Val((int64_t)(fd > 0 ? fd : 0));
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_socket_close")
|
||
return(add([](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
|
||
::socket_close((u64)args[0].i64());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_socket_write")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String data;
|
||
self->hostcall_read(args[1].i32(), args[2].i32(), data);
|
||
results[0] = Val(::socket_write((u64)args[0].i64(), data) ? (int32_t)1 : (int32_t)0);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_socket_read")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
u64 sockfd = (u64)args[0].i64();
|
||
u32 max_length = (u32)args[1].i32();
|
||
u32 timeout = (u32)args[2].i32();
|
||
int32_t buf = args[3].i32();
|
||
u32 cap = (u32)args[4].i32();
|
||
String key = std::to_string(sockfd) + ":" + std::to_string(max_length) + ":" + std::to_string(timeout);
|
||
String out;
|
||
if(buf != 0 && self->staged_socket_read_key == key)
|
||
{
|
||
out = self->staged_socket_read_result;
|
||
self->staged_socket_read_key = "";
|
||
self->staged_socket_read_result = "";
|
||
}
|
||
else
|
||
{
|
||
out = ::socket_read(sockfd, max_length, timeout);
|
||
if(buf == 0)
|
||
{
|
||
self->staged_socket_read_key = key;
|
||
self->staged_socket_read_result = out;
|
||
}
|
||
}
|
||
if(buf != 0 && cap >= out.size())
|
||
self->hostcall_write(buf, out);
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_server_start_http")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String key, bind, file, function, current;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), key);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), bind);
|
||
self->hostcall_read(args[4].i32(), args[5].i32(), file);
|
||
self->hostcall_read(args[6].i32(), args[7].i32(), function);
|
||
self->hostcall_read(args[8].i32(), args[9].i32(), current);
|
||
String resolved = self->resolve_guest_file(file, current);
|
||
pid_t pid = 0;
|
||
try
|
||
{
|
||
if(resolved != "")
|
||
pid = ::server_start_http(key, bind, resolved, function);
|
||
}
|
||
catch(const std::exception& e)
|
||
{
|
||
fprintf(stderr, "[wasm server] start failed for key '%s': %s\n", key.c_str(), e.what());
|
||
}
|
||
catch(...)
|
||
{
|
||
fprintf(stderr, "[wasm server] start failed for key '%s'\n", key.c_str());
|
||
}
|
||
results[0] = Val((int32_t)pid);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_server_stop")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String key;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), key);
|
||
results[0] = Val(::server_stop(key) ? (int32_t)1 : (int32_t)0);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_task_spawn")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String key;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), key);
|
||
u64 callback_id = (u64)args[2].i64();
|
||
f64 interval = args[3].f64();
|
||
u64 timeout = (u64)args[4].i64();
|
||
bool repeat = args[5].i32() != 0;
|
||
// task()/task_repeat() fork and invoke this lambda only in the child
|
||
// before the hostcall stack unwinds, so `self` points to the child's
|
||
// copy of this per-request workspace. The parent request can return and
|
||
// destroy its workspace without invalidating the child copy.
|
||
auto run_callback = [self, callback_id]() {
|
||
String error = self->run_task_callback(callback_id);
|
||
if(error != "")
|
||
fprintf(stderr, "[wasm task] callback failed: %s\n", error.c_str());
|
||
};
|
||
pid_t pid = 0;
|
||
try
|
||
{
|
||
if(!repeat || (interval > 0 && std::isfinite(interval)))
|
||
pid = repeat
|
||
? ::task_repeat(key, interval, run_callback, timeout)
|
||
: ::task(key, run_callback, timeout);
|
||
}
|
||
catch(const std::exception& e)
|
||
{
|
||
fprintf(stderr, "[wasm task] spawn failed for key '%s': %s\n", key.c_str(), e.what());
|
||
}
|
||
catch(...)
|
||
{
|
||
fprintf(stderr, "[wasm task] spawn failed for key '%s'\n", key.c_str());
|
||
}
|
||
results[0] = Val((int32_t)pid);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_task_pid")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String key;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), key);
|
||
results[0] = Val((int32_t)::task_pid(key));
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_task_kill")
|
||
return(add([](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
results[0] = Val((int32_t)::task_kill((pid_t)args[0].i32(), args[1].i32()));
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_sleep_us")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
u64 usec = (u64)args[0].i64();
|
||
while(usec >= 1000000ull)
|
||
{
|
||
unsigned int remaining = ::sleep((unsigned int)(usec / 1000000ull));
|
||
if(remaining != 0)
|
||
{
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
results[0] = Val((int32_t)remaining);
|
||
return(std::monostate());
|
||
}
|
||
usec %= 1000000ull;
|
||
}
|
||
if(usec > 0)
|
||
::usleep((useconds_t)usec);
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
results[0] = Val((int32_t)0);
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_regex")
|
||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
// {op,pattern,subject,flags,replacement} in (UCEB2) → result out.
|
||
// PCRE2 lives host-side; this runs the native regex_*.
|
||
String encoded;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||
DValue request;
|
||
String decode_error;
|
||
DValue response;
|
||
if(ucb_decode(encoded, request, &decode_error))
|
||
{
|
||
String op = request["op"].to_string();
|
||
String pattern = request["pattern"].to_string();
|
||
String subject = request["subject"].to_string();
|
||
String flags = request["flags"].to_string();
|
||
if(op == "match")
|
||
response["bool"].set_bool(regex_match(pattern, subject, flags));
|
||
else if(op == "search")
|
||
response["tree"] = regex_search(pattern, subject, flags);
|
||
else if(op == "search_all")
|
||
response["tree"] = regex_search_all(pattern, subject, flags);
|
||
else if(op == "replace")
|
||
response["text"] = regex_replace(pattern, request["replacement"].to_string(), subject, flags);
|
||
else if(op == "split")
|
||
for(auto& part : regex_split(pattern, subject, flags))
|
||
{
|
||
DValue value;
|
||
value = part;
|
||
response["list"].push(value);
|
||
}
|
||
}
|
||
String out = ucb_encode(response);
|
||
u32 cap = (u32)args[3].i32();
|
||
int32_t buf = args[2].i32();
|
||
if(buf != 0 && cap >= out.size())
|
||
self->hostcall_write(buf, out);
|
||
results[0] = Val((int32_t)out.size());
|
||
return(std::monostate());
|
||
}));
|
||
if(mod == "env" && name == "uce_host_component_resolve")
|
||
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||
String target, handler, current, resolved;
|
||
self->hostcall_read(args[0].i32(), args[1].i32(), target);
|
||
self->hostcall_read(args[2].i32(), args[3].i32(), handler);
|
||
self->hostcall_read(args[4].i32(), args[5].i32(), current);
|
||
int32_t slot = self->component_resolve(target, handler, current, resolved);
|
||
u32 cap = (u32)args[7].i32();
|
||
if(cap > 0)
|
||
{
|
||
if(resolved.size() >= cap)
|
||
resolved = resolved.substr(0, cap - 1);
|
||
resolved.push_back('\0');
|
||
self->hostcall_write(args[6].i32(), resolved);
|
||
}
|
||
results[0] = Val(slot);
|
||
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
|
||
return(std::monostate());
|
||
}));
|
||
|
||
// anything else (wasi-libc residue): a named trap — tolerated as long
|
||
// as it is never called
|
||
String label = mod + "." + name;
|
||
return(add([label](Caller, Span<const Val>, Span<Val>) -> Result<std::monostate, Trap> {
|
||
return(wasmtime::Trap("unimplemented host import called: " + std::string(label)));
|
||
}));
|
||
}
|
||
|
||
String hostcall_read(int32_t ptr, int32_t len, String& out)
|
||
{
|
||
return(guest_read((u32)ptr, (u32)len, out));
|
||
}
|
||
|
||
void hostcall_write(int32_t ptr, const String& data)
|
||
{
|
||
guest_write((u32)ptr, data);
|
||
}
|
||
};
|
||
|
||
// ---- public entry: one request through one workspace -----------------------
|
||
|
||
inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_tree, const String& entry_source_path,
|
||
const String& handler = "render", const Request* request = 0)
|
||
{
|
||
WasmResponse response;
|
||
WasmWorkspace workspace(worker);
|
||
if(request)
|
||
workspace.set_perf_snapshot(my_pid, (u64)parent_pid, request->server ? request->server->request_count : 0, request->stats.time_init, request->stats.time_start);
|
||
auto birth_start = std::chrono::steady_clock::now();
|
||
String error = workspace.birth();
|
||
workspace.workspace_birth_us = (u64)std::chrono::duration_cast<std::chrono::microseconds>(
|
||
std::chrono::steady_clock::now() - birth_start).count();
|
||
if(error == "")
|
||
error = workspace.apply_context(context_tree);
|
||
if(error == "")
|
||
error = workspace.invoke_entry(entry_source_path, handler, &response.handler_present);
|
||
if(error == "")
|
||
error = workspace.collect(response);
|
||
response.workspace_birth_us = workspace.workspace_birth_us;
|
||
response.component_resolve_count = workspace.component_resolve_count;
|
||
response.component_resolve_total_us = workspace.component_resolve_total_us;
|
||
if(error != "")
|
||
{
|
||
response.ok = false;
|
||
response.error = error;
|
||
return(response);
|
||
}
|
||
response.ok = true;
|
||
return(response);
|
||
}
|