ソースを参照

Add config file parsing

	Checks for a gamemode.ini in /usr/share/gamemode/ (or in the cwd for debugging)

	Currently allows for blacklisting and whitelisting clients based on rudimentary needle-haystack executable name checks

	See the example/gamemode.ini file for expected syntax

	Using the BSD licensed inih library (with additional meson.build file)
Marc Di Luzio 7 年 前
コミット
759cbc3c40
10 ファイル変更388 行追加0 行削除
  1. 1 0
      .gitignore
  2. 10 0
      README.md
  3. 245 0
      daemon/daemon_config.c
  4. 72 0
      daemon/daemon_config.h
  5. 19 0
      daemon/gamemode.c
  6. 2 0
      daemon/meson.build
  7. 21 0
      data/gamemoded.1
  8. 9 0
      example/gamemode.ini
  9. 4 0
      meson.build
  10. 5 0
      subprojects/inih.wrap

+ 1 - 0
.gitignore

@@ -1,2 +1,3 @@
 build/
+subprojects/inih/
 *.swp

+ 10 - 0
README.md

@@ -101,6 +101,16 @@ Add `-lgamemodeauto` to linker arguments and distribute `libgamemodeauto.so` wit
 #### Option 3: Distribute and script
 Distribute `libgamemodeauto.so` with the game and add to LD\_PRELOAD in a launch script
 
