uce/src/linux_fastcgi.cpp

629 lines
18 KiB
C++

#include "lib/uce_lib.cpp"
#include <csetjmp>
ServerState server_state;
#include "fastcgi/src/fcgicc.cc"
FastCGIServer server;
pid_t http_worker_pid = 0;
pid_t proactive_compiler_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 = "";
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);
print("UCE runtime error\n");
print("Request: ", first(request.params["REQUEST_URI"], request.params["SCRIPT_FILENAME"]), "\n");
print("Script: ", request.params["SCRIPT_FILENAME"], "\n");
print("Error: ", title, "\n");
if(details != "")
print("Details: ", details, "\n");
if(request_fault_signal != 0)
{
String sig_label = signal_name((int)request_fault_signal);
print("Signal: ", (s64)request_fault_signal);
if(sig_label != "")
print(" (", sig_label, ")");
print("\n");
}
if(trace != "")
print("\nTrace:\n", trace);
request.err += "UCE runtime error\n";
request.err += "Request: " + first(request.params["REQUEST_URI"], request.params["SCRIPT_FILENAME"]) + "\n";
request.err += "Script: " + request.params["SCRIPT_FILENAME"] + "\n";
request.err += "Error: " + title + "\n";
if(details != "")
request.err += "Details: " + details + "\n";
if(request_fault_signal != 0)
{
String sig_label = signal_name((int)request_fault_signal);
request.err += "Signal: " + std::to_string((int)request_fault_signal);
if(sig_label != "")
request.err += " (" + sig_label + ")";
request.err += "\n";
}
if(trace != "")
request.err += "\nTrace:\n" + trace;
request.flags.status = status_code;
restore_active_request(previous_context);
}
void on_request_fault_signal(int sig)
{
request_fault_signal = sig;
request_fault_trace = capture_backtrace_string(32, 1);
if(request_fault_active && request_fault_request)
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);
}
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);
}
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_message()
{
if(!context)
return("");
return(context->call["message"].to_string());
}
String ws_connection_id()
{
if(!context)
return("");
return(context->resources.websocket_connection_id);
}
String ws_scope()
{
return(current_ws_scope());
}
u8 ws_opcode()
{
if(!context)
return(0);
return(context->resources.websocket_opcode);
}
bool ws_is_binary()
{
if(!context)
return(false);
return(context->resources.websocket_is_binary);
}
StringList ws_connections(String scope)
{
return(server.websocket_connection_ids(normalize_ws_scope(scope)));
}
u64 ws_connection_count(String scope)
{
return(ws_connections(scope).size());
}
bool ws_send(String message, bool binary, String scope)
{
return(server.websocket_broadcast(normalize_ws_scope(scope), message, binary) > 0);
}
bool ws_send_to(String connection_id, String message, bool binary)
{
return(server.websocket_send_to(connection_id, message, binary));
}
bool ws_close(String connection_id)
{
if(connection_id == "")
connection_id = ws_connection_id();
if(connection_id == "")
return(false);
return(server.websocket_close(connection_id));
}
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) {
// This event occurs when data is received on the standard input stream.
// A simple String is used to hold the input stream, so it is the
// responsibility of the application to remember which data it has
// processed. The application may modify it; new data will be appended
// to it by the server. The same goes for the output and error streams:
// the application should append data to them; the server will remove
// all sent data from them.
return 0; // still OK
std::transform(request.in.begin(), request.in.end(),
std::back_inserter(request.err),
std::bind1st(std::plus<char>(), 1));
request.in.clear(); // don't process it again
return 0; // still OK
}
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"] = context->server->config["CONTENT_TYPE"];
request.get = parse_query(request.params["QUERY_STRING"]);
request.random_index = 0;
request.random_seed = gen_noise64(*reinterpret_cast<u64*>(&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
{
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);
}
}
request.call = DTree();
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();
restore_active_request(previous_context);
return request.flags.status;
}
int handle_websocket_message(FastCGIRequest& request, const String& message, u8 opcode)
{
Request event_request;
ByteStream ws_output;
Request* previous_context = set_active_request(event_request);
server_state.request_count += 1;
event_request.server = &server_state;
event_request.params = request.params;
event_request.params["REQUEST_METHOD"] = "WEBSOCKET";
event_request.get = parse_query(event_request.params["QUERY_STRING"]);
event_request.resources = request.resources;
if(event_request.resources.websocket_connection_state)
event_request.connection.set_reference(event_request.resources.websocket_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 = gen_noise64(*reinterpret_cast<u64*>(&event_request.stats.time_start));
event_request.response_code = "WEBSOCKET";
event_request.header["Content-Type"] = context->server->config["CONTENT_TYPE"];
event_request.in = message;
event_request.ob = &ws_output;
if(event_request.params["HTTP_COOKIE"].length() > 0)
event_request.cookies = parse_cookies(event_request.params["HTTP_COOKIE"]);
event_request.var["ws"]["message"] = message;
event_request.var["ws"]["connection_id"] = request.resources.websocket_connection_id;
event_request.var["ws"]["scope"] = request.resources.websocket_scope;
event_request.var["ws"]["connection_count"] = (f64)server.websocket_connection_ids(request.resources.websocket_scope).size();
event_request.var["ws"]["opcode"] = (f64)opcode;
event_request.var["ws"]["is_binary"].set_bool(request.resources.websocket_is_binary);
event_request.var["ws"]["is_text"].set_bool(request.resources.websocket_is_text);
event_request.var["ws"]["document_uri"] = first(
request.params["DOCUMENT_URI"],
request.params["REQUEST_URI"]
);
event_request.call["message"] = message;
event_request.call["connection_id"] = request.resources.websocket_connection_id;
event_request.call["scope"] = request.resources.websocket_scope;
event_request.call["opcode"] = (f64)opcode;
event_request.call["document_uri"] = event_request.var["ws"]["document_uri"].to_string();
compiler_invoke_websocket(&event_request, request.params["SCRIPT_FILENAME"]);
if(event_request.session_id.length() > 0)
save_session_data(event_request.session_id, event_request.session);
cleanup_mysql_connections();
restore_active_request(previous_context);
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();
}
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;
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, 60.0);
my_pid = getpid();
context = &background_context;
background_context.server = &server_state;
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);
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);
next_scan_at = time_precise();
for(;;)
{
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);
}
clear_shared_unit_cache(server_state);
usleep(250000);
continue;
}
usleep(250000);
}
}
bool proactive_compiler_alive()
{
return(proactive_compiler_pid > 0 && task_kill(proactive_compiler_pid, 0) == 0);
}
void ensure_proactive_compiler()
{
if(float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) <= 0)
return;
if(proactive_compiler_alive())
return;
pid_t p = fork();
if(p == 0)
{
prctl(PR_SET_PDEATHSIG, SIGHUP);
run_proactive_compiler();
exit(0);
}
proactive_compiler_pid = p;
printf("(P) proactive compiler spawned: PID %i\n", p);
}
void listen_for_connections()
{
signal(SIGSEGV, on_segfault);
signal(SIGABRT, on_segfault);
signal(SIGBUS, on_segfault);
signal(SIGILL, on_segfault);
signal(SIGFPE, on_segfault);
signal(SIGPIPE, SIG_IGN);
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_websocket_message = &handle_websocket_message;
for(;;)
{
server.process();
}
}
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["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;
}