#ifdef __UCE_WASM_CORE__ #include #include "types.h" #include "functionlib.h" #include "hash.h" #include "uri.h" #include "sys.h" extern "C" { uint64_t uce_host_time(void); double uce_host_time_precise(void); size_t uce_host_env(const char* key, size_t key_len, char* buf, size_t cap); size_t uce_host_random(char* buf, size_t len); void uce_host_log(int level, const char* buf, size_t len); // read-only file membrane, policy-gated host-side to the site tree; // relative paths resolve against the current unit's directory (the native // cwd convention); file_read uses the length-query convention int uce_host_file_exists(const char* path, size_t path_len, const char* current, size_t current_len); size_t uce_host_file_read(const char* path, size_t path_len, const char* current, size_t current_len, char* buf, size_t cap); int uce_host_file_write(const char* path, size_t path_len, const char* current, size_t current_len, const char* content, size_t content_len, int append); void uce_host_file_unlink(const char* path, size_t path_len, const char* current, size_t current_len); size_t uce_host_file_list(const char* path, size_t path_len, const char* current, size_t current_len, char* buf, size_t cap); int uce_host_file_mkdir(const char* path, size_t path_len, const char* current, size_t current_len); int64_t uce_host_file_mtime(const char* path, size_t path_len, const char* current, size_t current_len); int uce_host_task_spawn(const char* key, size_t key_len, uint64_t callback_id, double interval, uint64_t timeout, int repeat); int uce_host_task_pid(const char* key, size_t key_len); int uce_host_task_kill(int pid, int sig); unsigned int uce_host_sleep_us(uint64_t usec); uint64_t uce_host_socket_connect(const char* host, size_t host_len, int port); void uce_host_socket_close(uint64_t sockfd); int uce_host_socket_write(uint64_t sockfd, const char* data, size_t data_len); size_t uce_host_socket_read(uint64_t sockfd, uint32_t max_length, uint32_t timeout, char* buf, size_t cap); int uce_host_server_start_http(const char* key, size_t key_len, const char* bind, size_t bind_len, const char* file, size_t file_len, const char* function, size_t function_len, const char* current, size_t current_len); int uce_host_server_stop(const char* key, size_t key_len); size_t uce_host_memcache_command(uint64_t sockfd, const char* command, size_t command_len, char* buf, size_t cap); size_t uce_host_mysql(const char* in, size_t in_len, char* out, size_t cap); size_t uce_host_request_perf(const char* in, size_t in_len, char* out, size_t cap); size_t uce_host_shell_exec(const char* cmd, size_t cmd_len, char* buf, size_t cap); int uce_host_file_open_locked(const char* path, size_t path_len, int open_flags, int lock_type, int create_mode, double wait_timeout_seconds, const char* purpose, size_t purpose_len); void uce_host_file_close_locked(int handle); void uce_host_file_release_process_locks(const char* reason, size_t reason_len); size_t uce_host_file_read_locked_fd(int handle, char* buf, size_t cap); int uce_host_file_write_locked_fd(int handle, const char* content, size_t content_len); size_t uce_host_path_real(const char* path, size_t path_len, char* buf, size_t cap); int uce_host_path_is_within(const char* path, size_t path_len, const char* root, size_t root_len); size_t uce_host_cwd_get(char* buf, size_t cap); int uce_host_cwd_set(const char* path, size_t path_len); size_t uce_host_process_start_directory(char* buf, size_t cap); size_t uce_host_last_trap_trace(char* buf, size_t cap); } static String wasm_current_unit_file() { return(context ? context->resources.current_unit_file : String("")); } String shell_exec(String cmd) { size_t required = uce_host_shell_exec(cmd.data(), cmd.size(), 0, 0); if(required == 0) return(""); String output(required, 0); size_t got = uce_host_shell_exec(cmd.data(), cmd.size(), &output[0], required); output.resize(got <= required ? got : 0); return(output); } String shell_escape(String raw) { String result; for(auto c : raw) { if(c == '\'') result.append("'\\''"); else result.append(1, c); } return("'" + result + "'"); } String basename(String fn) { while(fn.find("/") != String::npos) fn = fn.substr(fn.find("/") + 1); return(fn); } String dirname(String fn) { auto pos = fn.find_last_of('/'); return(pos == String::npos ? "" : fn.substr(0, pos)); } String path_join(String base, String child) { if(base == "") return(child); if(child == "") return(base); if(child[0] == '/') return(child); return(base + (base.back() == '/' ? "" : "/") + child); } String path_real(String path) { size_t required = uce_host_path_real(path.data(), path.size(), 0, 0); if(required == 0) return(""); String resolved(required, 0); size_t got = uce_host_path_real(path.data(), path.size(), &resolved[0], required); resolved.resize(got <= required ? got : 0); return(resolved); } bool path_is_within(String path, String root) { return(uce_host_path_is_within(path.data(), path.size(), root.data(), root.size()) != 0); } bool mkdir(String path) { String current = wasm_current_unit_file(); return(uce_host_file_mkdir(path.data(), path.size(), current.data(), current.size()) != 0); } bool file_exists(String path) { String current = wasm_current_unit_file(); return(uce_host_file_exists(path.data(), path.size(), current.data(), current.size()) != 0); } int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode, f64 wait_timeout_seconds, String purpose) { return(uce_host_file_open_locked( file_name.data(), file_name.size(), open_flags, lock_type, create_mode, wait_timeout_seconds, purpose.data(), purpose.size() )); } void file_close_locked(int fd) { uce_host_file_close_locked(fd); } void file_release_process_locks(String reason) { uce_host_file_release_process_locks(reason.data(), reason.size()); } String file_get_contents_locked_fd(int fd) { size_t required = uce_host_file_read_locked_fd(fd, 0, 0); if(required == 0) return(""); String content(required, 0); size_t got = uce_host_file_read_locked_fd(fd, &content[0], required); content.resize(got <= required ? got : 0); return(content); } bool file_put_contents_locked_fd(int fd, String content) { return(uce_host_file_write_locked_fd(fd, content.data(), content.size()) != 0); } String file_get_contents(String file_name) { String current = wasm_current_unit_file(); size_t required = uce_host_file_read(file_name.data(), file_name.size(), current.data(), current.size(), 0, 0); if(required == 0) return(""); String content(required, 0); size_t got = uce_host_file_read(file_name.data(), file_name.size(), current.data(), current.size(), &content[0], required); content.resize(got <= required ? got : 0); return(content); } bool file_put_contents(String file_name, String content) { String current = wasm_current_unit_file(); return(uce_host_file_write(file_name.data(), file_name.size(), current.data(), current.size(), content.data(), content.size(), 0) != 0); } bool file_append_contents(String file_name, String content) { String current = wasm_current_unit_file(); return(uce_host_file_write(file_name.data(), file_name.size(), current.data(), current.size(), content.data(), content.size(), 1) != 0); } String cwd_get() { size_t required = uce_host_cwd_get(0, 0); if(required == 0) return(""); String cwd(required, 0); size_t got = uce_host_cwd_get(&cwd[0], required); cwd.resize(got <= required ? got : 0); return(cwd); } void cwd_set(String path) { uce_host_cwd_set(path.data(), path.size()); } String process_start_directory() { size_t required = uce_host_process_start_directory(0, 0); if(required == 0) return(""); String cwd(required, 0); size_t got = uce_host_process_start_directory(&cwd[0], required); cwd.resize(got <= required ? got : 0); return(cwd); } time_t file_mtime(String file_name) { String current = wasm_current_unit_file(); return((time_t)uce_host_file_mtime(file_name.data(), file_name.size(), current.data(), current.size())); } void file_unlink(String file_name) { String current = wasm_current_unit_file(); uce_host_file_unlink(file_name.data(), file_name.size(), current.data(), current.size()); } String expand_path(String path, String relative_to_path) { return(path_join(relative_to_path, path)); } StringList ls(String dir) { String current = wasm_current_unit_file(); size_t required = uce_host_file_list(dir.data(), dir.size(), current.data(), current.size(), 0, 0); if(required == 0) return(StringList()); String listing(required, 0); size_t got = uce_host_file_list(dir.data(), dir.size(), current.data(), current.size(), &listing[0], required); listing.resize(got <= required ? got : 0); if(listing == "") return(StringList()); return(split(listing, "\n")); } u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; unsigned long long v = strtoull(raw.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : fallback); } f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; double v = strtod(raw.c_str(), &end); return(end && *end == 0 ? (f64)v : fallback); } bool config_bool_value(String raw, bool fallback) { if(raw == "") return(fallback); return(raw != "0" && raw != "false" && raw != "no" && raw != "off"); } bool config_map_bool(StringMap& cfg, String key, bool fallback) { return(config_bool_value(cfg[key], fallback)); } u64 config_u64(String key, u64 fallback) { return(context ? config_map_u64(context->server->config, key, fallback) : fallback); } f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(context->server->config, key, fallback) : fallback); } bool config_bool(String key, bool fallback) { return(context ? config_map_bool(context->server->config, key, fallback) : fallback); } DValue request_perf() { size_t required = uce_host_request_perf("", 0, 0, 0); if(required == 0) return(DValue()); String encoded_response(required, 0); size_t got = uce_host_request_perf("", 0, &encoded_response[0], required); if(got == 0 || got > required) return(DValue()); DValue response; String decode_error; if(ucb_decode(encoded_response, response, &decode_error)) return(response); return(DValue()); } f64 time_precise() { return(uce_host_time_precise()); } u64 time() { return(uce_host_time()); } // The native build shells out to `date`; the wasm core has no shell, so it // formats with wasi-libc strftime (no TZ data → local == UTC, acceptable). static String wasm_time_strftime(String format, u64 timestamp, bool utc) { if(timestamp == 0) timestamp = time(); time_t t = (time_t)timestamp; struct tm tmv; if(utc) gmtime_r(&t, &tmv); else localtime_r(&t, &tmv); char buffer[512]; size_t n = strftime(buffer, sizeof(buffer), format.c_str(), &tmv); return(String(buffer, n)); } String time_format_local(String format, u64 timestamp) { return(wasm_time_strftime(format, timestamp, false)); } String time_format_utc(String format, u64 timestamp) { if(format == "RFC1123") format = "%a, %d %b %Y %T GMT"; return(wasm_time_strftime(format, timestamp, true)); } static String wasm_time_expand_delta(String format, u64 timestamp, u64 now_timestamp) { u64 delta_seconds = now_timestamp > timestamp ? now_timestamp - timestamp : 0; format = replace(format, "%deltaY", std::to_string(delta_seconds / (60 * 60 * 24 * 365))); format = replace(format, "%deltam", std::to_string(delta_seconds / (60 * 60 * 24 * 30))); format = replace(format, "%deltad", std::to_string(delta_seconds / (60 * 60 * 24))); format = replace(format, "%deltaH", std::to_string(delta_seconds / (60 * 60))); format = replace(format, "%deltaM", std::to_string(delta_seconds / 60)); format = replace(format, "%deltaS", std::to_string(delta_seconds)); return(format); } String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent) { u64 now_timestamp = time(); u64 delta_seconds = now_timestamp > timestamp ? now_timestamp - timestamp : 0; format_very_recent = first(format_very_recent, "just now"); medium_recency_seconds = medium_recency_seconds > 0 ? medium_recency_seconds : 90; format_medium_recent = first(format_medium_recent, "%deltaM minutes ago"); not_recent_seconds = not_recent_seconds > 0 ? not_recent_seconds : 90 * 60; format_not_recent = first(format_not_recent, "%deltaH hours ago"); if(delta_seconds < medium_recency_seconds) return(wasm_time_expand_delta(format_very_recent, timestamp, now_timestamp)); if(delta_seconds < not_recent_seconds) return(wasm_time_expand_delta(format_medium_recent, timestamp, now_timestamp)); return(wasm_time_expand_delta(format_not_recent, timestamp, now_timestamp)); } u64 time_parse(String time_String) { return(int_val(trim(shell_exec("date -u -d " + shell_escape(time_String) + " +'%s'")))); } u64 socket_connect(String host, short port) { return(uce_host_socket_connect(host.data(), host.size(), port)); } void socket_close(u64 sockfd) { uce_host_socket_close(sockfd); } bool socket_write(u64 sockfd, String data) { return(uce_host_socket_write(sockfd, data.data(), data.size()) != 0); } String socket_read(u64 sockfd, u32 max_length, u32 timeout) { size_t required = uce_host_socket_read(sockfd, max_length, timeout, 0, 0); if(required == 0) return(""); String content(required, 0); size_t got = uce_host_socket_read(sockfd, max_length, timeout, &content[0], required); content.resize(got <= required ? got : 0); return(content); } String ws_message() { return(context ? context->in : ""); } String ws_connection_id() { return(context ? context->resources.websocket_connection_id : ""); } String ws_scope() { return(context ? context->resources.websocket_scope : ""); } u8 ws_opcode() { return(context ? context->resources.websocket_opcode : 0); } bool ws_is_binary() { return(context && context->resources.websocket_is_binary); } StringList ws_connections(String scope) { (void)scope; return(context ? context->resources.websocket_scope_connection_ids : StringList()); } u64 ws_connection_count(String scope) { return(ws_connections(scope).size()); } // The wasm workspace owns no connections; ws_send/ws_close record dispatch // commands (same shape as the native websocket_exec capture) that the host // carries back to the broker, which sends them over the client connections. bool ws_send(String message, bool binary, String scope) { if(!context) return(false); DValue command; command["action"] = "broadcast"; command["binary"].set_bool(binary); command["message_b64"] = base64_encode(message); command["scope"] = scope; context->resources.websocket_dispatch_commands.push(command); return(true); } bool ws_send_to(String connection_id, String message, bool binary) { if(!context) return(false); DValue command; command["action"] = "send_to"; command["binary"].set_bool(binary); command["message_b64"] = base64_encode(message); command["connection_id"] = connection_id; context->resources.websocket_dispatch_commands.push(command); return(true); } bool ws_close(String connection_id) { if(!context) return(false); if(connection_id == "") connection_id = ws_connection_id(); DValue command; command["action"] = "close"; command["connection_id"] = connection_id; command["status_code"] = (f64)1000; command["reason"] = ""; context->resources.websocket_dispatch_commands.push(command); return(true); } String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames) { (void)frames; (void)size; (void)skip_frames; return(capture_backtrace_string(0, 0)); } String capture_backtrace_string(u32 max_frames, u32 skip_frames) { (void)max_frames; (void)skip_frames; size_t required = uce_host_last_trap_trace(0, 0); if(required == 0) return(""); String trace(required, 0); size_t got = uce_host_last_trap_trace(&trace[0], required); trace.resize(got <= required ? got : 0); return(trace); } String signal_name(int sig) { switch(sig) { case 6: return("SIGABRT"); case 7: return("SIGBUS"); case 8: return("SIGFPE"); case 4: return("SIGILL"); case 2: return("SIGINT"); case 11: return("SIGSEGV"); case 15: return("SIGTERM"); default: return(""); } } String memcache_escape_key(String key) { String result; for(auto c : key) { if(isspace((unsigned char)c)) c = '_'; result.append(1, c); } return(result); } StringList memcache_escape_keys(StringList keys) { StringList result; for(auto s : keys) result.push_back(memcache_escape_key(s)); return(result); } u64 memcache_connect(String host, short port) { if(host == "") host = "127.0.0.1"; if(port == 0) port = 11211; return(socket_connect(host, port)); } String memcache_command(u64 connection, String command) { size_t required = uce_host_memcache_command(connection, command.data(), command.size(), 0, 0); if(required == 0) return(""); String content(required, 0); size_t got = uce_host_memcache_command(connection, command.data(), command.size(), &content[0], required); content.resize(got <= required ? got : 0); return(content); } bool memcache_set(u64 connection, String key, String value, u64 expires_in) { String response = memcache_command( connection, String("set ") + memcache_escape_key(key) + " 0 " + std::to_string(expires_in) + " " + std::to_string(value.length()) + "\r\n" + value ); return("STORED" == trim(response)); } bool memcache_delete(u64 connection, String key) { String response = memcache_command(connection, String("delete ") + memcache_escape_key(key)); return("DELETED" == trim(response)); } String memcache_get(u64 connection, String key, String default_value) { String res = memcache_command(connection, String("get ") + memcache_escape_key(key)); String header_end = "\r\n"; size_t header_pos = res.find(header_end); if(res.rfind("VALUE ", 0) != 0 || header_pos == String::npos) return(default_value); String header = res.substr(0, header_pos); StringList parts = split(header, " "); if(parts.size() < 4) return(default_value); u64 length = int_val(parts[3]); size_t data_pos = header_pos + header_end.size(); if(res.size() < data_pos + length) return(default_value); return(res.substr(data_pos, length)); } StringMap memcache_get_multiple(u64 connection, StringList keys) { StringMap result; String res = memcache_command(connection, String("get ") + join(memcache_escape_keys(keys), " ")); while(res.rfind("VALUE ", 0) == 0) { size_t header_pos = res.find("\r\n"); if(header_pos == String::npos) break; StringList parts = split(res.substr(0, header_pos), " "); if(parts.size() < 4) break; String key = parts[1]; u64 length = int_val(parts[3]); size_t data_pos = header_pos + 2; if(res.size() < data_pos + length) break; result[key] = res.substr(data_pos, length); res = res.substr(data_pos + length); if(res.rfind("\r\n", 0) == 0) res = res.substr(2); } return(result); } void on_segfault(int sig) { (void)sig; } static u64 wasm_next_task_callback_id = 1; static std::map> wasm_task_callbacks; extern "C" int uce_wasm_task_run(uint64_t callback_id) { auto it = wasm_task_callbacks.find(callback_id); if(it == wasm_task_callbacks.end()) return(1); it->second(); return(0); } int task_kill(pid_t pid, int sig) { return(uce_host_task_kill(pid, sig)); } String runtime_safe_key(String key, String label) { (void)label; key = trim(key); if(key == "") return(""); return(gen_sha1(key)); } pid_t task(String key, std::function exec_after_spawn, u64 timeout) { u64 id = wasm_next_task_callback_id++; wasm_task_callbacks[id] = exec_after_spawn; return((pid_t)uce_host_task_spawn(key.data(), key.size(), id, 0.0, timeout, 0)); } pid_t task_repeat(String key, f64 interval, std::function exec_after_spawn, u64 timeout) { if(!(interval > 0) || !std::isfinite(interval)) return(0); u64 id = wasm_next_task_callback_id++; wasm_task_callbacks[id] = exec_after_spawn; return((pid_t)uce_host_task_spawn(key.data(), key.size(), id, interval, timeout, 1)); } pid_t task_pid(String key) { return((pid_t)uce_host_task_pid(key.data(), key.size())); } extern "C" unsigned int sleep(unsigned int seconds) { return(uce_host_sleep_us((uint64_t)seconds * 1000000ull)); } extern "C" int usleep(unsigned int usec) { uce_host_sleep_us(usec); return(0); } pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_filename, String call_function) { String current = wasm_current_unit_file(); return((pid_t)uce_host_server_start_http( key.data(), key.size(), socket_fn_or_port.data(), socket_fn_or_port.size(), call_uce_filename.data(), call_uce_filename.size(), call_function.data(), call_function.size(), current.data(), current.size() )); } bool server_stop(String key) { return(uce_host_server_stop(key.data(), key.size()) != 0); } StringMap default_config() { StringMap cfg; cfg["SESSION_TIME"] = std::to_string(60*60*24*30); cfg["MAX_MEMORY"] = std::to_string(1024*1024*16); return(cfg); } #else #include #include #include #include #include #include #include #include #include #include #include "sys.h" #include "hash.h" // Single definitions for the native split build (declared extern in sys.h). pid_t parent_pid = 0; pid_t my_pid = 0; namespace { constexpr f64 FILE_LOCK_WAIT_TIMEOUT_SECONDS = 3.0; } String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames) { if(size == 0) return(""); char** symbols = backtrace_symbols(frames, size); if(!symbols) return(""); String trace; for(size_t i = skip_frames; i < size; i++) { trace += symbols[i]; trace += "\n"; } free(symbols); return(trace); } String capture_backtrace_string(u32 max_frames, u32 skip_frames) { if(max_frames == 0) return(""); std::vector frames(max_frames); size_t size = backtrace(frames.data(), max_frames); return(backtrace_frames_string(frames.data(), size, skip_frames)); } String signal_name(int sig) { switch(sig) { case SIGABRT: return("SIGABRT"); case SIGBUS: return("SIGBUS"); case SIGFPE: return("SIGFPE"); case SIGILL: return("SIGILL"); case SIGINT: return("SIGINT"); case SIGSEGV: return("SIGSEGV"); case SIGTERM: return("SIGTERM"); default: return(""); } } namespace { String time_format_expand_delta_tokens(String format, u64 timestamp, u64 now_timestamp) { u64 delta_seconds = 0; if(now_timestamp > timestamp) delta_seconds = now_timestamp - timestamp; format = replace(format, "%deltaY", std::to_string(delta_seconds / (60 * 60 * 24 * 365))); format = replace(format, "%deltam", std::to_string(delta_seconds / (60 * 60 * 24 * 30))); format = replace(format, "%deltad", std::to_string(delta_seconds / (60 * 60 * 24))); format = replace(format, "%deltaH", std::to_string(delta_seconds / (60 * 60))); format = replace(format, "%deltaM", std::to_string(delta_seconds / 60)); format = replace(format, "%deltaS", std::to_string(delta_seconds)); return(format); } String time_format_shell(String format, u64 timestamp, bool use_utc) { String ts; String fmt; u64 effective_timestamp = (timestamp > 0 ? timestamp : time()); String expanded_format = time_format_expand_delta_tokens(format, effective_timestamp, time()); if(timestamp > 0) ts = String("-d '@")+std::to_string(timestamp)+"'"; if(expanded_format != "") fmt = String("+'")+expanded_format+"'"; return(trim(shell_exec(String("date ") + (use_utc ? "-u " : "") + ts + " " + fmt))); } } String shell_exec(String cmd) { //printf("(i) shell_exec(%s)\n", cmd.c_str()); String data; FILE * stream; const int max_buffer = 256; char buffer[max_buffer]; cmd.append(" 2>&1"); stream = popen(cmd.c_str(), "r"); if (stream) { while (!feof(stream)) if (fgets(buffer, max_buffer, stream) != NULL) data.append(buffer); pclose(stream); } return data; } String shell_escape(String raw) { // FIXME String result = ""; for(auto c : raw) { if(c == '\'') { result.append("'\\''"); } else { result.append(1, c); } } return("\'" + result + "\'"); /* ` U+0060 (Grave Accent) Backtick Command substitution ~ U+007E Tilde Tilde expansion ! U+0021 Exclamation mark History expansion # U+0023 Number sign Hash Comments $ U+0024 Dollar sign Parameter expansion & U+0026 Ampersand Background commands * U+002A Asterisk Filename expansion and globbing ( U+0028 Left Parenthesis Subshells ) U+0029 Right Parenthesis Subshells U+0009 Tab (⇥) Word splitting (whitespace) { U+007B Left Curly Bracket Left brace Brace expansion [ U+005B Left Square Bracket Filename expansion and globbing | U+007C Vertical Line Vertical bar Pipelines \ U+005C Reverse Solidus Backslash Escape character ; U+003B Semicolon Separating commands ' U+0027 Apostrophe Single quote String quoting " U+0022 Quotation Mark Double quote String quoting with interpolation ↩ U+000A Line Feed Newline Line break < U+003C Less than Input redirection > U+003E Greater than Output redirection ? U+003F Question mark Filename expansion and globbing   U+0020 Space Word splitting1 (whitespace) */ } String basename(String fn) { String result; while(fn.length() > 0) result = nibble("/", fn); //printf("basename(%s) %s\n", fn.c_str(), result.c_str()); return(result); } String dirname(String fn) { String result; auto seg = split(fn, "/"); seg.pop_back(); result = join(seg, "/"); //printf("dirname(%s) %s seg#%i\n", fn.c_str(), result.c_str(), seg.size()); return(result); } String path_join(String base, String child) { if(base == "") return(child); if(child == "") return(base); if(child[0] == '/') return(child); if(base[base.length() - 1] == '/') return(base + child); return(base + "/" + child); } String path_real(String path) { char resolved[PATH_MAX]; if(realpath(path.c_str(), resolved)) return(String(resolved)); return(""); } bool path_is_within(String path, String root) { String real_path = path_real(path); String real_root = path_real(root); if(real_path == "" || real_root == "") return(false); if(real_path == real_root) return(true); if(real_root[real_root.length() - 1] != '/') real_root += "/"; return(str_starts_with(real_path, real_root)); } bool mkdir(String path) { shell_exec(String("mkdir -p ")+" "+shell_escape(path)); return(true); } bool file_exists(String path) { std::filesystem::path fp{ path }; return(std::filesystem::exists(fp)); } int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode, f64 wait_timeout_seconds, String purpose) { (void)wait_timeout_seconds; (void)purpose; int fd = open(file_name.c_str(), open_flags, create_mode); if(fd == -1) return(-1); fcntl(fd, F_SETFD, FD_CLOEXEC); if(flock(fd, lock_type) != 0) { close(fd); return(-1); } return(fd); } void file_close_locked(int fd) { if(fd == -1) return; flock(fd, LOCK_UN); close(fd); } void file_release_process_locks(String reason) { (void)reason; } String file_get_contents_locked_fd(int fd) { if(fd == -1) return(""); char buf[512]; String content; s64 bytes_read = 0; lseek(fd, 0, SEEK_SET); while((bytes_read = read(fd, buf, sizeof(buf))) > 0) content.append(buf, bytes_read); return(content); } namespace { bool file_write_all(int fd, const char* data, size_t remaining) { while(remaining > 0) { auto bytes_written = write(fd, data, remaining); if(bytes_written < 0) return(false); data += bytes_written; remaining -= bytes_written; } return(true); } } bool file_put_contents_locked_fd(int fd, String content) { if(fd == -1) return(false); lseek(fd, 0, SEEK_SET); if(ftruncate(fd, 0) != 0) return(false); if(!file_write_all(fd, content.data(), content.length())) return(false); return(true); } String file_get_contents(String file_name) { s32 fd = file_open_locked(file_name, O_RDONLY, LOCK_SH, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_get_contents:" + file_name); if(fd == -1) { printf("(!) Could not read %s\n", file_name.c_str()); return(""); } String content = file_get_contents_locked_fd(fd); file_close_locked(fd); return(content); } bool file_put_contents(String file_name, String content) { s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_put_contents:" + file_name); if(fd == -1) { printf("(!) Could not write %s\n", file_name.c_str()); return(false); } bool ok = file_put_contents_locked_fd(fd, content); file_close_locked(fd); if(!ok) { printf("(!) Could not fully write %s\n", file_name.c_str()); return(false); } return(true); } bool file_append_contents(String file_name, String content) { s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_append:" + file_name); if(fd == -1) { printf("(!) Could not append %s\n", file_name.c_str()); return(false); } lseek(fd, 0, SEEK_END); bool ok = file_write_all(fd, content.data(), content.length()); file_close_locked(fd); if(!ok) { printf("(!) Could not fully append %s\n", file_name.c_str()); return(false); } return(true); } String cwd_get() { return(std::filesystem::current_path()); } String process_start_directory() { // Primed in main() before any unit handler can chdir; fault recovery and // config-relative path resolution anchor to this instead of the volatile // per-unit working directory. static String start_directory = cwd_get(); return(start_directory); } void cwd_set(String path) { chdir(path.c_str()); } time_t file_mtime(String file_name) { struct stat info; if (stat(file_name.c_str(), &info) != 0) { return(0); } else { return(info.st_mtime); } } void file_unlink(String file_name) { remove(file_name.c_str()); } String expand_path(String path, String relative_to_path) { String result; if(relative_to_path == "") relative_to_path = cwd_get(); auto base_path = split(relative_to_path, "/"); auto rel_path = split(path, "/"); for(auto& s : rel_path) { if(s == "..") { base_path.pop_back(); } else if(s == ".") { } else { base_path.push_back(s); } } return(join(base_path, "/")); } f64 time_precise() { return ((f64)std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch()).count()) / 1000000; } u64 time() { return(std::time(0)); } String time_format_local(String format, u64 timestamp) { return(time_format_shell(format, timestamp, false)); } String time_format_utc(String format, u64 timestamp) { if(format == "RFC1123") format = "%a, %d %b %Y %T GMT"; return(time_format_shell(format, timestamp, true)); } String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent) { u64 now_timestamp = time(); u64 delta_seconds = 0; if(now_timestamp > timestamp) delta_seconds = now_timestamp - timestamp; format_very_recent = first(format_very_recent, "just now"); medium_recency_seconds = (medium_recency_seconds > 0 ? medium_recency_seconds : 90); format_medium_recent = first(format_medium_recent, "%deltaM minutes ago"); not_recent_seconds = (not_recent_seconds > 0 ? not_recent_seconds : 90 * 60); format_not_recent = first(format_not_recent, "%deltaH hours ago"); if(delta_seconds < medium_recency_seconds) return(time_format_expand_delta_tokens(format_very_recent, timestamp, now_timestamp)); if(delta_seconds < not_recent_seconds) return(time_format_expand_delta_tokens(format_medium_recent, timestamp, now_timestamp)); return(time_format_expand_delta_tokens(format_not_recent, timestamp, now_timestamp)); } u64 time_parse(String time_String) { return(int_val(trim(shell_exec("date -u -d "+shell_escape(time_String)+" +'%s'")))); } u64 socket_connect(String host, short port) { /*String addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; String sockaddr *ai_addr; char *ai_canonname; String addrinfo *ai_next; };*/ auto sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(sockfd < 0) { print("SOCKET ERROR (could not open socket)\n"); perror("SOCKET ERROR "); return(0); } struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(host.c_str()); if(connect(sockfd, (struct sockaddr*) &addr, sizeof(addr)) < 0) { print("SOCKET ERROR (could not connect to address " + String(host) + ":" + std::to_string(port) + ")\n"); perror("SOCKET ERROR "); return(0); } context->resources.sockets.push_back(sockfd); return(sockfd); } void socket_close(u64 sockfd) { close(sockfd); } bool socket_write(u64 sockfd, String data) { return(write(sockfd, data.c_str(), data.length()) >= 0); } String socket_read(u64 sockfd, u32 max_length, u32 timeout) { struct timeval tv; tv.tv_sec = timeout; tv.tv_usec = 0; setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv); if(max_length == 0) return(""); std::vector buf(max_length); auto byte_count = recv(sockfd, buf.data(), buf.size(), 0); if(byte_count > 0) return(String(buf.data(), byte_count)); return(""); } String memcache_escape_key(String key) { String result; for(auto c : key) { if(isspace((unsigned char)c)) c = '_'; result.append(1, c); } return(result); } StringList memcache_escape_keys(StringList keys) { StringList result; for(auto s : keys) { result.push_back(memcache_escape_key(s)); } return(result); } u64 memcache_connect(String host, short port) { return(socket_connect(host, port)); } String memcache_command(u64 connection, String command) { socket_write(connection, command+"\r\n"); return(socket_read(connection)); // FIXME: do multi-chunk until END line is received! } bool memcache_set(u64 connection, String key, String value, u64 expires_in) { socket_write(connection, // set KEY META_DATA EXPIRY_TIME LENGTH_IN_BYTES String("set ") + memcache_escape_key(key) + " 0 " + std::to_string(expires_in) + " " + std::to_string(value.length()) + "\r\n" + value + "\r\n"); return("STORED" == trim(socket_read(connection))); } bool memcache_delete(u64 connection, String key) { socket_write(connection, // set KEY META_DATA EXPIRY_TIME LENGTH_IN_BYTES String("delete ") + memcache_escape_key(key) + "\r\n" ); return("DELETED" == trim(socket_read(connection))); } String memcache_get(u64 connection, String key, String default_value) { auto res = memcache_command(connection, String("get ")+memcache_escape_key(key)); String t = nibble(res, " "); if(t == "VALUE") { String key = nibble(res, " "); String meta = nibble(res, " "); u32 length = stoi(nibble(res, "\r\n")); return(res.substr(0, length)); } return(default_value); } StringMap memcache_get_multiple(u64 connection, StringList keys) { StringMap result; // to do: escape key String auto res = memcache_command(connection, String("get ")+join(memcache_escape_keys(keys), " ")); while(res.length() > 0) { String t = nibble(res, " "); if(t == "VALUE") { String key = nibble(res, " "); String meta = nibble(res, " "); u32 length = stoi(nibble(res, "\r\n")); result[key] = res.substr(0, length); res = res.substr(length+2); } } return(result); } void on_segfault(int sig) { String trace = capture_backtrace_string(32, 1); String sig_label = signal_name(sig); if(sig_label != "") fprintf(stderr, "SEG FAULT: %d (%s):\n%s", sig, sig_label.c_str(), trace.c_str()); else fprintf(stderr, "SEG FAULT: %d:\n%s", sig, trace.c_str()); exit(1); } struct Worker { pid_t pid; }; std::map workers; #include #include #include pid_t spawn_subprocess(std::function exec_after_spawn) { parent_pid = getpid(); pid_t p; p = fork(); if(p == 0) { file_release_process_locks("fork child startup"); my_pid = getpid(); //printf("(C) child procress started, PID:%i\n", my_pid); prctl(PR_SET_PDEATHSIG, SIGHUP); exec_after_spawn(); return(0); } else { Worker w; w.pid = p; workers[w.pid] = w; printf("(P) child procress spawned: PID %i\n", p); return(p); } } String runtime_safe_key(String key, String label) { key = trim(key); if(key == "") throw std::runtime_error(label + " cannot be empty"); return(gen_sha1(key)); } String task_file_prefix(String key) { return(path_join(context->server->config["BIN_DIRECTORY"], "task-" + runtime_safe_key(key, "task key"))); } struct TaskStatus { pid_t pid = 0; String process_start_ticks = ""; }; TaskStatus task_status_parse(String status_file) { TaskStatus status; auto lines = split(trim(status_file), "\n"); if(lines.size() > 0) status.pid = (pid_t)int_val(trim(lines[0])); if(lines.size() > 1) status.process_start_ticks = trim(lines[1]); return(status); } String task_process_start_ticks(pid_t pid) { if(pid <= 0) return(""); String stat_file_name = "/proc/" + std::to_string(pid) + "/stat"; int fd = open(stat_file_name.c_str(), O_RDONLY); if(fd == -1) return(""); char buffer[4096]; ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1); close(fd); if(bytes_read <= 0) return(""); buffer[bytes_read] = '\0'; String stat = buffer; size_t command_end = stat.rfind(") "); if(command_end == String::npos) return(""); String after_command = stat.substr(command_end + 2); auto fields = split_space(after_command); if(fields.size() <= 19) return(""); return(fields[19]); } String task_status_content(pid_t pid) { return(std::to_string(pid) + "\n" + task_process_start_ticks(pid) + "\n"); } bool task_status_is_alive(TaskStatus status) { if(status.pid <= 0) return(false); if(kill(status.pid, 0) != 0) return(false); if(status.process_start_ticks == "") return(true); return(task_process_start_ticks(status.pid) == status.process_start_ticks); } void task_close_inherited_fds() { long max_fd = sysconf(_SC_OPEN_MAX); if(max_fd < 0 || max_fd > 65536) max_fd = 4096; for(int fd = 3; fd < max_fd; fd++) close(fd); } int task_kill(pid_t pid, int sig) { if(pid <= 0) { errno = EINVAL; return(-1); } return(kill(pid, sig)); } pid_t task_pid(String key) { String status_file_name = task_file_prefix(key); String lock_file_name = status_file_name + ".lock"; int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task-pid:" + key); if(lock_fd == -1) { fprintf(stderr, "task_pid(): could not lock task key '%s'\n", key.c_str()); return(0); } String status_file = file_exists(status_file_name) ? file_get_contents(status_file_name) : ""; if(status_file != "") { TaskStatus status = task_status_parse(status_file); if(task_status_is_alive(status)) { file_close_locked(lock_fd); return(status.pid); } file_unlink(status_file_name); } file_close_locked(lock_fd); return(0); } pid_t task(String key, std::function exec_after_spawn, u64 timeout) { String status_file_name = task_file_prefix(key); String lock_file_name = status_file_name + ".lock"; int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task:" + key); if(lock_fd == -1) { fprintf(stderr, "task(): could not lock task key '%s'\n", key.c_str()); return(0); } String status_file = file_exists(status_file_name) ? file_get_contents(status_file_name) : ""; pid_t p = 0; if(status_file != "") { TaskStatus status = task_status_parse(status_file); if(task_status_is_alive(status)) { printf("(P) worker process '%s' already running: PID %i\n", key.c_str(), status.pid); file_close_locked(lock_fd); return(status.pid); } file_unlink(status_file_name); } p = fork(); if(p < 0) { fprintf(stderr, "task(): fork failed for key '%s': %s\n", key.c_str(), strerror(errno)); file_close_locked(lock_fd); return(0); } if(p == 0) { file_release_process_locks("task child startup"); file_close_locked(lock_fd); my_pid = getpid(); signal(SIGALRM, SIG_DFL); if(timeout > 0) alarm(timeout); if(context->resources.client_socket > 0) { close(context->resources.client_socket); context->resources.client_socket = 0; } task_close_inherited_fds(); exec_after_spawn(); int exit_lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task-exit:" + key); if(exit_lock_fd != -1) { file_unlink(status_file_name); file_close_locked(exit_lock_fd); } else { fprintf(stderr, "task(): could not lock task key '%s' during child cleanup\n", key.c_str()); } printf("(P) worker process '%s' terminated: PID %i\n", key.c_str(), my_pid); exit(0); } if(!file_put_contents(status_file_name, task_status_content(p))) { fprintf(stderr, "task(): could not write status file for key '%s'; terminating child PID %i\n", key.c_str(), p); kill(p, SIGTERM); file_close_locked(lock_fd); return(0); } file_close_locked(lock_fd); printf("(P) worker process '%s' spawned: PID %i\n", key.c_str(), p); return(p); } #include pid_t task_repeat(String key, f64 interval, std::function exec_after_spawn, u64 timeout) { if(interval <= 0) throw std::runtime_error("task_repeat(): interval must be greater than zero"); auto repeater_function = [key, interval, exec_after_spawn, timeout]() { f64 started_at = time_precise(); while (timeout == 0 || time_precise() - started_at < (f64)timeout) { exec_after_spawn(); f64 elapsed = time_precise() - started_at; if(timeout > 0 && elapsed >= (f64)timeout) break; f64 sleep_seconds = interval; if(timeout > 0 && elapsed + sleep_seconds > (f64)timeout) sleep_seconds = (f64)timeout - elapsed; printf("(P) worker process '%s' sleeping\n", key.c_str()); if(sleep_seconds > 0) usleep((useconds_t)(sleep_seconds * 1000000.0)); } }; return(task(key, repeater_function, timeout)); } void on_child_exit(int sig) { (void)sig; pid_t pid; int status; while((pid = waitpid(-1, &status, WNOHANG)) > 0) { if(workers.count(pid) > 0) { workers.erase(pid); printf("(P) child terminated (PID:%i)\n", pid); } else { printf("(P) task child reaped (PID:%i)\n", pid); } } } StringList ls(String dir) { return(split(trim(shell_exec("ls -1 "+shell_escape(dir))), "\n")); } u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = trim(cfg[key]); if(raw == "") return(fallback); return(int_val(raw)); } f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = trim(cfg[key]); if(raw == "") return(fallback); return(float_val(raw)); } bool config_bool_value(String raw, bool fallback) { raw = trim(to_lower(raw)); if(raw == "") return(fallback); if(raw == "1" || raw == "true" || raw == "yes" || raw == "on") return(true); if(raw == "0" || raw == "false" || raw == "no" || raw == "off") return(false); return(fallback); } bool config_map_bool(StringMap& cfg, String key, bool fallback) { return(config_bool_value(cfg[key], fallback)); } u64 config_u64(String key, u64 fallback) { if(!context || !context->server) return(fallback); return(config_map_u64(context->server->config, key, fallback)); } f64 config_f64(String key, f64 fallback) { if(!context || !context->server) return(fallback); return(config_map_f64(context->server->config, key, fallback)); } bool config_bool(String key, bool fallback) { if(!context || !context->server) return(fallback); return(config_map_bool(context->server->config, key, fallback)); } StringMap make_server_settings() { StringMap cfg; cfg["BIN_DIRECTORY"] = "/tmp/uce/work"; cfg["WASM_COMPILE_SCRIPT"] = "scripts/compile_wasm_unit"; cfg["WASM_BACKEND_VERBOSE"] = "0"; cfg["WASM_CORE_PATH"] = ""; cfg["WASM_MEMORY_LIMIT_BYTES"] = std::to_string(512ull * 1024 * 1024); cfg["WASM_EPOCH_DEADLINE_TICKS"] = "200"; cfg["WASM_EPOCH_PERIOD_MS"] = "50"; cfg["SETUP_TEMPLATE"] = "scripts/setup.h.template"; cfg["LIT_ESC"] = "3d5b5_1"; cfg["CONTENT_TYPE"] = "text/html; charset=utf-8"; cfg["FCGI_SOCKET_PATH"] = "/run/uce.sock"; cfg["CLI_SOCKET_PATH"] = "/run/uce/cli.sock"; // Command socket the WS broker listens on; workers flush ws_* dispatch // command batches here at workspace teardown. cfg["WS_BROKER_SOCKET_PATH"] = "/run/uce/ws-broker.sock"; cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads"; cfg["SESSION_PATH"] = "/tmp/uce/sessions"; cfg["COMPILER_SYS_PATH"] = "."; cfg["PRECOMPILE_FILES_IN"] = ""; cfg["SITE_DIRECTORY"] = "site"; cfg["JIT_COMPILE_ON_REQUEST"] = "1"; cfg["PROACTIVE_COMPILE_ENABLED"] = "1"; cfg["COMPILE_FAILURE_RETRY_SECONDS"] = std::to_string(10); cfg["PROACTIVE_COMPILE_CHECK_INTERVAL"] = std::to_string(60); cfg["TRANSPORT_MAX_CLIENT_CONNECTIONS"] = std::to_string(256); cfg["TRANSPORT_MAX_HTTP_HEADER_BYTES"] = std::to_string(16 * 1024); cfg["TRANSPORT_MAX_HTTP_BODY_BYTES"] = std::to_string(1024 * 1024); cfg["TRANSPORT_MAX_WEBSOCKET_FRAME_BYTES"] = std::to_string(1024 * 1024); cfg["TRANSPORT_MAX_WEBSOCKET_MESSAGE_BYTES"] = std::to_string(1024 * 1024); cfg["TRANSPORT_MAX_WEBSOCKET_OUTPUT_BYTES"] = std::to_string(4 * 1024 * 1024); cfg["TRANSPORT_MAX_RESPONSE_BYTES"] = std::to_string(8 * 1024 * 1024); cfg["TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS"] = "15"; cfg["TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS"] = "120"; cfg["HTTP_DOCUMENT_ROOT"] = ""; cfg["CUSTOM_SERVER_MAX_SERVERS"] = "16"; cfg["CUSTOM_SERVER_MIN_PORT"] = "1024"; cfg["CUSTOM_SERVER_MAX_PORT"] = "65535"; cfg["CUSTOM_SERVER_ALLOW_PUBLIC_BIND"] = "0"; cfg["CUSTOM_SERVER_UNIX_SOCKET_PREFIX"] = "/tmp/uce/custom-servers/"; cfg["CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS"] = "30"; cfg["CUSTOM_SERVER_UCE_ROOT"] = ""; cfg["ARCHIVE_MAX_INPUT_BYTES"] = std::to_string(64 * 1024 * 1024); cfg["ARCHIVE_MAX_OUTPUT_BYTES"] = std::to_string(64 * 1024 * 1024); cfg["ARCHIVE_MAX_ZIP_ENTRIES"] = "4096"; cfg["HTTP_PORT"] = std::to_string(8080); cfg["SESSION_TIME"] = std::to_string(60*60*24*30); cfg["WORKER_COUNT"] = std::to_string(4); cfg["MAX_MEMORY"] = std::to_string(1024*1024*16); for(auto& it : split_kv(file_get_contents("/etc/uce/settings.cfg"))) { cfg[it.first] = it.second; } if(cfg["FCGI_SOCKET_PATH"] == "" && cfg["SOCKET_PATH"] != "") cfg["FCGI_SOCKET_PATH"] = cfg["SOCKET_PATH"]; if(cfg["SOCKET_PATH"] == "" && cfg["FCGI_SOCKET_PATH"] != "") cfg["SOCKET_PATH"] = cfg["FCGI_SOCKET_PATH"]; if(cfg["FCGI_PORT"] == "" && cfg["LISTEN_PORT"] != "") cfg["FCGI_PORT"] = cfg["LISTEN_PORT"]; if(cfg["LISTEN_PORT"] == "" && cfg["FCGI_PORT"] != "") cfg["LISTEN_PORT"] = cfg["FCGI_PORT"]; return(cfg); } #endif