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:
+59
-24
@@ -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("");
|
||||
}
|
||||
|
||||
|
||||
+6
-18
@@ -6,27 +6,15 @@
|
||||
// backend.cpp — so it does not have to compile worker.cpp + wasmtime.hh on
|
||||
// every build. Include only after uce_lib.h (needs Request / String).
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Resolve-kind selector for wasm_backend_serve, mirrored from
|
||||
// WasmWorkspace::ResolveKind in worker.cpp. Only the entry kinds actually wired
|
||||
// to the wasm backend live here; WebSocket/serve_http re-enter when W7b/W7c land
|
||||
// (via the broker→worker-pool forwarding model, not in-fork rendering).
|
||||
namespace wasm_kind {
|
||||
enum {
|
||||
RENDER = 1,
|
||||
CLI = 4,
|
||||
};
|
||||
}
|
||||
|
||||
// True if this request should be served by the wasm backend (config + artifact
|
||||
// + fallback-token gate); mode-agnostic, the caller picks the kind.
|
||||
// + fallback-token gate); handler-agnostic, the caller names the handler.
|
||||
bool wasm_backend_should_handle(Request& request, const String& entry_unit);
|
||||
|
||||
// Serve a request through a wasm workspace using the unit handler for `kind`,
|
||||
// populating the native Request. Returns "" on success or a collapsed error.
|
||||
String wasm_backend_serve(Request& request, const String& entry_unit,
|
||||
int32_t kind = wasm_kind::RENDER);
|
||||
// Serve a request through a wasm workspace by invoking a named unit handler —
|
||||
// "render", "cli", "websocket", "serve_http", "serve_http:named" — and populate
|
||||
// the native Request. Returns "" on success or a collapsed error. The handler is
|
||||
// just an export name; there is no per-mode machinery.
|
||||
String wasm_backend_serve(Request& request, const String& entry_unit, const String& handler = "render");
|
||||
|
||||
// Join the per-process epoch ticker before the worker process exits.
|
||||
void wasm_backend_shutdown();
|
||||
|
||||
+68
-33
@@ -475,7 +475,8 @@ extern "C" void uce_wasm_link_anchors()
|
||||
// slots (loading units lazily) and writes the resolved unit path back so
|
||||
// nested relative component resolution keeps working.
|
||||
extern "C" int32_t uce_host_component_resolve(
|
||||
const char* target, size_t target_len, int32_t kind,
|
||||
const char* target, size_t target_len,
|
||||
const char* handler, size_t handler_len,
|
||||
const char* current_unit, size_t current_unit_len,
|
||||
char* resolved_buf, size_t resolved_cap);
|
||||
|
||||
@@ -534,38 +535,37 @@ struct RequestPropsScope
|
||||
}
|
||||
};
|
||||
|
||||
// kind values shared with the host loader (src/wasm/worker.cpp)
|
||||
enum WasmResolveKind {
|
||||
WASM_RESOLVE_COMPONENT = 0,
|
||||
WASM_RESOLVE_RENDER = 1,
|
||||
WASM_RESOLVE_EXISTS = 2,
|
||||
WASM_RESOLVE_ONCE = 3,
|
||||
WASM_RESOLVE_CLI = 4,
|
||||
};
|
||||
|
||||
static s32 wasm_resolve_target(String target, s32 kind, String* resolved_out = 0)
|
||||
// A unit is a bag of exported handlers; invoking any of them is one operation —
|
||||
// the host resolves __uce_<handler> in the loaded module to a funcref slot. The
|
||||
// handler is just a string: "render", "component:CARD", "render:VARIANT",
|
||||
// "once", "cli", "websocket", "serve_http:named" — or "exists" (an existence
|
||||
// probe that loads nothing). No per-mode kinds.
|
||||
static s32 wasm_resolve_target(String unit_target, String handler, String* resolved_out = 0)
|
||||
{
|
||||
String cache_key = std::to_string(kind) + ":" + target;
|
||||
String cache_key = handler + "\t" + unit_target;
|
||||
bool is_exists = (handler == "exists");
|
||||
auto cached = wasm_component_slots.find(cache_key);
|
||||
if(cached != wasm_component_slots.end() && kind != WASM_RESOLVE_EXISTS)
|
||||
if(cached != wasm_component_slots.end() && !is_exists)
|
||||
return(cached->second);
|
||||
char resolved[512];
|
||||
String current = context ? context->resources.current_unit_file : "";
|
||||
s32 slot = uce_host_component_resolve(
|
||||
target.data(), target.size(), kind,
|
||||
unit_target.data(), unit_target.size(), handler.data(), handler.size(),
|
||||
current.data(), current.size(),
|
||||
resolved, sizeof(resolved));
|
||||
if(resolved_out && slot)
|
||||
*resolved_out = String(resolved, strnlen(resolved, sizeof(resolved)));
|
||||
if(kind != WASM_RESOLVE_EXISTS)
|
||||
if(!is_exists)
|
||||
wasm_component_slots[cache_key] = slot;
|
||||
return(slot);
|
||||
}
|
||||
|
||||
String component_resolve(String name)
|
||||
{
|
||||
String file_name, render_name;
|
||||
component_parse_target(trim(name), file_name, render_name);
|
||||
String resolved;
|
||||
if(wasm_resolve_target(trim(name), WASM_RESOLVE_EXISTS, &resolved))
|
||||
if(wasm_resolve_target(file_name, "exists", &resolved))
|
||||
return(resolved);
|
||||
return("");
|
||||
}
|
||||
@@ -585,7 +585,7 @@ static void wasm_run_once(const String& resolved, Request& request)
|
||||
if(request.once_units.find(resolved) != request.once_units.end())
|
||||
return;
|
||||
request.once_units.insert(resolved);
|
||||
s32 once_slot = wasm_resolve_target(resolved, WASM_RESOLVE_ONCE);
|
||||
s32 once_slot = wasm_resolve_target(resolved, "once");
|
||||
if(once_slot == 0)
|
||||
return;
|
||||
String previous_unit = request.resources.current_unit_file;
|
||||
@@ -597,8 +597,11 @@ static void wasm_run_once(const String& resolved, Request& request)
|
||||
|
||||
void component_render(String name, DValue props, Request& request)
|
||||
{
|
||||
String file_name, render_name;
|
||||
component_parse_target(trim(name), file_name, render_name);
|
||||
String handler = render_name == "" ? String("component") : "component:" + render_name;
|
||||
String resolved;
|
||||
s32 slot = wasm_resolve_target(trim(name), WASM_RESOLVE_COMPONENT, &resolved);
|
||||
s32 slot = wasm_resolve_target(file_name, handler, &resolved);
|
||||
if(!slot)
|
||||
{
|
||||
print(component_error_banner("component not found: " + trim(name)));
|
||||
@@ -611,8 +614,8 @@ void component_render(String name, DValue props, Request& request)
|
||||
request.resources.current_unit_file = resolved;
|
||||
// a wasm function pointer is its index in the shared funcref table; the
|
||||
// host returned the handler's slot, so this is a plain call_indirect
|
||||
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
|
||||
handler(request);
|
||||
request_ref_handler handler_fn = (request_ref_handler)(uintptr_t)slot;
|
||||
handler_fn(request);
|
||||
request.resources.current_unit_file = previous_unit;
|
||||
}
|
||||
|
||||
@@ -633,8 +636,11 @@ String component(String name, DValue props) { return(component(name, props, *con
|
||||
|
||||
void unit_render(String file_name, Request& request)
|
||||
{
|
||||
String unit_name, render_name;
|
||||
component_parse_target(trim(file_name), unit_name, render_name);
|
||||
String handler = render_name == "" ? String("render") : "render:" + render_name;
|
||||
String resolved;
|
||||
s32 slot = wasm_resolve_target(trim(file_name), WASM_RESOLVE_RENDER, &resolved);
|
||||
s32 slot = wasm_resolve_target(unit_name, handler, &resolved);
|
||||
if(!slot)
|
||||
{
|
||||
print(component_error_banner("unit not found: " + trim(file_name)));
|
||||
@@ -644,8 +650,8 @@ void unit_render(String file_name, Request& request)
|
||||
String previous_unit = request.resources.current_unit_file;
|
||||
if(resolved != "")
|
||||
request.resources.current_unit_file = resolved;
|
||||
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
|
||||
handler(request);
|
||||
request_ref_handler handler_fn = (request_ref_handler)(uintptr_t)slot;
|
||||
handler_fn(request);
|
||||
request.resources.current_unit_file = previous_unit;
|
||||
}
|
||||
|
||||
@@ -653,25 +659,25 @@ void unit_render(String file_name) { unit_render(file_name, *context); }
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Host calls this to invoke the request's entry unit through one of its
|
||||
// directive handlers (render/cli/websocket/serve_http). Routing through the
|
||||
// resolver gives the entry the same ONCE() + dispatch semantics as components
|
||||
// (the host pre-loaded it, so resolution is a cache hit). The host pre-checks
|
||||
// that the handler export exists, so a missing slot here is a silent no-op.
|
||||
void uce_wasm_invoke_entry(const char* path, size_t len, int32_t kind)
|
||||
// Host calls this to invoke the request's entry unit through a named handler
|
||||
// ("render"/"cli"/"websocket"/"serve_http:named"). It is the same resolve +
|
||||
// ONCE() + dispatch path as components — the entry handler is just another
|
||||
// export. The host pre-checks the export exists, so a missing slot is a no-op.
|
||||
void uce_wasm_invoke_entry(const char* path, size_t path_len, const char* handler, size_t handler_len)
|
||||
{
|
||||
// the host always runs uce_wasm_core_init + apply_context before this
|
||||
String file_name(path, len);
|
||||
String file_name(path, path_len);
|
||||
String handler_name(handler, handler_len);
|
||||
String resolved;
|
||||
s32 slot = wasm_resolve_target(trim(file_name), kind, &resolved);
|
||||
s32 slot = wasm_resolve_target(trim(file_name), handler_name, &resolved);
|
||||
if(!slot)
|
||||
return;
|
||||
wasm_run_once(resolved, *context);
|
||||
String previous_unit = context->resources.current_unit_file;
|
||||
if(resolved != "")
|
||||
context->resources.current_unit_file = resolved;
|
||||
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
|
||||
handler(*context);
|
||||
request_ref_handler handler_fn = (request_ref_handler)(uintptr_t)slot;
|
||||
handler_fn(*context);
|
||||
context->resources.current_unit_file = previous_unit;
|
||||
}
|
||||
|
||||
@@ -761,6 +767,27 @@ int uce_wasm_apply_context(const char* buf, size_t len)
|
||||
wasm_request.resources.current_unit_file = entry->to_string();
|
||||
DValue* raw_in = decoded.key("in");
|
||||
wasm_request.in = raw_in ? raw_in->to_string() : "";
|
||||
// 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.resources.websocket_dispatch_commands = DValue();
|
||||
wasm_request.resources.websocket_dispatch_capture = false;
|
||||
DValue* ws = decoded.key("ws");
|
||||
if(ws)
|
||||
{
|
||||
wasm_request.resources.websocket_connection_id = (*ws)["connection_id"].to_string();
|
||||
wasm_request.resources.websocket_scope = (*ws)["scope"].to_string();
|
||||
wasm_request.resources.websocket_opcode = (u8)(*ws)["opcode"].to_u64();
|
||||
wasm_request.resources.websocket_is_binary = (*ws)["binary"].to_bool();
|
||||
wasm_request.resources.websocket_scope_connection_ids.clear();
|
||||
if(DValue* conns = ws->key("connections"))
|
||||
conns->each([&](const DValue& v, String) {
|
||||
wasm_request.resources.websocket_scope_connection_ids.push_back(v.to_string());
|
||||
});
|
||||
wasm_request.resources.websocket_dispatch_capture = true;
|
||||
if(DValue* cstate = ws->key("connection_state"))
|
||||
wasm_request.connection = *cstate;
|
||||
}
|
||||
return(0);
|
||||
}
|
||||
|
||||
@@ -787,6 +814,14 @@ void uce_wasm_finish_response_meta()
|
||||
}
|
||||
for(auto& entry : wasm_request.session)
|
||||
meta["session"][entry.first] = entry.second;
|
||||
// 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())
|
||||
{
|
||||
meta["ws_commands"] = wasm_request.resources.websocket_dispatch_commands;
|
||||
meta["ws_connection_state"] = wasm_request.connection;
|
||||
}
|
||||
wasm_response_meta = ucb_encode(meta);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,5 +22,8 @@ uce_host_file_exists
|
||||
uce_host_file_read
|
||||
uce_host_file_write
|
||||
uce_host_file_unlink
|
||||
uce_host_file_list
|
||||
uce_host_file_mkdir
|
||||
uce_host_file_mtime
|
||||
uce_host_regex
|
||||
uce_host_sqlite
|
||||
|
||||
+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