+---
+## Configuration
+
+The daemon can currently be configured using a `gamemode.ini` file in `/usr/share/gamemode/`. It will load the file when starting up.
+
+An example of what the file could look like is found in the `example` directory.
+
+The file parsing uses [inih](https://github.com/benhoyt/inih).
+
+
 ---
 ## Pull Requests
 Pull requests must match with the coding style found in the `.clang-format` file

+ 245 - 0
daemon/daemon_config.c

@@ -0,0 +1,245 @@
+/*
+
+Copyright (c) 2017, Feral Interactive
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+ * Neither the name of Feral Interactive nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+ */
+#define _GNU_SOURCE
+
+#include "daemon_config.h"
+#include "logging.h"
+
+/* Ben Hoyt's inih library */
+#include "ini.h"
+
+#include <linux/limits.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <string.h>
+
+/* Name and possible location of the config file */
+#define CONFIG_NAME "gamemode.ini"
+#define CONFIG_DIR "/usr/share/gamemode/"
+
+/* Maximum values in the whilelist and blacklist */
+#define MAX_LIST_VALUES 32
+
+/* Maximum length of values in the whilelist or blacklist */
+#define MAX_LIST_VALUE_LENGTH 256
+
+/**
+ * The config holds various details as needed
+ * and a rwlock to allow config_reload to be called
+ */
+struct GameModeConfig {
+	pthread_rwlock_t rwlock;
+	int inotfd;
+	int inotwd;
+
+	char whitelist[MAX_LIST_VALUES][MAX_LIST_VALUE_LENGTH];
+	char blacklist[MAX_LIST_VALUES][MAX_LIST_VALUE_LENGTH];
+};
+
+/*
+ * Handler for the inih callback
+ */
+static int inih_handler(void *user, const char *section, const char *name, const char *value)
+{
+	GameModeConfig *self = (GameModeConfig *)user;
+	bool valid = false;
+
+	/* Filter subsection */
+	if (strcmp(section, "filter") == 0) {
+		if (strcmp(name, "whitelist") == 0) {
+			valid = true;
+
+			unsigned int i = 0;
+			while (*self->whitelist[i] && ++i < MAX_LIST_VALUES)
+				;
+
+			if (i < MAX_LIST_VALUES) {
+				strncpy(self->whitelist[i], value, MAX_LIST_VALUE_LENGTH);
+			} else {
+				LOG_MSG("Could not add [%s] to the whitelist, exceeds limit of %d\n",
+				        value,
+				        MAX_LIST_VALUES);
+			}
+		} else if (strcmp(name, "blacklist") == 0) {
+			valid = true;
+
+			unsigned int i = 0;
+			while (*self->blacklist[i] && ++i < MAX_LIST_VALUES)
+				;
+
+			if (i < MAX_LIST_VALUES) {
+				strncpy(self->blacklist[i], value, MAX_LIST_VALUE_LENGTH);
+			} else {
+				LOG_MSG("Could not add [%s] to the blacklist, exceeds limit of %d\n",
+				        value,
+				        MAX_LIST_VALUES);
+			}
+		}
+	}
+
+	if (!valid) {
+		/* We hit an unknown section succeed but with a log */
+		LOG_MSG("Unknown value in config file [%s] %s=%s\n", section, name, value);
+	}
+
+	return 1;
+}
+
+/*
+ * Load the config file
+ */
+static void load_config_file(GameModeConfig *self)
+{
+	/* Take the write lock for the internal data */
+	pthread_rwlock_wrlock(&self->rwlock);
+
+	/* Clear our config values */
+	memset(self->whitelist, 0, sizeof(self->whitelist));
+	memset(self->blacklist, 0, sizeof(self->blacklist));
+
+	/* try locally first */
+	FILE *f = fopen(CONFIG_NAME, "r");
+	if (!f) {
+		f = fopen(CONFIG_DIR CONFIG_NAME, "r");
+		if (!f) {
+			/* Failure here isn't fatal */
+			LOG_ERROR("Note: No config file found [%s] in working directory or in [%s]\n",
+			          CONFIG_NAME,
+			          CONFIG_DIR);
+		}
+	}
+
+	if (f) {
+		int error = ini_parse_file(f, inih_handler, (void *)self);
+
+		/* Failure here isn't fatal */
+		if (error) {
+			LOG_MSG("Failed to parse config file - error on line %d!\n", error);
+		}
+
+		fclose(f);
+	}
+
+	/* Release the lock */
+	pthread_rwlock_unlock(&self->rwlock);
+}
+
+/*
+ * Create a context object
+ */
+GameModeConfig *config_create(void)
+{
+	GameModeConfig *newconfig = (GameModeConfig *)malloc(sizeof(GameModeConfig));
+
+	return newconfig;
+}
+
+/*
+ * Initialise the config
+ */
+void config_init(GameModeConfig *self)
+{
+	pthread_rwlock_init(&self->rwlock, NULL);
+
+	/* load the initial config */
+	load_config_file(self);
+}
+
+/*
+ * Re-load the config file
+ */
+void config_reload(GameModeConfig *self)
+{
+	load_config_file(self);
+}
+
+/*
+ * Destroy the config
+ */
+void config_destroy(GameModeConfig *self)
+{
+	pthread_rwlock_destroy(&self->rwlock);
+
+	/* Finally, free the memory */
+	free(self);
+}
+
+/*
+ * Checks if the client is whitelisted
+ */
+bool config_get_client_whitelisted(GameModeConfig *self, const char *client)
+{
+	/* Take the read lock for the internal data */
+	pthread_rwlock_rdlock(&self->rwlock);
+
+	/* If the whitelist is empty then everything passes */
+	bool found = true;
+	if (self->whitelist[0][0]) {
+		/*
+		 * Check if the value is found in our whitelist
+		 * Currently is a simple strstr check, but could be modified for wildcards etc.
+		 */
+		found = false;
+		for (unsigned int i = 0; i < MAX_LIST_VALUES && self->whitelist[i][0]; i++) {
+			if (strstr(client, self->whitelist[i])) {
+				found = true;
+			}
+		}
+	}
+
+	/* release the lock */
+	pthread_rwlock_unlock(&self->rwlock);
+	return found;
+}
+
+/*
+ * Checks if the client is blacklisted
+ */
+bool config_get_client_blacklisted(GameModeConfig *self, const char *client)
+{
+	/* Take the read lock for the internal data */
+	pthread_rwlock_rdlock(&self->rwlock);
+
+	/*
+	 * Check if the value is found in our whitelist
+	 * Currently is a simple strstr check, but could be modified for wildcards etc.
+	 */
+	bool found = false;
+	for (unsigned int i = 0; i < MAX_LIST_VALUES && self->blacklist[i][0]; i++) {
+		if (strstr(client, self->blacklist[i])) {
+			found = true;
+		}
+	}
+
+	/* release the lock */
+	pthread_rwlock_unlock(&self->rwlock);
+	return found;
+}

+ 72 - 0
daemon/daemon_config.h

@@ -0,0 +1,72 @@
+/*
+
+Copyright (c) 2017, Feral Interactive
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+ * Neither the name of Feral Interactive nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+ */
+#pragma once
+
+#include <stdbool.h>
+
+/*
+ * Opaque config context type
+ */
+typedef struct GameModeConfig GameModeConfig;
+
+/*
+ * Initialise a config
+ */
+GameModeConfig *config_create(void);
+
+/*
+ * Initialise a config
+ * Must be called before using any config later config functions
+ */
+void config_init(GameModeConfig *self);
+
+/*
+ * Reload a config from disk
+ * Thread safe to call
+ */
+void config_reload(GameModeConfig *self);
+
+/*
+ * Destroy a config
+ * Invalidates the config
+ */
+void config_destroy(GameModeConfig *self);
+
+/*
+ * Get if the client is in the whitelist
+ * returns false for an empty whitelist
+ */
+bool config_get_client_whitelisted(GameModeConfig *self, const char *client);
+
+/*
+ * Get if the client is in the blacklist
+ */
+bool config_get_client_blacklisted(GameModeConfig *self, const char *client);

+ 19 - 0
daemon/gamemode.c

@@ -32,6 +32,7 @@ POSSIBILITY OF SUCH DAMAGE.
 #define _GNU_SOURCE
 
 #include "gamemode.h"
+#include "daemon_config.h"
 #include "governors.h"
 #include "logging.h"
 
@@ -56,6 +57,8 @@ struct GameModeContext {
 	_Atomic int refcount;    /**<Allow cycling the game mode */
 	GameModeClient *client;  /**<Pointer to first client */
 
+	GameModeConfig *config; /**<Pointer to config object */
+
 	bool performance_mode; /**<Only updates when we can */
 
 	/* Reaper control */
@@ -98,6 +101,10 @@ void game_mode_context_init(GameModeContext *self)
 	had_context_init = true;
 	self->refcount = ATOMIC_VAR_INIT(0);
 
+	/* Initialise the config */
+	self->config = config_create();
+	config_init(self->config);
+
 	/* Read current governer state before setting up any message handling */
 	update_initial_gov_state();
 	LOG_MSG("governor is set to [%s]\n", get_initial_governor());
@@ -139,6 +146,9 @@ void game_mode_context_destroy(GameModeContext *self)
 	pthread_cond_destroy(&self->reaper.condition);
 	pthread_mutex_destroy(&self->reaper.mutex);
 
+	/* Destroy the config object */
+	config_destroy(self->config);
+
 	pthread_rwlock_destroy(&self->rwlock);
 }
 
@@ -253,6 +263,15 @@ bool game_mode_context_register(GameModeContext *self, pid_t client)
 		return false;
 	}
 
