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:
@@ -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
@@ -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"] = ".";
|
||||
|
||||
Reference in New Issue
Block a user