1530 lines
43 KiB
C++
1530 lines
43 KiB
C++
#include "lib/uce_lib.cpp"
|
|
#include <csetjmp>
|
|
#include <deque>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <sys/socket.h>
|
|
|
|
ServerState server_state;
|
|
|
|
#include "fastcgi/src/fcgicc.cc"
|
|
|
|
FastCGIServer server;
|
|
pid_t http_worker_pid = 0;
|
|
pid_t proactive_compiler_pid = 0;
|
|
pid_t websocket_exec_pid = 0;
|
|
bool worker_accepts_http = false;
|
|
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;
|
|
static String request_fault_trace = "";
|
|
static int websocket_exec_fd = -1;
|
|
static String websocket_exec_read_buffer = "";
|
|
static std::deque<DTree> websocket_exec_pending_jobs;
|
|
static DTree websocket_exec_inflight_job;
|
|
static String websocket_exec_write_buffer = "";
|
|
|
|
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();
|
|
}
|
|
|
|
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);
|
|
|
|
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_trace = capture_backtrace_string(32, 1);
|
|
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);
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool websocket_ipc_set_nonblocking(int fd)
|
|
{
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
if(flags == -1)
|
|
return(false);
|
|
return(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0);
|
|
}
|
|
|
|
bool websocket_exec_enabled_for_process()
|
|
{
|
|
return(worker_accepts_http);
|
|
}
|
|
|
|
u64 websocket_exec_queue_limit_bytes()
|
|
{
|
|
u64 configured = int_val(server_state.config["WEBSOCKET_EXEC_QUEUE_BYTES"]);
|
|
if(configured < 64 * 1024)
|
|
configured = 1024 * 1024;
|
|
return(configured);
|
|
}
|
|
|
|
bool websocket_exec_has_inflight_job()
|
|
{
|
|
return(websocket_exec_inflight_job.to_bool());
|
|
}
|
|
|
|
FastCGIServer::Connection* websocket_find_connection(String connection_id)
|
|
{
|
|
for(auto& item : server.client_sockets)
|
|
{
|
|
FastCGIServer::Connection* connection = item.second;
|
|
if(connection->is_websocket && connection->websocket_connection_id == connection_id)
|
|
return(connection);
|
|
}
|
|
return(0);
|
|
}
|
|
|
|
void websocket_exec_clear_ipc_state()
|
|
{
|
|
if(websocket_exec_fd != -1)
|
|
close(websocket_exec_fd);
|
|
websocket_exec_fd = -1;
|
|
websocket_exec_read_buffer = "";
|
|
websocket_exec_write_buffer = "";
|
|
websocket_exec_inflight_job.clear();
|
|
}
|
|
|
|
void websocket_exec_close_connection(String connection_id, u16 status_code = 1011, String reason = "websocket handler unavailable")
|
|
{
|
|
if(connection_id == "")
|
|
return;
|
|
server.websocket_close(connection_id, status_code, reason);
|
|
}
|
|
|
|
void websocket_exec_fail_inflight_job(String reason = "websocket handler unavailable")
|
|
{
|
|
if(!websocket_exec_has_inflight_job())
|
|
return;
|
|
websocket_exec_close_connection(websocket_exec_inflight_job["connection_id"].to_string(), 1011, reason);
|
|
websocket_exec_inflight_job.clear();
|
|
websocket_exec_write_buffer = "";
|
|
}
|
|
|
|
void websocket_exec_queue_job(DTree job)
|
|
{
|
|
if(job["connection_id"].to_string() == "")
|
|
return;
|
|
|
|
u64 queued_bytes = websocket_exec_write_buffer.length();
|
|
if(websocket_exec_has_inflight_job())
|
|
queued_bytes += websocket_exec_inflight_job["serialized"].to_string().length();
|
|
for(auto& pending : websocket_exec_pending_jobs)
|
|
queued_bytes += pending["serialized"].to_string().length();
|
|
queued_bytes += json_encode(job).length();
|
|
|
|
if(queued_bytes > websocket_exec_queue_limit_bytes())
|
|
{
|
|
printf("(!) websocket dispatch queue overflow for %s\n", job["connection_id"].to_string().c_str());
|
|
websocket_exec_close_connection(job["connection_id"].to_string(), 1013, "websocket server busy");
|
|
return;
|
|
}
|
|
|
|
job["serialized"] = json_encode(job) + "\n";
|
|
websocket_exec_pending_jobs.push_back(job);
|
|
}
|
|
|
|
StringList websocket_exec_snapshot_connections(String scope)
|
|
{
|
|
return(server.websocket_connection_ids(scope));
|
|
}
|
|
|
|
void websocket_exec_append_command(DTree command)
|
|
{
|
|
if(!context)
|
|
return;
|
|
context->resources.websocket_dispatch_commands.push(command);
|
|
}
|
|
|
|
DTree websocket_exec_make_message_command(String action, String message, bool binary)
|
|
{
|
|
DTree command;
|
|
command["action"] = action;
|
|
command["binary"].set_bool(binary);
|
|
command["message_b64"] = base64_encode(message);
|
|
return(command);
|
|
}
|
|
|
|
DTree websocket_exec_make_close_command(String connection_id, u16 status_code = 1000, String reason = "")
|
|
{
|
|
DTree command;
|
|
command["action"] = "close";
|
|
command["connection_id"] = connection_id;
|
|
command["status_code"] = (f64)status_code;
|
|
command["reason"] = reason;
|
|
return(command);
|
|
}
|
|
|
|
void websocket_exec_apply_command(DTree command)
|
|
{
|
|
String action = command["action"].to_string();
|
|
if(action == "broadcast")
|
|
{
|
|
bool ok = false;
|
|
String payload = base64_decode(command["message_b64"].to_string(), ok);
|
|
if(!ok)
|
|
return;
|
|
server.websocket_broadcast(command["scope"].to_string(), payload, command["binary"].to_bool());
|
|
return;
|
|
}
|
|
if(action == "send_to")
|
|
{
|
|
bool ok = false;
|
|
String payload = base64_decode(command["message_b64"].to_string(), ok);
|
|
if(!ok)
|
|
return;
|
|
server.websocket_send_to(command["connection_id"].to_string(), payload, command["binary"].to_bool());
|
|
return;
|
|
}
|
|
if(action == "close")
|
|
{
|
|
server.websocket_close(
|
|
command["connection_id"].to_string(),
|
|
(u16)command["status_code"].to_u64(),
|
|
command["reason"].to_string()
|
|
);
|
|
}
|
|
}
|
|
|
|
void websocket_exec_apply_result(DTree result)
|
|
{
|
|
if(result["type"].to_string() != "result")
|
|
return;
|
|
|
|
String inflight_connection_id = websocket_exec_inflight_job["connection_id"].to_string();
|
|
String result_connection_id = result["connection_id"].to_string();
|
|
if(inflight_connection_id != "" && result_connection_id != "" && inflight_connection_id != result_connection_id)
|
|
{
|
|
printf("(!) websocket dispatch result mismatch: expected %s got %s\n",
|
|
inflight_connection_id.c_str(),
|
|
result_connection_id.c_str());
|
|
}
|
|
|
|
FastCGIServer::Connection* connection = websocket_find_connection(result_connection_id);
|
|
if(connection)
|
|
connection->websocket_state = result["connection_state"];
|
|
|
|
result["commands"].each([] (DTree command, String) {
|
|
websocket_exec_apply_command(command);
|
|
});
|
|
|
|
websocket_exec_inflight_job.clear();
|
|
}
|
|
|
|
void websocket_exec_handle_ipc_line(String line)
|
|
{
|
|
line = trim(line);
|
|
if(line == "")
|
|
return;
|
|
DTree result = json_decode(line);
|
|
websocket_exec_apply_result(result);
|
|
}
|
|
|
|
void websocket_exec_read_results()
|
|
{
|
|
if(websocket_exec_fd == -1)
|
|
return;
|
|
|
|
char buffer[4096];
|
|
for(;;)
|
|
{
|
|
ssize_t read_result = read(websocket_exec_fd, buffer, sizeof(buffer));
|
|
if(read_result == 0)
|
|
{
|
|
printf("(!) websocket executor disconnected\n");
|
|
websocket_exec_fail_inflight_job();
|
|
websocket_exec_clear_ipc_state();
|
|
return;
|
|
}
|
|
if(read_result < 0)
|
|
{
|
|
if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
|
|
break;
|
|
perror("websocket executor read");
|
|
websocket_exec_fail_inflight_job();
|
|
websocket_exec_clear_ipc_state();
|
|
return;
|
|
}
|
|
|
|
websocket_exec_read_buffer.append(buffer, read_result);
|
|
for(;;)
|
|
{
|
|
size_t line_end = websocket_exec_read_buffer.find('\n');
|
|
if(line_end == String::npos)
|
|
break;
|
|
String line = websocket_exec_read_buffer.substr(0, line_end);
|
|
websocket_exec_read_buffer.erase(0, line_end + 1);
|
|
websocket_exec_handle_ipc_line(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
void websocket_exec_flush_queue()
|
|
{
|
|
if(websocket_exec_fd == -1)
|
|
return;
|
|
|
|
if(websocket_exec_write_buffer == "" && !websocket_exec_has_inflight_job() && websocket_exec_pending_jobs.size() > 0)
|
|
{
|
|
websocket_exec_inflight_job = websocket_exec_pending_jobs.front();
|
|
websocket_exec_pending_jobs.pop_front();
|
|
websocket_exec_write_buffer = websocket_exec_inflight_job["serialized"].to_string();
|
|
}
|
|
|
|
while(websocket_exec_write_buffer != "")
|
|
{
|
|
ssize_t write_result = write(websocket_exec_fd, websocket_exec_write_buffer.data(), websocket_exec_write_buffer.length());
|
|
if(write_result < 0)
|
|
{
|
|
if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
|
|
return;
|
|
perror("websocket executor write");
|
|
websocket_exec_fail_inflight_job();
|
|
websocket_exec_clear_ipc_state();
|
|
return;
|
|
}
|
|
if(write_result == 0)
|
|
return;
|
|
websocket_exec_write_buffer.erase(0, write_result);
|
|
}
|
|
}
|
|
|
|
Request websocket_exec_build_event_request(DTree job, String message)
|
|
{
|
|
Request event_request;
|
|
event_request.server = &server_state;
|
|
event_request.params = job["params"].to_stringmap();
|
|
event_request.params["REQUEST_METHOD"] = "WEBSOCKET";
|
|
prepare_request_body_maps(event_request);
|
|
event_request.resources.is_websocket = true;
|
|
event_request.resources.websocket_connection_id = job["connection_id"].to_string();
|
|
event_request.resources.websocket_scope = job["scope"].to_string();
|
|
event_request.resources.websocket_opcode = (u8)job["opcode"].to_u64();
|
|
event_request.resources.websocket_is_binary = job["is_binary"].to_bool();
|
|
event_request.resources.websocket_is_text = job["is_text"].to_bool();
|
|
event_request.resources.websocket_dispatch_capture = true;
|
|
job["scope_connections"].each([&] (DTree item, String) {
|
|
event_request.resources.websocket_scope_connection_ids.push_back(item.to_string());
|
|
});
|
|
event_request.connection = job["connection_state"];
|
|
event_request.stats.time_init = time_precise();
|
|
event_request.stats.time_start = event_request.stats.time_init;
|
|
event_request.random_index = 0;
|
|
event_request.random_seed = request_seed_from_time(event_request.stats.time_start);
|
|
event_request.response_code = "WEBSOCKET";
|
|
event_request.header["Content-Type"] = server_state.config["CONTENT_TYPE"];
|
|
event_request.in = message;
|
|
|
|
event_request.params["WS_MESSAGE"] = message;
|
|
event_request.params["WS_CONNECTION_ID"] = event_request.resources.websocket_connection_id;
|
|
event_request.params["WS_SCOPE"] = event_request.resources.websocket_scope;
|
|
event_request.params["WS_CONNECTION_COUNT"] = (f64)event_request.resources.websocket_scope_connection_ids.size();
|
|
event_request.params["WS_OPCODE"] = (f64)event_request.resources.websocket_opcode;
|
|
event_request.params["WS_MESSAGE_TYPE"] = (event_request.resources.websocket_is_binary ? "BINARY" : "TEXT");
|
|
event_request.params["WS_DOCUMENT_URI"] = first(
|
|
event_request.params["DOCUMENT_URI"],
|
|
event_request.params["REQUEST_URI"]
|
|
);
|
|
|
|
return(event_request);
|
|
}
|
|
|
|
bool websocket_exec_send_response(int fd, DTree response)
|
|
{
|
|
String encoded = json_encode(response) + "\n";
|
|
size_t offset = 0;
|
|
while(offset < encoded.length())
|
|
{
|
|
ssize_t write_result = write(fd, encoded.data() + offset, encoded.length() - offset);
|
|
if(write_result < 0)
|
|
{
|
|
if(errno == EINTR)
|
|
continue;
|
|
return(false);
|
|
}
|
|
offset += (size_t)write_result;
|
|
}
|
|
return(true);
|
|
}
|
|
|
|
void websocket_exec_process_job_line(int fd, String line)
|
|
{
|
|
line = trim(line);
|
|
if(line == "")
|
|
return;
|
|
|
|
DTree job = json_decode(line);
|
|
if(job["type"].to_string() != "dispatch")
|
|
return;
|
|
|
|
bool decoded = false;
|
|
String message = base64_decode(job["message_b64"].to_string(), decoded);
|
|
if(!decoded)
|
|
{
|
|
printf("(!) invalid websocket IPC payload for %s\n", job["connection_id"].to_string().c_str());
|
|
return;
|
|
}
|
|
|
|
Request event_request = websocket_exec_build_event_request(job, message);
|
|
Request* previous_context = set_active_request(event_request);
|
|
server_state.request_count += 1;
|
|
|
|
compiler_invoke_websocket(&event_request, event_request.params["SCRIPT_FILENAME"]);
|
|
|
|
if(event_request.session_id.length() > 0)
|
|
save_session_data(event_request.session_id, event_request.session);
|
|
cleanup_mysql_connections();
|
|
cleanup_sqlite_connections();
|
|
|
|
DTree response;
|
|
response["type"] = "result";
|
|
response["connection_id"] = event_request.resources.websocket_connection_id;
|
|
response["connection_state"] = event_request.connection;
|
|
response["commands"] = event_request.resources.websocket_dispatch_commands;
|
|
|
|
restore_active_request(previous_context);
|
|
if(!websocket_exec_send_response(fd, response))
|
|
exit(1);
|
|
}
|
|
|
|
void websocket_exec_child_loop(int fd)
|
|
{
|
|
Request background_context;
|
|
my_pid = getpid();
|
|
context = &background_context;
|
|
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, 5);
|
|
|
|
String read_buffer;
|
|
char buffer[4096];
|
|
for(;;)
|
|
{
|
|
ssize_t read_result = read(fd, buffer, sizeof(buffer));
|
|
if(read_result == 0)
|
|
exit(0);
|
|
if(read_result < 0)
|
|
{
|
|
if(errno == EINTR)
|
|
continue;
|
|
exit(1);
|
|
}
|
|
|
|
read_buffer.append(buffer, read_result);
|
|
for(;;)
|
|
{
|
|
size_t line_end = read_buffer.find('\n');
|
|
if(line_end == String::npos)
|
|
break;
|
|
String line = read_buffer.substr(0, line_end);
|
|
read_buffer.erase(0, line_end + 1);
|
|
websocket_exec_process_job_line(fd, line);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool websocket_exec_alive()
|
|
{
|
|
return(websocket_exec_pid > 0 && task_kill(websocket_exec_pid, 0) == 0 && websocket_exec_fd != -1);
|
|
}
|
|
|
|
void ensure_websocket_executor()
|
|
{
|
|
if(!websocket_exec_enabled_for_process())
|
|
return;
|
|
if(websocket_exec_alive())
|
|
return;
|
|
|
|
if(websocket_exec_pid > 0 || websocket_exec_fd != -1)
|
|
{
|
|
websocket_exec_fail_inflight_job();
|
|
websocket_exec_clear_ipc_state();
|
|
websocket_exec_pid = 0;
|
|
}
|
|
|
|
int sockets[2] = {-1, -1};
|
|
if(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0)
|
|
{
|
|
perror("socketpair");
|
|
return;
|
|
}
|
|
|
|
pid_t p = fork();
|
|
if(p < 0)
|
|
{
|
|
perror("fork");
|
|
close(sockets[0]);
|
|
close(sockets[1]);
|
|
return;
|
|
}
|
|
if(p == 0)
|
|
{
|
|
parent_pid = getppid();
|
|
file_release_process_locks("websocket executor fork");
|
|
prctl(PR_SET_PDEATHSIG, SIGHUP);
|
|
close(sockets[0]);
|
|
websocket_exec_child_loop(sockets[1]);
|
|
exit(0);
|
|
}
|
|
|
|
close(sockets[1]);
|
|
if(!websocket_ipc_set_nonblocking(sockets[0]))
|
|
{
|
|
printf("(!) failed to set websocket executor socket nonblocking\n");
|
|
close(sockets[0]);
|
|
return;
|
|
}
|
|
|
|
websocket_exec_fd = sockets[0];
|
|
websocket_exec_pid = p;
|
|
printf("(P) websocket executor spawned: PID %i\n", p);
|
|
}
|
|
|
|
void websocket_exec_tick()
|
|
{
|
|
if(!websocket_exec_enabled_for_process())
|
|
return;
|
|
if(!websocket_exec_alive())
|
|
{
|
|
websocket_exec_fail_inflight_job();
|
|
websocket_exec_clear_ipc_state();
|
|
websocket_exec_pid = 0;
|
|
ensure_websocket_executor();
|
|
}
|
|
websocket_exec_read_results();
|
|
websocket_exec_flush_queue();
|
|
}
|
|
|
|
}
|
|
|
|
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)
|
|
{
|
|
if(context && context->resources.websocket_dispatch_capture)
|
|
{
|
|
String normalized_scope = normalize_ws_scope(scope);
|
|
if(normalized_scope == context->resources.websocket_scope)
|
|
return(context->resources.websocket_scope_connection_ids);
|
|
return(StringList());
|
|
}
|
|
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)
|
|
{
|
|
String normalized_scope = normalize_ws_scope(scope);
|
|
if(context && context->resources.websocket_dispatch_capture)
|
|
{
|
|
DTree command = websocket_exec_make_message_command("broadcast", message, binary);
|
|
command["scope"] = normalized_scope;
|
|
websocket_exec_append_command(command);
|
|
return(true);
|
|
}
|
|
return(server.websocket_broadcast(normalized_scope, message, binary) > 0);
|
|
}
|
|
|
|
bool ws_send_to(String connection_id, String message, bool binary)
|
|
{
|
|
if(context && context->resources.websocket_dispatch_capture)
|
|
{
|
|
DTree command = websocket_exec_make_message_command("send_to", message, binary);
|
|
command["connection_id"] = connection_id;
|
|
websocket_exec_append_command(command);
|
|
return(true);
|
|
}
|
|
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);
|
|
if(context && context->resources.websocket_dispatch_capture)
|
|
{
|
|
websocket_exec_append_command(websocket_exec_make_close_command(connection_id));
|
|
return(true);
|
|
}
|
|
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)
|
|
{
|
|
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 = DTree();
|
|
compiler_invoke_cli(&request, script_filename);
|
|
}
|
|
}
|
|
}
|
|
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(), capture_backtrace_string(32, 1), 500);
|
|
}
|
|
catch(...)
|
|
{
|
|
render_request_failure(request, "unknown uncaught exception during CLI request", "", capture_backtrace_string(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.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) {
|
|
// The event handler can also be a class member function. This
|
|
// event occurs when the parameters and standard input streams are
|
|
// both closed, and thus the request is complete.
|
|
// printf("(i) request handle\n");
|
|
|
|
Request* previous_context = set_active_request(request);
|
|
server_state.request_count += 1;
|
|
request.server = &server_state;
|
|
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_trace = "";
|
|
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 = request_fault_trace;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
prepare_request_body_maps(request);
|
|
request.props = DTree();
|
|
if(request.resources.is_cli)
|
|
compiler_invoke_cli(&request, request.params["SCRIPT_FILENAME"]);
|
|
else if(request.params["UCE_SERVE_HTTP"] == "1")
|
|
compiler_invoke_serve_http(&request, request.params["SCRIPT_FILENAME"], request.params["UCE_SERVE_HTTP_FUNCTION"]);
|
|
else
|
|
compiler_invoke(&request, request.params["SCRIPT_FILENAME"]);
|
|
}
|
|
catch(const std::exception& e)
|
|
{
|
|
failure_title = "uncaught exception during request";
|
|
failure_details = e.what();
|
|
failure_trace = capture_backtrace_string(32, 1);
|
|
}
|
|
catch(...)
|
|
{
|
|
failure_title = "unknown uncaught exception during request";
|
|
failure_trace = capture_backtrace_string(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;
|
|
}
|
|
|
|
int handle_websocket_message(FastCGIRequest& request, const String& message, u8 opcode)
|
|
{
|
|
ensure_websocket_executor();
|
|
if(!websocket_exec_alive())
|
|
{
|
|
printf("(!) websocket executor unavailable for %s\n", request.resources.websocket_connection_id.c_str());
|
|
server.websocket_close(request.resources.websocket_connection_id, 1011, "websocket handler unavailable");
|
|
return(0);
|
|
}
|
|
|
|
DTree job;
|
|
job["type"] = "dispatch";
|
|
job["connection_id"] = request.resources.websocket_connection_id;
|
|
job["scope"] = request.resources.websocket_scope;
|
|
job["opcode"] = (f64)opcode;
|
|
job["is_binary"].set_bool(request.resources.websocket_is_binary);
|
|
job["is_text"].set_bool(request.resources.websocket_is_text);
|
|
job["message_b64"] = base64_encode(message);
|
|
if(request.resources.websocket_connection_state)
|
|
job["connection_state"] = *request.resources.websocket_connection_state;
|
|
|
|
for(auto& item : request.params)
|
|
job["params"][item.first] = item.second;
|
|
|
|
for(auto& connection_id : websocket_exec_snapshot_connections(request.resources.websocket_scope))
|
|
{
|
|
DTree snapshot_item;
|
|
snapshot_item = connection_id;
|
|
job["scope_connections"].push(snapshot_item);
|
|
}
|
|
|
|
websocket_exec_queue_job(job);
|
|
return 0;
|
|
}
|
|
|
|
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);
|
|
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)
|
|
{
|
|
DTree 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 = config_map_u64(server_state.config, "CUSTOM_SERVER_MIN_PORT", 1024);
|
|
u64 max_port = config_map_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 = config_map_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 = "";
|
|
|
|
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 = config_map_u64(server_state.config, "CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS", 30);
|
|
if(timeout > 0)
|
|
alarm(timeout);
|
|
int status = handle_complete(request);
|
|
if(timeout > 0)
|
|
alarm(0);
|
|
return(status);
|
|
}
|
|
|
|
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 = config_map_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(!config_map_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<String, f64> 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, false);
|
|
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(!config_map_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)
|
|
{
|
|
file_release_process_locks("proactive compiler fork");
|
|
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();
|
|
if(worker_accepts_http)
|
|
{
|
|
// Keep the dedicated HTTP/WebSocket worker alive. If it ages out like a
|
|
// normal FastCGI worker, nginx can connect to the shared listening socket
|
|
// while no child is actively accepting, which makes `.ws.uce` page loads
|
|
// appear to hang until the parent respawns a replacement worker.
|
|
server.calls_until_termination = -1;
|
|
}
|
|
if(!worker_accepts_http)
|
|
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;
|
|
server.on_websocket_message = &handle_websocket_message;
|
|
if(worker_accepts_http)
|
|
ensure_websocket_executor();
|
|
for(;;)
|
|
{
|
|
file_release_process_locks("worker loop cleanup");
|
|
server.process(worker_accepts_http ? 50 : -1);
|
|
if(worker_accepts_http)
|
|
websocket_exec_tick();
|
|
}
|
|
}
|
|
|
|
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());
|
|
|
|
server_state.config["COMPILE_SCRIPT"] =
|
|
server_state.config["COMPILER_SYS_PATH"] + "/" + server_state.config["COMPILE_SCRIPT"];
|
|
|
|
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);
|
|
}
|
|
|
|
if(server_state.config["HTTP_PORT"] != "")
|
|
server.listen_http(int_val(server_state.config["HTTP_PORT"]));
|
|
|
|
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)
|
|
{
|
|
init_base_process();
|
|
ensure_proactive_compiler();
|
|
|
|
for(;;)
|
|
{
|
|
if(!proactive_compiler_alive())
|
|
proactive_compiler_pid = 0;
|
|
if(!termination_signal_received)
|
|
ensure_proactive_compiler();
|
|
|
|
while(workers.size() < int_val(server_state.config["WORKER_COUNT"]))
|
|
{
|
|
if(!termination_signal_received)
|
|
{
|
|
worker_accepts_http = (http_worker_pid == 0 || workers.count(http_worker_pid) == 0);
|
|
pid_t child_pid = spawn_subprocess(listen_for_connections);
|
|
if(child_pid > 0 && worker_accepts_http)
|
|
http_worker_pid = child_pid;
|
|
}
|
|
}
|
|
sleep(1);
|
|
}
|
|
|
|
return 0;
|
|
}
|