Bladeren bron

Transform into a full D-BUS service with Polkit support

Primarily we convert the service into a thread safe one that isn't reliant
on signaling for control flow, eliminating data race conditions. We also
enable interleaving by separating game mode pivoting from explicit client
registration.

The static pid list is now converted into a dynamic list that is OOM safe
to store all registered clients (with a reasonable upper limit of 256 clients)
to better handle cases where LD_PRELOAD is used for a large process group.
Additionally we begin storing some metadata on the connected clients such
as their executable path, which will enable us to perform some basic
whitelisting in future.

The cpugovctl binary is now moved into the libexecdir as an explicit helper
of the D-BUS service, using the shared library to merge some code back into
the daemon. This saves having to execute a process to query the state of the
governors, as we don't need a privileged client to do this.

In order to sanely set the governors, we require that the binary is running
as euid 0, and execute this using `pkexec`. A PolKit policy definition is
provided which allows active/logged in users to execute this helper through
a path whitelist. As such we can convert the daemon into user-mode only, with
the privileged helper being dispatched exclusively via polkit. This removes
the need for a setuid helper or having a system mode daemon.

Lastly we clean up the codebase a bit to be consistent with modern C code
conventions, using pragmas where available. The library component still uses
the older ifdef approach to support older compilers, but the daemon portion
uses the directive to simplify intent and speed up compilation. Additionally
we move all comments to C style comments for consistency, instead of mixing
in C++ style single line comments, in order to establish a formal coding
style.

The net result is a more robust service which can be D-BUS activated when
clients need it, that can perform scaling automatically without harassing
the user with authentication popups.

Signed-off-by: Ikey Doherty <ikey@solus-project.com>
Ikey Doherty 7 jaren geleden
bovenliggende
commit
68e326de60

+ 18 - 110
daemon/cpugovctl.c

@@ -31,118 +31,15 @@ POSSIBILITY OF SUCH DAMAGE.
 
 #define _GNU_SOURCE
 
+#include "governors-query.h"
 #include "logging.h"
 
 #include <ctype.h>
-#include <dirent.h>
-#include <linux/limits.h>
 #include <sys/types.h>
 
-#define MAX_GOVERNORS 128
-#define MAX_GOVERNOR_LENGTH PATH_MAX + 1
-
-// Governers are located at /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
-static int fetch_governors(char governors[MAX_GOVERNORS][MAX_GOVERNOR_LENGTH])
-{
-	const char *cpu_base_path = "/sys/devices/system/cpu/";
-	DIR *dir = opendir(cpu_base_path);
-	if (!dir) {
-		FATAL_ERRORNO("cpu device path not found");
-	}
-
-	int num_governors = 0;
-
-	// Explore the directory
-	struct dirent *ent;
-	while ((ent = readdir(dir)) && num_governors < MAX_GOVERNORS) {
-		// CPU directories all start with "cpu"
-		if (strncmp(ent->d_name, "cpu", 3) == 0) {
-			// Check if this matches "cpu\d+"
-			const int len = strlen(ent->d_name);
-			if (len > 3 && len < 5 && isdigit(ent->d_name[3])) {
-				// Construct the full path
-				char path[PATH_MAX] = {};
-				snprintf(path,
-				         sizeof(path),
-				         "%s%s/cpufreq/scaling_governor",
-				         cpu_base_path,
-				         ent->d_name);
-
-				// Get the real path to the file
-				// Traditionally cpufreq symlinks to a policy directory that can be
-				// shared So let's prevent duplicates
-				char fullpath[PATH_MAX] = {};
-				const char *ptr = realpath(path, fullpath);
-				if (fullpath != ptr) {
-					continue;
-				}
-
-				// Only add if unique
-				for (int i = 0; i < num_governors; i++) {
-					if (strncmp(fullpath, governors[i], MAX_GOVERNOR_LENGTH) == 0) {
-						continue;
-					}
-				}
-
-				strncpy(governors[num_governors], fullpath, MAX_GOVERNOR_LENGTH);
-				num_governors++;
-			}
-		}
-	}
-	closedir(dir);
-
-	return num_governors;
-}
-
-// Get the current governor state
-const char *get_gov_state()
-{
-	// To be returned
-	static char governor[64] = { 0 };
-
-	// State of all the overnors
-	char governors[MAX_GOVERNORS][MAX_GOVERNOR_LENGTH] = { { 0 } };
-	int num = fetch_governors(governors);
-
-	// Check the list
-	for (int i = 0; i < num; i++) {
-		const char *gov = governors[i];
-
-		FILE *f = fopen(gov, "r");
-		if (!f) {
-			LOG_ERROR("Failed to open file for read %s\n", gov);
-			continue;
-		}
-
-		// Pull out the file contents
-		fseek(f, 0, SEEK_END);
-		int length = ftell(f);
-		fseek(f, 0, SEEK_SET);
-
-		char contents[length];
-
-		if (fread(contents, 1, length, f) > 0) {
-			// Files have a newline
-			strtok(contents, "\n");
-			if (strlen(governor) > 0 && strncmp(governor, contents, 64) != 0) {
-				// Don't handle the mixed case, this shouldn't ever happen
-				// But it is a clear sign we shouldn't carry on
-				LOG_ERROR("Governors malformed: got \"%s\", expected \"%s\"", contents, governor);
-				return "malformed";
-			}
-
-			strncpy(governor, contents, sizeof(governor));
-		} else {
-			LOG_ERROR("Failed to read contents of %s\n", gov);
-		}
-
-		fclose(f);
-	}
-
-	return governor;
-}
-
-// Sets all governors to a value
+/**
+ * Sets all governors to a value
+ */
 void set_gov_state(const char *value)
 {
 	char governors[MAX_GOVERNORS][MAX_GOVERNOR_LENGTH] = { { 0 } };
@@ -162,20 +59,31 @@ void set_gov_state(const char *value)
 	}
 }
 
-// Main entry point
+/**
+ * Main entry point, dispatch to the appropriate helper
+ */
 int main(int argc, char *argv[])
 {
 	if (argc < 2) {
 		fprintf(stderr, "usage: cpugovctl [get] [set VALUE]\n");
-		exit(EXIT_FAILURE);
+		return EXIT_FAILURE;
 	}
 
 	if (strncmp(argv[1], "get", 3) == 0) {
 		printf("%s", get_gov_state());
 	} else if (strncmp(argv[1], "set", 3) == 0) {
 		const char *value = argv[2];
+
+		/* Must be root to set the state */
+		if (geteuid() != 0) {
+			fprintf(stderr, "This program must be run as root\n");
+			return EXIT_FAILURE;
+		}
+
 		set_gov_state(value);
 	} else {
-		exit(EXIT_FAILURE);
+		return EXIT_FAILURE;
 	}
+
+	return EXIT_SUCCESS;
 }

+ 6 - 4
daemon/daemonize.c

@@ -36,10 +36,12 @@ POSSIBILITY OF SUCH DAMAGE.
 #include <sys/types.h>
 #include <unistd.h>
 
