taisei/src/dynstage.c
2024-05-17 14:11:48 +02:00

328 lines
7.6 KiB
C

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "dynstage.h"
#include "events.h"
#include "filewatch/filewatch.h"
#ifndef TAISEI_BUILDCONF_HAVE_POSIX
#error Stage hot reloading is only supported on POSIX systems
#endif
#include <dirent.h>
#include <dlfcn.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#define HT_SUFFIX fwatchset
#define HT_KEY_TYPE FileWatch*
#define HT_VALUE_TYPE struct ht_empty
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_uint64((uintptr_t)(key))
#define HT_FUNC_COPY_KEY(dst, src) (*(dst) = (FileWatch*)(src))
#define HT_KEY_FMT "p"
#define HT_KEY_PRINTABLE(key) (key)
#define HT_VALUE_FMT "i"
#define HT_VALUE_PRINTABLE(val) (1)
#define HT_KEY_CONST
#define HT_DECL
#define HT_IMPL
#include "hashtable_incproxy.inc.h"
typedef void *stageslib_t;
// in frames
#define RECOMPILE_DELAY 6
#define RESCAN_DELAY 6
#define LIBRARY_BUMP_DELAY 6
static struct {
stageslib_t lib;
StagesExports *exports;
FileWatch *lib_watch;
uint32_t lib_generation;
uint32_t src_generation;
ht_fwatchset_t src_watches;
bool monitoring_initialized;
int recompile_delay;
int rescan_delay;
int lib_gen_bump_delay;
} dynstage;
static stageslib_t dynstage_dlopen(void) {
int attempts = 7;
int delay = 10;
for(;;) {
stageslib_t lib = dlopen(TAISEI_BUILDCONF_DYNSTAGE_LIB, RTLD_NOW | RTLD_LOCAL);
if(LIKELY(lib != NULL)) {
return lib;
}
if(--attempts) {
log_error("Failed to load stages library (%i attempt%s left): %s", attempts, attempts > 1? "s" : "", dlerror());
SDL_Delay(delay);
delay *= 2;
} else {
break;
}
}
log_fatal("Failed to load stages library: %s", dlerror());
}
static void dynstage_bump_lib_generation(void) {
// Delay reporting library update to avoid trying to load a partially written file
dynstage.lib_gen_bump_delay = max(dynstage.lib_gen_bump_delay, LIBRARY_BUMP_DELAY);
}
static void dynstage_bump_src_generation(bool deleted) {
++dynstage.src_generation;
dynstage.recompile_delay = max(dynstage.recompile_delay, RECOMPILE_DELAY);
if(deleted) {
// Some text editors may delete the original file and replace it when saving.
// In case that happens, rescan shortly after detecting a delete.
// Not very efficient, but simple since we don't have to track file names.
dynstage.rescan_delay = max(dynstage.rescan_delay, RESCAN_DELAY);
}
log_debug("%i", dynstage.src_generation);
}
static void unwatch_sources(void);
static void watch_sources(void);
static void recompile(void);
static bool dynstage_frame_event(SDL_Event *e, void *ctx) {
if(!dynstage.lib_watch) {
dynstage.lib_watch = filewatch_watch(TAISEI_BUILDCONF_DYNSTAGE_LIB);
if(dynstage.lib_watch) {
log_debug("Created watch for %s", TAISEI_BUILDCONF_DYNSTAGE_LIB);
dynstage_bump_lib_generation();
}
}
if(dynstage.rescan_delay > 0 && --dynstage.rescan_delay == 0) {
unwatch_sources();
watch_sources();
}
if(dynstage.recompile_delay > 0 && --dynstage.recompile_delay == 0) {
recompile();
}
if(dynstage.lib_gen_bump_delay > 0 && --dynstage.lib_gen_bump_delay == 0) {
++dynstage.lib_generation;
log_debug("Bumped library generation: %i", dynstage.lib_generation);
}
return false;
}
static bool is_src_watch(FileWatch *w) {
return ht_fwatchset_lookup(&dynstage.src_watches, w, NULL);
}
static bool dynstage_filewatch_event(SDL_Event *e, void *ctx) {
FileWatch *watch = NOT_NULL(e->user.data1);
FileWatchEvent fevent = e->user.code;
if(watch == dynstage.lib_watch) {
switch(fevent) {
case FILEWATCH_FILE_UPDATED:
log_debug("Library updated");
dynstage_bump_lib_generation();
break;
case FILEWATCH_FILE_DELETED:
log_debug("Library deleted");
break;
}
return false;
}
if(is_src_watch(watch)) {
dynstage_bump_src_generation(fevent == FILEWATCH_FILE_DELETED);
}
return false;
}
static void dynstage_sigchld(int sig, siginfo_t *sinfo, void *ctx) {
attr_unused pid_t cpid;
int status;
while((cpid = waitpid(-1, &status, WNOHANG)) > 0) {
log_debug("Process %i terminated with status %i", cpid, status);
}
}
void dynstage_init(void) {
struct sigaction sa = {
.sa_sigaction = dynstage_sigchld,
.sa_flags = SA_SIGINFO,
};
if(sigaction(SIGCHLD, &sa, NULL) == -1) {
log_fatal("sigaction() failed: %s", strerror(errno));
}
dynstage_reload_library();
}
static void unwatch_sources(void) {
ht_fwatchset_iter_t iter;
ht_fwatchset_iter_begin(&dynstage.src_watches, &iter);
for(;iter.has_data; ht_fwatchset_iter_next(&iter)) {
filewatch_unwatch(iter.key);
}
ht_fwatchset_iter_end(&iter);
ht_fwatchset_unset_all(&dynstage.src_watches);
}
static void watch_source_file(const char *path) {
FileWatch *w = filewatch_watch(path);
if(!w) {
return;
}
if(is_src_watch(w)) {
// filewatches are reference counted; keep only 1 reference
filewatch_unwatch(w);
} else {
ht_fwatchset_set(&dynstage.src_watches, w, HT_EMPTY);
}
}
static int watch_source_dir(const char *path) {
DIR *d = opendir(path);
if(!d) {
return errno;
}
size_t pathlen = strlen(path);
struct dirent *e;
while((e = readdir(d))) {
const char *n = e->d_name;
if(!*n || !strcmp(n, ".") || !strcmp(n, "..")) {
continue;
}
size_t nlen = strlen(n);
char pathbuf[pathlen + nlen + 2];
memcpy(pathbuf, path, pathlen);
pathbuf[pathlen] = '/';
memcpy(pathbuf + pathlen + 1, n, nlen + 1);
log_debug("%s", pathbuf);
if(watch_source_dir(pathbuf) == ENOTDIR) {
watch_source_file(pathbuf);
}
}
closedir(d);
return 0;
}
static void watch_sources(void) {
watch_source_dir(TAISEI_BUILDCONF_DYNSTAGE_SRCROOT);
}
void dynstage_init_monitoring(void) {
events_register_handler(&(EventHandler) {
.proc = dynstage_frame_event,
.priority = EPRIO_SYSTEM,
.event_type = MAKE_TAISEI_EVENT(TE_FRAME),
});
events_register_handler(&(EventHandler) {
.proc = dynstage_filewatch_event,
.priority = EPRIO_SYSTEM,
.event_type = MAKE_TAISEI_EVENT(TE_FILEWATCH),
});
ht_fwatchset_create(&dynstage.src_watches);
watch_sources();
dynstage.monitoring_initialized = true;
}
static void recompile(void) {
pid_t cpid = fork();
if(UNLIKELY(cpid < 0)) {
log_error("fork() failed: %s", strerror(errno));
return;
}
if(cpid == 0) {
static char *const cmd[] = TAISEI_BUILDCONF_DYNSTAGE_REBUILD_CMD;
execvp(cmd[0], cmd);
abort();
}
log_debug("Child pid: %ji", (intmax_t)cpid);
}
void dynstage_shutdown(void) {
if(dynstage.monitoring_initialized) {
events_unregister_handler(dynstage_frame_event);
events_unregister_handler(dynstage_filewatch_event);
if(dynstage.lib_watch) {
filewatch_unwatch(dynstage.lib_watch);
}
unwatch_sources();
ht_fwatchset_destroy(&dynstage.src_watches);
}
dlclose(dynstage.lib);
sigaction(SIGCHLD, &(struct sigaction) { .sa_handler = SIG_DFL }, NULL);
memset(&dynstage, 0, sizeof(dynstage));
}
uint32_t dynstage_get_generation(void) {
return dynstage.lib_generation;
}
bool dynstage_reload_library(void) {
if(dynstage.lib) {
// In-flight async log messages may still have pointers to static strings inside the old lib
// Wait for them to finish processing so it's safe to unload
log_sync(false);
dlclose(dynstage.lib);
}
dynstage.exports = dlsym((dynstage.lib = dynstage_dlopen()), "stages_exports");
if(UNLIKELY(dynstage.exports == NULL)) {
log_fatal("No stages_exports symbol in stages library");
}
log_info("Reloaded stages module");
return true;
}
StagesExports *dynstage_get_exports(void) {
return dynstage.exports;
}