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:
+127
-48
@@ -42,6 +42,9 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/time.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <dirent.h>
|
||||
#include <cerrno>
|
||||
|
||||
struct WasmDylinkInfo
|
||||
{
|
||||
@@ -388,8 +391,6 @@ public:
|
||||
|
||||
// resolve-kind values shared with the guest core (src/wasm/core.cpp)
|
||||
// must match WasmResolveKind in src/wasm/core.cpp
|
||||
enum ResolveKind { RESOLVE_COMPONENT = 0, RESOLVE_RENDER = 1, RESOLVE_EXISTS = 2, RESOLVE_ONCE = 3,
|
||||
RESOLVE_CLI = 4 };
|
||||
|
||||
String birth()
|
||||
{
|
||||
@@ -482,38 +483,52 @@ public:
|
||||
return("");
|
||||
}
|
||||
|
||||
// Invoke the entry unit through one of its directive handlers
|
||||
// (render/cli/websocket/serve_http, selected by kind). handler_present, when
|
||||
// provided, reports whether the unit actually exports that handler — the
|
||||
// caller maps a missing render to an empty body (native parity) and a
|
||||
// missing cli/serve handler to a 404.
|
||||
String invoke_entry(const String& entry_source_path, int32_t kind, bool* handler_present = 0)
|
||||
// Invoke the entry unit through a named handler ("render"/"cli"/"websocket"/
|
||||
// "serve_http:named"). The handler is just an export name — same resolve +
|
||||
// ONCE + dispatch path as a component. handler_present, when provided,
|
||||
// reports whether the unit exports it (caller maps a missing render to an
|
||||
// empty body, a missing cli/serve handler to a 404).
|
||||
String invoke_entry(const String& entry_source_path, const String& handler, bool* handler_present = 0)
|
||||
{
|
||||
entry_dir = dir_of(entry_source_path);
|
||||
size_t unit_index = 0;
|
||||
String error = load_unit(entry_source_path, unit_index);
|
||||
if(error != "")
|
||||
return(error);
|
||||
String symbol = kind == RESOLVE_CLI ? "__uce_cli" : "__uce_render";
|
||||
bool present = (bool)unit_func(unit_index, symbol);
|
||||
bool present = (bool)unit_func(unit_index, handler_export_symbol(handler));
|
||||
if(handler_present)
|
||||
*handler_present = present;
|
||||
if(!present)
|
||||
return("");
|
||||
// Invoke through the core so the entry gets the same ONCE() + dispatch
|
||||
// path as components (unit is pre-loaded above; resolution is cached).
|
||||
// Invoke through the core: it resolves the same handler and runs ONCE +
|
||||
// dispatch (unit is pre-loaded above; resolution is cached). The core
|
||||
// entry takes two guest buffers — the unit path and the handler name.
|
||||
auto entry = core_func("uce_wasm_invoke_entry");
|
||||
if(!entry)
|
||||
return("core does not export uce_wasm_invoke_entry");
|
||||
int32_t guest_ptr = 0;
|
||||
error = call_core("uce_alloc", { (int32_t)entry_source_path.size() }, &guest_ptr);
|
||||
if(error != "" || guest_ptr == 0)
|
||||
int32_t path_ptr = 0, handler_ptr = 0;
|
||||
error = call_core("uce_alloc", { (int32_t)entry_source_path.size() }, &path_ptr);
|
||||
if(error != "" || path_ptr == 0)
|
||||
return(error == "" ? String("guest uce_alloc failed for entry path") : error);
|
||||
error = guest_write((u32)guest_ptr, entry_source_path);
|
||||
error = guest_write((u32)path_ptr, entry_source_path);
|
||||
if(error == "")
|
||||
{
|
||||
error = call_core("uce_alloc", { (int32_t)handler.size() }, &handler_ptr);
|
||||
if(error == "" && handler_ptr == 0)
|
||||
error = "guest uce_alloc failed for handler";
|
||||
}
|
||||
if(error == "")
|
||||
error = guest_write((u32)handler_ptr, handler);
|
||||
if(error != "")
|
||||
{
|
||||
if(path_ptr) call_core("uce_free", { path_ptr }, 0);
|
||||
if(handler_ptr) call_core("uce_free", { handler_ptr }, 0);
|
||||
return(error);
|
||||
auto result = entry->call(ctx(), { wasmtime::Val(guest_ptr), wasmtime::Val((int32_t)entry_source_path.size()), wasmtime::Val(kind) });
|
||||
call_core("uce_free", { guest_ptr }, 0);
|
||||
}
|
||||
auto result = entry->call(ctx(), { wasmtime::Val(path_ptr), wasmtime::Val((int32_t)entry_source_path.size()),
|
||||
wasmtime::Val(handler_ptr), wasmtime::Val((int32_t)handler.size()) });
|
||||
call_core("uce_free", { path_ptr }, 0);
|
||||
call_core("uce_free", { handler_ptr }, 0);
|
||||
if(!result)
|
||||
return(trap_text(result.err()));
|
||||
return("");
|
||||
@@ -521,7 +536,7 @@ public:
|
||||
|
||||
String render_entry(const String& entry_source_path)
|
||||
{
|
||||
return(invoke_entry(entry_source_path, RESOLVE_RENDER));
|
||||
return(invoke_entry(entry_source_path, "render"));
|
||||
}
|
||||
|
||||
String collect(WasmResponse& response)
|
||||
@@ -891,6 +906,12 @@ private:
|
||||
return(stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode));
|
||||
}
|
||||
|
||||
static bool dir_exists_host(const String& path)
|
||||
{
|
||||
struct stat st;
|
||||
return(stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
|
||||
}
|
||||
|
||||
String resolve_source_path(const String& file_name, const String& current_unit)
|
||||
{
|
||||
std::vector<String> bases;
|
||||
@@ -926,7 +947,7 @@ private:
|
||||
// guest file access policy: only inside the site tree, resolved against
|
||||
// the entry unit's directory first (the native cwd convention), then the
|
||||
// site root; containment checked on the canonicalized path
|
||||
String resolve_guest_file(const String& raw, const String& current_unit = "")
|
||||
String resolve_guest_file(const String& raw, const String& current_unit = "", bool allow_dir = false)
|
||||
{
|
||||
if(raw == "" || raw.find('\0') != String::npos)
|
||||
return("");
|
||||
@@ -970,7 +991,7 @@ private:
|
||||
}
|
||||
if(!allowed)
|
||||
continue;
|
||||
if(file_exists_host(path))
|
||||
if(file_exists_host(path) || (allow_dir && dir_exists_host(path)))
|
||||
return(path);
|
||||
}
|
||||
return("");
|
||||
@@ -1022,8 +1043,20 @@ private:
|
||||
return(result);
|
||||
}
|
||||
|
||||
// hostcall body: uce_host_component_resolve(target, kind, current) → slot
|
||||
int32_t component_resolve(const String& target, int32_t kind, const String& current_unit, String& resolved_out)
|
||||
// __uce_<base>[_<suffix>] for a handler spec like "component:CARD" / "render".
|
||||
static String handler_export_symbol(const String& handler)
|
||||
{
|
||||
auto colon = handler.find(":");
|
||||
String symbol = "__uce_" + (colon == String::npos ? handler : handler.substr(0, colon));
|
||||
if(colon != String::npos)
|
||||
symbol += "_" + sanitize_symbol_suffix(handler.substr(colon + 1));
|
||||
return(symbol);
|
||||
}
|
||||
|
||||
// hostcall body: uce_host_component_resolve(unit, handler, current) → slot.
|
||||
// `handler` names the export ("render", "component:CARD", "cli",
|
||||
// "serve_http:named", "once") or is "exists" (probe only, loads nothing).
|
||||
int32_t component_resolve(const String& target, const String& handler, const String& current_unit, String& resolved_out)
|
||||
{
|
||||
auto probe_start = std::chrono::steady_clock::now();
|
||||
auto record_probe = [&]() {
|
||||
@@ -1032,13 +1065,6 @@ private:
|
||||
std::chrono::steady_clock::now() - probe_start).count();
|
||||
};
|
||||
String file_name = target;
|
||||
String render_name;
|
||||
auto split = target.find(":");
|
||||
if(split != String::npos)
|
||||
{
|
||||
render_name = target.substr(split + 1);
|
||||
file_name = target.substr(0, split);
|
||||
}
|
||||
if(file_name == "" && current_unit != "")
|
||||
file_name = current_unit;
|
||||
if(file_name == "")
|
||||
@@ -1054,7 +1080,7 @@ private:
|
||||
return(0);
|
||||
}
|
||||
resolved_out = resolved;
|
||||
if(kind == RESOLVE_EXISTS)
|
||||
if(handler == "exists")
|
||||
{
|
||||
record_probe();
|
||||
return(1);
|
||||
@@ -1068,12 +1094,7 @@ private:
|
||||
record_probe();
|
||||
return(0);
|
||||
}
|
||||
String symbol = kind == RESOLVE_RENDER ? "__uce_render"
|
||||
: kind == RESOLVE_ONCE ? "__uce_once"
|
||||
: kind == RESOLVE_CLI ? "__uce_cli"
|
||||
: "__uce_component";
|
||||
if(render_name != "")
|
||||
symbol += "_" + sanitize_symbol_suffix(render_name);
|
||||
String symbol = handler_export_symbol(handler);
|
||||
String slot_key = resolved + ":" + symbol;
|
||||
auto cached = handler_slots.find(slot_key);
|
||||
if(cached != handler_slots.end())
|
||||
@@ -1081,17 +1102,17 @@ private:
|
||||
record_probe();
|
||||
return((int32_t)cached->second);
|
||||
}
|
||||
auto handler = unit_func(unit_index, symbol);
|
||||
if(!handler)
|
||||
auto handler_fn = unit_func(unit_index, symbol);
|
||||
if(!handler_fn)
|
||||
{
|
||||
// ONCE is optional per unit; a missing __uce_once is not an error
|
||||
if(kind != RESOLVE_ONCE)
|
||||
if(handler != "once")
|
||||
fprintf(stderr, "[wasm] %s does not export %s\n", resolved.c_str(), symbol.c_str());
|
||||
record_probe();
|
||||
return(0);
|
||||
}
|
||||
u32 slot = 0;
|
||||
error = place_funcref(*handler, slot);
|
||||
error = place_funcref(*handler_fn, slot);
|
||||
if(error != "")
|
||||
{
|
||||
fprintf(stderr, "[wasm] %s\n", error.c_str());
|
||||
@@ -1184,6 +1205,31 @@ private:
|
||||
results[0] = Val(resolved != "" ? (int32_t)1 : (int32_t)0);
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_file_mkdir")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String path, current;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||||
String resolved = self->resolve_guest_write(path, current);
|
||||
int ok = 0;
|
||||
if(resolved != "")
|
||||
ok = (::mkdir(resolved.c_str(), 0777) == 0 || errno == EEXIST) ? 1 : 0;
|
||||
results[0] = Val((int32_t)ok);
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_file_mtime")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String path, current;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||||
String resolved = self->resolve_guest_file(path, current);
|
||||
int64_t mtime = 0;
|
||||
struct stat st;
|
||||
if(resolved != "" && stat(resolved.c_str(), &st) == 0)
|
||||
mtime = (int64_t)st.st_mtime;
|
||||
results[0] = Val((int64_t)mtime);
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_file_read")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String path, current;
|
||||
@@ -1204,6 +1250,38 @@ private:
|
||||
results[0] = Val((int32_t)bytes.size());
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_file_list")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String path, current;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||||
String resolved = self->resolve_guest_file(path, current, true /*allow_dir*/);
|
||||
String listing;
|
||||
if(resolved != "")
|
||||
{
|
||||
std::vector<String> names;
|
||||
if(DIR* d = opendir(resolved.c_str()))
|
||||
{
|
||||
while(struct dirent* e = readdir(d))
|
||||
{
|
||||
String n = e->d_name;
|
||||
if(n != "." && n != "..")
|
||||
names.push_back(n);
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
// match the native ls -1 convention: bare names, sorted
|
||||
std::sort(names.begin(), names.end());
|
||||
listing = join(names, "\n");
|
||||
}
|
||||
u32 cap = (u32)args[5].i32();
|
||||
int32_t buf = args[4].i32();
|
||||
// length-query convention: no copy unless the buffer fits
|
||||
if(buf != 0 && cap >= listing.size())
|
||||
self->hostcall_write(buf, listing);
|
||||
results[0] = Val((int32_t)listing.size());
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_file_write")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String path, current, content;
|
||||
@@ -1714,17 +1792,18 @@ private:
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_component_resolve")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String target, current, resolved;
|
||||
String target, handler, current, resolved;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), target);
|
||||
self->hostcall_read(args[3].i32(), args[4].i32(), current);
|
||||
int32_t slot = self->component_resolve(target, args[2].i32(), current, resolved);
|
||||
u32 cap = (u32)args[6].i32();
|
||||
self->hostcall_read(args[2].i32(), args[3].i32(), handler);
|
||||
self->hostcall_read(args[4].i32(), args[5].i32(), current);
|
||||
int32_t slot = self->component_resolve(target, handler, current, resolved);
|
||||
u32 cap = (u32)args[7].i32();
|
||||
if(cap > 0)
|
||||
{
|
||||
if(resolved.size() >= cap)
|
||||
resolved = resolved.substr(0, cap - 1);
|
||||
resolved.push_back('\0');
|
||||
self->hostcall_write(args[5].i32(), resolved);
|
||||
self->hostcall_write(args[6].i32(), resolved);
|
||||
}
|
||||
results[0] = Val(slot);
|
||||
return(std::monostate());
|
||||
@@ -1752,7 +1831,7 @@ private:
|
||||
// ---- public entry: one request through one workspace -----------------------
|
||||
|
||||
inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_tree, const String& entry_source_path,
|
||||
int32_t kind = WasmWorkspace::RESOLVE_RENDER)
|
||||
const String& handler = "render")
|
||||
{
|
||||
WasmResponse response;
|
||||
WasmWorkspace workspace(worker);
|
||||
@@ -1763,7 +1842,7 @@ inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_
|
||||
if(error == "")
|
||||
error = workspace.apply_context(context_tree);
|
||||
if(error == "")
|
||||
error = workspace.invoke_entry(entry_source_path, kind, &response.handler_present);
|
||||
error = workspace.invoke_entry(entry_source_path, handler, &response.handler_present);
|
||||
if(error == "")
|
||||
error = workspace.collect(response);
|
||||
response.workspace_birth_us = workspace.workspace_birth_us;
|
||||
|
||||
Reference in New Issue
Block a user