docs
This commit is contained in:
+23
-20
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user