diff options
author | turret <turret@duck.com> | 2024-03-30 16:04:45 -0500 |
---|---|---|
committer | turret <turret@duck.com> | 2024-03-30 16:04:45 -0500 |
commit | 80a67b7d20393a29aa5d2cb92197f3381be7fd96 (patch) | |
tree | f0c313da5ef509e79e5067972edd91976513ce0e /util | |
parent | f01745a2ee84f11b8cc54e37c5f7f596184ab785 (diff) | |
download | discord-bot-skeleton-80a67b7d20393a29aa5d2cb92197f3381be7fd96.tar.gz discord-bot-skeleton-80a67b7d20393a29aa5d2cb92197f3381be7fd96.tar.bz2 discord-bot-skeleton-80a67b7d20393a29aa5d2cb92197f3381be7fd96.zip |
*: directory changes
since this project is a skeleton and not meant to clutter up the code
that will actually consume the bot, i've opted to consolidate the
majority of files under a single directory and minimise extra files
*: move code to util/
*: move include files to include/dbs/
net: consolidate net functions into single file
config: remove config
Diffstat (limited to 'util')
-rw-r--r-- | util/init.c | 200 | ||||
-rw-r--r-- | util/log.c | 210 | ||||
-rw-r--r-- | util/net.c | 383 | ||||
-rw-r--r-- | util/subsys.c | 179 |
4 files changed, 972 insertions, 0 deletions
diff --git a/util/init.c b/util/init.c new file mode 100644 index 0000000..77cd4eb --- /dev/null +++ b/util/init.c @@ -0,0 +1,200 @@ +#include <fcntl.h> +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/param.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include <curl/curl.h> + +#include <dbs/init.h> +#include <dbs/log.h> +#include <dbs/util.h> + +extern int subsystem_handle_term(int pid); +extern int subsystem_count; +int mainpid = 0; +long stack_size = 8192 * 512; + +/* For some reason, I get SIGSEGV'd when running because a random-ass + byte was inserted where it isnt supposed to be. Added a safety byte + because I cannot be asked to try to figure out how to do this cleanly. */ +static unsigned long __1bsafebuf + __attribute__((used)) __attribute__((section(".1bsafebuf.init"))) = 0; + +/* We start initcall levels at [1] instead of [0], so we must adjust + in code for this minor design choice. Math is done on the level passed + through i.e. do_initcall_level so that you can call it with (1) and have + the expected initcall (l1_initcall) run. */ +extern initcall_entry_t __initcall1_start[]; +extern initcall_entry_t __initcall2_start[]; +extern initcall_entry_t __initcall3_start[]; +extern initcall_entry_t __initcall4_start[]; +extern initcall_entry_t __initcall5_start[]; +extern initcall_entry_t __initcall_end[]; + +static initcall_entry_t *initcall_levels[] = { + __initcall1_start, + __initcall2_start, + __initcall3_start, + __initcall4_start, + __initcall5_start, + __initcall_end, +}; + +static void do_initcall_level(int level) +{ + initcall_entry_t *fn; + + for (fn = initcall_levels[level - 1]; + fn < initcall_levels[level]; + fn++) + initcall_from_entry(fn)(); +} + +static void do_initcalls(void) +{ + unsigned long level; + for (level = 1; level < ARRAY_SIZE(initcall_levels); level++) { + do_initcall_level(level); + } +} + +static void doenv(char *path) +{ + int fd = open(path, O_RDONLY); + if(fd < 0) + return; + + struct stat statbuf; + if(fstat(fd, &statbuf) < 0) + return; + + char *file_mmap = mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if(file_mmap == NULL) + return; + + char *file = malloc(statbuf.st_size + 1); + file[statbuf.st_size + 1] = 0; + memcpy(file, file_mmap, statbuf.st_size); + munmap(file_mmap, statbuf.st_size); + + int offset = 0; + while(1) { + char *line = &(file[offset]); + if(*line == '\0') + break; + + char *eol = strchrnul(line, '\n'); + *eol = '\0'; + if(*line == '#') + goto nextline; + + char *divider = strchr(line, '='); + if(divider == NULL) + goto nextline; + + *divider = '\0'; + setenv(line, divider + 1, 0); + +nextline: + offset += (eol - line) + 1; + continue; + } + + free(file); +} + +int main(void) +{ + /* Hello, World! */ + + /* set mainpid for the subsystem service so it is fully accessible + during l1 */ + mainpid = getpid(); + + /* set stack_size for subsystem service */ + struct rlimit *stack_rlimit = malloc(sizeof(struct rlimit)); + getrlimit(RLIMIT_STACK, stack_rlimit); + if(stack_rlimit->rlim_cur != RLIM_INFINITY) { + stack_size = MIN(stack_rlimit->rlim_cur, stack_size); + } + free(stack_rlimit); + + /* configure signal handlers early to prevent race condition where subsystems + can terminate main process on accident, and disable Terminated output during + early-mode panic */ + static sigset_t set; + sigaddset(&set, SIGCHLD); + sigaddset(&set, SIGINT); + sigaddset(&set, SIGTERM); + sigprocmask(SIG_BLOCK, &set, NULL); + + /* use .env files if present */ + doenv(".env"); + + /* find directory of self and use env from there if it exists */ + { + char *buf = calloc(PATH_MAX, sizeof(char)); + ssize_t self_size = readlink("/proc/self/exe", buf, PATH_MAX); + if(self_size + strlen(".env") + 1 > PATH_MAX) + goto skip_self; + + char *lastslash = strrchr(buf, '/'); + *lastslash = '\0'; + + char *cwd = get_current_dir_name(); + int cwd_is_exec_dir = strcmp(buf, cwd) == 0; + free(cwd); + if(cwd_is_exec_dir) + goto skip_self; + + strcat(buf, "/.env"); + doenv(buf); +skip_self: + free(buf); + } + + /* init curl */ + if(curl_global_init(CURL_GLOBAL_DEFAULT)) + panic("init: curl init failed"); + + /* init random seed */ + srand(time(NULL)); + + /* Perform initcalls */ + do_initcalls(); + + /* Reaper. Much like init. */ + + siginfo_t siginfo; + while(subsystem_count > 0) { + sigwaitinfo(&set, &siginfo); + int sig = siginfo.si_signo; + switch(sig) { + case SIGCHLD: ; + int process = 0; + while((process = waitpid(-1, NULL, WNOHANG)) > 0) + if(subsystem_handle_term(process) > 0) + print(LOG_WARNING "init: failed to reap process %d", + process); + if(siginfo.si_status != 0) { + panic("init: process %d exited with non-zero status (%d)", siginfo.si_pid, siginfo.si_status); + } + break; + case SIGINT: + panic("init: keyboard interrupt"); + break; + case SIGTERM: + exit(0); + break; + default: + break; + } + } + + panic("init: no more subsystems"); +} diff --git a/util/log.c b/util/log.c new file mode 100644 index 0000000..afbc2e3 --- /dev/null +++ b/util/log.c @@ -0,0 +1,210 @@ +#include <execinfo.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/file.h> +#include <sys/syscall.h> +#include <sys/time.h> +#include <unistd.h> + +#include <dbs/log.h> +#include <dbs/util.h> + +extern int subsystem_change_mode(int pid, char mode); +extern char *subsystem_get_name(int pid); +extern int mainpid; + +static const char *colors[] = { + [EMERG_LOGLEVEL] = ANSI_BLINK ANSI_REVERSE ANSI_BOLD ANSI_RED, + [ALERT_LOGLEVEL] = ANSI_REVERSE ANSI_BOLD ANSI_RED, + [CRIT_LOGLEVEL] = ANSI_BOLD ANSI_RED, + [ERR_LOGLEVEL] = ANSI_RED, + [WARNING_LOGLEVEL] = ANSI_BOLD, + [NOTICE_LOGLEVEL] = ANSI_BRIGHT_WHITE, + [INFO_LOGLEVEL] = ANSI_RESET, + [DEBUG_LOGLEVEL] = ANSI_ITALIC ANSI_BRIGHT_BLUE, +}; + +static const char *mode_to_string[] = { + [PANICMODE_DEBUGONLY] = "subsystem OOPS", + [PANICMODE_RESPAWN] = "subsystem failure", + [PANICMODE_DIE] = "catastrophic failure", +}; + +static int console_lock = 0; + +#define MAX_TRY_COUNT 1 << 17 +static void obtain_console_lock(void) +{ + int try_count = 0; + register int rax asm("rax"); +retry: + while(console_lock && try_count <= MAX_TRY_COUNT) + try_count += 1; + + asm("mov %0, 1 \n" + "xchg %0, %1" : "=r" (rax), "=m" (console_lock)); + + if(rax > 0 && try_count <= MAX_TRY_COUNT) + goto retry; + + if(try_count > MAX_TRY_COUNT) { + print(LOG_SOH "\3" "4" "log: broken console lock"); + } + + return; +} + +static int vaprint(const char *fmt, va_list ap) +{ + int loglevel = DEFAULT_LOGLEVEL; + int dolocks = 1; + int parsecolon = 1; + if(fmt[0] == LOG_SOH_ASCII) { + loglevel = (fmt[2] - 0x30) % 10; + char flags = fmt[1]; + if(flags & 1 << 1) + dolocks = 0; + if(flags & 1 << 2) + parsecolon = 0; + fmt += 3; + } + + /* not going to be printed? dont bother! */ + if(loglevel > CONSOLE_LOGLEVEL) + return 0; + + /* we essentially print the user's raw input to its own buffer, + later we will parse it and print out ANSI colors and what not */ + char buf[512]; + + vsnprintf(buf, 512, fmt, ap); + buf[512 - 1] = '\0'; + + size_t colon = 0; + if(parsecolon) { + for(; colon < strlen(buf); ++colon) { + if(buf[colon] == ':') + break; + } + } + + char tsbuf[64] = "\0"; + struct timeval time; + gettimeofday(&time, NULL); + snprintf(tsbuf, sizeof(tsbuf), "[%5ld.%06ld] ", + (long)time.tv_sec % 100000, (long)time.tv_usec); + + /* spin lock, at the cost of architecture portability + concurrency is something that we need to adjust for, and the + console will be scrambled and unreadable if we allow writing all + at the same time. I considered simply writing all at once, but + ended up just not caring enough to the point where spinlocks + prevail. */ + if(dolocks) + obtain_console_lock(); + + + /* we want to support stuff without colons, but frankly I havent + tested this at time of writing. will find out later */ + writeputs(ANSI_RESET ANSI_GREEN); + writeputs(tsbuf); + writeputs(ANSI_RESET); + if(parsecolon && buf[colon] == ':') { + writeputs(colors[loglevel]); + writeputs(ANSI_YELLOW); + write(STDOUT_FILENO, buf, colon); + writeputs(ANSI_RESET); + } + writeputs(colors[loglevel]); + if(colon && *(buf + colon)) { + writeputs(buf + colon); + } else { + writeputs(buf); + } + writeputs(ANSI_RESET); + write(STDOUT_FILENO, "\n", 1); + if(dolocks) + console_lock = 0; + return 0; +} + +void _panic(const char *fileorigin, + const int lineorigin, + const char *fmt, ...) +{ + char mode = PANICMODE_DIE; + int pid = getpid(); + if(fmt[0] == LOG_SOH_ASCII) { + mode = fmt[1]; + /* cannot respawn main thread */ + if(pid == mainpid && mode == PANICMODE_RESPAWN) + mode = PANICMODE_DIE; + fmt += 2; + } + +#define NOLOCK(loglevel) LOG_SOH "\3" loglevel + va_list ap; + va_start(ap, fmt); + char *_fmt = malloc(strlen(fmt) + 4 * sizeof(char)); + sprintf(_fmt, NOLOCK("1") "%s", fmt); + + void **backtrace_addresses = malloc(sizeof(void*) * 32); + int backtrace_count = backtrace(backtrace_addresses, 32); + char **backtrace_symbolnames = + backtrace_symbols(backtrace_addresses, backtrace_count); + + obtain_console_lock(); + + print(NOLOCK("5") "------------[ cut here ]------------"); + print(LOG_SOH "\7""0" "%s at %s:%d", mode_to_string[(int)mode], + fileorigin, lineorigin); + vaprint(_fmt, ap); + print(LOG_SOH "\7""7" "Call Trace:"); + for(int i = 0; i < backtrace_count; ++i) { + print(NOLOCK("7") " [0x%016x] %s", backtrace_addresses[i], + backtrace_symbolnames[i]); + } + if(mainpid == pid){ + print(NOLOCK("7") " <start of main thread>"); + } else { + print(NOLOCK("7") " <start of %s[%d]>", + subsystem_get_name(pid), pid); + } + + /* if we are going to die, we dont really need to clean up */ + if(mode == PANICMODE_DIE) { + kill(0, SIGTERM); + raise(SIGTERM); + exit(0); + } + + print(NOLOCK("5") "------------[ cut here ]------------"); + + console_lock = 0; + free(_fmt); + free(backtrace_symbolnames); + free(backtrace_addresses); + va_end(ap); + + if(mode == PANICMODE_DEBUGONLY) + return; + + if(pid != mainpid && mode == PANICMODE_RESPAWN) { + /* we want to let the main process handle the rest */ + subsystem_change_mode(pid, mode); + syscall(SYS_exit_group, 0); + } +} + +int print(const char *fmt, ...) +{ + int ret = 0; + va_list ap; + va_start(ap, fmt); + ret = vaprint(fmt, ap); + va_end(ap); + return ret; +} diff --git a/util/net.c b/util/net.c new file mode 100644 index 0000000..98f371a --- /dev/null +++ b/util/net.c @@ -0,0 +1,383 @@ +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <pthread.h> +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <sys/signalfd.h> +#include <unistd.h> + +#include <cJSON.h> +#include <curl/curl.h> + +#include <dbs/api.h> +#include <dbs/init.h> +#include <dbs/log.h> +#include <dbs/subsys.h> + +/* functions */ +int http_request(HTTPMethod method, char *url, + struct curl_slist *headers, char *writebuf, size_t bufsiz); +int api_request(HTTPMethod method, char *url, + struct curl_slist *headers, char *writebuf, size_t bufsiz); +static void setup_token_header(); + +static void ws_send_heartbeat(); +static void ws_handle_event(cJSON *event); + +int net_subsystem(); +void net_get_gateway_url(); + +/* variables */ +static CURL *ws_handle; +static char *gateway_url; +static char *token_header; + +static long last_sequence = -1; +static struct timeval heartbeat_time; + +int http_request(HTTPMethod method, char *url, + struct curl_slist *headers, char *writebuf, size_t bufsiz) +{ + int inputpipe[2]; + int outputpipe[2]; + + if(pipe(inputpipe) < 0) + return -(errno << 8); + if(pipe(outputpipe) < 0) + return -(errno << 8); + + if(writebuf && bufsiz > 0) + write(inputpipe[1], writebuf, bufsiz); + close(inputpipe[1]); + + FILE *input_read = fdopen(inputpipe[0], "r"); + FILE *output_write = fdopen(outputpipe[1], "w"); + + int ret = outputpipe[0]; + + CURL *job = curl_easy_init(); + if(job == NULL) + panic("api: curl_easy_init failed"); + + curl_easy_setopt(job, CURLOPT_URL, url); + curl_easy_setopt(job, CURLOPT_READDATA, input_read); + curl_easy_setopt(job, CURLOPT_WRITEDATA, output_write); + char *requestmethod = "GET"; + switch(method) { + case HTTP_PATCH: + requestmethod = "PATCH"; + break; + case HTTP_DELETE: + requestmethod = "DELETE"; + break; + case HTTP_PUT: + requestmethod = "PUT"; + break; + case HTTP_POST: + requestmethod = "POST"; + break; + case HTTP_GET: /* fallthrough */ + default: + break; + } + curl_easy_setopt(job, CURLOPT_CUSTOMREQUEST, requestmethod); + if(headers) + curl_easy_setopt(job, CURLOPT_HTTPHEADER, headers); + CURLcode res = curl_easy_perform(job); + + if(res > 0) { + close(outputpipe[0]); + ret = -res; + } + + curl_easy_cleanup(job); + fclose(input_read); + fclose(output_write); + return ret; +} + +static void setup_token_header() +{ + if(token_header != NULL) + return; + char *token = getenv("TOKEN"); + if(!token) + panic("api: cannot find TOKEN in env"); + token_header = calloc(strlen(token) + strlen("Authorization: Bot ") + 1, sizeof(char)); + strcpy(token_header, "Authorization: Bot "); + strcat(token_header, token); +} +l1_initcall(setup_token_header); + +int api_request(HTTPMethod method, char *url, + struct curl_slist *headers, char *writebuf, size_t bufsiz) +{ + char *new_url = calloc((strlen("https://discord.com/api") + strlen(url) + 1), + sizeof(char)); + strcpy(new_url, "https://discord.com/api"); + strcat(new_url, url); + if(token_header == NULL) + setup_token_header(); + struct curl_slist *headers_auth = curl_slist_append(headers, token_header); + int ret = http_request(method, new_url, headers_auth, writebuf, bufsiz); + free(new_url); + curl_slist_free_all(headers_auth); + return ret; +} + +static void ws_send_heartbeat() +{ + char buf[128] = "{\"op\":1,\"d\":null}"; + if(last_sequence > 0) + snprintf(buf, 128, "{\"op\":1,\"d\":%ld}", last_sequence); + size_t sent; + curl_ws_send(ws_handle, buf, strnlen(buf, 128), &sent, 0, CURLWS_TEXT); + + /* if we receive a heartbeat request from discord, we need to fix + the itimer so we don't send another one before the desired + heartbeat interval. if our itimer is off more than 2 seconds + then we fix it up and reset it */ + struct itimerval itimer; + getitimer(ITIMER_REAL, &itimer); + if(itimer.it_value.tv_sec < heartbeat_time.tv_sec - 2) { + itimer.it_value = heartbeat_time; + setitimer(ITIMER_REAL, &itimer, NULL); + } +} + +static void ws_handle_event(cJSON *event) +{ + int op = cJSON_GetObjectItem(event, "op")->valueint; + cJSON *data = cJSON_GetObjectItem(event, "d"); + switch(op) { + case 0: /* Event dispatch */ + break; + case 1: /* Heartbeat request */ + ws_send_heartbeat(); + break; + case 9: /* Invalid Session */ + if(!cJSON_IsTrue(data)) { + /* discord sets data to true if we can reconnect, + but in this statement it is false, so we just die */ + /* note: discord closes the websocket after sending this, + so we let our ws code accept and handle the error */ + break; + } + /* FALLTHROUGH */ + case 7: /* Reconnect */ + /* TODO */ + panic("ws: cannot reconnect to ws after failure"); + break; + case 10: ; /* Hello */ + int heartbeat_wait = cJSON_GetObjectItem(data, + "heartbeat_interval")->valueint; + float jitter = (float)rand() / (RAND_MAX * 1.0f); + + heartbeat_time.tv_sec = heartbeat_wait / 1000; + heartbeat_time.tv_usec = (heartbeat_wait % 1000) * 1000; + struct timeval jitter_time = { + .tv_sec = heartbeat_time.tv_sec * jitter, + .tv_usec = heartbeat_time.tv_usec * jitter, + }; + struct itimerval new_itimer = { + .it_interval = heartbeat_time, + .it_value = jitter_time + }; + setitimer(ITIMER_REAL, &new_itimer, NULL); + break; + case 11: /* Heartbeat ACK */ + print(LOG_DEBUG "ws: heartbeat ACK"); + break; + default: + print(LOG_ERR "ws: received unknown WS opcode %d", op); + break; + } +} + +int net_subsystem(void) +{ + if(!gateway_url) + panic("net: gateway url invalid"); + + /* Initialise CURL */ + ws_handle = curl_easy_init(); + + curl_easy_setopt(ws_handle, CURLOPT_URL, gateway_url); + curl_easy_setopt(ws_handle, CURLOPT_CONNECT_ONLY, 2L); + + CURLcode ret = curl_easy_perform(ws_handle); + + if(ret > 0) { + panic("net: cannot open websocket: %s", curl_easy_strerror(ret)); + } + + int ws_sockfd; + if((ret = curl_easy_getinfo(ws_handle, + CURLINFO_ACTIVESOCKET, &ws_sockfd)) != CURLE_OK) + panic("net: curl cannot get active socket: " + "%s", curl_easy_strerror(ret)); + + + /* Block ALRM */ + sigset_t *set = malloc(sizeof(sigset_t)); + sigemptyset(set); + sigaddset(set, SIGALRM); + sigprocmask(SIG_BLOCK, set, NULL); + int alrmfd = signalfd(-1, set, 0); + free(set); + + /* Prepare poll */ + struct pollfd pollarray[2] = { + { + .fd = ws_sockfd, + .events = POLLIN, + .revents = POLLIN + }, + { + .fd = alrmfd, + .events = POLLIN, + .revents = 0 + } + }; + + struct pollfd *sockpoll = &(pollarray[0]); + struct pollfd *alrmpoll = &(pollarray[1]); + + /* Misc. variables */ + char *inbuf = malloc(1<<16 * sizeof(char)); + size_t rlen; + const struct curl_ws_frame *meta; + + errno = 0; + do { + if((sockpoll->revents & POLLIN) == POLLIN) { + ret = curl_ws_recv(ws_handle, inbuf, 1<<16, &rlen, &meta); + /* sometimes only SSL information gets sent through, so no actual + data is received. curl uses NONBLOCK internally so it lets us + know if there is no more data remaining */ + if(ret == CURLE_AGAIN) + goto sockpoll_continue; + if(ret != CURLE_OK) { + print(LOG_ERR "net: encountered error while reading socket: " + "%s", curl_easy_strerror(ret)); + break; + } + + /* TODO: partial frames */ + if((meta->offset | meta->bytesleft) > 0) { + print(LOG_ERR "net: dropped partial frame"); + goto sockpoll_continue; + } + + switch(meta->flags) { + case(CURLWS_PING): + curl_ws_send(ws_handle, NULL, 0, NULL, 0, CURLWS_PONG); + goto sockpoll_continue; + case(CURLWS_CLOSE): + default: + break; + } + + cJSON *event = cJSON_ParseWithLength(inbuf, rlen); + if(!event) { + print(LOG_ERR "net: dropped malformed frame"); + goto sockpoll_continue; + } + ws_handle_event(event); + cJSON_Delete(event); + } else if((sockpoll->revents & + (POLLRDHUP | POLLERR | POLLHUP | POLLNVAL)) > 0) { + break; + } +sockpoll_continue: + + if((alrmpoll->revents & POLLIN) == POLLIN) { + struct signalfd_siginfo siginfo; + read(alrmfd, &siginfo, sizeof(struct signalfd_siginfo)); + ws_send_heartbeat(); + } + } while(poll(pollarray, 2, -1) >= 0); + + if(errno > 0) { + print(LOG_ERR "net: poll: %s", strerror(errno)); + } + + free(inbuf); + + curl_easy_cleanup(ws_handle); + + panic("net: websocket closed unexpectedly"); + + return 0; +} /* net_subsystem */ +declare_subsystem(net_subsystem); + +void net_get_gateway_url() +{ + /* determine if websockets are supported */ + curl_version_info_data *curl_version = + curl_version_info(CURLVERSION_NOW); + const char * const* curl_protocols = curl_version->protocols; + int wss_supported = 0; + for(int i = 0; curl_protocols[i]; ++i) { + if(strcmp(curl_protocols[i], "wss") == 0) { + wss_supported = 1; + break; + } + } + + if(!wss_supported) + panic("net: wss not supported by libcurl"); + + /* fetch preferred url from discord */ + int fd = api_get("/gateway/bot", NULL, NULL, 0); + if(fd < 0) { + print(LOG_ERR "net: cannot get gateway url: %s", curl_easy_strerror(-fd)); + goto assume; + } + + char buf[512]; + int buf_length = read(fd, buf, 512); + close(fd); + + cJSON *gateway_info = cJSON_ParseWithLength(buf, buf_length); + cJSON *gateway_url_json = + cJSON_GetObjectItemCaseSensitive(gateway_info, "url"); + if(!cJSON_IsString(gateway_url_json) || + gateway_url_json->valuestring == NULL) { + + cJSON *gateway_message = + cJSON_GetObjectItemCaseSensitive(gateway_info, "message"); + + if(cJSON_IsString(gateway_message)) { + print(LOG_ERR "net: cannot get gateway url from api: " + "%s: assuming url", cJSON_GetStringValue(gateway_message)); + } else { + print(LOG_ERR "net: cannot get gateway url from api " + "(unknown error): assuming url"); + } + cJSON_Delete(gateway_info); + goto assume; + } + + /* curl requires websocket secure URLs to begin with WSS instead + of wss, so we fix up the received url for curl */ + gateway_url = calloc(strlen(gateway_url_json->valuestring) + 1, + sizeof(char)); + strcpy(gateway_url, gateway_url_json->valuestring); + gateway_url[0] = 'W'; + gateway_url[1] = 'S'; + gateway_url[2] = 'S'; + + cJSON_Delete(gateway_info); + return; + +assume: + gateway_url = calloc(strlen("WSS://gateway.discord.gg") + 1, + sizeof(char)); + strcpy(gateway_url, "WSS://gateway.discord.gg"); + return; +} +l1_initcall(net_get_gateway_url); diff --git a/util/subsys.c b/util/subsys.c new file mode 100644 index 0000000..a2dc057 --- /dev/null +++ b/util/subsys.c @@ -0,0 +1,179 @@ +#include <errno.h> +#include <sched.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/prctl.h> +#include <sys/syscall.h> +#include <sys/mman.h> +#include <unistd.h> + +#include <dbs/log.h> +#include <dbs/util.h> + +#define MAX_SUBSYSTEMS 32 +#define MAX_RESPAWN 3 + +struct subsystem_info { + char *fn_name; + int (*fn)(void); + int pid; + void *stack; + char mode; + int respawn_count; +}; + +extern long stack_size; +extern int mainpid; +static struct subsystem_info *subsystems[MAX_SUBSYSTEMS + 1]; +int subsystem_count = 0; + +static int __subsystem_entry(struct subsystem_info *info) +{ + /* entry point from clone(). we setup the process name so we know + what we are looking at from a glance in a ps view or htop or + whatever. */ + char *name = malloc(16 * sizeof(char)); + snprintf(name, 16, "DBS: %s", info->fn_name); + name[15] = '\0'; + prctl(PR_SET_NAME, name); + free(name); + + /* clear signal handlers so SIGTERM is no longer caught */ + static sigset_t set; + sigprocmask(SIG_SETMASK, &set, NULL); + + int ret = info->fn(); + + return ret; +} + +char *subsystem_get_name(int pid) +{ + for(int i = 0; i < MAX_SUBSYSTEMS; ++i) { + struct subsystem_info *subsystem = subsystems[i]; + if(!subsystem || subsystem->pid != pid) + continue; + + return subsystem->fn_name; + } + return 0; +} + +int subsystem_change_mode(int pid, char mode) +{ + for(int i = 0; i < MAX_SUBSYSTEMS; ++i) { + struct subsystem_info *subsystem = subsystems[i]; + if(!subsystem || subsystem->pid != pid) + continue; + + subsystem->mode = mode; + return 0; + } + + return 1; +} + +int subsystem_handle_term(int pid) +{ + for(int i = 0; i < MAX_SUBSYSTEMS; ++i) { + struct subsystem_info *subsystem = subsystems[i]; + if(!subsystem || subsystem->pid != pid) + continue; + + if(subsystem->mode == PANICMODE_RESPAWN + && subsystem->respawn_count < MAX_RESPAWN) { + ++(subsystem->respawn_count); + + int pid = clone((int (*)(void *))__subsystem_entry, + (void *)((long)(subsystem->stack) + stack_size), + CLONE_FILES | CLONE_VM | SIGCHLD, subsystem); + subsystem->pid = pid; + if(pid < 0) { + print(LOG_CRIT "subsys: cannot re-start subsystem %s: " + "clone failed (errno %d)", subsystem->fn_name, errno); + if(munmap(subsystem->stack, stack_size) < 0) + print(LOG_CRIT "subsys: failed to deallocate " + "stack for subsystem %s (%d) (errno %d)", + subsystem->fn_name, pid, errno); + free(subsystem); + return 0; + } + + subsystem->mode = 'o'; + return 0; + } else if(subsystem->mode == PANICMODE_RESPAWN) { + panic("subsys: exceeded maximum respawn count for subsystem " + "%s (%d)", subsystem->fn_name, subsystem->pid); + } + + if(munmap(subsystem->stack, stack_size) < 0) + print(LOG_CRIT "subsys: failed to deallocate stack " + "for subsystem %s (%d) (errno %d)", + subsystem->fn_name, pid, errno); + subsystems[i] = 0; + --subsystem_count; + free(subsystem); + + return 0; + } + + return 1; +} + +int __impl_start_subsystem(char *fn_name, int (*fn)(void)) +{ + if(getpid() != mainpid) { + print(LOG_CRIT "subsys: cannot perform subsystem inception " + "(attempted from %d)", getpid()); + return 1; + } + if(subsystem_count >= MAX_SUBSYSTEMS) { + print(LOG_CRIT "subsys: cannot start subsystem %s: " + "reached maximum number of subsystems", fn_name); + return 1; + } + + /* because CLONE_VM is being set, our stack is not duplicated and + therefore we need to map a stack */ + void *stack = mmap(NULL, stack_size, PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_GROWSDOWN | MAP_STACK | MAP_PRIVATE, -1, 0); + if((long)stack <= 0) { + print(LOG_CRIT "subsys: cannot start subsystem %s: " + "failed to allocate stack (errno %d)", fn_name, errno); + return 1; + } + + /* the libc gods have graced us with the ability to pass one (1) arg + to the function. struct required. the absence of a free is not a + memory leak because we free it above. */ + struct subsystem_info *info = malloc(sizeof(struct subsystem_info)); + info->fn_name = fn_name; + info->fn = fn; + info->stack = stack; + info->mode = 'o'; + info->respawn_count = 0; + + int pid = clone((int (*)(void *))__subsystem_entry, + (void *)((long)stack + stack_size), + CLONE_FILES | CLONE_VM | SIGCHLD, info); + info->pid = pid; + if(pid < 0) { + print(LOG_CRIT "subsys: cannot start subsystem %s: " + "clone failed (errno %d)", fn_name, errno); + munmap(stack, stack_size); + free(info); + return 1; + } + + for(int i = 0; i < MAX_SUBSYSTEMS; ++i) { + if(!subsystems[i]) { + subsystems[i] = info; + ++subsystem_count; + break; + } + } + + return 0; +} |