-// function to daemonize the process
+/**
+ * Helper to perform standard UNIX daemonization
+ */
 void daemonize(char *name)
 {
-	// Fork once
+	/* Initial fork */
 	pid_t pid = fork();
 	if (pid < 0) {
 		FATAL_ERRORNO("Failed to fork");
@@ -50,7 +52,7 @@ void daemonize(char *name)
 		exit(EXIT_SUCCESS);
 	}
 
-	// Fork a second time
+	/* Fork a second time */
 	pid = fork();
 	if (pid < 0) {
 		FATAL_ERRORNO("Failed to fork");
@@ -58,7 +60,7 @@ void daemonize(char *name)
 		exit(EXIT_SUCCESS);
 	}
 
-	// Continue to set up as a daemon
+	/* Now continue execution */
 	umask(0);
 	if (setsid() < 0) {
 		FATAL_ERRORNO("Failed to create process group\n");

+ 6 - 6
daemon/daemonize.h

@@ -28,11 +28,11 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
  */
-#ifndef _DAEMONIZE_GAMEMODE_H_
-#define _DAEMONIZE_GAMEMODE_H_
 
-// Function to daemonize the process
-// Exits with error in case of failure
-void daemonize(char *name);
+#pragma once
 
-#endif // _DAEMONIZE_GAMEMODE_H_
+/**
+ * Attempt daemonization of the process.
+ * If this fails, the process will exit
+ */
+void daemonize(char *name);

+ 31 - 22
daemon/dbus_messaging.c

@@ -28,6 +28,7 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
  */
+
 #include "dbus_messaging.h"
 #include "daemonize.h"
 #include "gamemode.h"
@@ -38,11 +39,13 @@ POSSIBILITY OF SUCH DAMAGE.
 
 #include <systemd/sd-bus.h>
 
-// sd-bus tracker values
+/* systemd dbus components */
 static sd_bus *bus = NULL;
 static sd_bus_slot *slot = NULL;
 
-// Clean up any resources as needed
+/**
+ * Clean up our private dbus state
+ */
 static void clean_up()
 {
 	if (slot) {
@@ -55,10 +58,13 @@ static void clean_up()
 	bus = NULL;
 }
 
-// Callback for RegisterGame
+/**
+ * Handles the RegisterGame D-BUS Method
+ */
 static int method_register_game(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
 {
 	int pid = 0;
+	GameModeContext *context = userdata;
 
 	int ret = sd_bus_message_read(m, "i", &pid);
 	if (ret < 0) {
@@ -66,15 +72,18 @@ static int method_register_game(sd_bus_message *m, void *userdata, sd_bus_error
 		return ret;
 	}
 
-	register_game(pid);
+	game_mode_context_register(context, (pid_t)pid);
 
 	return sd_bus_reply_method_return(m, "i", 0);
 }
 
-// Callback for UnregisterGame
+/**
+ * Handles the UnregisterGame D-BUS Method
+ */
 static int method_unregister_game(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
 {
 	int pid = 0;
+	GameModeContext *context = userdata;
 
 	int ret = sd_bus_message_read(m, "i", &pid);
 	if (ret < 0) {
@@ -82,49 +91,49 @@ static int method_unregister_game(sd_bus_message *m, void *userdata, sd_bus_erro
 		return ret;
 	}
 
-	unregister_game(pid);
+	game_mode_context_unregister(context, (pid_t)pid);
 
 	return sd_bus_reply_method_return(m, "i", 0);
 }
 
-// Vtable for function dispatch
+/**
+ * D-BUS vtable to dispatch virtual methods
+ */
 static const sd_bus_vtable gamemode_vtable[] =
     { SD_BUS_VTABLE_START(0),
 	  SD_BUS_METHOD("RegisterGame", "i", "i", method_register_game, SD_BUS_VTABLE_UNPRIVILEGED),
 	  SD_BUS_METHOD("UnregisterGame", "i", "i", method_unregister_game, SD_BUS_VTABLE_UNPRIVILEGED),
 	  SD_BUS_VTABLE_END };
 
-// Main loop, will not return until something request a quit
-void run_dbus_main_loop(bool system_dbus)
+/**
+ * Main process loop for the daemon. Run until quitting has been requested.
+ */
+void game_mode_context_loop(GameModeContext *context)
 {
-	// Set up function to handle clean up of resources
+	/* Set up function to handle clean up of resources */
 	atexit(clean_up);
 	int ret = 0;
 
-	// Connec to the desired bus
-	if (system_dbus) {
-		ret = sd_bus_open_system(&bus);
-	} else {
-		ret = sd_bus_open_user(&bus);
-	}
+	/* Connect to the session bus */
+	ret = sd_bus_open_user(&bus);
 
 	if (ret < 0) {
 		FATAL_ERROR("Failed to connect to the bus: %s", strerror(-ret));
 	}
 
-	// Create the object to allow connections
+	/* Create the object to allow connections */
 	ret = sd_bus_add_object_vtable(bus,
 	                               &slot,
 	                               "/com/feralinteractive/GameMode",
 	                               "com.feralinteractive.GameMode",
 	                               gamemode_vtable,
-	                               NULL);
+	                               context);
 
 	if (ret < 0) {
 		FATAL_ERROR("Failed to install GameMode object: %s", strerror(-ret));
 	}
 
-	// Request our name
+	/* Request our name */
 	ret = sd_bus_request_name(bus, "com.feralinteractive.GameMode", 0);
 	if (ret < 0) {
 		FATAL_ERROR("Failed to acquire service name: %s", strerror(-ret));
@@ -132,19 +141,19 @@ void run_dbus_main_loop(bool system_dbus)
 
 	LOG_MSG("Successfully initialised bus with name [%s]...\n", "com.feralinteractive.GameMode");
 
-	// Now loop, waiting for callbacks
+	/* Now loop, waiting for callbacks */
 	for (;;) {
 		ret = sd_bus_process(bus, NULL);
 		if (ret < 0) {
 			FATAL_ERROR("Failure when processing the bus: %s", strerror(-ret));
 		}
 
-		// We're done processing
+		/* We're done processing */
 		if (ret > 0) {
 			continue;
 		}
 
-		// Wait for more
+		/* Wait for more */
 		ret = sd_bus_wait(bus, (uint64_t)-1);
 		if (ret < 0 && -ret != EINTR) {
 			FATAL_ERROR("Failure when waiting on bus: %s", strerror(-ret));

+ 7 - 6
daemon/dbus_messaging.h

@@ -28,13 +28,14 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
  */
-#ifndef _DBUS_MESSAGING_GAMEMODE_H_
-#define _DBUS_MESSAGING_GAMEMODE_H_
+
+#pragma once
 
 #include <stdbool.h>
 
-// Run the main dbus loop
-// Will not return until finished
-void run_dbus_main_loop(bool system_dbus);
+#include "gamemode.h"
 
-#endif // _DBUS_MESSAGING_GAMEMODE_H_
+/**
+ * Run the main D-BUS loop "forever"
+ */
+void game_mode_context_loop(GameModeContext *context);

+ 318 - 89
daemon/gamemode.c

@@ -35,142 +35,371 @@ POSSIBILITY OF SUCH DAMAGE.
 #include "governors.h"
 #include "logging.h"
 
+#include <linux/limits.h>
+#include <pthread.h>
 #include <signal.h>
+#include <stdatomic.h>
 #include <string.h>
 
-// Storage for game PIDs
-#define MAX_GAMES 16
-static int game_pids[MAX_GAMES];
-static int num_games = 0;
+/**
+ * The GameModeClient encapsulates the remote connection, providing a list
+ * form to contain the pid and credentials.
+ */
+typedef struct GameModeClient {
+	pid_t pid;                   /**< Process ID */
+	struct GameModeClient *next; /**<Next client in the list */
+	char *executable;            /**<Process executable */
+} GameModeClient;
 
-// Constant to control how often we'll wake up and check process ids
-static const int wakeup_timer = 5;
-static void start_alarm_timer();
+struct GameModeContext {
+	pthread_rwlock_t rwlock; /**<Guard access to the client list */
+	_Atomic int refcount;    /**<Allow cycling the game mode */
+	GameModeClient *client;  /**<Pointer to first client */
 
-// Called once to enter game mode
-// Must be followed by a call to leave_game_mode
-// Must be called after init_game_mode
-static void enter_game_mode()
-{
-	LOG_MSG("Entering Game Mode...\n");
-	set_governors("performance");
+	bool performance_mode; /**<Only updates when we can */
 
-	// Set up the alarm callback
-	start_alarm_timer();
-}
+	/* Reaper control */
+	struct {
+		pthread_t thread;
+		bool running;
+		pthread_mutex_t mutex;
+		pthread_cond_t condition;
+	} reaper;
+};
 
-// Called once to leave game mode
-// Must be called after an equivelant call to enter_game_mode
-static void leave_game_mode()
-{
-	LOG_MSG("Leaving Game Mode...\n");
-	set_governors(NULL);
-}
+static GameModeContext instance = { 0 };
 
-// Set up an alarm callback to check for current process ids
-static void alarm_handler(int sig)
+/* Maximum number of concurrent processes we'll sanely support */
+#define MAX_GAMES 256
+
+/* How often our reaper thread will run */
+#define SLEEP_INTERVAL 5
+
+/**
+ * Protect against signals
+ */
+static volatile bool had_context_init = false;
+
+static GameModeClient *game_mode_client_new(pid_t pid);
+static void game_mode_client_free(GameModeClient *client);
+static bool game_mode_context_has_client(GameModeContext *self, pid_t client);
+static int game_mode_context_num_clients(GameModeContext *self);
+static void *game_mode_context_reaper(void *userdata);
+static void game_mode_context_enter(GameModeContext *self);
+static void game_mode_context_leave(GameModeContext *self);
+static char *game_mode_context_find_exe(pid_t pid);
+
+void game_mode_context_init(GameModeContext *self)
 {
-	// Quick return if no games, and don't register another callback
-	if (num_games == 0) {
+	if (had_context_init) {
+		LOG_ERROR("Context already initialised\n");
 		return;
 	}
+	had_context_init = true;
+	self->refcount = ATOMIC_VAR_INIT(0);
+
+	/* 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());
 
-	// Check if games are alive at all
-	for (int i = 0; i < num_games;) {
-		int game = game_pids[i];
+	pthread_mutex_init(&self->reaper.mutex, NULL);
+	pthread_cond_init(&self->reaper.condition, NULL);
 
-		if (kill(game, 0) != 0) {
-			LOG_MSG("Removing expired game [%i]...\n", game);
-			memmove(&game_pids[i], &game_pids[i + 1], MAX_GAMES - (i + 1));
-			num_games--;
-		} else {
-			i++;
-		}
+	/* Get the reaper thread going */
+	self->reaper.running = true;
+	if (pthread_create(&self->reaper.thread, NULL, game_mode_context_reaper, self) != 0) {
+		FATAL_ERROR("Couldn't construct a new thread");
 	}
+}
 
-	// Either trigger another alarm, or reset the governors
-	if (num_games) {
-		start_alarm_timer();
-	} else {
-		leave_game_mode();
+void game_mode_context_destroy(GameModeContext *self)
+{
+	if (!had_context_init) {
+		return;
 	}
+
+	/* Leave game mode now */
+	if (game_mode_context_num_clients(self) > 0) {
+		game_mode_context_leave(self);
+	}
+
+	had_context_init = false;
+	game_mode_client_free(self->client);
+	self->reaper.running = false;
+
+	/* We might be stuck waiting, so wake it up again */
+	pthread_mutex_lock(&self->reaper.mutex);
+	pthread_cond_signal(&self->reaper.condition);
+	pthread_mutex_unlock(&self->reaper.mutex);
+
+	/* Join the thread as soon as possible */
+	pthread_join(self->reaper.thread, NULL);
+
+	pthread_cond_destroy(&self->reaper.condition);
+	pthread_mutex_destroy(&self->reaper.mutex);
+
+	pthread_rwlock_destroy(&self->rwlock);
 }
 
-// Call to trigger starting the alarm timer for pid checks
-static void start_alarm_timer()
+/**
+ * Pivot into game mode.
+ *
+ * This is only possible after game_mode_context_init has made a GameModeContext
+ * usable, and should always be followed by a game_mode_context_leave.
+ */
+static void game_mode_context_enter(GameModeContext *self)
 {
-	// Set up process check alarms
-	signal(SIGALRM, alarm_handler);
-	alarm(wakeup_timer);
+	LOG_MSG("Entering Game Mode...\n");
+	if (!self->performance_mode && set_governors("performance")) {
+		self->performance_mode = true;
+	}
 }
 
-// Intialise any game mode needs
-void init_game_mode()
+/**
+ * Pivot out of game mode.
+ *
+ * Should only be called after both init and game_mode_context_enter have
+ * been performed.
+ */
+static void game_mode_context_leave(GameModeContext *self)
 {
-	// 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());
+	LOG_MSG("Leaving Game Mode...\n");
+	if (self->performance_mode && set_governors(NULL)) {
+		self->performance_mode = false;
+	}
 }
 
-// Called on exit to clean up the governors if appropriate
-void term_game_mode()
+/**
+ * Automatically expire all dead processes
+ *
+ * This has to take special care to ensure thread safety and ensuring that our
+ * pointer is never cached incorrectly.
+ */
+static void game_mode_context_auto_expire(GameModeContext *self)
 {
-	if (num_games) {
-		leave_game_mode();
+	bool removing = true;
+
+	while (removing) {
+		pthread_rwlock_rdlock(&self->rwlock);
+		removing = false;
+
+		/* Each time we hit an expired game, start the loop back */
+		for (GameModeClient *client = self->client; client; client = client->next) {
+			if (kill(client->pid, 0) != 0) {
+				LOG_MSG("Removing expired game [%i]...\n", client->pid);
+				pthread_rwlock_unlock(&self->rwlock);
+				game_mode_context_unregister(self, client->pid);
+				removing = true;
+				break;
+			}
+		}
+
+		if (!removing) {
+			pthread_rwlock_unlock(&self->rwlock);
+			break;
+		}
 	}
 }
 
-// Register a game pid with the game mode
-// Will trigger enter game mode if appropriate
-void register_game(int pid)
+/**
+ * Determine if the client is already known to the context
+ */
+static bool game_mode_context_has_client(GameModeContext *self, pid_t client)
 {
-	// Check for duplicates
-	for (int i = 0; i < num_games; i++) {
-		if (game_pids[i] == pid) {
-			LOG_ERROR("Addition requested for already known process [%i]\n", pid);
-			return;
+	bool found = false;
+	pthread_rwlock_rdlock(&self->rwlock);
+
+	/* Walk all clients and find a matching pid */
+	for (GameModeClient *cl = self->client; cl; cl = cl->next) {
+		if (cl->pid == client) {
+			found = true;
+			break;
 		}
 	}
 
-	// Check we've not already hit max
-	if (num_games == MAX_GAMES) {
-		LOG_ERROR("Max games (%i) reached, could not add [%i]\n", MAX_GAMES, pid);
-		return;
+	pthread_rwlock_unlock(&self->rwlock);
+	return found;
+}
+
+/**
+ * Helper to grab the current number of clients we know about
+ */
+static int game_mode_context_num_clients(GameModeContext *self)
+{
+	return atomic_load(&self->refcount);
+}
+
+bool game_mode_context_register(GameModeContext *self, pid_t client)
+{
+	/* Construct a new client if we can */
+	GameModeClient *cl = NULL;
+
+	cl = game_mode_client_new(client);
+	if (!cl) {
+		fputs("OOM\n", stderr);
+		return false;
 	}
+	cl->executable = game_mode_context_find_exe(client);
 
-	// Add the game to the database
-	LOG_MSG("Adding game: %i\n", pid);
-	game_pids[num_games] = pid;
-	num_games++;
+	if (game_mode_context_has_client(self, client)) {
+		LOG_ERROR("Addition requested for already known process [%d]\n", client);
+		return false;
+	}
 
-	if (num_games == 1) {
-		enter_game_mode();
+	/* Cap the total number of active clients */
+	if (game_mode_context_num_clients(self) + 1 > MAX_GAMES) {
+		LOG_ERROR("Max games (%d) reached, not registering %d\n", MAX_GAMES, client);
+		return false;
 	}
+
+	/* Begin a write lock now to insert our new client at list start */
+	pthread_rwlock_wrlock(&self->rwlock);
+
+	LOG_MSG("Adding game: %d [%s]\n", client, cl->executable);
+
+	/* Update the list */
+	cl->next = self->client;
+	self->client = cl;
+	pthread_rwlock_unlock(&self->rwlock);
+
+	/* First add, init */
+	if (atomic_fetch_add_explicit(&self->refcount, 1, memory_order_seq_cst) == 0) {
+		game_mode_context_enter(self);
+	}
+
+	return true;
 }
 
-// Remove a game from game mode
-// Will exit game mode if appropriate
-void unregister_game(int pid)
+bool game_mode_context_unregister(GameModeContext *self, pid_t client)
 {
+	GameModeClient *cl = NULL;
+	GameModeClient *prev = NULL;
 	bool found = false;
 
-	// Check list even contains this entry
-	for (int i = 0; i < num_games; i++) {
-		if (game_pids[i] == pid) {
-			LOG_MSG("Removing game: %i\n", pid);
-			memmove(&game_pids[i], &game_pids[i + 1], MAX_GAMES - (i + 1));
-			num_games--;
-			found = true;
+	/* Requires locking. */
+	pthread_rwlock_wrlock(&self->rwlock);
+
+	for (prev = cl = self->client; cl; cl = cl->next) {
+		if (cl->pid != client) {
+			prev = cl;
+			continue;
 		}
+
+		LOG_MSG("Removing game: %d [%s]\n", client, cl->executable);
+
+		/* Found it */
+		found = true;
+		prev->next = cl->next;
+		if (cl == self->client) {
+			self->client = cl->next;
+		}
+		cl->next = NULL;
+		game_mode_client_free(cl);
+		break;
 	}
 
+	/* Unlock here, potentially yielding */
+	pthread_rwlock_unlock(&self->rwlock);
+
 	if (!found) {
-		LOG_ERROR("Removal requested for unknown process [%i]\n", pid);
+		LOG_ERROR("Removal requested for unknown process [%d]\n", client);
+		return false;
+	}
+
+	/* When we hit bottom then end the game mode */
+	if (atomic_fetch_sub_explicit(&self->refcount, 1, memory_order_seq_cst) == 1) {
+		game_mode_context_leave(self);
+	}
+
+	return true;
+}
+
+/**
+ * Construct a new GameModeClient for the given process ID
+ *
+ * This is deliberately OOM safe
+ */
+static GameModeClient *game_mode_client_new(pid_t pid)
+{
+	GameModeClient c = {
+		.next = NULL,
+		.pid = pid,
+	};
+	GameModeClient *ret = NULL;
+
+	ret = calloc(1, sizeof(struct GameModeClient));
+	if (!ret) {
+		return NULL;
+	}
+	*ret = c;
+	return ret;
+}
+
+/**
+ * Free a client and the next element in the list.
+ */
+static void game_mode_client_free(GameModeClient *client)
+{
+	if (!client) {
 		return;
 	}
+	if (client->next) {
+		game_mode_client_free(client->next);
+	}
+	if (client->executable) {
+		free(client->executable);
+	}
+	free(client);
+}
+
+/**
+ * We continuously run until told otherwise.
+ */
+static void *game_mode_context_reaper(void *userdata)
+{
+	/* Stack, not allocated, won't disappear. */
+	GameModeContext *self = userdata;
+	struct timespec ts = { 0, 0 };
+	ts.tv_sec = time(NULL) + SLEEP_INTERVAL;
+
+	while (self->reaper.running) {
+		/* Wait for condition */
+		pthread_mutex_lock(&self->reaper.mutex);
+		pthread_cond_timedwait(&self->reaper.condition, &self->reaper.mutex, &ts);
+		pthread_mutex_unlock(&self->reaper.mutex);
+
+		/* Highly possible the main thread woke us up to exit */
+		if (!self->reaper.running) {
+			return NULL;
+		}
 
-	// Leave game mode if needed
-	if (num_games == 0) {
-		leave_game_mode();
+		/* Expire remaining entries */
+		game_mode_context_auto_expire(self);
+
+		ts.tv_sec = time(NULL) + SLEEP_INTERVAL;
+	}
+
+	return NULL;
+}
+
+GameModeContext *game_mode_context_instance()
+{
+	return &instance;
+}
+
+/**
+ * Attempt to locate the exe for the process.
+ * We might run into issues if the process is running under an odd umask.
+ */
+static char *game_mode_context_find_exe(pid_t pid)
+{
+	static char proc_path[PATH_MAX] = { 0 };
+
+	if (snprintf(proc_path, sizeof(proc_path), "/proc/%d/exe", pid) < 0) {
+		LOG_ERROR("Unable to find executable for PID %d: %s\n", pid, strerror(errno));
+		return NULL;
 	}
+
+	/* Allocate the realpath if possible */
+	return realpath(proc_path, NULL);
 }

+ 42 - 10
daemon/gamemode.h

@@ -28,16 +28,48 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
  */
-#ifndef _GAME_MODE_GAMEMODE_H_
-#define _GAME_MODE_GAMEMODE_H_
 
-// Initialise or terminate the game mode system
-void init_game_mode();
-void term_game_mode();
+#pragma once
 
-// Add or remove games to the tracker
-// Tracker will automatically start and stop game mode as appropriate
-void register_game(int pid);
-void unregister_game(int pid);
+#include <stdbool.h>
+#include <sys/types.h>
 
-#endif // _GAME_MODE_GAMEMODE_H_
+/**
+ * Opaque context
+ */
+typedef struct GameModeContext GameModeContext;
+
+/**
+ * Return the singleton instance
+ */
+GameModeContext *game_mode_context_instance(void);
+
+/**
+ * Initialise the GameModeContext
+ *
+ * This is performed in a thread-safe fashion.
+ */
+void game_mode_context_init(GameModeContext *self);
+
+/**
+ * Destroy the previously initialised GameModeContext.
+ *
+ * This is performed in a thread safe fashion.
+ */
+void game_mode_context_destroy(GameModeContext *self);
+
+/**
+ * Register a new game client with the context
+ *
+ * @param pid Process ID for the remote client
+ * @returns True if the new client could be registered
+ */
+bool game_mode_context_register(GameModeContext *self, pid_t pid);
+
+/**
+ * Unregister an existing remote game client from the context
+ *
+ * @param pid Process ID for the remote client
+ * @returns True if the client was removed, and existed.
+ */
+bool game_mode_context_unregister(GameModeContext *self, pid_t pid);

+ 143 - 0
daemon/governors-query.c

@@ -0,0 +1,143 @@
+/*
+
+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 "governors-query.h"
+#include "logging.h"
+
+#include <glob.h>
+#include <stdio.h>
+#include <string.h>
+
+/**
+ * Discover all governers on the system.
+ *
+ * Located at /sys/devices/system/cpu/cpu(*)/cpufreq/scaling_governor
+ */
+int fetch_governors(char governors[MAX_GOVERNORS][MAX_GOVERNOR_LENGTH])
+{
+	glob_t glo = { 0 };
+	static const char *path = "/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor";
+
+	/* Assert some sanity on this glob */
+	if (glob(path, GLOB_NOSORT, NULL, &glo) != 0) {
+		FATAL_ERRORNO("Broken glob implementation");
+	}
+
+	if (glo.gl_pathc < 1) {
+		globfree(&glo);
+		FATAL_ERROR("cpu device path not found");
+	}
+
+	int num_governors = 0;
+
+	/* Walk the glob set */
+	for (size_t i = 0; i < glo.gl_pathc; i++) {
+		if (i >= MAX_GOVERNORS) {
+			break;
+		}
+
+		/* Get the real path to the file.
+		 * Traditionally cpufreq symlinks to a policy directory that can
+		 * be shared, so let's prevent duplicates.
+		 */
+		char fullpath[PATH_MAX] = { 0 };
+		const char *ptr = realpath(glo.gl_pathv[i], fullpath);
+		if (fullpath != ptr) {
+			continue;
+		}
+
+		/* Only add this governor if it is unique */
+		for (int i = 0; i < num_governors; i++) {
+			if (strncmp(fullpath, governors[i], MAX_GOVERNOR_LENGTH) == 0) {
+				continue;
+			}
+		}
+
+		/* Copy this governor into the output set */
+		strncpy(governors[num_governors], fullpath, MAX_GOVERNOR_LENGTH);
+		num_governors++;
+	}
+
+	globfree(&glo);
+	return num_governors;
+}
+
+/**
+ * Return the current governor state
+ */
+const char *get_gov_state()
+{
+	/* Cached primary governor state */
+	static char governor[64] = { 0 };
+
+	/* State for all governors */
+	char governors[MAX_GOVERNORS][MAX_GOVERNOR_LENGTH] = { { 0 } };
+	int num = fetch_governors(governors);
+
+	/* Check the list */
+	for (int i = 0; i < num; i++) {
+		const char *gov = governors[i];
+
+		FILE *f = fopen(gov, "r");
+		if (!f) {
+			LOG_ERROR("Failed to open file for read %s\n", gov);
+			continue;
+		}
+
+		/* Grab the file length */
+		fseek(f, 0, SEEK_END);
+		int length = ftell(f);
+		fseek(f, 0, SEEK_SET);
+
+		char contents[length];
+
+		if (fread(contents, 1, length, f) > 0) {
+			/* Files have a newline */
+			strtok(contents, "\n");
+			if (strlen(governor) > 0 && strncmp(governor, contents, 64) != 0) {
+				/* Don't handle the mixed case, this shouldn't ever happen
+				 * But it is a clear sign we shouldn't carry on */
+				LOG_ERROR("Governors malformed: got \"%s\", expected \"%s\"", contents, governor);
+				return "malformed";
+			}
+
+			strncpy(governor, contents, sizeof(governor));
+		} else {
+			LOG_ERROR("Failed to read contents of %s\n", gov);
+		}
+
+		fclose(f);
+	}
+
+	return governor;
+}

+ 47 - 0
daemon/governors-query.h

@@ -0,0 +1,47 @@
+/*
+
+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 <linux/limits.h>
+
+#define MAX_GOVERNORS 128
+#define MAX_GOVERNOR_LENGTH PATH_MAX + 1
+
+/**
+ * Grab all of the governors
+ */
+int fetch_governors(char governors[MAX_GOVERNORS][MAX_GOVERNOR_LENGTH]);
+
+/**
+ * Get the current governor state
+ */
+const char *get_gov_state();

+ 51 - 28
daemon/governors.c

@@ -32,51 +32,74 @@ POSSIBILITY OF SUCH DAMAGE.
 #define _GNU_SOURCE
 
 #include "governors.h"
+#include "config.h"
+#include "governors-query.h"
 #include "logging.h"
 
 #include <linux/limits.h>
 #include <stdio.h>
+#include <sys/wait.h>
 #include <unistd.h>
 
-static char initial[32];
+static const char *initial = NULL;
 
-// Store the initial governor state to be referenced later
+/**
+ * Cache the governor state as seen at startup
+ */
 void update_initial_gov_state()
 {
-	static char *command = "cpugovctl get";
-
-	FILE *f = popen(command, "r");
-	if (!f) {
-		FATAL_ERRORNO("Failed to launch \"%s\" script", command);
-	}
-
-	if (!fgets(initial, sizeof(initial) - 1, f)) {
-		FATAL_ERROR("Failed to get output from \"%s\"", command);
-	}
-
-	pclose(f);
-
-	strtok(initial, "\n");
+	initial = get_gov_state();
 }
 
-// Sets all governors to a value, if NULL argument provided, will reset them back
-void set_governors(const char *value)
+/**
+ * Update the governors to the given argument, via pkexec
+ */
+bool set_governors(const char *value)
 {
-	const char *newval = value ? value : initial;
-	LOG_MSG("Setting governors to %s\n", newval ? newval : "initial values");
-
-	char command[PATH_MAX] = {};
-	snprintf(command, sizeof(command), "cpugovctl set %s", newval);
+	pid_t p;
+	int status = 0;
+	int ret = 0;
+	int r = -1;
+
+	const char *govern = value ? value : initial;
+	char *exec_args[] = {
+		"/usr/bin/pkexec", LIBEXECDIR "/cpugovctl", "set", (char *)govern, NULL,
+	};
+
+	LOG_MSG("Requesting update of governor policy to %s\n", govern);
+
+	if ((p = fork()) < 0) {
+		LOG_ERROR("Failed to fork(): %s\n", strerror(errno));
+		return false;
+	} else if (p == 0) {
+		/* Execute the command */
+		if ((r = execv(exec_args[0], exec_args)) != 0) {
+			LOG_ERROR("Failed to execute cpugovctl helper: %s %s\n", exec_args[1], strerror(errno));
+			exit(EXIT_FAILURE);
+		}
+		_exit(EXIT_SUCCESS);
+	} else {
+		if (waitpid(p, &status, 0) < 0) {
+			LOG_ERROR("Failed to waitpid(%d): %s\n", (int)p, strerror(errno));
+			return false;
+		}
+		/* i.e. sigsev */
+		if (!WIFEXITED(status)) {
+			LOG_ERROR("Child process '%s' exited abnormally\n", exec_args[0]);
+		}
+	}
 
-	FILE *f = popen(command, "r");
-	if (!f) {
-		FATAL_ERRORNO("Failed to launch %s script", command);
+	if ((ret = WEXITSTATUS(status)) != 0) {
+		LOG_ERROR("Failed to update cpu governor policy\n");
+		return false;
 	}
 
-	pclose(f);
+	return true;
 }
 
-// Return the initial governor
+/**
+ * Return the cached governor seen at startup
+ */
 const char *get_initial_governor()
 {
 	return initial;

+ 15 - 8
daemon/governors.h

@@ -28,16 +28,23 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
  */
-#ifndef _GOVERNORS_GAMEMODE_H_
-#define _GOVERNORS_GAMEMODE_H_
 
-// Store the initial governor state to be referenced later
+#pragma once
+
+#include <stdbool.h>
+
+/**
+ * Store initial governor so we can use it again
+ */
 void update_initial_gov_state();
 
-// Get the initial governor state
+/**
+ * Return the governer set in update_initial_gov_state
+ */
 const char *get_initial_governor();
 
-// Sets all governors to a value, if null argument provided, will reset them back
-void set_governors(const char *value);
-
-#endif // _GOVERNORS_GAMEMODE_H_
+/**
+ * Update all governors to the given value. If this is NULL, restore the
+ * initial governor.
+ */
+bool set_governors(const char *value);

+ 8 - 3
daemon/logging.c

@@ -28,20 +28,25 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
  */
+
 #include "logging.h"
 #include "syslog.h"
 
 static bool use_syslog = false;
 
-// Control if we want to use the system logger
+/**
+ * Control if we want to use the system logger
+ */
 void set_use_syslog(const char *name)
 {
-	// Open the syslog
+	/* Open the syslog */
 	openlog(name, LOG_PID, LOG_DAEMON);
 	use_syslog = true;
 }
 
-// Simple getter for the syslog var
+/**
+ *  Simple getter for the syslog var
+ */
 bool get_use_syslog()
 {
 	return use_syslog;

+ 7 - 7
daemon/logging.h

@@ -28,8 +28,8 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
  */
-#ifndef _LOGGING_GAMEMODE_H_
-#define _LOGGING_GAMEMODE_H_
+
+#pragma once
 
 #include <errno.h>
 #include <stdbool.h>
@@ -39,7 +39,7 @@ POSSIBILITY OF SUCH DAMAGE.
 #include <syslog.h>
 #include <unistd.h>
 
-// Logging helpers
+/* Macros to help with basic logging */
 #define PLOG_MSG(msg, ...) printf(msg, ##__VA_ARGS__)
 #define SYSLOG_MSG(msg, ...) syslog(LOG_INFO, msg, ##__VA_ARGS__)
 #define LOG_MSG(msg, ...)                                                                          \
@@ -62,7 +62,7 @@ POSSIBILITY OF SUCH DAMAGE.
 		}                                                                                          \
 	} while (0)
 
-// Fatal errors trigger an exit
+/* Fatal warnings trigger an exit */
 #define FATAL_ERRORNO(msg, ...)                                                                    \
 	do {                                                                                           \
 		LOG_ERROR(msg " (%s)\n", ##__VA_ARGS__, strerror(errno));                                  \
@@ -74,8 +74,8 @@ POSSIBILITY OF SUCH DAMAGE.
 		exit(EXIT_FAILURE);                                                                        \
 	} while (0)
 
-// Control if we want to use the system logger
+/**
+ * Control if and how how we use syslog
+ */
 void set_use_syslog(const char *name);
 bool get_use_syslog();
-
-#endif //_LOGGING_GAMEMODE_H_

+ 38 - 19
daemon/main.c

@@ -29,7 +29,23 @@ POSSIBILITY OF SUCH DAMAGE.
 
  */
 
-// Simple daemon to allow user space programs to control the CPU governors
+/**
+ * Simple daemon to allow user space programs to control the CPU governors
+ *
+ * The main process is responsible for bootstrapping the D-BUS daemon, caching
+ * the initial governor settings, and then responding to requests over D-BUS.
+ *
+ * Clients register their pid(s) with the service, which are routinely checked
+ * to see if they've expired. Once we reach our first actively registered client
+ * we put the system into "game mode", i.e. move the CPU governor into a performance
+ * mode.
+ *
+ * Upon exit, or when all clients have stopped running, we put the system back
+ * into the default governor policy, which is invariably powersave or similar
+ * on laptops. This ensures that the system is obtaining the maximum performance
+ * whilst gaming, and allowed to sanely return to idle once the workload is
+ * complete.
+ */
 
 #define _GNU_SOURCE
 
@@ -46,59 +62,62 @@ static void sigint_handler(int signo)
 {
 	LOG_MSG("Quitting by request...\n");
 
-	// Terminate the game mode
-	term_game_mode();
+	/* Clean up nicely */
+	game_mode_context_destroy(game_mode_context_instance());
 
 	exit(EXIT_SUCCESS);
 }
 
-// Main entry point
+/**
+ * Main bootstrap entry into gamemoded
+ */
 int main(int argc, char *argv[])
 {
-	// Gather command line options
+	GameModeContext *context = NULL;
+
+	/* Gather command line options */
 	bool daemon = false;
-	bool system_dbus = false;
 	bool use_syslog = false;
 	int opt = 0;
-	while ((opt = getopt(argc, argv, "dsl")) != -1) {
+	while ((opt = getopt(argc, argv, "dl")) != -1) {
 		switch (opt) {
 		case 'd':
 			daemon = true;
 			break;
-		case 's':
-			system_dbus = true;
-			break;
 		case 'l':
 			use_syslog = true;
 			break;
 		default:
-			fprintf(stderr, "Usage: %s [-d] [-s] [-l]\n", argv[0]);
+			fprintf(stderr, "Usage: %s [-d] [-l]\n", argv[0]);
 			exit(EXIT_FAILURE);
 			break;
 		}
 	}
 
-	// Use syslog if requested
+	/* If syslog is requested, set it up with our process name */
 	if (use_syslog) {
 		set_use_syslog(argv[0]);
 	}
 
-	// Daemonize ourselves first if asked
+	/* Daemonize ourselves first if asked */
 	if (daemon) {
 		daemonize(argv[0]);
 	}
 
-	// Set up the game mode
-	init_game_mode();
+	/* Set up the game mode context */
+	context = game_mode_context_instance();
+	game_mode_context_init(context);
 
-	// Set up the SIGINT handler
+	/* Handle quits cleanly */
 	if (signal(SIGINT, sigint_handler) == SIG_ERR) {
 		FATAL_ERRORNO("Could not catch SIGINT");
 	}
 
-	// Run the main dbus message loop
-	run_dbus_main_loop(system_dbus);
+	/* Run the main dbus message loop */
+	game_mode_context_loop(context);
+
+	game_mode_context_destroy(context);
 
-	// Log we're finished
+	/* Log we're finished */
 	LOG_MSG("Quitting naturally...\n");
 }

+ 6 - 0
daemon/meson.build

@@ -1,6 +1,7 @@
 # Convenience library for the duplicated logging functionality
 common_sources = [
     'logging.c',
+    'governors-query.c',
 ]
 
 daemon_common = static_library(
@@ -27,8 +28,12 @@ executable(
     sources: daemon_sources,
     dependencies: [
         link_daemon_common,
+        dep_threads,
         dep_systemd,
     ],
+    include_directories: [
+        config_h_dir,
+    ],
     install: true,
 )
 
@@ -44,4 +49,5 @@ cpugovctl = executable(
         link_daemon_common,
     ],
     install: true,
+    install_dir: path_libexecdir,
 )

+ 26 - 0
data/com.feralinteractive.GameMode.policy.in

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE policyconfig PUBLIC
+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
+<policyconfig>
+
+  <!--
+    Copyright (c) 2017-2018, Feral Interactive
+    All rights reserved.
+  -->
+
+  <vendor>Feral GameMode Activation</vendor>
+  <vendor_url>http://www.feralinteractive.com</vendor_url>
+
+  <action id="com.feralinteractive.GameMode.governor-helper">
+    <description>Modify the CPU governor</description>
+    <message>Authentication is required to modify the CPU governor</message>
+    <defaults>
+      <allow_any>no</allow_any>
+      <allow_inactive>no</allow_inactive>
+      <allow_active>yes</allow_active>
+    </defaults>
+    <annotate key="org.freedesktop.policykit.exec.path">@LIBEXECDIR@/cpugovctl</annotate>
+  </action>
+
+</policyconfig>

+ 3 - 0
data/com.feralinteractive.GameMode.service.in

@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=com.feralinteractive.GameMode
+Exec=@BINDIR@/gamemoded -d

+ 0 - 5
data/cpugovctl_perms.sh

@@ -1,5 +0,0 @@
-#!/bin/bash
-
-# Allow cpugovctl to control the governors
-PREFIX=${MESON_INSTALL_PREFIX:-/usr}
-chmod +4555 ${DESTDIR}${PREFIX}/bin/cpugovctl

+ 0 - 10
data/gamemoded.service

@@ -1,10 +0,0 @@
-[Unit]
-Description=gamemoded
-
-[Service]
-Type=dbus
-BusName=com.feralinteractive.GameMode
-ExecStart=/usr/bin/gamemoded -l
-
-[Install]
-WantedBy=default.target

+ 17 - 6
data/meson.build

@@ -1,8 +1,19 @@
-# Install the service file
-install_data('gamemoded.service', install_dir: path_systemd_unit_dir)
+data_conf = configuration_data()
+data_conf.set('BINDIR', path_bindir)
+data_conf.set('LIBEXECDIR', path_libexecdir)
 
-# Give cpugovctl the permissions it needs
-meson.add_install_script(
-    'cpugovctl_perms.sh',
-    dependencies: cpugovctl,
+# Install the D-BUS service file
+configure_file(
+    input: 'com.feralinteractive.GameMode.service.in',
+    output: 'com.feralinteractive.GameMode.service',
+    configuration: data_conf,
+    install_dir: path_dbus_service_dir,
+)
+
+# Install the Polkit action file
+configure_file(
+    input: 'com.feralinteractive.GameMode.policy.in',
+    output: 'com.feralinteractive.GameMode.policy',
+    configuration: data_conf,
+    install_dir: path_polkit_action_dir,
 )

+ 4 - 3
example/main.c

@@ -28,6 +28,7 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
  */
+
 #include "gamemode_client.h"
 
 #include <stdio.h>
@@ -35,15 +36,15 @@ POSSIBILITY OF SUCH DAMAGE.
 
 int main()
 {
-	// Request we start game mode
+	/* Request we start game mode */
 	if (gamemode_request_start() != 0) {
 		printf("Failed to request gamemode start: %s...\n", gamemode_error_string());
 	}
 
-	// Simulate running a game
+	/* Simulate running a game */
 	sleep(10);
 
-	// Request we end game mode (optional)
+	/* Request we end game mode (optional) */
 	if (gamemode_request_end() != 0) {
 		printf("Failed to request gamemode end: %s...\n", gamemode_error_string());
 	}

+ 29 - 20
lib/gamemode_client.h

@@ -40,34 +40,39 @@ POSSIBILITY OF SUCH DAMAGE.
 
 char _client_error_string[512] = {};
 
-// Load libgamemode dynamically to dislodge us from most dependencies
-// This allows clients to link and/or use this regardless of runtime
-// See SDL2 for an example of the reasoning behind this in terms of
-// dynamic versioning as well
-int _libgamemode_loaded = 1;
+/**
+ * Load libgamemode dynamically to dislodge us from most dependencies.
+ * This allows clients to link and/or use this regardless of runtime.
+ * See SDL2 for an example of the reasoning behind this in terms of
+ * dynamic versioning as well.
+ */
+volatile int _libgamemode_loaded = 1;
 
-// Typedefs for the functions to load
+/* Typedefs for the functions to load */
 typedef int (*_gamemode_request_start)();
 typedef int (*_gamemode_request_end)();
 typedef const char *(*_gamemode_error_string)();
 
-// Storage for functors
+/* Storage for functors */
 _gamemode_request_start _REAL_gamemode_request_start = NULL;
 _gamemode_request_end _REAL_gamemode_request_end = NULL;
 _gamemode_error_string _REAL_gamemode_error_string = NULL;
 
-// Loads libgamemode and needed functions
-// returns 0 on success and -1 on failure
+/**
+ * Loads libgamemode and needed functions
+ *
+ * Returns 0 on success and -1 on failure
+ */
 __attribute__((always_inline)) inline int _load_libgamemode()
 {
-	// We start at 1, 0 is a success and -1 is a fail
+	/* We start at 1, 0 is a success and -1 is a fail */
 	if (_libgamemode_loaded != 1) {
 		return _libgamemode_loaded;
 	}
 
 	void *libgamemode = NULL;
 
-	// Try and load libgamemode
+	/* Try and load libgamemode */
 	libgamemode = dlopen("libgamemode.so", RTLD_NOW);
 	if (!libgamemode) {
 		snprintf(_client_error_string,
@@ -82,7 +87,7 @@ __attribute__((always_inline)) inline int _load_libgamemode()
 		_REAL_gamemode_error_string =
 		    (_gamemode_error_string)dlsym(libgamemode, "real_gamemode_error_string");
 
-		// Verify we have the functions we want
+		/* Verify we have the functions we want */
 		if (_REAL_gamemode_request_start && _REAL_gamemode_request_end &&
 		    _REAL_gamemode_error_string) {
 			_libgamemode_loaded = 0;
@@ -99,10 +104,12 @@ __attribute__((always_inline)) inline int _load_libgamemode()
 	return -1;
 }
 
-// Redirect to the real libgamemode
+/**
+ * Redirect to the real libgamemode
+ */
 __attribute__((always_inline)) inline const char *gamemode_error_string()
 {
-	// If we fail to load the system gamemode, return our error string
+	/* If we fail to load the system gamemode, return our error string */
 	if (_load_libgamemode() < 0) {
 		return _client_error_string;
 	}
@@ -110,9 +117,11 @@ __attribute__((always_inline)) inline const char *gamemode_error_string()
 	return _REAL_gamemode_error_string();
 }
 
-// Redirect to the real libgamemode
-// Allow automatically requesting game mode
-// Also prints errors as they happen
+/**
+ * Redirect to the real libgamemod
+ * Allow automatically requesting game mode
+ * Also prints errors as they happen.
+ */
 #ifdef GAMEMODE_AUTO
 __attribute__((constructor))
 #else
@@ -120,7 +129,7 @@ __attribute__((always_inline)) inline
 #endif
 int gamemode_request_start()
 {
-	// Need to load gamemode
+	/* Need to load gamemode */
 	if (_load_libgamemode() < 0) {
 #ifdef GAMEMODE_AUTO
 		fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
@@ -138,7 +147,7 @@ int gamemode_request_start()
 	return 0;
 }
 
-// Redirect to the real libgamemode
+/* Redirect to the real libgamemode */
 #ifdef GAMEMODE_AUTO
 __attribute__((destructor))
 #else
@@ -146,7 +155,7 @@ __attribute__((always_inline)) inline
 #endif
 int gamemode_request_end()
 {
-	// Need to load gamemode
+	/* Need to load gamemode */
 	if (_load_libgamemode() < 0) {
 #ifdef GAMEMODE_AUTO
 		fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());

+ 22 - 7
meson.build

@@ -13,24 +13,37 @@ path_bindir = join_paths(path_prefix, get_option('bindir'))
 path_datadir = join_paths(path_prefix, get_option('datadir'))
 path_includedir = join_paths(path_prefix, get_option('includedir'))
 path_libdir = join_paths(path_prefix, get_option('libdir'))
+path_libexecdir = join_paths(path_prefix, get_option('libexecdir'))
 
 # Find systemd via pkgconfig
 dep_systemd = dependency('libsystemd')
 
+# Allow meson to figure out how the compiler sets up threading
+dep_threads = dependency('threads')
+
 # On non glibc systems this might be a stub, i.e. for musl
 libdl = cc.find_library('dl', required: false)
 
-# If the path isn't explicitly set, ask systemd for the systemd user unit directory
-path_systemd_unit_dir = get_option('with-systemd-user-unit-dir')
-if path_systemd_unit_dir == ''
-    message('Asking pkg-config for systemd\'s directories')
-    pkgconfig_systemd = dependency('systemd')
-    path_systemd_unit_dir = pkgconfig_systemd.get_pkgconfig_variable('systemduserunitdir')
+# Set the dbus path as appropriate.
+path_dbus_service_dir = get_option('with-dbus-service-dir')
+if path_dbus_service_dir == ''
+    path_dbus_service_dir = join_paths(path_datadir, 'dbus-1', 'services')
 endif
 
+path_polkit_action_dir = join_paths(path_datadir, 'polkit-1', 'actions')
+
 with_daemon = get_option('with-daemon')
 with_examples = get_option('with-examples')
 
+# Provide config.h so the daemon knows where the helper is
+cdata = configuration_data()
+cdata.set_quoted('LIBEXECDIR', path_libexecdir)
+config_h = configure_file(
+    configuration: cdata,
+    output: 'config.h',
+)
+config_h_dir = include_directories('.')
+
 # Library is always required
 subdir('lib')
 
@@ -57,8 +70,10 @@ report = [
     '    bindir:                                 @0@'.format(path_bindir),
     '    datadir:                                @0@'.format(path_datadir),
     '    libdir:                                 @0@'.format(path_libdir),
+    '    libexecdir:                             @0@'.format(path_libexecdir),
     '    includedir:                             @0@'.format(path_includedir),
-    '    systemd user unit directory:            @0@'.format(path_systemd_unit_dir),
+    '    D-BUS service directory:                @0@'.format(path_dbus_service_dir),
+    '    PolKit Action Directory:                @0@'.format(path_polkit_action_dir),
     '',
     '    Options:',
     '    ========',

+ 1 - 1
meson_options.txt

@@ -1,3 +1,3 @@
-option('with-systemd-user-unit-dir', type: 'string', description: 'Explicitly set the systemd user unit directory')
+option('with-dbus-service-dir', type: 'string', description: 'Explicitly set the D-BUS session directory')
 option('with-examples', type: 'boolean', description: 'Build sample programs', value: 'true')
 option('with-daemon', type: 'boolean', description: 'Build the daemon', value: 'true')