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
+172
View File
@@ -0,0 +1,172 @@
#pragma once
// Minimal FastCGI client used by the connection brokers (the custom HTTP server
// dispatcher and the websocket exec child) to render a request through a normal
// worker on /run/uce.sock instead of rendering wasm in the broker's own forked
// process. Wasmtime cannot be safely re-created across fork, so the broker owns
// the connection but forwards the actual unit invocation to a clean-engine
// worker — the "broker holds connections, units respond like RENDER()" model.
//
// Native-only; compiled into the main object. Needs String/StringMap (uce_lib.h)
// plus the socket headers below.
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <algorithm>
struct FcgiForwardResult {
bool ok = false;
String error;
int status = 200; // parsed from a CGI "Status:" header if present
StringMap headers; // response headers (excluding Status)
String body; // response body
};
// One FastCGI stream (PARAMS/STDIN): data records (<=64KB each) then the
// terminating empty record the protocol requires to close the stream.
inline void fcgi_forward_stream(String& out, unsigned char type, const String& content)
{
size_t off = 0, len = content.size();
while(off < len)
{
size_t chunk = std::min(len - off, (size_t)0xffff);
unsigned char hdr[8] = { 1, type, 0, 1, (unsigned char)((chunk >> 8) & 0xff), (unsigned char)(chunk & 0xff), 0, 0 };
out.append((const char*)hdr, 8);
out.append(content.data() + off, chunk);
off += chunk;
}
unsigned char term[8] = { 1, type, 0, 1, 0, 0, 0, 0 };
out.append((const char*)term, 8);
}
// FastCGI name/value pair length prefix: 1 byte if < 128, else 4 bytes (high bit set).
inline void fcgi_forward_put_len(String& out, size_t n)
{
if(n < 128)
out.push_back((char)(unsigned char)n);
else
{
out.push_back((char)(unsigned char)(((n >> 24) & 0xff) | 0x80));
out.push_back((char)(unsigned char)((n >> 16) & 0xff));
out.push_back((char)(unsigned char)((n >> 8) & 0xff));
out.push_back((char)(unsigned char)(n & 0xff));
}
}
// Build the full FastCGI request bytes (BEGIN_REQUEST + PARAMS + STDIN) for a
// RESPONDER request id 1. The broker reuses this to fire renders non-blocking.
inline String fcgi_build_request(const StringMap& params, const String& stdin_body)
{
String request;
unsigned char begin[16] = { 1, 1, 0, 1, 0, 8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 };
request.append((const char*)begin, 16);
String params_encoded;
for(auto& kv : params)
{
fcgi_forward_put_len(params_encoded, kv.first.size());
fcgi_forward_put_len(params_encoded, kv.second.size());
params_encoded.append(kv.first);
params_encoded.append(kv.second);
}
fcgi_forward_stream(request, 4 /*FCGI_PARAMS*/, params_encoded);
fcgi_forward_stream(request, 5 /*FCGI_STDIN*/, stdin_body);
return(request);
}
// Forward `params` + `stdin_body` to the FastCGI responder at unix `socket_path`
// and return the parsed CGI response. Times out (recv) at `timeout_seconds`.
inline FcgiForwardResult fcgi_forward_request(const String& socket_path,
const StringMap& params, const String& stdin_body, u32 timeout_seconds = 30)
{
FcgiForwardResult result;
int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0)
{
result.error = "fcgi_forward: socket() failed";
return(result);
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1);
if(::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
{
::close(fd);
result.error = "fcgi_forward: connect(" + socket_path + ") failed";
return(result);
}
struct timeval tv;
tv.tv_sec = timeout_seconds;
tv.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
String request = fcgi_build_request(params, stdin_body);
if(::send(fd, request.data(), request.size(), MSG_NOSIGNAL) != (ssize_t)request.size())
{
::close(fd);
result.error = "fcgi_forward: short write to responder";
return(result);
}
// Read records; collect FCGI_STDOUT content until FCGI_END_REQUEST or EOF.
String inbuf, stdout_data;
char buf[65536];
bool ended = false;
while(!ended)
{
ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
if(n <= 0)
break;
inbuf.append(buf, n);
while(inbuf.size() >= 8)
{
const unsigned char* h = (const unsigned char*)inbuf.data();
size_t content = ((size_t)h[4] << 8) + h[5];
size_t padding = h[6];
size_t record_len = 8 + content + padding;
if(inbuf.size() < record_len)
break;
unsigned char type = h[1];
if(type == 6 /*FCGI_STDOUT*/)
stdout_data.append(inbuf.data() + 8, content);
else if(type == 3 /*FCGI_END_REQUEST*/)
ended = true;
inbuf.erase(0, record_len);
}
}
::close(fd);
// Parse the CGI response: header block, then body. Status: header (if any)
// sets the HTTP status; everything else is a response header.
size_t sep = stdout_data.find("\r\n\r\n");
size_t sep_len = 4;
if(sep == String::npos)
{
sep = stdout_data.find("\n\n");
sep_len = 2;
}
String header_block = sep == String::npos ? String() : stdout_data.substr(0, sep);
result.body = sep == String::npos ? stdout_data : stdout_data.substr(sep + sep_len);
for(String line : split(header_block, "\n"))
{
line = trim(line);
if(line == "")
continue;
size_t colon = line.find(":");
if(colon == String::npos)
continue;
String name = trim(line.substr(0, colon));
String value = trim(line.substr(colon + 1));
if(to_lower(name) == "status")
result.status = (int)int_val(value);
else
result.headers[name] = value;
}
result.ok = true;
return(result);
}
+73 -8
View File
@@ -2,6 +2,7 @@
#include <cmath>
#include "types.h"
#include "functionlib.h"
#include "uri.h"
#include "sys.h"
extern "C" {
@@ -17,6 +18,9 @@ int uce_host_file_exists(const char* path, size_t path_len, const char* current,
size_t uce_host_file_read(const char* path, size_t path_len, const char* current, size_t current_len, char* buf, size_t cap);
int uce_host_file_write(const char* path, size_t path_len, const char* current, size_t current_len, const char* content, size_t content_len, int append);
void uce_host_file_unlink(const char* path, size_t path_len, const char* current, size_t current_len);
size_t uce_host_file_list(const char* path, size_t path_len, const char* current, size_t current_len, char* buf, size_t cap);
int uce_host_file_mkdir(const char* path, size_t path_len, const char* current, size_t current_len);
int64_t uce_host_file_mtime(const char* path, size_t path_len, const char* current, size_t current_len);
int uce_host_task_spawn(const char* key, size_t key_len, uint64_t callback_id, double interval, uint64_t timeout, int repeat);
int uce_host_task_pid(const char* key, size_t key_len);
int uce_host_task_kill(int pid, int sig);
@@ -43,7 +47,11 @@ String dirname(String fn) { auto pos = fn.find_last_of('/'); return(pos == Strin
String path_join(String base, String child) { if(base == "") return(child); if(child == "") return(base); if(child[0] == '/') return(child); return(base + (base.back() == '/' ? "" : "/") + child); }
String path_real(String path) { return(path); }
bool path_is_within(String path, String root) { return(str_starts_with(path, root)); }
bool mkdir(String path) { (void)path; return(false); }
bool mkdir(String path)
{
String current = wasm_current_unit_file();
return(uce_host_file_mkdir(path.data(), path.size(), current.data(), current.size()) != 0);
}
bool file_exists(String path)
{
String current = wasm_current_unit_file();
@@ -78,14 +86,30 @@ bool file_append_contents(String file_name, String content)
String cwd_get() { return("/"); }
void cwd_set(String path) { (void)path; }
String process_start_directory() { return("/"); }
time_t file_mtime(String file_name) { (void)file_name; return(0); }
time_t file_mtime(String file_name)
{
String current = wasm_current_unit_file();
return((time_t)uce_host_file_mtime(file_name.data(), file_name.size(), current.data(), current.size()));
}
void file_unlink(String file_name)
{
String current = wasm_current_unit_file();
uce_host_file_unlink(file_name.data(), file_name.size(), current.data(), current.size());
}
String expand_path(String path, String relative_to_path) { return(path_join(relative_to_path, path)); }
StringList ls(String dir) { (void)dir; return(StringList()); }
StringList ls(String dir)
{
String current = wasm_current_unit_file();
size_t required = uce_host_file_list(dir.data(), dir.size(), current.data(), current.size(), 0, 0);
if(required == 0)
return(StringList());
String listing(required, 0);
size_t got = uce_host_file_list(dir.data(), dir.size(), current.data(), current.size(), &listing[0], required);
listing.resize(got <= required ? got : 0);
if(listing == "")
return(StringList());
return(split(listing, "\n"));
}
u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; unsigned long long v = strtoull(raw.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : fallback); }
f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; double v = strtod(raw.c_str(), &end); return(end && *end == 0 ? (f64)v : fallback); }
bool config_bool_value(String raw, bool fallback) { if(raw == "") return(fallback); return(raw != "0" && raw != "false" && raw != "no" && raw != "off"); }
@@ -169,11 +193,49 @@ String ws_connection_id() { return(context ? context->resources.websocket_connec
String ws_scope() { return(context ? context->resources.websocket_scope : ""); }
u8 ws_opcode() { return(context ? context->resources.websocket_opcode : 0); }
bool ws_is_binary() { return(context && context->resources.websocket_is_binary); }
StringList ws_connections(String scope) { (void)scope; return(StringList()); }
u64 ws_connection_count(String scope) { (void)scope; return(0); }
bool ws_send(String message, bool binary, String scope) { (void)message; (void)binary; (void)scope; return(false); }
bool ws_send_to(String connection_id, String message, bool binary) { (void)connection_id; (void)message; (void)binary; return(false); }
bool ws_close(String connection_id) { (void)connection_id; return(false); }
StringList ws_connections(String scope) { (void)scope; return(context ? context->resources.websocket_scope_connection_ids : StringList()); }
u64 ws_connection_count(String scope) { return(ws_connections(scope).size()); }
// The wasm workspace owns no connections; ws_send/ws_close record dispatch
// commands (same shape as the native websocket_exec capture) that the host
// carries back to the broker, which sends them over the real connections.
bool ws_send(String message, bool binary, String scope)
{
if(!context)
return(false);
DValue command;
command["action"] = "broadcast";
command["binary"].set_bool(binary);
command["message_b64"] = base64_encode(message);
command["scope"] = scope;
context->resources.websocket_dispatch_commands.push(command);
return(true);
}
bool ws_send_to(String connection_id, String message, bool binary)
{
if(!context)
return(false);
DValue command;
command["action"] = "send_to";
command["binary"].set_bool(binary);
command["message_b64"] = base64_encode(message);
command["connection_id"] = connection_id;
context->resources.websocket_dispatch_commands.push(command);
return(true);
}
bool ws_close(String connection_id)
{
if(!context)
return(false);
if(connection_id == "")
connection_id = ws_connection_id();
DValue command;
command["action"] = "close";
command["connection_id"] = connection_id;
command["status_code"] = (f64)1000;
command["reason"] = "";
context->resources.websocket_dispatch_commands.push(command);
return(true);
}
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames) { (void)frames; (void)size; (void)skip_frames; return(""); }
String capture_backtrace_string(u32 max_frames, u32 skip_frames) { (void)max_frames; (void)skip_frames; return(""); }
String signal_name(int sig) { (void)sig; return(""); }
@@ -1266,6 +1328,9 @@ StringMap make_server_settings()
cfg["CONTENT_TYPE"] = "text/html; charset=utf-8";
cfg["FCGI_SOCKET_PATH"] = "/run/uce.sock";
cfg["CLI_SOCKET_PATH"] = "/run/uce/cli.sock";
// Command socket the WS broker listens on; workers flush ws_* dispatch
// command batches here at workspace teardown.
cfg["WS_BROKER_SOCKET_PATH"] = "/run/uce/ws-broker.sock";
cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads";
cfg["SESSION_PATH"] = "/tmp/uce/sessions";
cfg["COMPILER_SYS_PATH"] = ".";