This commit is contained in:
root
2026-06-15 21:42:50 +00:00
parent 34a97e2577
commit 99cd92fb4a
126 changed files with 1615 additions and 1057 deletions
+23 -20
View File
@@ -1,7 +1,7 @@
// W4 — FastCGI backend glue for the W3 wasm workspace runtime.
//
// Included into the native server TU (src/linux_fastcgi.cpp) after uce_lib.cpp,
// so it shares String/DValue/config and the UCEB1 codec. Provides a
// so it shares String/DValue/config and the UCEB2 codec. Provides a
// Always-on wasm backend: every unit request is served through a per-request
// wasm workspace. The legacy native dlopen execution path has been removed.
@@ -50,9 +50,9 @@ static String wasm_backend_ensure_started(Request* context)
for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" })
if(cfg[key] != "")
wc.write_roots.push_back(cfg[key]);
wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024);
wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200);
wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false);
wc.memory_limit = (int64_t)to_u64(cfg["WASM_MEMORY_LIMIT_BYTES"], 512ull * 1024 * 1024);
wc.epoch_deadline_ticks = to_u64(cfg["WASM_EPOCH_DEADLINE_TICKS"], 200);
wc.verbose = to_bool(cfg["WASM_BACKEND_VERBOSE"], false);
g_wasm_worker = new WasmWorker(wc);
g_wasm_init_error = g_wasm_worker->init();
@@ -65,7 +65,7 @@ static String wasm_backend_ensure_started(Request* context)
g_wasm_epoch_running.store(true);
WasmWorker* worker = g_wasm_worker;
u64 period_ms = config_u64("WASM_EPOCH_PERIOD_MS", 50);
u64 period_ms = to_u64(cfg["WASM_EPOCH_PERIOD_MS"], 50);
g_wasm_epoch_ticker = new std::thread([worker, period_ms] {
while(g_wasm_epoch_running.load())
{
@@ -84,12 +84,13 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit)
struct stat wasm_st;
if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode))
return(false);
// Require the artifact to be newer than the source. If it is stale,
// dispatch compiles the unit on demand and then rechecks this predicate.
struct stat src_st;
if(stat(entry_unit.c_str(), &src_st) != 0 || !S_ISREG(src_st.st_mode))
// Require the artifact to satisfy the full compiler freshness check. Source
// mtime alone misses runtime/unit ABI changes, setup-template changes, and
// metadata mismatches, which can leave stale wasm with old imports.
bool source_missing = false;
if(compiler_unit_needs_recompile(context, entry_unit, &source_missing))
return(false);
if(wasm_st.st_mtime < src_st.st_mtime)
if(source_missing)
return(false);
return(true);
}
@@ -151,23 +152,25 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri
if(!response.ok)
return(response.error == "" ? String("wasm workspace failed") : response.error);
// Any handler may have called ws_send/ws_close (not just WS handlers). If it
// left dispatch commands, flush the batch to the central WS broker, which owns
// all connections and resolves each command's target (id/scope/broadcast)
// against the full registry. This is the only path WS data takes out — the
// workspace owns no connections.
if(DValue* cmds = response.meta.key("ws_commands"))
// Any handler may have called ws_send/ws_close (not just WS handlers). Flush the
// websocket batch whenever either command frames or an updated per-connection
// state are present. This is the only path WS data takes out; the workspace
// owns no connections.
DValue* cmds = response.meta.key("ws_commands");
DValue* cstate = response.meta.key("ws_connection_state");
if(cmds || cstate)
{
DValue batch;
batch["commands"] = *cmds;
if(DValue* cstate = response.meta.key("ws_connection_state"))
if(cmds)
batch["commands"] = *cmds;
if(cstate)
{
batch["connection_id"] = request.resources.websocket_connection_id;
batch["connection_state"] = *cstate;
}
StringMap dispatch_params;
dispatch_params["UCE_WS_DISPATCH"] = "1";
String broker_socket = first(request.server ? request.server->config["WS_BROKER_SOCKET_PATH"] : String(),
String broker_socket = first(request.server->config["WS_BROKER_SOCKET_PATH"],
"/run/uce/ws-broker.sock");
fcgi_forward_request(broker_socket, dispatch_params, ucb_encode(batch), 5);
}
@@ -182,7 +185,7 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri
// Diagnostic timing headers are opt-in: they leak workspace internals and
// belong to the W5 benchmark harness, not public responses.
if(config_bool("WASM_BACKEND_VERBOSE", false))
if(to_bool(request.server->config["WASM_BACKEND_VERBOSE"], false))
{
request.header["X-UCE-Backend"] = "wasm";
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
+19 -8
View File
@@ -12,7 +12,7 @@
// ---- W3 connector membranes -----------------------------------------------
// sqlite/mysql run host-side (the host links the native connectors and owns the
// connections in per-workspace handle tables). UCEB1-marshalled hostcalls carry
// connections in per-workspace handle tables). UCEB2-marshalled hostcalls carry
// operation requests/responses; `connection` holds the host handle (>0).
static const char* WASM_DB_UNAVAILABLE =
@@ -303,7 +303,7 @@ DValue MySQL::query(String q, StringMap params) { return(query(parse_query_param
DValue MySQL::get_pending_result() { return(DValue()); }
// sqlite runs host-side (the host links libsqlite and owns the connections in
// a per-workspace handle table). One UCEB1-marshalled hostcall carries
// a per-workspace handle table). One UCEB2-marshalled hostcall carries
// {op,handle,path,query,params} in and {handle,result,insert_id,affected,
// error_code,statement_info} out. `connection` holds the host handle (>0).
extern "C" size_t uce_host_sqlite(const char* in, size_t in_len, char* out, size_t cap);
@@ -766,7 +766,7 @@ void uce_wasm_core_reset_request()
wasm_component_slots.clear();
}
// Host pushes the UCEB1-encoded request context into a guest buffer
// Host pushes the UCEB2-encoded request context into a guest buffer
// (uce_alloc) and applies it here; mirrors the native param population.
int uce_wasm_apply_context(const char* buf, size_t len)
{
@@ -800,6 +800,8 @@ int uce_wasm_apply_context(const char* buf, size_t len)
// websocket event context: ws_send()/ws_close() capture into the dispatch
// list (the workspace owns no connections), which collect() carries back to
// the broker. Reset per invocation.
wasm_request.connection = DValue();
wasm_request.resources.websocket_connection_state_before = DValue();
wasm_request.resources.websocket_dispatch_commands = DValue();
wasm_request.resources.websocket_dispatch_capture = false;
DValue* ws = decoded.key("ws");
@@ -817,6 +819,7 @@ int uce_wasm_apply_context(const char* buf, size_t len)
wasm_request.resources.websocket_dispatch_capture = true;
if(DValue* cstate = ws->key("connection_state"))
wasm_request.connection = *cstate;
wasm_request.resources.websocket_connection_state_before = wasm_request.connection;
}
return(0);
}
@@ -829,7 +832,7 @@ Request* uce_wasm_request()
}
// After render: response metadata (status line, headers, cookies, session)
// goes back to the host as UCEB1.
// goes back to the host as UCEB2.
void uce_wasm_finish_response_meta()
{
DValue meta;
@@ -844,14 +847,22 @@ void uce_wasm_finish_response_meta()
}
for(auto& entry : wasm_request.session)
meta["session"][entry.first] = entry.second;
bool ws_has_commands = !wasm_request.resources.websocket_dispatch_commands._map.empty();
bool ws_state_changed = false;
if(wasm_request.resources.websocket_dispatch_capture)
{
String prior_state = ucb_encode(wasm_request.resources.websocket_connection_state_before);
String current_state = ucb_encode(wasm_request.connection);
ws_state_changed = (prior_state != current_state);
}
// Any unit code (not just WS handlers) may call ws_send/ws_close; whenever the
// dispatch list is non-empty, carry it back so the worker can flush it to the
// broker. ws_connection_state rides along for stateful WS handlers.
if(!wasm_request.resources.websocket_dispatch_commands._map.empty())
{
// broker. If only connection state changed, flush a command-less state-only
// batch so the broker can persist the connection mutation.
if(ws_has_commands)
meta["ws_commands"] = wasm_request.resources.websocket_dispatch_commands;
if(ws_state_changed)
meta["ws_connection_state"] = wasm_request.connection;
}
wasm_response_meta = ucb_encode(meta);
}
+3 -3
View File
@@ -280,7 +280,7 @@ int main(int argc, char** argv)
int32_t encoded_ptr = call_i32(core, "uce_alloc", { encoded_len });
CHECK(call_i32(core, "uce_dv_encode", { root, encoded_ptr, encoded_len }) == encoded_len, "encode length mismatch");
std::string encoded = read_bytes(memory, encoded_ptr, encoded_len);
CHECK(encoded.rfind("UCEB\x01", 0) == 0, "UCEB1 header missing");
CHECK(encoded.size() >= 5 && encoded.compare(0, 4, "UCEB") == 0 && (unsigned char)encoded[4] == 2, "UCEB2 header missing");
int32_t decoded = call_i32(core, "uce_dv_decode", { encoded_ptr, encoded_len });
CHECK(decoded != 0, "uce_dv_decode failed");
CHECK(call_i32(core, "uce_dv_count", { decoded }) == 1, "decoded root count mismatch");
@@ -290,8 +290,8 @@ int main(int argc, char** argv)
std::string bad = "bad";
int32_t bad_ptr = call_i32(core, "uce_alloc", { (int32_t)bad.size() });
write_bytes(memory, bad_ptr, bad);
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB1 decode unexpectedly succeeded");
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB1 decode did not set error");
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB2 decode unexpectedly succeeded");
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB2 decode did not set error");
std::string out = "W1 output";
int32_t out_ptr = call_i32(core, "uce_alloc", { (int32_t)out.size() });
+1 -1
View File
@@ -1,7 +1,7 @@
// W3 CLI driver — the exit gate for WASM-PROPOSAL §9.1 W3.
//
// Serves requests through the production workspace runtime
// (src/wasm/worker.cpp): UCEB1 context in → core + lazily loaded
// (src/wasm/worker.cpp): UCEB2 context in → core + lazily loaded
// generated units → body/response-meta out. Each --repeat gets a fresh
// workspace, proving birth/drop. An epoch ticker thread enforces the CPU
// budget; the store limiter enforces memory; traps come back as collapsed
+57 -2
View File
@@ -1012,6 +1012,56 @@ private:
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;
@@ -1378,7 +1428,8 @@ private:
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);
results[0] = Val(::chdir(path.c_str()) == 0 ? (int32_t)1 : (int32_t)0);
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")
@@ -1991,6 +2042,10 @@ private:
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 != "")
@@ -2049,7 +2104,7 @@ private:
}));
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 (UCEB1) → result out.
// {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);