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:
@@ -1,5 +1,13 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
// Fault-injection demo. Under the wasm runtime every unit fault is a guest
|
||||
// trap that the workspace turns into a clean 500 without harming the worker;
|
||||
// these modes exercise the three distinct trap causes (mirrors tests/wasm-kill).
|
||||
u64 error_reporting_recurse(volatile u64 depth)
|
||||
{
|
||||
volatile u64 next = depth + 1;
|
||||
return(next + error_reporting_recurse(next));
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
@@ -10,12 +18,19 @@ RENDER(Request& context)
|
||||
}
|
||||
String mode = context.get["mode"];
|
||||
|
||||
if(mode == "exception")
|
||||
throw std::runtime_error("Intentional test exception from /test/error-reporting.uce");
|
||||
if(mode == "abort")
|
||||
raise(SIGABRT);
|
||||
if(mode == "segfault")
|
||||
raise(SIGSEGV);
|
||||
if(mode == "trap")
|
||||
__builtin_trap();
|
||||
if(mode == "recurse")
|
||||
{
|
||||
volatile u64 sink = error_reporting_recurse(0);
|
||||
(void)sink;
|
||||
}
|
||||
if(mode == "loop")
|
||||
{
|
||||
volatile u64 i = 0;
|
||||
while(i >= 0)
|
||||
i++;
|
||||
}
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
||||
@@ -23,11 +38,11 @@ RENDER(Request& context)
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
Error reporting
|
||||
</h1>
|
||||
<p>These actions intentionally trigger failures so you can verify that UCE returns a usable `500` response instead of dropping the upstream connection.</p>
|
||||
<p>These actions intentionally trigger guest traps so you can verify that UCE returns a usable `500` response from a healthy worker instead of dropping the upstream connection.</p>
|
||||
<ul>
|
||||
<li><a href="?mode=exception">Trigger uncaught exception</a></li>
|
||||
<li><a href="?mode=abort">Trigger SIGABRT</a></li>
|
||||
<li><a href="?mode=segfault">Trigger SIGSEGV</a></li>
|
||||
<li><a href="?mode=trap">Trigger an explicit trap (`__builtin_trap`)</a></li>
|
||||
<li><a href="?mode=recurse">Trigger stack exhaustion (unbounded recursion)</a></li>
|
||||
<li><a href="?mode=loop">Trigger a runaway loop (epoch interrupt)</a></li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
auto p = compiler_load_shared_unit(&context, "post.uce");
|
||||
if(p)
|
||||
print(to_string(p));
|
||||
// The native SharedUnit loader has been retired. Unit metadata is now
|
||||
// exposed through the wasm membrane via unit_info() (uce_host_units), which
|
||||
// resolves and introspects another unit without dlopen-ing a native .so.
|
||||
DValue info = unit_info("post.uce");
|
||||
print(json_encode(info));
|
||||
}
|
||||
|
||||
@@ -155,6 +155,10 @@ void cli_run_demo_smoke()
|
||||
void cli_run_http_smoke()
|
||||
{
|
||||
cli_expect_http("uce_http_smoke:doc index", "/doc/index.uce", 200, "<html>");
|
||||
// Regression guard: the index enumerates pages via ls("pages/"). When that
|
||||
// returned empty (wasm ls() was a stub), the page still rendered <html> but
|
||||
// listed nothing — so assert the function grid is actually populated.
|
||||
cli_expect_http("uce_http_smoke:doc index lists functions", "/doc/index.uce", 200, "func-item");
|
||||
cli_expect_http("uce_http_smoke:doc singlepage", "/doc/singlepage.uce", 200, "<html>");
|
||||
cli_expect_http("uce_http_smoke:doc component page", "/doc/index.uce?p=component", 200, "component()");
|
||||
cli_expect_http("uce_http_smoke:doc regex page", "/doc/index.uce?p=regex_search", 200, "regex_search");
|
||||
|
||||
+14
-34
@@ -49,33 +49,19 @@ RENDER(Request& context)
|
||||
check("zip_read()", hello == "Hello ZIP", hello);
|
||||
check("zip_extract()", extracted && nested == "Nested file" && value == "42", nested + " / " + value);
|
||||
|
||||
bool unsafe_rejected = false;
|
||||
try
|
||||
{
|
||||
DValue unsafe_entries;
|
||||
unsafe_entries["/absolute.txt"] = "bad";
|
||||
zip_create(path_join(base, "unsafe.zip"), unsafe_entries);
|
||||
}
|
||||
catch(std::exception& e)
|
||||
{
|
||||
unsafe_rejected = contains(e.what(), "unsafe");
|
||||
}
|
||||
// Unsafe member names are rejected host-side; the membrane reports the
|
||||
// failure as a false return (no thrown exception crosses into the unit).
|
||||
DValue unsafe_entries;
|
||||
unsafe_entries["/absolute.txt"] = "bad";
|
||||
bool unsafe_rejected = !zip_create(path_join(base, "unsafe.zip"), unsafe_entries);
|
||||
check("zip_create() rejects unsafe names", unsafe_rejected, "absolute member name rejected");
|
||||
|
||||
bool nul_name_rejected = false;
|
||||
try
|
||||
{
|
||||
DValue unsafe_entries;
|
||||
String nul_name = "prefix";
|
||||
nul_name.push_back((char)0x00);
|
||||
nul_name += "suffix.txt";
|
||||
unsafe_entries[nul_name] = "bad";
|
||||
zip_create(path_join(base, "nul-name.zip"), unsafe_entries);
|
||||
}
|
||||
catch(std::exception& e)
|
||||
{
|
||||
nul_name_rejected = contains(e.what(), "unsafe");
|
||||
}
|
||||
DValue nul_entries;
|
||||
String nul_name = "prefix";
|
||||
nul_name.push_back((char)0x00);
|
||||
nul_name += "suffix.txt";
|
||||
nul_entries[nul_name] = "bad";
|
||||
bool nul_name_rejected = !zip_create(path_join(base, "nul-name.zip"), nul_entries);
|
||||
check("zip_create() rejects NUL entry names", nul_name_rejected, "embedded NUL member name rejected");
|
||||
|
||||
String binary_source("UCE", 3);
|
||||
@@ -98,15 +84,9 @@ RENDER(Request& context)
|
||||
check("gz_compress()", gz_body.size() > gz_source.size() && (u8)gz_body[0] == 0x1f && (u8)gz_body[1] == 0x8b, "bytes=" + std::to_string((u64)gz_body.size()));
|
||||
check("gz_uncompress()", gz_roundtrip == gz_source && gz_binary_roundtrip == binary_source && gz_binary_roundtrip.size() == binary_source.size(), "text=" + gz_roundtrip + ", binary bytes=" + std::to_string((u64)gz_binary_roundtrip.size()));
|
||||
|
||||
bool bad_gz_rejected = false;
|
||||
try
|
||||
{
|
||||
gz_uncompress("not gzip data");
|
||||
}
|
||||
catch(std::exception& e)
|
||||
{
|
||||
bad_gz_rejected = contains(e.what(), "gz_uncompress");
|
||||
}
|
||||
// Invalid gzip input fails host-side; the membrane returns an empty result
|
||||
// rather than throwing into the unit.
|
||||
bool bad_gz_rejected = gz_uncompress("not gzip data") == "";
|
||||
check("gz_uncompress() rejects invalid data", bad_gz_rejected, "invalid stream rejected");
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "ZIP tests write only under /tmp/uce-site-tests-zip.");
|
||||
|
||||
Reference in New Issue
Block a user