#include "lib/uce_lib.cpp" // The wasm backend is its own object (src/wasm/wasm_module.cpp → wasm.o); the // main object only needs its declarations, so editing the wasm runtime no // longer recompiles the whole native TU. #include "wasm/backend.h" // Minimal FastCGI client: connection brokers forward to a clean-engine worker. #include "lib/fcgi_forward.h" #include #include #include #include ServerState server_state; #include "fastcgi/src/fcgicc.cc" FastCGIServer server; pid_t proactive_compiler_pid = 0; // The central WS broker process: owns the WS port + every connection, forwards // renders to the worker pool over uce.sock, and applies ws_* commands flushed // back from workers. ws_broker_outbound holds in-flight async render // connections and their enqueue timestamps. struct WsBrokerOutbound { String pending; f64 started_at; }; FastCGIServer ws_broker; pid_t ws_broker_pid = 0; std::map ws_broker_outbound; static sigjmp_buf request_fault_jmp; static volatile sig_atomic_t request_fault_active = 0; static volatile sig_atomic_t request_fault_signal = 0; static Request* request_fault_request = 0; // Raw frame pointers only: the signal handler must not allocate, so frames are // captured here and symbolized after the siglongjmp. static void* request_fault_frames[64]; static volatile sig_atomic_t request_fault_frame_count = 0; void close_inherited_server_sockets(); u64 request_seed_from_time(f64 time_value); void prepare_request_body_maps(Request& request); Request* set_active_request(Request& request) { Request* previous_context = context; context = &request; return(previous_context); } void restore_active_request(Request* previous_context) { context = previous_context; } String request_status_line(Request& request, int status_code, String reason) { String status = std::to_string(status_code) + " " + reason; if(request.params["GATEWAY_INTERFACE"] != "") return("Status: " + status); return("HTTP/1.1 " + status); } void clear_request_output(Request& request) { for(auto* stream : request.ob_stack) delete stream; request.ob_stack.clear(); request.ob_start(); } bool render_wasm_error_page(Request& request, String config_key, s32 status_code, String status_reason, DValue error_info) { if(!(request.params["REQUEST_METHOD"] != "" && request.ob && request.ob_stack.size() > 0)) return(false); if(!request.server || request.resources.error_page_active) return(false); String unit_file = compiler_error_page_unit(&request, config_key); if(unit_file == "") return(false); String previous_response_code = request.response_code; s32 previous_status = request.flags.status; String previous_content_type = request.header["Content-Type"]; request.resources.error_page_active = true; request.call["error"] = error_info; request.set_status(status_code, status_reason); request.header["Content-Type"] = first(request.server->config["CONTENT_TYPE"], "text/html; charset=utf-8"); String unit = compiler_normalize_unit_path(&request, unit_file); if(!wasm_backend_should_handle(request, unit)) get_shared_unit(&request, unit); ob_start(); String wasm_error = ""; if(wasm_backend_should_handle(request, unit)) wasm_error = wasm_backend_serve(request, unit, "render"); else wasm_error = "error page wasm unit unavailable after compile: " + unit_file; String html = ob_get_close(); request.resources.error_page_active = false; if(wasm_error != "") { printf("(!) configured %s page %s failed to render: %s\n", config_key.c_str(), unit_file.c_str(), trim(wasm_error).c_str()); request.response_code = previous_response_code; request.flags.status = previous_status; request.header["Content-Type"] = previous_content_type; request.call.remove("error"); return(false); } // Rendering the error-page unit set the response to its own (200) status; // re-assert the error status + html content-type so the client sees e.g. 500. request.set_status(status_code, status_reason); request.header["Content-Type"] = first(request.server->config["CONTENT_TYPE"], "text/html; charset=utf-8"); print(html); return(true); } void render_request_failure(Request& request, String title, String details, String trace, int status_code = 500) { request.response_code = request_status_line(request, status_code, "Internal Server Error"); request.header.clear(); request.set_cookies.clear(); request.header["Content-Type"] = "text/plain; charset=utf-8"; request.err.clear(); Request* previous_context = set_active_request(request); clear_request_output(request); if(!request.resources.is_cli) { DValue error_info; error_info["type"] = (title == "function disabled by server policy") ? "policy_blocked" : "runtime_error"; error_info["title"] = title; error_info["details"] = details; error_info["source"] = request.params["SCRIPT_FILENAME"]; error_info["request_uri"] = first(request.params["REQUEST_URI"], request.params["SCRIPT_FILENAME"]); if(request.server && request.params["SCRIPT_FILENAME"] != "") error_info["generated_cpp"] = compiler_generated_cpp_path(&request, request.params["SCRIPT_FILENAME"]); if(request_fault_signal != 0) { error_info["signal"] = (f64)request_fault_signal; error_info["signal_name"] = signal_name((int)request_fault_signal); } error_info["trace"] = trace; if(render_wasm_error_page(request, "page_runtime_error", status_code, "Internal Server Error", error_info)) { request.err = "UCE runtime error: " + title + (details != "" ? " (" + details + ")" : ""); restore_active_request(previous_context); return; } } String body; body += "UCE runtime error\n"; body += "Request: " + first(request.params["REQUEST_URI"], request.params["SCRIPT_FILENAME"]) + "\n"; body += "Script: " + request.params["SCRIPT_FILENAME"] + "\n"; body += "Error: " + title + "\n"; if(details != "") body += "Details: " + details + "\n"; if(request.server && request.params["SCRIPT_FILENAME"] != "") { String generated = compiler_generated_cpp_path(&request, request.params["SCRIPT_FILENAME"]); body += "Generated C++: " + generated + "\n"; body += "Hint: if this came from template code, inspect the generated C++ and the nearest .uce literal/code delimiter before changing runtime code.\n"; } else body += "Hint: inspect the requested .uce file, its generated C++, and any component/unit called immediately before this failure.\n"; if(request_fault_signal != 0) { String sig_label = signal_name((int)request_fault_signal); body += "Signal: " + std::to_string((int)request_fault_signal); if(sig_label != "") body += " (" + sig_label + ")"; body += "\n"; } if(trace != "") body += "\nTrace:\n" + trace; print(body); request.err = body; request.flags.status = status_code; restore_active_request(previous_context); } void on_request_fault_signal(int sig) { request_fault_signal = sig; if(request_fault_active && request_fault_request) { request_fault_frame_count = backtrace(request_fault_frames, 64); siglongjmp(request_fault_jmp, 1); } on_segfault(sig); } void install_request_fault_handlers() { signal(SIGSEGV, on_request_fault_signal); signal(SIGABRT, on_request_fault_signal); signal(SIGBUS, on_request_fault_signal); signal(SIGILL, on_request_fault_signal); signal(SIGFPE, on_request_fault_signal); signal(SIGALRM, on_request_fault_signal); } void restore_request_fault_handlers() { signal(SIGSEGV, on_segfault); signal(SIGABRT, on_segfault); signal(SIGBUS, on_segfault); signal(SIGILL, on_segfault); signal(SIGFPE, on_segfault); signal(SIGALRM, on_segfault); } String current_ws_scope() { if(!context) return(""); return(first( context->resources.websocket_scope, context->params["SCRIPT_FILENAME"] )); } String normalize_ws_scope(String scope) { if(scope == "") return(current_ws_scope()); if(scope[0] == '/') return(scope); return(expand_path(scope, cwd_get())); } String ws_connection_id() { if(!context) return(""); return(context->resources.websocket_connection_id); } String ws_message() { if(!context) return(""); return(context->in); } String ws_scope() { return(current_ws_scope()); } u8 ws_opcode() { if(!context) return(0); return(context->resources.websocket_opcode); } bool ws_is_binary() { if(!context) return(false); return(context->resources.websocket_is_binary); } StringList ws_connections(String scope) { return(server.websocket_connection_ids(normalize_ws_scope(scope))); } u64 ws_connection_count(String scope) { return(ws_connections(scope).size()); } bool ws_send(String message, bool binary, String scope) { return(server.websocket_broadcast(normalize_ws_scope(scope), message, binary) > 0); } bool ws_send_to(String connection_id, String message, bool binary) { return(server.websocket_send_to(connection_id, message, binary)); } bool ws_close(String connection_id) { if(connection_id == "") connection_id = ws_connection_id(); if(connection_id == "") return(false); return(server.websocket_close(connection_id)); } bool cli_path_is_safe(String command) { for(auto& segment : split(command, "/")) { if(segment == "..") return(false); } return(true); } String cli_resolve_unit_path(Request& request, String command, String& document_root) { document_root = first(request.params["DOCUMENT_ROOT"], cwd_get()); if(document_root.length() > 1 && document_root[document_root.length()-1] == '/') document_root.resize(document_root.length()-1); String script_filename = document_root + command; if(file_exists(script_filename)) return(script_filename); String site_filename = document_root + "/site" + command; if(file_exists(site_filename)) { document_root = document_root + "/site"; return(site_filename); } return(""); } u64 request_seed_from_time(f64 time_value) { u64 bits = 0; static_assert(sizeof(bits) == sizeof(time_value)); memcpy(&bits, &time_value, sizeof(bits)); return(gen_noise64(bits)); } void prepare_request_body_maps(Request& request) { if(request.server) request.params["UCE_BIN_DIRECTORY"] = request.server->config["BIN_DIRECTORY"]; String route_path; request.get = parse_query(request.params["QUERY_STRING"], &route_path); request_populate_context_params_from_route(request, route_path); if(request.params["HTTP_COOKIE"].length() > 0) request.cookies = parse_cookies(request.params["HTTP_COOKIE"]); String ct_info = request.params["CONTENT_TYPE"]; String ct_type = nibble(ct_info, ";"); if(request.params["REQUEST_METHOD"] == "POST") { if(ct_type == "multipart/form-data") { nibble("boundary=", ct_info); request.post = parse_multipart(request.in, String("--")+ct_info, request.uploaded_files); } else { request.post = parse_query(request.in); } } } int handle_cli_complete(FastCGIRequest& request) { Request* previous_context = set_active_request(request); server_state.request_count += 1; request.server = &server_state; request.resources.is_cli = true; request.params["UCE_CLI"] = "1"; request.stats.time_start = time_precise(); request.header["Content-Type"] = "text/plain; charset=utf-8"; request.random_index = 0; request.random_seed = request_seed_from_time(request.stats.time_start); request.ob_start(); String method = trim(request.params["REQUEST_METHOD"]); String command = trim(first(request.params["DOCUMENT_URI"], request.params["REQUEST_URI"])); try { if(method != "GET" && method != "POST") { request.set_status(405, "Method Not Allowed"); request.header["Allow"] = "GET, POST"; print("UCE CLI socket accepts GET and POST commands only\n"); } else if(command == "/" || command == "/help") { print("UCE CLI command socket\n\nAvailable test hooks:\n GET /ping\n GET /status\n"); } else if(command == "/ping" || command == "/test") { print("uce-cli: ok\n"); } else if(command == "/status") { print("pid=", std::to_string(getpid()), "\nclients=", std::to_string(server.client_sockets.size()), "\n"); } else if(command.length() >= 4 && command.substr(command.length() - 4) == ".uce") { if(!cli_path_is_safe(command)) { request.set_status(400, "Bad Request"); print("invalid UCE CLI unit path\n"); } else { String document_root; String script_filename = cli_resolve_unit_path(request, command, document_root); if(script_filename == "") { request.set_status(404, "Not Found"); print("UCE CLI unit not found: ", command, "\n"); } else { request.params["DOCUMENT_ROOT"] = document_root; request.params["SCRIPT_FILENAME"] = script_filename; prepare_request_body_maps(request); request.props = DValue(); // The CLI socket path installs no native fault handler, so the // wasm worker (whose traps are signal-based) can run directly. String cli_unit = compiler_normalize_unit_path(&request, script_filename); // W7e: compile a cold/stale unit on demand; native execution has // been removed, so a unit that still cannot be served by wasm is a // request failure instead of a fallback path. if(!wasm_backend_should_handle(request, cli_unit)) get_shared_unit(&request, cli_unit); if(wasm_backend_should_handle(request, cli_unit)) { String wasm_error = wasm_backend_serve(request, cli_unit, "cli"); if(wasm_error != "") { request.set_status(500, "Internal Server Error"); print("UCE CLI wasm error: ", wasm_error, "\n"); } } else { request.set_status(500, "Internal Server Error"); print("UCE CLI wasm unit unavailable after compile: ", script_filename, "\n"); } } } } else { request.set_status(404, "Not Found"); print("unknown UCE CLI command: ", command, "\n"); } } catch(const std::exception& e) { render_request_failure(request, "uncaught exception during CLI request", e.what(), backtrace_capture(32, 1), 500); } catch(...) { render_request_failure(request, "unknown uncaught exception during CLI request", "", backtrace_capture(32, 1), 500); } for(auto &f : request.uploaded_files) file_unlink(f.tmp_name); cleanup_mysql_connections(); cleanup_sqlite_connections(); restore_active_request(previous_context); return(request.flags.status); } int handle_request(FastCGIRequest& request) { // This is always the first event to occur. It occurs when the // server receives all parameters. There may be more data coming on the // standard input stream. if(request.stats.time_init == 0) request.stats.time_init = time_precise(); if(request.params.count("REQUEST_URI")) return(0); // OK, continue processing else return(1); // stop processing and return error code } int handle_data(FastCGIRequest& request) { // Request bodies are accumulated by the FastCGI transport and parsed once // the input stream is closed in handle_complete(). return 0; } int handle_complete(FastCGIRequest& request) { Request* previous_context = set_active_request(request); server_state.request_count += 1; request.server = &server_state; if(request.stats.time_init == 0) request.stats.time_init = time_precise(); request.stats.time_start = time_precise(); //request.stats.mem_alloc = 0; //request.stats.mem_high = 0; request.header["Content-Type"] = (request.resources.is_cli ? "text/plain; charset=utf-8" : context->server->config["CONTENT_TYPE"]); request.random_index = 0; request.random_seed = request_seed_from_time(request.stats.time_start); request.ob_start(); request_fault_request = &request; request_fault_active = 1; request_fault_signal = 0; request_fault_frame_count = 0; install_request_fault_handlers(); String failure_title = ""; String failure_details = ""; String failure_trace = ""; if(sigsetjmp(request_fault_jmp, 1) != 0) { failure_title = "fatal signal during request"; failure_details = "worker recovered before closing the upstream connection"; failure_trace = backtrace_get_frames(request_fault_frames, request_fault_frame_count, 1); // The siglongjmp skipped UnitInvocationScope's destructor, so the // worker may still sit in the crashed unit's source directory. cwd_set(process_start_directory()); } else { try { prepare_request_body_maps(request); request.props = DValue(); // Suspend the native SIGSEGV/SIGILL request recovery handler around a // wasm invocation: Wasmtime uses host signals internally to implement // guest traps, and the native handler would otherwise turn a clean // guest trap into a native fatal signal. Sets failure_* on error. auto serve_via_wasm = [&](const String& entry_unit, const String& handler) { request_fault_active = 0; restore_request_fault_handlers(); String wasm_error = wasm_backend_serve(request, entry_unit, handler); install_request_fault_handlers(); request_fault_active = 1; if(wasm_error != "") { size_t blocked_at = wasm_error.find("UCE_POLICY_BLOCKED:"); if(blocked_at != String::npos) { String fn = wasm_error.substr(blocked_at + 19); size_t fn_end = fn.find_first_of(" \t\r\n\""); if(fn_end != String::npos) fn = fn.substr(0, fn_end); failure_title = "function disabled by server policy"; failure_details = "this unit called " + fn + ", which is disabled on this server by configuration (UCE_HOSTCALL_BLOCKLIST)"; failure_trace = ""; } else { failure_title = "wasm runtime error during request"; failure_details = ""; failure_trace = wasm_error; } } }; String entry_unit = compiler_normalize_unit_path(&request, request.params["SCRIPT_FILENAME"]); // W7e: every unit runs on wasm. When the artifact is missing or stale // (cold worker, or source edited since the last compile), compile the // unit on demand — get_shared_unit() builds the .wasm side-module — and // recheck. Native execution has been removed, so a unit that still cannot // be served by wasm becomes a clean 500 request failure. auto wasm_ready = [&](const String& unit) -> bool { if(!wasm_backend_should_handle(request, unit)) get_shared_unit(&request, unit); return(wasm_backend_should_handle(request, unit)); }; auto fail_wasm_unavailable = [&](const String& handler) { failure_title = "wasm unit unavailable after compile"; failure_details = handler + " handler could not be served by wasm"; failure_trace = "source: " + request.params["SCRIPT_FILENAME"]; }; if(request.params["UCE_WS"] == "1") { // A WS message the broker forwarded here: rebuild the connection // context the broker passed as params, then run __uce_websocket. request.resources.websocket_connection_id = request.params["UCE_WS_CONNECTION_ID"]; request.resources.websocket_scope = request.params["UCE_WS_SCOPE"]; request.resources.websocket_opcode = (u8)int_val(request.params["UCE_WS_OPCODE"]); request.resources.websocket_is_binary = request.params["UCE_WS_BINARY"] == "1"; bool msg_ok = false; request.in = base64_decode(request.params["UCE_WS_MESSAGE"], msg_ok); for(auto& id : split(request.params["UCE_WS_CONNECTIONS"], "\n")) if(id != "") request.resources.websocket_scope_connection_ids.push_back(id); bool decoded = false; String state_raw = base64_decode(request.params["UCE_WS_STATE"], decoded); if(state_raw != "") { DValue state; String e; if(ucb_decode(state_raw, state, &e)) request.connection = state; } if(wasm_ready(entry_unit)) serve_via_wasm(entry_unit, "websocket"); else fail_wasm_unavailable("websocket"); } else if(request.resources.is_cli) { if(wasm_ready(entry_unit)) serve_via_wasm(entry_unit, "cli"); else fail_wasm_unavailable("cli"); } else if(request.params["UCE_SERVE_HTTP"] == "1") { // W7c: this runs in a normal worker (clean engine) — the custom-server // dispatcher forwarded the request here via FastCGI rather than render // wasm in its own fork (Wasmtime cannot be re-created across fork). if(wasm_ready(entry_unit)) { String fn = request.params["UCE_SERVE_HTTP_FUNCTION"]; serve_via_wasm(entry_unit, fn == "" ? String("serve_http") : "serve_http:" + fn); } else fail_wasm_unavailable("serve_http"); } else if(wasm_ready(entry_unit)) serve_via_wasm(entry_unit, "render"); else fail_wasm_unavailable("render"); } catch(const std::exception& e) { failure_title = "uncaught exception during request"; failure_details = e.what(); failure_trace = backtrace_capture(32, 1); } catch(...) { failure_title = "unknown uncaught exception during request"; failure_trace = backtrace_capture(32, 1); } } request_fault_active = 0; request_fault_request = 0; restore_request_fault_handlers(); if(failure_title != "") render_request_failure(request, failure_title, failure_details, failure_trace, 500); for( auto &f : request.uploaded_files) { file_unlink(f.tmp_name); } if(failure_title == "" && request.session_id.length() > 0) save_session_data(request.session_id, request.session); cleanup_mysql_connections(); cleanup_sqlite_connections(); restore_active_request(previous_context); return request.flags.status; } volatile bool termination_signal_received = false; void on_terminate(int sig) { if(termination_signal_received) return; termination_signal_received = true; if(getpid() != parent_pid) exit(1); printf("Terminating... PID %i:%i\n", getpid(), parent_pid); wasm_backend_shutdown(); server.shutdown(); exit(1); } void clear_shared_unit_cache(ServerState& state) { for(auto& it : state.units) delete it.second; state.units.clear(); } void close_inherited_server_sockets() { for(auto socket_handle : server.server_sockets) close(socket_handle); server.server_sockets.clear(); server.server_socket_types.clear(); } void install_process_fault_handlers() { signal(SIGSEGV, on_segfault); signal(SIGABRT, on_segfault); signal(SIGBUS, on_segfault); signal(SIGILL, on_segfault); signal(SIGFPE, on_segfault); signal(SIGPIPE, SIG_IGN); } String custom_server_safe_key(String key) { return(runtime_safe_key(key, "server key")); } String custom_server_config_file(String key) { return(path_join(server_state.config["BIN_DIRECTORY"], "server-" + custom_server_safe_key(key) + ".cfg")); } String custom_server_task_key(String key) { return("server:" + custom_server_safe_key(key)); } String custom_server_config_encode(String key, String type, String bind, String file_name, String function_name) { DValue config; config["key"] = key; config["type"] = type; config["bind"] = bind; config["file"] = file_name; config["function"] = function_name; return(json_encode(config)); } StringMap custom_server_config_decode(String content) { StringMap result; content = trim(content); if(content == "") return(result); if(content[0] == '{') return(json_decode(content).to_stringmap()); // Compatibility for early newline/key=value config files from development. for(auto line : split(content, "\n")) { line = trim(line); if(line == "") continue; String key = trim(nibble(line, "=")); if(key != "") result[key] = json_decode(line).to_string(); } return(result); } bool custom_server_is_numeric_port(String value) { value = trim(value); if(value == "") return(false); for(char c : value) { if(c < '0' || c > '9') return(false); } return(true); } u64 custom_server_registry_count() { u64 count = 0; for(auto file_name : ls(server_state.config["BIN_DIRECTORY"])) { if(str_starts_with(file_name, "server-") && str_ends_with(file_name, ".cfg")) count++; } return(count); } bool custom_server_wait_for_stop(String task_key, f64 timeout_seconds = 2.0) { f64 deadline = time_precise() + timeout_seconds; while(time_precise() < deadline) { if(task_pid(task_key) == 0) return(true); usleep(50000); } return(task_pid(task_key) == 0); } int custom_server_bind_http(FastCGIServer& dispatcher, String bind) { bind = trim(bind); if(custom_server_is_numeric_port(bind)) { u64 port = int_val(bind); u64 min_port = to_u64(server_state.config["CUSTOM_SERVER_MIN_PORT"], 1024); u64 max_port = to_u64(server_state.config["CUSTOM_SERVER_MAX_PORT"], 65535); if(port < min_port || port > max_port) throw std::runtime_error("server_start_http(): TCP port is outside configured custom server range"); String bind_address = to_bool(server_state.config["CUSTOM_SERVER_ALLOW_PUBLIC_BIND"], false) ? "0.0.0.0" : "127.0.0.1"; return(dispatcher.listen_http((unsigned)port, bind_address)); } String socket_prefix = first(server_state.config["CUSTOM_SERVER_UNIX_SOCKET_PREFIX"], "/tmp/uce/custom-servers/"); if(!str_starts_with(bind, socket_prefix)) throw std::runtime_error("server_start_http(): Unix socket path is outside configured custom server prefix"); int socket_handle = dispatcher.listen(bind); dispatcher.server_socket_types[socket_handle] = 'H'; return(socket_handle); } StringMap custom_server_read_config(String key) { String config_file = custom_server_config_file(key); if(!file_exists(config_file)) return(StringMap()); return(custom_server_config_decode(file_get_contents(config_file))); } static String custom_server_dispatcher_key = ""; // Forward a request to a normal worker over the FastCGI socket and populate the // response from the worker's reply. Shared by the connection brokers (the custom // HTTP server dispatcher and the WS broker): they own a listening socket but // render through the clean-engine worker pool rather than host a Wasmtime engine // (which cannot be re-created across fork). The caller sets the routing params // (UCE_SERVE_HTTP / UCE_WS, SCRIPT_FILENAME, etc.) before calling. int forward_request_to_worker(FastCGIRequest& request, u32 timeout_seconds = 30) { String fcgi_socket = first(server_state.config["FCGI_SOCKET_PATH"], "/run/uce.sock"); FcgiForwardResult fwd = fcgi_forward_request(fcgi_socket, request.params, request.in, timeout_seconds); request.ob_start(); if(!fwd.ok) { request.set_status(502, "Bad Gateway"); request.header["Content-Type"] = "text/plain; charset=utf-8"; (*request.ob) << "worker forward failed: " << fwd.error << "\n"; return(request.flags.status); } request.set_status(fwd.status); for(auto& h : fwd.headers) request.header[h.first] = h.second; request.ob->write(fwd.body.data(), fwd.body.size()); return(request.flags.status); } int custom_server_http_complete(FastCGIRequest& request) { String key = first(request.params["UCE_SERVER_KEY"], custom_server_dispatcher_key); StringMap cfg = custom_server_read_config(key); if(cfg["type"] != "http" || cfg["file"] == "") { request.set_status(503, "Service Unavailable"); request.header["Content-Type"] = "text/plain; charset=utf-8"; request.ob_start(); (*request.ob) << "custom HTTP server is not configured\n"; return(request.flags.status); } request.params["UCE_SERVE_HTTP"] = "1"; request.params["UCE_SERVE_HTTP_KEY"] = key; request.params["UCE_SERVE_HTTP_BIND"] = cfg["bind"]; request.params["UCE_SERVE_HTTP_FUNCTION"] = cfg["function"]; request.params["SCRIPT_FILENAME"] = cfg["file"]; u64 timeout = to_u64(server_state.config["CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS"], 30); return(forward_request_to_worker(request, timeout > 0 ? (u32)timeout : 30)); } // ---- central WS broker ----------------------------------------------------- // Connect to a unix socket (blocking — local, sub-ms) and set it non-blocking. static int ws_broker_connect_unix(const String& path) { int fd = ::socket(AF_UNIX, SOCK_STREAM, 0); if(fd < 0) return(-1); struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; if(path.size() >= sizeof(addr.sun_path)) { fprintf(stderr, "ws broker unix socket path too long: %s\n", path.c_str()); ::close(fd); return(-1); } strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1); if(::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { ::close(fd); return(-1); } int fl = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, fl | O_NONBLOCK); return(fd); } // Apply a ws_* dispatch batch a worker flushed at workspace teardown. The broker // owns every connection, so any target (id / scope / broadcast / close) resolves. void ws_broker_apply_commands(FastCGIRequest& request) { DValue batch; String err; bool ok = ucb_decode(request.in, batch, &err); if(ok) { if(DValue* cmds = batch.key("commands")) cmds->each([&](const DValue& cmd_const, String) { DValue cmd = cmd_const; // .each yields const; copy for [] access String action = cmd["action"].to_string(); bool ok = false; String msg = base64_decode(cmd["message_b64"].to_string(), ok); bool binary = cmd["binary"].to_bool(); if(action == "broadcast") ws_broker.websocket_broadcast(cmd["scope"].to_string(), msg, binary); else if(action == "send_to") ws_broker.websocket_send_to(cmd["connection_id"].to_string(), msg, binary); else if(action == "close") ws_broker.websocket_close(cmd["connection_id"].to_string(), (u16)cmd["status_code"].to_u64(), cmd["reason"].to_string()); }); // persist any updated per-connection state back onto the live connection String cid = batch["connection_id"].to_string(); if(cid != "" && batch.key("connection_state")) for(auto& item : ws_broker.client_sockets) if(item.second->is_websocket && item.second->websocket_connection_id == cid) item.second->websocket_state = batch["connection_state"]; } request.set_status(200); request.ob_start(); } // on_complete for the broker: either a worker's command flush, or an un-upgraded // HTTP request that hit the WS port (forwarded to the pool via the shared facility). int ws_broker_complete(FastCGIRequest& request) { if(request.params["UCE_WS_DISPATCH"] == "1") { ws_broker_apply_commands(request); return(request.flags.status); } return(forward_request_to_worker(request)); } // A complete WS message: fire a render to the worker pool WITHOUT blocking the // broker loop (the connection identity + state ride as params; the message is // the body). The worker renders __uce_websocket and flushes ws_* back to us. int ws_broker_ws_message(FastCGIRequest& request, const String& message, u8 opcode) { StringMap params; params["SCRIPT_FILENAME"] = request.params["SCRIPT_FILENAME"]; params["REQUEST_METHOD"] = "GET"; // handle_request() rejects any request without REQUEST_URI before on_complete. params["REQUEST_URI"] = first(request.params["REQUEST_URI"], request.params["DOCUMENT_URI"], request.params["SCRIPT_FILENAME"]); params["UCE_WS"] = "1"; // The message rides as a param (empty STDIN) so the forwarded request // completes cleanly — a STDIN body makes the FastCGI transport flush a // premature response before on_complete (handle_complete) ever runs. params["UCE_WS_MESSAGE"] = base64_encode(message); params["UCE_WS_CONNECTION_ID"] = request.resources.websocket_connection_id; params["UCE_WS_SCOPE"] = request.resources.websocket_scope; params["UCE_WS_OPCODE"] = std::to_string((int)opcode); params["UCE_WS_BINARY"] = request.resources.websocket_is_binary ? "1" : "0"; params["UCE_WS_CONNECTIONS"] = join(ws_broker.websocket_connection_ids(request.resources.websocket_scope), "\n"); if(request.resources.websocket_connection_state) params["UCE_WS_STATE"] = base64_encode(ucb_encode(*request.resources.websocket_connection_state)); int fd = ws_broker_connect_unix(first(server_state.config["FCGI_SOCKET_PATH"], "/run/uce.sock")); if(fd < 0) return(0); ws_broker_outbound[fd] = {fcgi_build_request(params, ""), time_precise()}; return(0); } // Drive in-flight render forwards non-blocking: finish writing the request, then // drain/discard the reply (the unit's ws_* came back via the command socket). void ws_broker_drain_outbound(u64 timeout_seconds) { f64 now = time_precise(); for(auto it = ws_broker_outbound.begin(); it != ws_broker_outbound.end(); ) { int fd = it->first; WsBrokerOutbound& outbound = it->second; String& pending = outbound.pending; bool done = false; bool timed_out = timeout_seconds > 0 && now - outbound.started_at > (f64)timeout_seconds; if(timed_out) { fprintf(stderr, "ws broker dropping stale outbound forward fd=%i after %.1fs\n", fd, now - outbound.started_at); done = true; } if(!done && !pending.empty()) { ssize_t n = ::send(fd, pending.data(), pending.size(), MSG_NOSIGNAL | MSG_DONTWAIT); if(n > 0) pending.erase(0, n); else if(n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) done = true; } if(!done && pending.empty()) { char buf[8192]; ssize_t r = ::recv(fd, buf, sizeof(buf), MSG_DONTWAIT); if(r == 0) done = true; // worker closed the connection: render complete else if(r < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) done = true; } if(done) { ::close(fd); it = ws_broker_outbound.erase(it); } else ++it; } } void run_ws_broker() { my_pid = getpid(); close_inherited_server_sockets(); // drop the worker listeners we inherited install_process_fault_handlers(); ws_broker.calls_until_termination = -1; ws_broker.http_document_root = first(server_state.config["HTTP_DOCUMENT_ROOT"], path_join(server_state.config["COMPILER_SYS_PATH"], server_state.config["SITE_DIRECTORY"])); // The broker renders nothing itself, so accept every request through to // on_complete (it either forwards to the pool or applies a ws_* batch). ws_broker.on_request = [](FastCGIRequest&) { return 0; }; ws_broker.on_data = [](FastCGIRequest&) { return 0; }; ws_broker.on_complete = &ws_broker_complete; ws_broker.on_websocket_message = &ws_broker_ws_message; if(server_state.config["HTTP_PORT"] != "") ws_broker.listen_http(int_val(server_state.config["HTTP_PORT"])); u64 ws_broker_outbound_timeout_seconds = to_u64(server_state.config["WS_BROKER_OUTBOUND_TIMEOUT_SECONDS"], 30); if(server_state.config["WS_BROKER_SOCKET_PATH"] != "") { ws_broker.listen(server_state.config["WS_BROKER_SOCKET_PATH"]); chmod(server_state.config["WS_BROKER_SOCKET_PATH"].c_str(), S_IRWXU | S_IRGRP | S_IWGRP); } for(;;) { ws_broker.process(50); ws_broker_drain_outbound(ws_broker_outbound_timeout_seconds); } } bool ws_broker_alive() { return(ws_broker_pid > 0 && task_kill(ws_broker_pid, 0) == 0); } void ensure_ws_broker() { if(ws_broker_alive()) return; pid_t p = fork(); if(p < 0) { perror("fork ws_broker"); return; } if(p == 0) { run_ws_broker(); exit(0); } ws_broker_pid = p; printf("(P) WS broker spawned: PID %i\n", p); } void custom_server_http_dispatcher_loop(String key) { my_pid = getpid(); custom_server_dispatcher_key = key; close_inherited_server_sockets(); install_process_fault_handlers(); StringMap cfg = custom_server_read_config(key); if(cfg["type"] != "http" || cfg["bind"] == "") { fprintf(stderr, "server_start_http(): missing config for key '%s'\n", key.c_str()); exit(1); } try { FastCGIServer dispatcher; custom_server_bind_http(dispatcher, cfg["bind"]); dispatcher.calls_until_termination = -1; dispatcher.resolve_http_script_filename = false; dispatcher.on_complete = &custom_server_http_complete; for(;;) dispatcher.process(-1); } catch(const std::exception& e) { fprintf(stderr, "server_start_http(): dispatcher for key '%s' stopped: %s\n", key.c_str(), e.what()); exit(1); } catch(...) { fprintf(stderr, "server_start_http(): dispatcher for key '%s' stopped with unknown error\n", key.c_str()); exit(1); } } pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_filename, String call_function) { key = trim(key); socket_fn_or_port = trim(socket_fn_or_port); call_uce_filename = trim(call_uce_filename); call_function = trim(call_function); if(key == "") throw std::runtime_error("server_start_http(): key cannot be empty"); if(socket_fn_or_port == "") throw std::runtime_error("server_start_http(): socket_fn_or_port cannot be empty"); if(call_uce_filename == "") throw std::runtime_error("server_start_http(): call_uce_filename cannot be empty"); if(call_uce_filename[0] != '/') call_uce_filename = expand_path(call_uce_filename, cwd_get()); String allowed_root = first(server_state.config["CUSTOM_SERVER_UCE_ROOT"], path_join(server_state.config["COMPILER_SYS_PATH"], server_state.config["SITE_DIRECTORY"])); String real_call_uce_filename = path_real(call_uce_filename); if(real_call_uce_filename == "" || !path_is_within(real_call_uce_filename, allowed_root)) throw std::runtime_error("server_start_http(): call_uce_filename is outside configured custom server UCE root"); call_uce_filename = real_call_uce_filename; String config_file = custom_server_config_file(key); StringMap previous_config; if(file_exists(config_file)) previous_config = custom_server_config_decode(file_get_contents(config_file)); String new_config = custom_server_config_encode(key, "http", socket_fn_or_port, call_uce_filename, call_function); String task_key = custom_server_task_key(key); u64 max_servers = to_u64(server_state.config["CUSTOM_SERVER_MAX_SERVERS"], 16); if(!file_exists(config_file) && max_servers > 0 && custom_server_registry_count() >= max_servers) throw std::runtime_error("server_start_http(): custom server quota exceeded"); pid_t existing_pid = task_pid(task_key); if(existing_pid != 0 && previous_config["bind"] != "" && previous_config["bind"] != socket_fn_or_port) { task_kill(existing_pid, SIGTERM); custom_server_wait_for_stop(task_key); existing_pid = 0; } if(!file_put_contents(config_file, new_config)) throw std::runtime_error("server_start_http(): could not write server config"); if(existing_pid != 0) return(existing_pid); return(task(task_key, [key]() { custom_server_http_dispatcher_loop(key); }, 0)); } bool server_stop(String key) { key = trim(key); if(key == "") return(false); String task_key = custom_server_task_key(key); pid_t pid = task_pid(task_key); file_unlink(custom_server_config_file(key)); if(pid == 0) return(true); if(task_kill(pid, SIGTERM) != 0) return(false); return(custom_server_wait_for_stop(task_key)); } bool proactive_compile_queue_has(StringList& queue, String file_name) { return(std::find(queue.begin(), queue.end(), file_name) != queue.end()); } void proactive_compile_queue_push(StringList& queue, String file_name) { if(file_name == "" || proactive_compile_queue_has(queue, file_name)) return; queue.push_back(file_name); } void run_proactive_compiler() { Request background_context; StringList compile_queue; background_context.server = &server_state; set_active_request(background_context); if(!to_bool(server_state.config["PROACTIVE_COMPILE_ENABLED"], true)) return; f64 check_interval = float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]); f64 failure_retry_interval = 0; f64 next_scan_at = 0; std::map retry_after; if(check_interval < 1) check_interval = 1; failure_retry_interval = std::max( check_interval, (f64)std::max((s64)10, (s64)int_val(server_state.config["COMPILE_FAILURE_RETRY_SECONDS"])) ); my_pid = getpid(); close_inherited_server_sockets(); signal(SIGSEGV, on_segfault); signal(SIGABRT, on_segfault); signal(SIGBUS, on_segfault); signal(SIGILL, on_segfault); signal(SIGFPE, on_segfault); signal(SIGPIPE, SIG_IGN); setpriority(PRIO_PROCESS, 0, 10); try { auto known_units = compiler_list_known_units(&background_context); auto site_units = compiler_scan_site_units(&background_context); known_units.insert(known_units.end(), site_units.begin(), site_units.end()); compiler_set_known_units(&background_context, known_units); } catch(const std::exception& e) { printf("(!) proactive compiler initial scan failed: %s\n", e.what()); } catch(...) { printf("(!) proactive compiler initial scan failed: unknown exception\n"); } next_scan_at = time_precise(); for(;;) { try { if(compile_queue.size() == 0 && time_precise() >= next_scan_at) { auto tracked_units = compiler_list_known_units(&background_context); StringList existing_units; for(auto& file_name : tracked_units) { bool source_missing = false; auto retry_it = retry_after.find(file_name); bool retry_allowed = (retry_it == retry_after.end() || time_precise() >= retry_it->second); if(compiler_unit_needs_recompile(&background_context, file_name, &source_missing) && retry_allowed) proactive_compile_queue_push(compile_queue, file_name); if(source_missing) { printf("(i) proactive compiler forget removed unit %s\n", file_name.c_str()); retry_after.erase(file_name); continue; } existing_units.push_back(file_name); } if(existing_units.size() != tracked_units.size()) compiler_set_known_units(&background_context, existing_units); next_scan_at = time_precise() + check_interval; } if(compile_queue.size() > 0) { auto file_name = compile_queue.front(); compile_queue.erase(compile_queue.begin()); bool source_missing = false; auto retry_it = retry_after.find(file_name); if(retry_it != retry_after.end() && time_precise() < retry_it->second) continue; if(compiler_unit_needs_recompile(&background_context, file_name, &source_missing)) { printf("(i) proactive compile %s\n", file_name.c_str()); auto su = get_shared_unit(&background_context, file_name); if(su && su->compiler_messages == "") retry_after.erase(file_name); else retry_after[file_name] = time_precise() + failure_retry_interval; } else if(source_missing) { printf("(i) proactive compiler forget removed unit %s\n", file_name.c_str()); compiler_untrack_known_unit(&background_context, file_name); retry_after.erase(file_name); } else { retry_after.erase(file_name); } background_context.session.clear(); background_context.session_loaded_hash = ""; clear_shared_unit_cache(server_state); usleep(250000); continue; } usleep(250000); } catch(const std::exception& e) { printf("(!) proactive compiler scan failed: %s\n", e.what()); next_scan_at = time_precise() + check_interval; usleep(250000); } catch(...) { printf("(!) proactive compiler scan failed: unknown exception\n"); next_scan_at = time_precise() + check_interval; usleep(250000); } } } bool proactive_compiler_alive() { return(proactive_compiler_pid > 0 && task_kill(proactive_compiler_pid, 0) == 0); } void ensure_proactive_compiler() { if(!to_bool(server_state.config["PROACTIVE_COMPILE_ENABLED"], true)) return; if(float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) <= 0) return; if(proactive_compiler_alive()) return; pid_t p = fork(); if(p < 0) { perror("fork proactive compiler"); return; } if(p == 0) { prctl(PR_SET_PDEATHSIG, SIGHUP); run_proactive_compiler(); exit(0); } proactive_compiler_pid = p; printf("(P) proactive compiler spawned: PID %i\n", p); } void listen_for_connections() { install_process_fault_handlers(); // Workers are uniform FastCGI/CLI renderers; the WS broker owns the HTTP/WS // port and every connection, so workers never accept raw HTTP themselves. server.close_http_listeners(); server.on_request = &handle_request; server.on_data = &handle_data; server.on_complete = &handle_complete; server.on_cli_complete = &handle_cli_complete; for(;;) { server.process(-1); } } void init_base_process() { printf("(P) Starting parent server PID:%i\n", getpid()); server_state.config = make_server_settings(); server_state.config["COMPILER_SYS_PATH"] = cwd_get(); printf("Compiler base path: %s\n", server_state.config["COMPILER_SYS_PATH"].c_str()); if(server_state.config["FCGI_PORT"] != "") server.listen(int_val(server_state.config["FCGI_PORT"])); printf("%s\n", var_dump(server_state.config).c_str()); if(server_state.config["FCGI_SOCKET_PATH"] != "") { server.listen(server_state.config["FCGI_SOCKET_PATH"]); chmod(server_state.config["FCGI_SOCKET_PATH"].c_str(), S_IRWXU | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); } if(server_state.config["CLI_SOCKET_PATH"] != "") { server.listen_cli(server_state.config["CLI_SOCKET_PATH"]); chmod(server_state.config["CLI_SOCKET_PATH"].c_str(), S_IRWXU | S_IRGRP | S_IWGRP); } // HTTP_PORT (WebSocket + raw HTTP) is owned by the dedicated WS broker, not // the worker pool — see ensure_ws_broker(). Workers handle FastCGI + CLI only. mkdir(server_state.config["BIN_DIRECTORY"]); mkdir(server_state.config["TMP_UPLOAD_PATH"]); mkdir(server_state.config["SESSION_PATH"]); signal(SIGCHLD, on_child_exit); signal(SIGINT, on_terminate); signal(SIGPIPE, SIG_IGN); srand(time()); } int main(int argc, char** argv) { // Warm up libgcc's backtrace state so the first in-handler backtrace() // after a fault does not allocate. backtrace(request_fault_frames, 4); process_start_directory(); init_base_process(); ensure_proactive_compiler(); ensure_ws_broker(); for(;;) { if(!proactive_compiler_alive()) proactive_compiler_pid = 0; if(!termination_signal_received) ensure_proactive_compiler(); // One dedicated WS broker owns all connections; respawn it if it dies // (live connections are lost on a broker restart, but unit-code crashes // happen in workers, not here, so the broker stays up in practice). if(!ws_broker_alive()) ws_broker_pid = 0; if(!termination_signal_received) ensure_ws_broker(); while(workers.size() < int_val(server_state.config["WORKER_COUNT"])) { if(!termination_signal_received) spawn_subprocess(listen_for_connections); } sleep(1); } return 0; }