wasm runtime: central WS broker, unified handlers, W7d holdouts, membrane completeness

- WS: a dedicated broker process owns HTTP_PORT + every connection; it forwards
  renders to the worker pool over uce.sock (non-blocking) and applies ws_*
  command batches flushed back at workspace teardown. Removes the now-dead
  per-worker websocket executor (-509 lines).
- Dispatch: unify CLI / WebSocket / serve_http / page render through one
  serve_via_wasm(entry_unit, handler) path; handler string -> __uce_<handler>
  export symbol.
- W7d: rewrite zip.uce to the membrane return-value error contract (no C++
  try/catch), error-reporting.uce to genuine wasm traps instead of throw, and
  sharedunit.uce to unit_info(); empty the native-only token gate.
- Membrane: wire ls / mkdir / file_mtime through new uce_host_file_list /
  uce_host_file_mkdir / uce_host_file_mtime hostcalls (resolve_guest_file gains
  directory support). Fixes /doc/index.uce listing nothing; adds a regression
  assertion that the index enumerates items.
- Docs: add docs/wasm-runtime-architecture.md; record the W7e staged native-
  deletion plan in WASM-PROPOSAL.md.

Verified: scripts/run_cli_tests.sh --include-wasm-kill -> 87 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-14 17:51:51 +00:00
co-authored by Claude Opus 4.8
parent 15e8d092bc
commit 8587fbc5aa
14 changed files with 1219 additions and 769 deletions
+59 -24
View File
@@ -19,6 +19,8 @@
#include <atomic>
#include <sys/stat.h>
#include <thread>
// Worker flushes ws_* dispatch batches to the broker via the FastCGI client.
#include "../lib/fcgi_forward.h"
// per forked worker process: one engine + compiled-core cache, one epoch ticker
static WasmWorker* g_wasm_worker = 0;
@@ -116,18 +118,13 @@ static bool wasm_backend_native_fallback_uncached(Request* context, const String
// Supported by the wasm core, intentionally NOT in this list:
// - regex_* (host PCRE2 hostcall), xml_*/yaml_*/markdown_* (compiled in),
// - unit_render()/component() (host resolver).
// What remains is genuinely host-owned / native-only for now:
// - zip pages that use C++ try/catch around zip_* error cases;
// - direct compiler_load_shared_unit() access to native SharedUnit internals.
// Background tasks, sleep/usleep, sockets/custom servers, memcache, mysql,
// and unit_call/unit_compile/unit_info/units_list are host-owned but now have
// membrane hostcalls, so they intentionally do not fallback here.
StringList native_only_tokens = {
"zip_", "compiler_load_shared_unit("
};
for(auto& token : native_only_tokens)
if(source.find(token) != String::npos)
return(true);
// W7d: every former native-only surface now goes through the membrane —
// zip_* (uce_host_zip), unit introspection (uce_host_units: unit_info/
// unit_call/units_list), regex/xml/yaml/markdown, filesystem, sqlite,
// background tasks, sleep, sockets/custom servers, memcache, mysql. No unit
// source token forces native anymore, so there is nothing left to scan for.
// (The native fallback now only covers cold/stale artifacts; W7e removes it.)
(void)source;
return(false);
}
@@ -168,11 +165,12 @@ bool wasm_backend_should_handle(Request& request, const String& entry_unit)
}
// Serve a request through a wasm workspace using the unit handler selected by
// `kind` (page render or cli). Populates the native Request
// (status/headers/cookies/session/body) so the existing transport writes the
// response unchanged. Returns "" on success, or a collapsed error/trace string
// for the caller to route into the configured error page.
String wasm_backend_serve(Request& request, const String& entry_unit, int32_t kind = WasmWorkspace::RESOLVE_RENDER)
// `kind` (page render / cli / serve_http, with an optional named serve_http
// handler). Populates the native Request (status/headers/cookies/session/body)
// so the existing transport writes the response unchanged. Returns "" on
// success, or a collapsed error/trace string for the caller to route into the
// configured error page.
String wasm_backend_serve(Request& request, const String& entry_unit, const String& handler = "render")
{
DValue ctx;
auto copy_map = [&](const StringMap& source, const char* key) {
@@ -186,19 +184,56 @@ String wasm_backend_serve(Request& request, const String& entry_unit, int32_t ki
copy_map(request.session, "session");
ctx["entry_unit"] = entry_unit;
// Raw request body: cli_input() parses a JSON CLI payload from context.in,
// carried into the workspace, not just the form-decoded post map.
// and serve_http handlers read it as req->in; carried into the workspace.
ctx["in"] = request.in;
// WebSocket event context: the workspace owns no connections, so the frame's
// connection identity goes in and the handler's ws_send/ws_close dispatch
// commands come back out (below) for the native broker to apply.
if(handler == "websocket")
{
ctx["ws"]["connection_id"] = request.resources.websocket_connection_id;
ctx["ws"]["scope"] = request.resources.websocket_scope;
ctx["ws"]["opcode"] = (f64)request.resources.websocket_opcode;
ctx["ws"]["binary"].set_bool(request.resources.websocket_is_binary);
for(auto& id : request.resources.websocket_scope_connection_ids)
{
DValue v; v = id;
ctx["ws"]["connections"].push(v);
}
ctx["ws"]["connection_state"] = request.connection;
}
WasmResponse response = wasm_worker_serve(*g_wasm_worker, ctx, entry_unit, kind);
WasmResponse response = wasm_worker_serve(*g_wasm_worker, ctx, entry_unit, handler);
if(!response.ok)
return(response.error == "" ? String("wasm workspace failed") : response.error);
// A cli unit that does not export __uce_cli is a 404, matching native
// compiler_invoke_cli ("CLI Entry Point Not Found"). For page render a
// missing handler is simply an empty body (native parity).
if(!response.handler_present && kind == WasmWorkspace::RESOLVE_CLI)
// 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"))
{
request.set_status(404, "CLI Entry Point Not Found");
DValue batch;
batch["commands"] = *cmds;
if(DValue* cstate = response.meta.key("ws_connection_state"))
{
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(),
"/run/uce/ws-broker.sock");
fcgi_forward_request(broker_socket, dispatch_params, ucb_encode(batch), 5);
}
// A cli/serve_http unit that does not export the requested handler is a 404,
// matching native compiler_invoke_cli. For page render a missing handler is
// simply an empty body (native parity).
if(!response.handler_present && handler != "render")
{
request.set_status(404, handler == "cli" ? "CLI Entry Point Not Found" : "Handler Not Found");
return("");
}