aboutsummaryrefslogtreecommitdiffstats
path: root/util
diff options
context:
space:
mode:
authorturret <turret@duck.com>2024-03-30 16:04:45 -0500
committerturret <turret@duck.com>2024-03-30 16:04:45 -0500
commit80a67b7d20393a29aa5d2cb92197f3381be7fd96 (patch)
treef0c313da5ef509e79e5067972edd91976513ce0e /util
parentf01745a2ee84f11b8cc54e37c5f7f596184ab785 (diff)
downloaddiscord-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.c200
-rw-r--r--util/log.c210
-rw-r--r--util/net.c383
-rw-r--r--util/subsys.c179
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;
+}