+	/* Check our blacklist and whitelist */
+	if (!config_get_client_whitelisted(self->config, cl->executable)) {
+		LOG_MSG("Client [%s] was rejected (not in whitelist)\n", cl->executable);
+		return false;
+	} else if (config_get_client_blacklisted(self->config, cl->executable)) {
+		LOG_MSG("Client [%s] was rejected (in blacklist)\n", cl->executable);
+		return false;
+	}
+
 	/* Begin a write lock now to insert our new client at list start */
 	pthread_rwlock_wrlock(&self->rwlock);
 

+ 2 - 0
daemon/meson.build

@@ -21,6 +21,7 @@ daemon_sources = [
     'daemonize.c',
     'dbus_messaging.c',
     'governors.c',
+    'daemon_config.c',
 ]
 
 executable(
@@ -30,6 +31,7 @@ executable(
         link_daemon_common,
         dep_threads,
         dep_systemd,
+        inih_dependency,
     ],
     include_directories: [
         config_h_dir,

+ 21 - 0
data/gamemoded.1

@@ -66,6 +66,27 @@ Atlernatively developers can define \fBGAMEMODE_AUTO\fR to mimic the behaviour o
 
 Or, distribute \fBlibgamemodeauto.so\fR and either link with \fB\-lgamemodeauto\fR or inject it as above with \fBLD\_PRELOAD\fR.
 
+.SH CONFIG
+
+\fBgamemoded\fR can be configured with a \fBgamemode.ini\fR file found in \fB/usr/share/gamemode/\fR. The daemon will load the config file on start-up if it exists.
+
+Behaviour of the config file can be explained by presenting a commented example:
+
+.RS 4
+.nf
+[filter]
+; If "whitelist" entry has a value(s)
+; gamemode will reject anything not in the whitelist
+;whitelist=RiseOfTheTombRaider
+
+; Gamemode will always reject anything in the blacklist
+blacklist=HalfLife3
+    glxgears
+.fi
+.RE
+
+This config file will currently reject any games that match \fIHalfLife3\fR or \fIglxgears\fR, but can be modified to only accept \fIRiseOfTheTombRaider\fR by removing the semicolon preceding the fourth line.
+
 .SH SEE ALSO
 systemd(1)
 

+ 9 - 0
example/gamemode.ini

@@ -0,0 +1,9 @@
+[filter]
+; Gamemode will always reject anything in the blacklist
+blacklist=HalfLife3
+    glxgears
+    steam
+
+; If "whitelist" entry has a value(s)
+; gamemode will reject anything not in the whitelist
+;whitelist=RiseOfTheTombRaider

+ 4 - 0
meson.build

@@ -82,6 +82,10 @@ subdir('lib')
 # The daemon can be disabled if necessary, allowing multilib builds of the
 # main library
 if with_daemon == true
+    # inih currently only needed by the daemon
+    inih = subproject('inih')
+    inih_dependency = inih.get_variable('inih_dependency')
+
     subdir('daemon')
 
     # All installed data is currently daemon specific

+ 5 - 0
subprojects/inih.wrap

@@ -0,0 +1,5 @@
+[wrap-git]
+directory=inih
+url=https://github.com/FeralInteractive/inih.git
+revision=head
+