diff --git a/docs/setup.md b/docs/setup.md index 6093c31..775bece 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -189,6 +189,8 @@ CLI_SOCKET_MODE=0600 SITE_DIRECTORY=/var/www/html HTTP_DOCUMENT_ROOT=/var/www/html JIT_COMPILE_ON_REQUEST=1 +SHOW_DYNAMIC_COMPILE_ERRORS=1 +SERVE_LAST_KNOWN_GOOD=0 PROACTIVE_COMPILE_ENABLED=1 PROACTIVE_COMPILE_JOBS=2 PROACTIVE_COMPILE_CHECK_INTERVAL=60 @@ -226,7 +228,9 @@ Important settings: - `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point. - `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` controls how long a forwarded WS message can remain queued in the broker before being dropped (default `30`). Set to `0` to disable the timeout. - `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host. -- `PROACTIVE_COMPILE_JOBS` selects 1–16 low-priority full-site scanner processes (default `2`). Each canonical unit path has one scanner owner. The separate higher-priority demand compiler remains reserved for stale units requested over HTTP, so total background compile concurrency can reach this value plus one. +- `SHOW_DYNAMIC_COMPILE_ERRORS=1` makes a failed dynamic `component()`, `unit_render()`, or `unit_call()` show the bounded compiler diagnostic instead of only a generic missing-handler message. Set it to `0` on deployments where source paths and compiler output must not reach HTTP responses. +- `SERVE_LAST_KNOWN_GOOD=1` lets HTTP GET/HEAD/OPTIONS requests keep using a compatible complete unit artifact while the proactive compiler builds changed source. It defaults to `0`; CLI and mutation requests always use current code or fail closed. The option requires an enabled proactive compiler with a positive check interval. Failed background builds preserve the prior Wasm, source map, and serialized module until a successful atomic publication replaces them. Missing source or an incompatible compiler/core ABI is never served as last-known-good. +- `PROACTIVE_COMPILE_JOBS` selects 1–16 low-priority full-site scanner processes (default `2`). Each canonical unit path has one scanner owner. When last-known-good serving is enabled, the separate higher-priority demand compiler remains reserved for stale units requested over HTTP, so total background compile concurrency can reach this value plus one. - `WASM_CORE_PATH` must point at the built `core.wasm` file. - `WASM_EPOCH_DEADLINE_TICKS` and `WASM_EPOCH_PERIOD_MS` bound one uninterrupted guest CPU segment. `WASM_INVOCATION_TIMEOUT_MS` is the diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 66fe119..2eb6d84 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -292,17 +292,19 @@ All compiler processes use the same per-unit lock, so concurrent demand and scan discovery cannot publish duplicate artifacts. The priority worker is idle when there is no demand and never scans the site on its own. Unit compilation writes and validates a process-unique temporary wasm file, then -publishes it with an atomic rename. When proactive compilation is enabled, -read-only HTTP requests keep using the last complete artifact while requesting -that stale unit at the head of the compiler queue. Non-read requests never run -a stale entry artifact: they return `503 Service Unavailable` with -`Retry-After: 1`, allowing the client to retry after the priority rebuild. -CLI and explicit compile paths remain synchronous. A failed rebuild removes -availability and surfaces the compiler error rather than serving the old unit -indefinitely. This freshness contract includes components resolved lazily from -an otherwise-current CLI entry unit: if the proactive compiler already owns a -stale child's lock, the CLI request joins that compile and waits for the fresh -artifact. Read-only HTTP may use the child's last complete artifact instead. +publishes it with an atomic rename. By default, requests do not execute stale +code: a missing or changed unit is rebuilt synchronously and a failed dynamic +build surfaces its bounded compiler diagnostic. `SERVE_LAST_KNOWN_GOOD=1` +opts ordinary read-only HTTP requests into using the last complete, +ABI-compatible artifact while requesting that stale unit at the head of the +compiler queue. The proactive compiler must be enabled for this mode. Failed +background builds preserve that artifact and its source map and serialized +module until a later successful atomic publication replaces them. Missing +source, incompatible ABI generations, CLI, WebSocket dispatch, and mutation +requests never use last-known-good code. A stale mutation returns `503 Service +Unavailable` with `Retry-After: 1`; CLI and explicit compile paths remain +synchronous. If a proactive compiler already owns a stale child's lock, a CLI +request joins that compile and waits for the current artifact. Before preprocessing, the compiler verifies that the worker can actually read the source. An unreadable path is reported as a source-read failure with a diff --git a/etc/uce/settings.cfg b/etc/uce/settings.cfg index 60ab1e0..1a3dbf3 100644 --- a/etc/uce/settings.cfg +++ b/etc/uce/settings.cfg @@ -37,6 +37,16 @@ SITE_DIRECTORY=site # ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT JIT_COMPILE_ON_REQUEST=1 +# SHOW THE BOUNDED COMPILER DIAGNOSTIC WHEN DYNAMIC DISPATCH TARGETS A UNIT +# THAT FAILED TO COMPILE. WHEN DISABLED, THE CALL REPORTS THE GENERIC MISSING +# COMPONENT OR UNIT ERROR. +SHOW_DYNAMIC_COMPILE_ERRORS=1 + +# KEEP SERVING A COMPATIBLE LAST-KNOWN-GOOD ARTIFACT TO HTTP READ REQUESTS +# WHILE THE PROACTIVE COMPILER BUILDS A CHANGED UNIT. MUTATIONS AND CLI NEVER +# SERVE STALE CODE. REQUIRES THE PROACTIVE COMPILER AND DEFAULTS FAIL-CLOSED. +SERVE_LAST_KNOWN_GOOD=0 + # WASM SIDE-MODULE COMPILER USED FOR .uce UNITS WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index a0fc53a..75932dd 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -86,6 +86,7 @@ if [[ "$action" == "run" ]]; then scripts/test_unit_export_surface.sh timeout --signal=TERM --kill-after=5s 120s scripts/test_wasm_metadata_buffer.sh timeout --signal=TERM --kill-after=5s 150s scripts/test_wasm_metadata_deadline.sh + timeout --signal=TERM --kill-after=5s 240s scripts/test_dynamic_compile_failures.sh scripts/test_wasm_source_locations.sh scripts/test_server_arguments.sh scripts/test_socket_activation.sh diff --git a/scripts/test_dynamic_compile_failures.sh b/scripts/test_dynamic_compile_failures.sh new file mode 100755 index 0000000..d433e8f --- /dev/null +++ b/scripts/test_dynamic_compile_failures.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +if [[ "${1:-}" != "--inside" ]]; then + exec timeout --signal=TERM --kill-after=5s 240s unshare --mount --net --fork --kill-child=TERM "$0" --inside +fi +ip link set lo up +root="/tmp/uce-dynamic-compile-failures-$$" +site="$root/site" +work="$root/work" +settings="$root/settings.cfg" +log="$root/service.log" +port=18080 +server_pid="" +cleanup() { + status=$? + if [[ -n "$server_pid" ]] && kill -0 "$server_pid" 2>/dev/null; then + kill -TERM "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi + if (( status != 0 )) && [[ -r "$log" ]]; then cat "$log" >&2; fi + while umount /etc/uce/settings.cfg 2>/dev/null; do :; done + rm -rf "$root" + return "$status" +} +trap cleanup EXIT +mkdir -p "$site" "$work" "$root/run" "$root/session" "$root/upload" +cat >"$site/driver.uce" <<'UCE' +RENDER(Request& context) { + print("driver:"); + if(context.get["mode"] == "render") unit_render("broken-render", context); + else if(context.get["mode"] == "call") unit_call("broken-call", "COMPONENT"); + else print(component("child", context)); +} +UCE +printf '%s\n' 'COMPONENT(Request& context) { print("good-v1"); }' >"$site/child.uce" +printf '%s\n' 'RENDER(Request& context) { print("render-ok"); }' >"$site/broken-render.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("call-ok"); }' >"$site/broken-call.uce" + +write_settings() { + local show_errors="$1" serve_good="$2" proactive="$3" + sed -E '/^[[:space:]]*(BIN_DIRECTORY|PRECOMPILE_FILES_IN|SITE_DIRECTORY|FCGI_SOCKET_PATH|FCGI_PORT|CLI_SOCKET_PATH|WS_BROKER_SOCKET_PATH|HTTP_PORT|HTTP_DOCUMENT_ROOT|SESSION_PATH|TMP_UPLOAD_PATH|WASM_CORE_PATH|PROACTIVE_COMPILE_ENABLED|PROACTIVE_COMPILE_CHECK_INTERVAL|PROACTIVE_COMPILE_JOBS|WORKER_COUNT|SHOW_DYNAMIC_COMPILE_ERRORS|SERVE_LAST_KNOWN_GOOD)[[:space:]]*=/d' \ + /etc/uce/settings.cfg >"$settings" + cat >>"$settings" <"$log" + bin/uce_fastcgi.linux.bin >"$log" 2>&1 & + server_pid=$! + for _ in $(seq 1 500); do + curl -sS --max-time 1 "http://127.0.0.1:$port/driver.uce" >/dev/null 2>&1 && return + kill -0 "$server_pid" 2>/dev/null || break + sleep 0.02 + done + cat "$log" >&2 + return 1 +} +stop_server() { + kill -TERM "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + server_pid="" +} +request() { + local method="${1:-GET}" query="${2:-}" + if [[ "$method" == POST ]]; then + curl -sS --max-time 30 -w $'\n%{http_code}' -X POST --data '' "http://127.0.0.1:$port/driver.uce?$query" + else + curl -sS --max-time 30 -w $'\n%{http_code}' "http://127.0.0.1:$port/driver.uce?$query" + fi +} +body_of() { printf '%s' "${1%$'\n'*}"; } +status_of() { printf '%s' "${1##*$'\n'}"; } + +write_settings 1 0 0 +start_server +printf '%s\n' 'COMPONENT(Request& context) { UCE_DYNAMIC_COMPILE_MARKER }' >"$site/child.uce" +response=$(request GET) +body=$(body_of "$response") +[[ "$(status_of "$response")" == 500 ]] +[[ "$body" == *UCE_DYNAMIC_COMPILE_MARKER* ]] +[[ "$body" != *"component not found"* ]] +printf '%s\n' 'RENDER(Request& context) { UCE_RENDER_COMPILE_MARKER }' >"$site/broken-render.uce" +response=$(request GET mode=render) +[[ "$(status_of "$response")" == 500 && "$(body_of "$response")" == *UCE_RENDER_COMPILE_MARKER* ]] +printf '%s\n' 'COMPONENT(Request& context) { UCE_CALL_COMPILE_MARKER }' >"$site/broken-call.uce" +response=$(request GET mode=call) +[[ "$(body_of "$response")" == *UCE_CALL_COMPILE_MARKER* ]] +[[ "$(body_of "$response")" != *"function 'COMPONENT' not found"* ]] +printf '%s\n' 'COMPONENT(Request& context) { print("recovered"); }' >"$site/child.uce" +response=$(request GET) +[[ "$(status_of "$response")" == 200 && "$(body_of "$response")" == "driver:recovered" ]] +stop_server + +write_settings 0 0 0 +start_server +printf '%s\n' 'COMPONENT(Request& context) { UCE_HIDDEN_COMPILE_MARKER }' >"$site/child.uce" +response=$(request GET) +body=$(body_of "$response") +[[ "$(status_of "$response")" == 500 ]] +[[ "$body" == *"component not found: child"* ]] +[[ "$body" != *UCE_HIDDEN_COMPILE_MARKER* ]] +stop_server + +printf '%s\n' 'COMPONENT(Request& context) { print("good-v1"); }' >"$site/child.uce" +write_settings 1 1 1 +start_server +for _ in $(seq 1 100); do + response=$(request GET) + [[ "$(status_of "$response")" == 200 && "$(body_of "$response")" == "driver:good-v1" ]] && break + sleep 0.1 +done +[[ "$(status_of "$response")" == 200 && "$(body_of "$response")" == "driver:good-v1" ]] +cache="$(scripts/unit_cache_directory "$work")$(realpath "$site")" +for artifact in "$cache/child.uce.wasm" "$cache/child.uce.cwasm" "$cache/child.uce.wasm.source-map"; do [[ -s "$artifact" ]]; done +before_hashes=$(sha256sum "$cache/child.uce.wasm" "$cache/child.uce.cwasm" "$cache/child.uce.wasm.source-map") +printf '%s\n' 'COMPONENT(Request& context) { UCE_STALE_COMPILE_MARKER }' >"$site/child.uce" +for _ in $(seq 1 30); do + response=$(request GET) + [[ "$(status_of "$response")" == 200 && "$(body_of "$response")" == "driver:good-v1" ]] + sleep 0.1 +done +[[ -s "$cache/child.uce.compile.txt" ]] +[[ "$(sha256sum "$cache/child.uce.wasm" "$cache/child.uce.cwasm" "$cache/child.uce.wasm.source-map")" == "$before_hashes" ]] +stop_server +start_server +response=$(request GET) +[[ "$(status_of "$response")" == 200 && "$(body_of "$response")" == "driver:good-v1" ]] || { echo "last-known-good artifact did not survive worker restart: $response" >&2; exit 1; } +printf '%s\n' 'COMPONENT(Request& context) { print("permission-repair"); }' >"$site/child.uce" +chmod 000 "$site/child.uce" +for _ in $(seq 1 100); do + grep -q "source file is not readable" "$cache/child.uce.compile.txt" 2>/dev/null && break + sleep 0.05 +done +[[ "$(sha256sum "$cache/child.uce.wasm" "$cache/child.uce.cwasm" "$cache/child.uce.wasm.source-map")" == "$before_hashes" ]] +response=$(request GET) +[[ "$(status_of "$response")" == 200 && "$(body_of "$response")" == "driver:good-v1" ]] || { echo "unreadable source removed last-known-good service: $response" >&2; exit 1; } +chmod 644 "$site/child.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("good-v2"); }' >"$site/child.uce" +for _ in $(seq 1 200); do + response=$(request GET) + if [[ "$(status_of "$response")" == 200 && "$(body_of "$response")" == "driver:good-v2" ]]; then break; fi + sleep 0.1 +done +[[ "$(status_of "$response")" == 200 && "$(body_of "$response")" == "driver:good-v2" ]] || { echo "repaired unit was not published: $response" >&2; exit 1; } + +echo "dynamic compile failure handling passed" diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index 39b83ac..67c9033 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -1236,6 +1236,8 @@ String compiler_format_source_read_failure(Request* context, SharedUnit* su, Str void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadline* deadline) { f64 comp_start = time_precise(); + bool preserve_last_known_good = compiler_preserve_last_known_good(context, su->file_name); + bool stage_artifacts = deadline || preserve_last_known_good; if(!file_exists(su->file_name)) { @@ -1261,7 +1263,8 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl file_put_contents(su->compile_output_file_name, su->compiler_messages + "\n"); file_put_contents(su->wasm_check_file_name, su->compiler_messages + "\n"); file_put_contents(su->meta_file_name, compiler_unit_metadata_text(context, su)); - compiler_unlink_unit_wasm_artifacts(su); + if(!preserve_last_known_good) + compiler_unlink_unit_wasm_artifacts(su); compiler_record_compile_result(su, time_precise() - comp_start, false, "compile_error", su->compiler_messages); printf("%s \n", compiler_format_source_read_failure(context, su, su->compiler_messages).c_str()); if(deadline) @@ -1281,7 +1284,7 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl String staged_api_name; String staged_meta_name; bool publication_failed = false; - if(deadline) + if(stage_artifacts) { u64 stage_id = compiler_invocation_stage_counter.fetch_add(1, std::memory_order_relaxed) + 1; String stage_suffix = ".invocation-" + std::to_string((u64)getpid()) + "-" + std::to_string(stage_id); @@ -1316,8 +1319,8 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl } if(deadline && (deadline->timed_out || deadline->operational_failure)) break; - if(!file_put_contents(deadline ? staged_pre_name : su->pre_path + "/" + su->pre_file_name, generated_source) || - !file_put_contents(deadline ? staged_api_name : su->api_file_name, join(su->api_declarations, "\n"))) + if(!file_put_contents(stage_artifacts ? staged_pre_name : su->pre_path + "/" + su->pre_file_name, generated_source) || + !file_put_contents(stage_artifacts ? staged_api_name : su->api_file_name, join(su->api_declarations, "\n"))) { su->compiler_messages = "could not write generated bounded compile inputs"; break; @@ -1327,8 +1330,8 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl shell_escape(su->src_path)+" "+ shell_escape(su->bin_path)+" "+ shell_escape(su->file_name)+" "+ - shell_escape(deadline ? staged_pre_file_name : su->pre_file_name)+" "+ - shell_escape(deadline ? staged_wasm_file_name : su->wasm_file_name)+" "+ + shell_escape(stage_artifacts ? staged_pre_file_name : su->pre_file_name)+" "+ + shell_escape(stage_artifacts ? staged_wasm_file_name : su->wasm_file_name)+" "+ shell_escape(compiler_unit_bin_directory(context)); if(deadline) { @@ -1353,7 +1356,7 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl else su->compiler_messages = trim(shell_exec(compile_command)); - String compiled_wasm_name = deadline ? staged_wasm_name : su->wasm_name; + String compiled_wasm_name = stage_artifacts ? staged_wasm_name : su->wasm_name; if(su->compiler_messages.length() == 0 && !file_exists(compiled_wasm_name)) su->compiler_messages = "wasm compile script completed without creating " + compiled_wasm_name; if(su->compiler_messages.length() > 0) @@ -1361,9 +1364,9 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl String current_input_signature = compiler_unit_input_signature(context, su); if(current_input_signature == compiled_input_signature) { - if(deadline) + if(stage_artifacts) { - if(deadline->expire_if_needed()) + if(deadline && deadline->expire_if_needed()) break; String source_map = file_get_contents(staged_map_name); if(source_map == "") @@ -1383,7 +1386,7 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl } break; } - if(deadline) + if(stage_artifacts) { file_unlink(staged_wasm_name); file_unlink(staged_map_name); @@ -1426,11 +1429,14 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl file_unlink(staged_api_name); file_unlink(staged_meta_name); compiler_record_compile_result(su, time_precise() - comp_start, false, "publish_error", su->compiler_messages); - deadline->operational_failure = true; - deadline->operational_error = su->compiler_messages; + if(deadline) + { + deadline->operational_failure = true; + deadline->operational_error = su->compiler_messages; + } return; } - if(deadline) + if(stage_artifacts) { file_unlink(staged_wasm_name); file_unlink(staged_map_name); @@ -1445,7 +1451,7 @@ void compile_shared_unit_bounded(Request* context, SharedUnit* su, CompilerDeadl file_put_contents(su->compile_output_file_name, raw_messages + "\n"); file_put_contents(su->wasm_check_file_name, raw_messages + "\n"); file_put_contents(su->meta_file_name, compiler_unit_metadata_text(context, su, compiled_input_signature)); - if(!publication_failed) + if(!publication_failed && !preserve_last_known_good) compiler_unlink_unit_wasm_artifacts(su); compiler_record_compile_result(su, time_precise() - comp_start, false, "compile_error", raw_messages); printf("%s \n", compiler_format_compile_failure(context, su, raw_messages).c_str()); @@ -1691,10 +1697,21 @@ bool compiler_unit_compile_in_progress(Request* context, String file_name) bool compiler_request_can_serve_stale_artifact(Request* context) { return(context && context->server && !context->resources.is_cli && + context->params["UCE_WS"] != "1" && + to_bool(context->server->config["SERVE_LAST_KNOWN_GOOD"], false) && to_bool(context->server->config["PROACTIVE_COMPILE_ENABLED"], true) && float_val(context->server->config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) > 0); } +bool compiler_preserve_last_known_good(Request* context, String file_name) +{ + return( + context && context->server && + to_bool(context->server->config["SERVE_LAST_KNOWN_GOOD"], false) && + compiler_unit_can_serve_stale_artifact(context, file_name) + ); +} + bool compiler_unit_can_serve_stale_artifact(Request* context, String file_name) { if(!compiler_request_can_serve_stale_artifact(context)) diff --git a/src/lib/compiler.h b/src/lib/compiler.h index d4e8ca8..993631d 100644 --- a/src/lib/compiler.h +++ b/src/lib/compiler.h @@ -34,6 +34,7 @@ String compiler_error_page_unit(Request* context, String config_key); bool compiler_unit_compile_pending(Request* context, String file_name); bool compiler_unit_compile_in_progress(Request* context, String file_name); bool compiler_request_can_serve_stale_artifact(Request* context); +bool compiler_preserve_last_known_good(Request* context, String file_name); bool compiler_unit_can_serve_stale_artifact(Request* context, String file_name); void compiler_prioritize_unit(Request* context, String file_name); StringList compiler_take_priority_units(Request* context); diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp index 7f1bf4b..045546b 100644 --- a/src/lib/sys.cpp +++ b/src/lib/sys.cpp @@ -1925,6 +1925,8 @@ StringMap make_server_settings() cfg["PRECOMPILE_JOBS"] = "2"; cfg["SITE_DIRECTORY"] = "site"; cfg["JIT_COMPILE_ON_REQUEST"] = "1"; + cfg["SHOW_DYNAMIC_COMPILE_ERRORS"] = "1"; + cfg["SERVE_LAST_KNOWN_GOOD"] = "0"; cfg["PROACTIVE_COMPILE_ENABLED"] = "1"; cfg["PROACTIVE_COMPILE_JOBS"] = "2"; cfg["COMPILE_FAILURE_RETRY_SECONDS"] = std::to_string(10); diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index b7c2690..8e71255 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -479,6 +479,7 @@ extern "C" int32_t uce_host_component_resolve( // but a single workspace can render the same component many times) static std::map wasm_component_slots; static std::map wasm_component_paths; +static std::map wasm_component_errors; // These mirror small page-runtime pieces that cannot include compiler.cpp in // the wasm core (compiler.cpp owns parser/clang/cache bookkeeping for the host @@ -548,7 +549,7 @@ struct RequestPropsScope // 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) +static s32 wasm_resolve_target(String unit_target, String handler, String* resolved_out = 0, String* error_out = 0) { String current = context ? context->resources.current_unit_file : ""; String cache_key = current + "\t" + handler + "\t" + unit_target; @@ -558,21 +559,30 @@ static s32 wasm_resolve_target(String unit_target, String handler, String* resol { if(resolved_out) *resolved_out = wasm_component_paths[cache_key]; + if(error_out) + *error_out = wasm_component_errors[cache_key]; return(cached->second); } - char resolved[512]; + char resolved[4096]; s32 once_slot = 0; s32 slot = uce_host_component_resolve( unit_target.data(), unit_target.size(), handler.data(), handler.size(), current.data(), current.size(), resolved, sizeof(resolved), &once_slot); - String resolved_path = slot ? String(resolved, strnlen(resolved, sizeof(resolved))) : String(""); + String response(resolved, strnlen(resolved, sizeof(resolved))); + String resolve_error = slot < 0 ? response : String(""); + if(slot < 0) + slot = 0; + String resolved_path = slot ? response : String(""); if(resolved_out && slot) *resolved_out = resolved_path; + if(error_out) + *error_out = resolve_error; if(!is_exists) { wasm_component_slots[cache_key] = slot; wasm_component_paths[cache_key] = resolved_path; + wasm_component_errors[cache_key] = resolve_error; bool runs_once = handler == "render" || handler.rfind("render:", 0) == 0 || handler == "component" || handler.rfind("component:", 0) == 0; if(slot && runs_once) @@ -636,10 +646,14 @@ DValue* unit_call(String file_name, String function_name, DValue* call_param) if(handler != "") { String resolved; - s32 slot = wasm_resolve_target(file_name, handler, &resolved); + String resolve_error; + s32 slot = wasm_resolve_target(file_name, handler, &resolved, &resolve_error); if(!slot) { - print("Error: unit_call() function '", function_name, "' not found"); + if(resolve_error != "") + print("Error: ", resolve_error); + else + print("Error: unit_call() function '", function_name, "' not found"); return(0); } DValue props = call_param ? *call_param : DValue(); @@ -656,10 +670,14 @@ DValue* unit_call(String file_name, String function_name, DValue* call_param) } String resolved; - s32 slot = wasm_resolve_target(file_name, "export:" + function_name, &resolved); + String resolve_error; + s32 slot = wasm_resolve_target(file_name, "export:" + function_name, &resolved, &resolve_error); if(!slot) { - print("Error: unit_call() function '", function_name, "' not found"); + if(resolve_error != "") + print("Error: ", resolve_error); + else + print("Error: unit_call() function '", function_name, "' not found"); return(0); } @@ -683,11 +701,12 @@ static void component_render_with_props(String name, DValue& props, Request& req 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(file_name, handler, &resolved); + String resolve_error; + s32 slot = wasm_resolve_target(file_name, handler, &resolved, &resolve_error); if(!slot) { request.set_status(500, "Internal Server Error"); - print(component_error_banner("component not found: " + trim(name))); + print(component_error_banner(resolve_error != "" ? resolve_error : "component not found: " + trim(name))); return; } wasm_run_once(resolved, request); @@ -724,11 +743,12 @@ void unit_render(String file_name, Request& request) 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(unit_name, handler, &resolved); + String resolve_error; + s32 slot = wasm_resolve_target(unit_name, handler, &resolved, &resolve_error); if(!slot) { request.set_status(500, "Internal Server Error"); - print(component_error_banner("unit not found: " + trim(file_name))); + print(component_error_banner(resolve_error != "" ? resolve_error : "unit not found: " + trim(file_name))); return; } wasm_run_once(resolved, request); @@ -857,6 +877,7 @@ void uce_wasm_core_reset_request() wasm_request.resources.current_unit_file = ""; wasm_component_slots.clear(); wasm_component_paths.clear(); + wasm_component_errors.clear(); } // Host pushes the worker-cached immutable configuration followed by the diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 4ef079d..5c4cca7 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -2995,12 +2995,15 @@ private: // `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, int32_t* once_slot_out = 0, bool* compile_timed_out = 0) + String& resolved_out, int32_t* once_slot_out = 0, bool* compile_timed_out = 0, + String* compile_error_out = 0) { if(once_slot_out) *once_slot_out = 0; if(compile_timed_out) *compile_timed_out = false; + if(compile_error_out) + compile_error_out->clear(); auto probe_start = std::chrono::steady_clock::now(); auto record_probe = [&]() { component_resolve_count += 1; @@ -3130,10 +3133,22 @@ private: { u64 remaining_ms = invocation_remaining_ms(); bool timed_out = false; + SharedUnit* compiled_unit = 0; if(remaining_ms == 0) timed_out = true; else - get_shared_unit_bounded(context, resolved, remaining_ms, &timed_out); + compiled_unit = get_shared_unit_bounded(context, resolved, remaining_ms, &timed_out); + if(compiled_unit && compile_error_out && + to_bool(context->server->config["SHOW_DYNAMIC_COMPILE_ERRORS"], true)) + { + String detail = trim(first(compiled_unit->compiler_messages, compiled_unit->compile_error_status)); + if(detail != "") + { + *compile_error_out = "compile failed for " + resolved + ":\n" + detail; + if(compile_error_out->size() > 4095) + *compile_error_out = compile_error_out->substr(0, 4064) + "\n[compile error truncated]"; + } + } if(timed_out) { if(compile_timed_out) @@ -4521,15 +4536,20 @@ private: })); if(mod == "env" && name == "uce_host_component_resolve") return(add([self](Caller caller, Span args, Span results) -> Result { - String target, handler, current, resolved; + String target, handler, current, resolved, compile_error; self->hostcall_read(args[0].i32(), args[1].i32(), target); self->hostcall_read(args[2].i32(), args[3].i32(), handler); self->hostcall_read(args[4].i32(), args[5].i32(), current); int32_t once_slot = 0; bool compile_timed_out = false; - int32_t slot = self->component_resolve(target, handler, current, resolved, &once_slot, &compile_timed_out); + int32_t slot = self->component_resolve(target, handler, current, resolved, &once_slot, &compile_timed_out, &compile_error); if(compile_timed_out) return(Trap(self->invocation_timeout_error())); + if(slot == 0 && compile_error != "") + { + resolved = compile_error; + slot = -1; + } u32 cap = (u32)args[7].i32(); if(cap > 0) {