328 lines
7.6 KiB
C
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;
|
|
}
|