diff --git a/daemon/daemon_config.c b/daemon/daemon_config.c index a03147b..550e9ef 100644 --- a/daemon/daemon_config.c +++ b/daemon/daemon_config.c @@ -37,11 +37,14 @@ POSSIBILITY OF SUCH DAMAGE. /* Ben Hoyt's inih library */ #include "ini.h" +#include #include #include #include #include #include +#include +#include #include /* Name and possible location of the config file */ @@ -59,6 +62,9 @@ POSSIBILITY OF SUCH DAMAGE. return value; \ } +/* The number of current locations for config files */ +#define CONFIG_NUM_LOCATIONS 4 + /** * The config holds various details as needed * and a rwlock to allow config_reload to be called @@ -66,7 +72,7 @@ POSSIBILITY OF SUCH DAMAGE. struct GameModeConfig { pthread_rwlock_t rwlock; int inotfd; - int inotwd; + int inotwd[CONFIG_NUM_LOCATIONS]; struct { char whitelist[CONFIG_LIST_MAX][CONFIG_VALUE_MAX]; @@ -342,7 +348,7 @@ static void load_config_files(GameModeConfig *self) const char *path; bool protected; }; - struct ConfigLocation locations[] = { + struct ConfigLocation locations[CONFIG_NUM_LOCATIONS] = { { "/usr/share/gamemode", true }, /* shipped default config */ { "/etc", true }, /* administrator config */ { config_location_home, false }, /* $XDG_CONFIG_HOME or $HOME/.config/ */ @@ -350,11 +356,12 @@ static void load_config_files(GameModeConfig *self) }; /* Load each file in order and overwrite values */ - for (unsigned int i = 0; i < sizeof(locations) / sizeof(locations[0]); i++) { + for (unsigned int i = 0; i < CONFIG_NUM_LOCATIONS; i++) { char *path = NULL; if (locations[i].path && asprintf(&path, "%s/" CONFIG_NAME, locations[i].path) > 0) { - FILE *f = fopen(path, "r"); - if (f) { + FILE *f = NULL; + DIR *d = NULL; + if ((f = fopen(path, "r"))) { LOG_MSG("Loading config file [%s]\n", path); load_protected = locations[i].protected; int error = ini_parse_file(f, inih_handler, (void *)self); @@ -364,6 +371,24 @@ static void load_config_files(GameModeConfig *self) LOG_MSG("Failed to parse config file - error on line %d!\n", error); } fclose(f); + + /* Register for inotify */ + /* Watch for modification, deletion, moves, or attribute changes */ + uint32_t fileflags = IN_MODIFY | IN_DELETE_SELF | IN_MOVE_SELF | IN_ATTRIB; + if ((self->inotwd[i] = inotify_add_watch(self->inotfd, path, fileflags)) == -1) { + LOG_ERROR("Failed to watch %s, error: %s", path, strerror(errno)); + } + + } else if ((d = opendir(locations[i].path))) { + /* We didn't find a file, so we'll wait on the directory */ + /* Notify if a file is created, or move to the directory, or if the directory itself + * is removed or moved away */ + uint32_t dirflags = IN_CREATE | IN_MOVED_TO | IN_DELETE_SELF | IN_MOVE_SELF; + if ((self->inotwd[i] = + inotify_add_watch(self->inotfd, locations[i].path, dirflags)) == -1) { + LOG_ERROR("Failed to watch %s, error: %s", path, strerror(errno)); + } + closedir(d); } free(path); } @@ -409,16 +434,104 @@ void config_init(GameModeConfig *self) { pthread_rwlock_init(&self->rwlock, NULL); + self->inotfd = inotify_init1(IN_NONBLOCK); + if (self->inotfd == -1) + LOG_ERROR( + "inotify_init failed: %s, gamemode will be able to watch config files for edits!\n", + strerror(errno)); + + for (unsigned int i = 0; i < CONFIG_NUM_LOCATIONS; i++) { + self->inotwd[i] = -1; + } + /* load the initial config */ load_config_files(self); } +/* + * Destroy internal parts of config + */ +static void internal_destroy(GameModeConfig *self) +{ + pthread_rwlock_destroy(&self->rwlock); + + for (unsigned int i = 0; i < CONFIG_NUM_LOCATIONS; i++) { + if (self->inotwd[i] != -1) { + /* TODO: Error handle */ + inotify_rm_watch(self->inotfd, self->inotwd[i]); + } + } + + if (self->inotfd != -1) + close(self->inotfd); +} + /* * Re-load the config file */ void config_reload(GameModeConfig *self) { - load_config_files(self); + internal_destroy(self); + + config_init(self); +} + +/* + * Check if the config needs to be reloaded + */ +bool config_needs_reload(GameModeConfig *self) +{ + bool need = false; + + /* Take a read lock while we use the inotify fd */ + pthread_rwlock_rdlock(&self->rwlock); + + const size_t buflen = sizeof(struct inotify_event) + NAME_MAX + 1; + char buffer[buflen] __attribute__((aligned(__alignof__(struct inotify_event)))); + + ssize_t len = read(self->inotfd, buffer, buflen); + if (len == -1) { + /* EAGAIN is returned when there's nothing to read on a non-blocking fd */ + if (errno != EAGAIN) + LOG_ERROR("Could not read inotify fd: %s\n", strerror(errno)); + } else if (len > 0) { + /* Iterate over each event we've been given */ + size_t i = 0; + while (i < (size_t)len) { + struct inotify_event *event = (struct inotify_event *)&buffer[i]; + /* We have picked up an event and need to handle it */ + if (event->mask & IN_ISDIR) { + /* If the event is a dir event we need to take a look */ + if (event->mask & IN_DELETE_SELF || event->mask & IN_MOVE_SELF) { + /* The directory itself changed, trigger a reload */ + need = true; + break; + + } else if (event->mask & IN_CREATE || event->mask & IN_MOVED_TO) { + /* A new file has appeared, check the file name */ + if (strncmp(basename(event->name), CONFIG_NAME, strlen(CONFIG_NAME)) == 0) { + /* This is a gamemode config file, trigger a reload */ + need = true; + break; + } + } + + } else { + /* When the event isn't a dir event - one of our files has been interacted with + * in some way, so let's reload regardless of the details + */ + need = true; + break; + } + + i += sizeof(struct inotify_event) + event->len; + } + } + + /* Return the read lock */ + pthread_rwlock_unlock(&self->rwlock); + + return need; } /* @@ -426,7 +539,7 @@ void config_reload(GameModeConfig *self) */ void config_destroy(GameModeConfig *self) { - pthread_rwlock_destroy(&self->rwlock); + internal_destroy(self); /* Finally, free the memory */ free(self); diff --git a/daemon/daemon_config.h b/daemon/daemon_config.h index 790d26c..6550666 100644 --- a/daemon/daemon_config.h +++ b/daemon/daemon_config.h @@ -68,6 +68,11 @@ void config_init(GameModeConfig *self); */ void config_reload(GameModeConfig *self); +/* + * Check if the config has changed and will need a reload + */ +bool config_needs_reload(GameModeConfig *self); + /* * Destroy a config * Invalidates the config diff --git a/daemon/dbus_messaging.c b/daemon/dbus_messaging.c index 4c03b14..ca729e4 100644 --- a/daemon/dbus_messaging.c +++ b/daemon/dbus_messaging.c @@ -213,7 +213,7 @@ static int method_refresh_config(sd_bus_message *m, void *userdata, __attribute__((unused)) sd_bus_error *ret_error) { GameModeContext *context = userdata; - int status = game_mode_refresh_config(context); + int status = game_mode_reload_config(context); return sd_bus_reply_method_return(m, "i", status); } diff --git a/daemon/gamemode.c b/daemon/gamemode.c index 4532db2..edf21d6 100644 --- a/daemon/gamemode.c +++ b/daemon/gamemode.c @@ -637,6 +637,35 @@ static void game_mode_client_free(GameModeClient *client) free(client); } +/* Internal refresh config function (assumes no contention with reaper thread) */ +static void game_mode_reload_config_internal(GameModeContext *self) +{ + LOG_MSG("Reloading config...\n"); + + /* Remove current client optimisations */ + pthread_rwlock_rdlock(&self->rwlock); + for (GameModeClient *cl = self->client; cl; cl = cl->next) + game_mode_remove_client_optimisations(self, cl->pid); + pthread_rwlock_unlock(&self->rwlock); + + /* Shut down the global context */ + game_mode_context_leave(self); + + /* Reload the config */ + config_reload(self->config); + + /* Start the global context back up */ + game_mode_context_enter(self); + + /* Re-apply all current optimisations */ + pthread_rwlock_rdlock(&self->rwlock); + for (GameModeClient *cl = self->client; cl; cl = cl->next) + game_mode_apply_client_optimisations(self, cl->pid); + pthread_rwlock_unlock(&self->rwlock); + + LOG_MSG("Config reload complete\n"); +} + /** * We continuously run until told otherwise. */ @@ -664,6 +693,12 @@ static void *game_mode_context_reaper(void *userdata) /* Expire remaining entries */ game_mode_context_auto_expire(self); + /* Check if we should be reloading the config, and do so if needed */ + if (config_needs_reload(self->config)) { + LOG_MSG("Detected config file changes\n"); + game_mode_reload_config_internal(self); + } + ts.tv_sec = time(NULL) + reaper_interval; } @@ -762,44 +797,21 @@ static void game_mode_execute_scripts(char scripts[CONFIG_LIST_MAX][CONFIG_VALUE } /* - * Refresh the current configuration + * Reload the current configuration * - * Refreshing the configuration live would be problematic for various optimisation values, to ensure - * we have a fully clean state, we tear down the whole gamemode state and regrow it with a new - * config, remembering the registered games + * Reloading the configuration completely live would be problematic for various optimisation values, + * to ensure we have a fully clean state, we tear down the whole gamemode state and regrow it with a + * new config, remembering the registered games */ -int game_mode_refresh_config(GameModeContext *self) +int game_mode_reload_config(GameModeContext *self) { - LOG_MSG("Refreshing config..."); - /* Stop the reaper thread first */ end_reaper_thread(self); - /* Remove current client optimisations */ - pthread_rwlock_rdlock(&self->rwlock); - for (GameModeClient *cl = self->client; cl; cl = cl->next) - game_mode_remove_client_optimisations(self, cl->pid); - pthread_rwlock_unlock(&self->rwlock); - - /* Shut down the global context */ - game_mode_context_leave(self); - - /* Reload the config */ - config_reload(self->config); - - /* Start the global context back up */ - game_mode_context_enter(self); - - /* Re-apply all current optimisations */ - pthread_rwlock_rdlock(&self->rwlock); - for (GameModeClient *cl = self->client; cl; cl = cl->next) - game_mode_apply_client_optimisations(self, cl->pid); - pthread_rwlock_unlock(&self->rwlock); + game_mode_reload_config_internal(self); /* Restart the reaper thread back up again */ start_reaper_thread(self); - LOG_MSG("Config refreshed..."); - return 0; } \ No newline at end of file diff --git a/daemon/gamemode.h b/daemon/gamemode.h index b9ddad1..1545aeb 100644 --- a/daemon/gamemode.h +++ b/daemon/gamemode.h @@ -112,9 +112,9 @@ int game_mode_context_query_status(GameModeContext *self, pid_t pid, pid_t reque GameModeConfig *game_mode_config_from_context(const GameModeContext *context); /* - * Refresh the current configuration + * Reload the current configuration */ -int game_mode_refresh_config(GameModeContext *context); +int game_mode_reload_config(GameModeContext *context); /** gamemode-env.c * Provides internal API functions specific to working environment