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)
This commit is contained in:
Marc Di Luzio
2018-03-23 13:30:25 +00:00
parent 142246366f
commit 759cbc3c40
10 changed files with 388 additions and 0 deletions

1
.gitignore vendored
View File

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

View File

@ -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
daemon/daemon_config.c Normal file
View File

@ -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
daemon/daemon_config.h Normal file
View File

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

View File

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

View File

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

View File

@ -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
example/gamemode.ini Normal file
View File

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

View File

@ -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
subprojects/inih.wrap Normal file
View File

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