Implement inotify check on the reaper thread for config changes

This commit is contained in:
Marc Di Luzio 2019-05-15 18:00:43 +01:00
parent ceb1808c95
commit ec55bda3b2
5 changed files with 169 additions and 39 deletions

View File

@ -37,11 +37,14 @@ POSSIBILITY OF SUCH DAMAGE.
/* Ben Hoyt's inih library */
#include "ini.h"
#include <dirent.h>
#include <linux/limits.h>
#include <pthread.h>
#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
/* 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);

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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