Browse Source

Merge pull request #86 from kakra/modular-refactor

Tidy up the code and refactor into modules
Alex Smith 6 years ago
parent
commit
2720711550

+ 96 - 0
daemon/gamemode-env.c

@@ -0,0 +1,96 @@
+/*
+
+Copyright (c) 2017-2018, 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 "gamemode.h"
+
+#include <fcntl.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/**
+ * Lookup the process environment for a specific variable or return NULL.
+ * Requires an open directory FD from /proc/PID.
+ */
+char *game_mode_lookup_proc_env(const procfd_t proc_fd, const char *var)
+{
+	char *environ = NULL;
+
+	int fd = openat(proc_fd, "environ", O_RDONLY | O_CLOEXEC);
+	if (fd != -1) {
+		FILE *stream = fdopen(fd, "r");
+		if (stream) {
+			/* Read every \0 terminated line from the environment */
+			char *line = NULL;
+			size_t len = 0;
+			size_t pos = strlen(var) + 1;
+			while (!environ && (getdelim(&line, &len, 0, stream) != -1)) {
+				/* Find a match including the "=" suffix */
+				if ((len > pos) && (strncmp(line, var, strlen(var)) == 0) && (line[pos - 1] == '='))
+					environ = strndup(line + pos, len - pos);
+			}
+			free(line);
+			fclose(stream);
+		} else
+			close(fd);
+	}
+
+	/* If found variable is empty, skip it */
+	if (environ && !strlen(environ)) {
+		free(environ);
+		environ = NULL;
+	}
+
+	return environ;
+}
+
+/**
+ * Lookup the home directory of the user in a safe way.
+ */
+char *game_mode_lookup_user_home(void)
+{
+	/* Try loading env HOME first */
+	const char *home = secure_getenv("HOME");
+	if (!home) {
+		/* If HOME is not defined (or out of context), fall back to passwd */
+		struct passwd *pw = getpwuid(getuid());
+		if (!pw)
+			return NULL;
+		home = pw->pw_dir;
+	}
+
+	/* Try to allocate into our heap */
+	return home ? strdup(home) : NULL;
+}

+ 142 - 0
daemon/gamemode-ioprio.c

@@ -0,0 +1,142 @@
+/*
+
+Copyright (c) 2017-2018, 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 "gamemode.h"
+#include "helpers.h"
+#include "logging.h"
+
+#include <errno.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+/**
+ * Define the syscall interface in Linux because it is missing from glibc
+ */
+
+#ifndef IOPRIO_BITS
+#define IOPRIO_BITS (16)
+#endif
+
+#ifndef IOPRIO_CLASS_SHIFT
+#define IOPRIO_CLASS_SHIFT (13)
+#endif
+
+#ifndef IOPRIO_PRIO_MASK
+#define IOPRIO_PRIO_MASK ((1UL << IOPRIO_CLASS_SHIFT) - 1)
+#endif
+
+#ifndef IOPRIO_PRIO_CLASS
+#define IOPRIO_PRIO_CLASS(mask) ((mask) >> IOPRIO_CLASS_SHIFT)
+#endif
+
+#ifndef IOPRIO_PRIO_DATA
+#define IOPRIO_PRIO_DATA(mask) ((mask)&IOPRIO_PRIO_MASK)
+#endif
+
+#ifndef IOPRIO_PRIO_VALUE
+#define IOPRIO_PRIO_VALUE(class, data) (((class) << IOPRIO_CLASS_SHIFT) | data)
+#endif
+
+enum {
+	IOPRIO_CLASS_NONE,
+	IOPRIO_CLASS_RT,
+	IOPRIO_CLASS_BE,
+	IOPRIO_CLASS_IDLE,
+};
+
+enum {
+	IOPRIO_WHO_PROCESS = 1,
+	IOPRIO_WHO_PGRP,
+	IOPRIO_WHO_USER,
+};
+
+static inline int ioprio_set(int which, int who, int ioprio)
+{
+	return (int)syscall(SYS_ioprio_set, which, who, ioprio);
+}
+
+/**
+ * Apply io priorities
+ *
+ * This tries to change the io priority of the client to a value specified
+ * and can possibly reduce lags or latency when a game has to load assets
+ * on demand.
+ */
+void game_mode_apply_ioprio(const GameModeContext *self, const pid_t client)
+{
+	GameModeConfig *config = game_mode_config_from_context(self);
+
+	LOG_MSG("Setting scheduling policies...\n");
+
+	/*
+	 * read configuration "ioprio" (0..7)
+	 */
+	int ioprio = 0;
+	config_get_ioprio_value(config, &ioprio);
+	if (IOPRIO_RESET_DEFAULT == ioprio) {
+		LOG_MSG("IO priority will be reset to default behavior (based on CPU priority).\n");
+		ioprio = 0;
+	} else if (IOPRIO_DONT_SET == ioprio) {
+		return;
+	} else {
+		/* maybe clamp the value */
+		int invalid_ioprio = ioprio;
+		ioprio = CLAMP(0, 7, ioprio);
+		if (ioprio != invalid_ioprio)
+			LOG_ONCE(ERROR,
+			         "IO priority value %d invalid, clamping to %d\n",
+			         invalid_ioprio,
+			         ioprio);
+
+		/* We support only IOPRIO_CLASS_BE as IOPRIO_CLASS_RT required CAP_SYS_ADMIN */
+		ioprio = IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, ioprio);
+	}
+
+	/*
+	 * Actually apply the io priority
+	 */
+	int c = IOPRIO_PRIO_CLASS(ioprio), p = IOPRIO_PRIO_DATA(ioprio);
+	if (ioprio_set(IOPRIO_WHO_PROCESS, client, ioprio) == 0) {
+		if (0 == ioprio)
+			LOG_MSG("Resetting client [%d] IO priority.\n", client);
+		else
+			LOG_MSG("Setting client [%d] IO priority to (%d,%d).\n", client, c, p);
+	} else {
+		LOG_ERROR("Setting client [%d] IO priority to (%d,%d) failed with error %d, ignoring\n",
+		          client,
+		          c,
+		          p,
+		          errno);
+	}
+}

+ 21 - 42
daemon/ioprio.h → daemon/gamemode-proc.c

@@ -29,54 +29,33 @@ POSSIBILITY OF SUCH DAMAGE.
 
  */
 
-#pragma once
-
 #define _GNU_SOURCE
-#include <sys/syscall.h>
+
+#include "gamemode.h"
+#include "helpers.h"
+
+#include <fcntl.h>
+#include <linux/limits.h>
 #include <unistd.h>
 
 /**
- * Define the syscall interface in Linux because it is missing from glibc
+ * Opens the process environment for a specific PID and returns
+ * a file descriptor to the directory /proc/PID. Doing it that way prevents
+ * the directory going MIA when a process exits while we are looking at it
+ * and allows us to handle fewer error cases.
  */
+procfd_t game_mode_open_proc(const pid_t pid)
+{
+	char buffer[PATH_MAX];
+	const char *proc_path = buffered_snprintf(buffer, "/proc/%d", pid);
 
-#ifndef IOPRIO_BITS
-#define IOPRIO_BITS (16)
-#endif
-
-#ifndef IOPRIO_CLASS_SHIFT
-#define IOPRIO_CLASS_SHIFT (13)
-#endif
-
-#ifndef IOPRIO_PRIO_MASK
-#define IOPRIO_PRIO_MASK ((1UL << IOPRIO_CLASS_SHIFT) - 1)
-#endif
-
-#ifndef IOPRIO_PRIO_CLASS
-#define IOPRIO_PRIO_CLASS(mask) ((mask) >> IOPRIO_CLASS_SHIFT)
-#endif
-
-#ifndef IOPRIO_PRIO_DATA
-#define IOPRIO_PRIO_DATA(mask) ((mask)&IOPRIO_PRIO_MASK)
-#endif
-
-#ifndef IOPRIO_PRIO_VALUE
-#define IOPRIO_PRIO_VALUE(class, data) (((class) << IOPRIO_CLASS_SHIFT) | data)
-#endif
-
-enum {
-	IOPRIO_CLASS_NONE,
-	IOPRIO_CLASS_RT,
-	IOPRIO_CLASS_BE,
-	IOPRIO_CLASS_IDLE,
-};
-
-enum {
-	IOPRIO_WHO_PROCESS = 1,
-	IOPRIO_WHO_PGRP,
-	IOPRIO_WHO_USER,
-};
+	return proc_path ? open(proc_path, O_RDONLY | O_CLOEXEC) : INVALID_PROCFD;
+}
 
-static inline int ioprio_set(int which, int who, int ioprio)
+/**
+ * Closes the process environment.
+ */
+int game_mode_close_proc(const procfd_t procfd)
 {
-	return (int)syscall(SYS_ioprio_set, which, who, ioprio);
+	return close(procfd);
 }

+ 149 - 0
daemon/gamemode-sched.c

@@ -0,0 +1,149 @@
+/*
+
+Copyright (c) 2017-2018, 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 "gamemode.h"
+#include "logging.h"
+
+#include <errno.h>
+#include <sched.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/sysinfo.h>
+
+/**
+ * Priority to renice the process to.
+ */
+#define NICE_DEFAULT_PRIORITY -4
+
+/**
+ * Apply scheduling policies
+ *
+ * This tries to change the scheduler of the client to soft realtime mode
+ * available in some kernels as SCHED_ISO. It also tries to adjust the nice
+ * level. If some of each fail, ignore this and log a warning.
+ *
+ * We don't need to store the current values because when the client exits,
+ * everything will be good: Scheduling is only applied to the client and
+ * its children.
+ */
+void game_mode_apply_renice(const GameModeContext *self, const pid_t client)
+{
+	GameModeConfig *config = game_mode_config_from_context(self);
+
+	/*
+	 * read configuration "renice" (1..20)
+	 */
+	long int renice = 0;
+	config_get_renice_value(config, &renice);
+	if ((renice < 1) || (renice > 20)) {
+		LOG_ONCE(ERROR,
+		         "Invalid renice value '%ld' reset to default: %d.\n",
+		         renice,
+		         -NICE_DEFAULT_PRIORITY);
+		renice = NICE_DEFAULT_PRIORITY;
+	} else {
+		renice = -renice;
+	}
+
+	/*
+	 * don't adjust priority if it was already adjusted
+	 */
+	if (getpriority(PRIO_PROCESS, (id_t)client) != 0) {
+		LOG_ERROR("Refused to renice client [%d]: already reniced\n", client);
+	} else if (setpriority(PRIO_PROCESS, (id_t)client, (int)renice)) {
+		LOG_HINTED(ERROR,
+		           "Failed to renice client [%d], ignoring error condition: %s\n",
+		           "    -- Your user may not have permission to do this. Please read the docs\n"
+		           "    -- to learn how to adjust the pam limits.\n",
+		           client,
+		           strerror(errno));
+	}
+}
+
+void game_mode_apply_scheduling(const GameModeContext *self, const pid_t client)
+{
+	GameModeConfig *config = game_mode_config_from_context(self);
+
+	/*
+	 * read configuration "softrealtime" (on, off, auto)
+	 */
+	char softrealtime[CONFIG_VALUE_MAX] = { 0 };
+	config_get_soft_realtime(config, softrealtime);
+
+	/*
+	 * Enable unconditionally or auto-detect soft realtime usage,
+	 * auto detection is based on observations where dual-core CPU suffered
+	 * priority inversion problems with the graphics driver thus running
+	 * slower as a result, so enable only with more than 3 cores.
+	 */
+	bool enable_softrealtime = (strcmp(softrealtime, "on") == 0) || (get_nprocs() > 3);
+
+	/*
+	 * Actually apply the scheduler policy if not explicitly turned off
+	 */
+	if (!(strcmp(softrealtime, "off") == 0) && (enable_softrealtime)) {
+		const struct sched_param p = { .sched_priority = 0 };
+		if (sched_setscheduler(client, SCHED_ISO | SCHED_RESET_ON_FORK, &p)) {
+			const char *hint = "";
+			HINT_ONCE_ON(
+			    errno == EPERM,
+			    hint,
+			    "    -- The error indicates that you may be running a resource management\n"
+			    "    -- daemon managing your game launcher and it leaks lower scheduling\n"
+			    "    -- classes into the games. This is likely a bug in the management daemon\n"
+			    "    -- and not a bug in GameMode, it should be reported upstream.\n"
+			    "    -- If unsure, please also look here:\n"
+			    "    -- https://github.com/FeralInteractive/gamemode/issues/68\n");
+			HINT_ONCE_ON(
+			    errno == EINVAL,
+			    hint,
+			    "    -- The error indicates that your kernel may not support this. If you\n"
+			    "    -- don't know what SCHED_ISO means, you can safely ignore this. If you\n"
+			    "    -- expected it to work, ensure you're running a kernel with MuQSS or\n"
+			    "    -- PDS scheduler.\n"
+			    "    -- For further technical reading on the topic start here:\n"
+			    "    -- https://lwn.net/Articles/720227/\n");
+			LOG_ERROR(
+			    "Failed setting client [%d] into SCHED_ISO mode, ignoring error condition: %s\n"
+			    "%s",
+			    client,
+			    strerror(errno),
+			    hint);
+		}
+	} else {
+		LOG_ERROR("Skipped setting client [%d] into SCHED_ISO mode: softrealtime setting is '%s'\n",
+		          client,
+		          softrealtime);
+	}
+}

+ 164 - 0
daemon/gamemode-wine.c

@@ -0,0 +1,164 @@
+/*
+
+Copyright (c) 2017-2018, 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 "gamemode.h"
+#include "helpers.h"
+#include "logging.h"
+
+#include <ctype.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/**
+ * Detect if the process is a wine preloader process
+ */
+bool game_mode_detect_wine_preloader(const char *exe)
+{
+	return (strtail(exe, "/wine-preloader") || strtail(exe, "/wine64-preloader"));
+}
+
+/**
+ * Detect if the process is a wine loader process
+ */
+bool game_mode_detect_wine_loader(const char *exe)
+{
+	return (strtail(exe, "/wine") || strtail(exe, "/wine64"));
+}
+
+/**
+ * Attempt to resolve the exe for wine-preloader.
+ * This function is used if game_mode_context_find_exe() identified the
+ * process as wine-preloader. Returns NULL when resolve fails.
+ */
+char *game_mode_resolve_wine_preloader(const pid_t pid)
+{
+	char buffer[PATH_MAX];
+	char *proc_path = NULL, *wine_exe = NULL, *wineprefix = NULL;
+
+	/* Open the directory, we are potentially reading multiple files from it */
+	procfd_t proc_fd = game_mode_open_proc(pid);
+
+	if (proc_fd == INVALID_PROCFD)
+		goto fail_proc;
+
+	/* Open the command line */
+	int fd = openat(proc_fd, "cmdline", O_RDONLY | O_CLOEXEC);
+	if (fd != -1) {
+		FILE *stream = fdopen(fd, "r");
+		if (stream) {
+			char *argv = NULL;
+			size_t args = 0;
+			int argc = 0;
+			while (!wine_exe && (argc++ < 2) && (getdelim(&argv, &args, 0, stream) != -1)) {
+				/* If we see the wine loader here, we have to use the next argument */
+				if (strtail(argv, "/wine") || strtail(argv, "/wine64"))
+					continue;
+				free(wine_exe); // just in case
+				/* Check presence of the drive letter, we assume that below */
+				wine_exe = args > 2 && argv[1] == ':' ? strndup(argv, args) : NULL;
+			}
+			free(argv);
+			fclose(stream);
+		} else
+			close(fd);
+	}
+
+	/* Did we get wine exe from cmdline? */
+	if (wine_exe)
+		LOG_MSG("Detected wine exe for client %d [%s].\n", pid, wine_exe);
+	else
+		goto fail_cmdline;
+
+	/* Open the process environment and find the WINEPREFIX */
+	errno = 0;
+	if (!(wineprefix = game_mode_lookup_proc_env(proc_fd, "WINEPREFIX"))) {
+		/* Lookup user home instead only if there was no error */
+		char *home = NULL;
+		if (errno == 0)
+			home = game_mode_lookup_user_home();
+
+		/* Append "/.wine" if we found the user home */
+		if (home)
+			wineprefix = safe_snprintf(buffer, "%s/.wine", home);
+
+		/* Cleanup and check result */
+		free(home);
+		if (!wineprefix)
+			goto fail_env;
+	}
+
+	/* Wine prefix was detected, log this for diagnostics */
+	LOG_MSG("Detected wine prefix for client %d: '%s'\n", pid, wineprefix);
+
+	/* Convert Windows to Unix path separators */
+	char *ix = wine_exe;
+	while (ix != NULL)
+		(ix = strchr(ix, '\\')) && (*ix++ = '/');
+
+	/* Convert the drive letter to lcase because wine handles it this way in the prefix */
+	wine_exe[0] = (char)tolower(wine_exe[0]);
+
+	/* Convert relative wine exe path to full unix path */
+	char *wine_path = buffered_snprintf(buffer, "%s/dosdevices/%s", wineprefix, wine_exe);
+	free(wine_exe);
+	wine_exe = wine_path ? realpath(wine_path, NULL) : NULL;
+
+	/* Fine? Successo? Fortuna! */
+	if (wine_exe)
+		LOG_MSG("Successfully mapped wine client %d [%s].\n", pid, wine_exe);
+	else
+		goto fail;
+
+error_cleanup:
+	game_mode_close_proc(proc_fd);
+	free(wineprefix);
+	free(proc_path);
+	return wine_exe;
+
+fail:
+	LOG_ERROR("Unable to find wine executable for client %d: %s\n", pid, strerror(errno));
+	goto error_cleanup;
+
+fail_cmdline:
+	LOG_ERROR("Wine loader has no accepted cmdline for client %d yet, deferring.\n", pid);
+	goto error_cleanup;
+
+fail_env:
+	LOG_ERROR("Failed to access process environment in '%s': %s\n", proc_path, strerror(errno));
+	goto error_cleanup;
+
+fail_proc:
+	LOG_ERROR("Failed to access process data in '%s': %s\n", proc_path, strerror(errno));
+	goto error_cleanup;
+}

+ 21 - 383
daemon/gamemode.c

@@ -35,24 +35,12 @@ POSSIBILITY OF SUCH DAMAGE.
 #include "daemon_config.h"
 #include "governors-query.h"
 #include "governors.h"
-#include "ioprio.h"
+#include "helpers.h"
 #include "logging.h"
 
-#include <ctype.h>
-#include <fcntl.h>
-#include <linux/limits.h>
-#include <linux/sched.h>
 #include <pthread.h>
-#include <pwd.h>
-#include <sched.h>
 #include <signal.h>
 #include <stdatomic.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/param.h>
-#include <sys/resource.h>
-#include <sys/sysinfo.h>
-#include <sys/types.h>
 #include <systemd/sd-daemon.h>
 
 /* SCHED_ISO may not be defined as it is a reserved value not yet
@@ -62,35 +50,6 @@ POSSIBILITY OF SUCH DAMAGE.
 #define SCHED_ISO 4
 #endif
 
-/* Priority to renice the process to.
- */
-#define NICE_DEFAULT_PRIORITY -4
-
-/* Value clamping helper.
- */
-#define CLAMP(lbound, ubound, value) MIN(MIN(lbound, ubound), MAX(MAX(lbound, ubound), value))
-
-/* Little helper to safely print into a buffer, returns a pointer into the buffer
- */
-#define buffered_snprintf(b, s, ...)                                                               \
-	(snprintf(b, sizeof(b), s, __VA_ARGS__) < (ssize_t)sizeof(b) ? b : NULL)
-
-/* Little helper to safely print into a buffer, returns a newly allocated string
- */
-#define safe_snprintf(b, s, ...)                                                                   \
-	(snprintf(b, sizeof(b), s, __VA_ARGS__) < (ssize_t)sizeof(b) ? strndup(b, sizeof(b)) : NULL)
-
-/**
- * Helper function: Test, if haystack ends with needle.
- */
-static inline const char *strtail(const char *haystack, const char *needle)
-{
-	char *pos = strstr(haystack, needle);
-	if (pos && (strlen(pos) == strlen(needle)))
-		return pos;
-	return NULL;
-}
-
 /**
  * The GameModeClient encapsulates the remote connection, providing a list
  * form to contain the pid and credentials.
@@ -197,159 +156,6 @@ void game_mode_context_destroy(GameModeContext *self)
 	pthread_rwlock_destroy(&self->rwlock);
 }
 
-/**
- * Apply scheduling policies
- *
- * This tries to change the scheduler of the client to soft realtime mode
- * available in some kernels as SCHED_ISO. It also tries to adjust the nice
- * level. If some of each fail, ignore this and log a warning.
- *
- * We don't need to store the current values because when the client exits,
- * everything will be good: Scheduling is only applied to the client and
- * its children.
- */
-static void game_mode_apply_scheduler(GameModeContext *self, pid_t client)
-{
-	/*
-	 * read configuration "renice" (1..20)
-	 */
-	long int renice = 0;
-	config_get_renice_value(self->config, &renice);
-	if ((renice < 1) || (renice > 20)) {
-		LOG_ERROR("Invalid renice value '%ld' reset to default: %d.\n",
-		          renice,
-		          -NICE_DEFAULT_PRIORITY);
-		renice = NICE_DEFAULT_PRIORITY;
-	} else {
-		renice = -renice;
-	}
-
-	/*
-	 * don't adjust priority if it was already adjusted
-	 */
-	if (getpriority(PRIO_PROCESS, (id_t)client) != 0) {
-		LOG_ERROR("Refused to renice client [%d]: already reniced\n", client);
-	} else if (setpriority(PRIO_PROCESS, (id_t)client, (int)renice)) {
-		LOG_ERROR(
-		    "Failed to renice client [%d], ignoring error condition: %s\n"
-		    "    -- Your user may not have permission to do this. Please read the docs\n"
-		    "    -- to learn how to adjust the pam limits.\n",
-		    client,
-		    strerror(errno));
-	}
-
-	/*
-	 * read configuration "softrealtime" (on, off, auto)
-	 */
-	char softrealtime[CONFIG_VALUE_MAX] = { 0 };
-	config_get_soft_realtime(self->config, softrealtime);
-
-	/*
-	 * Enable unconditionally or auto-detect soft realtime usage,
-	 * auto detection is based on observations where dual-core CPU suffered
-	 * priority inversion problems with the graphics driver thus running
-	 * slower as a result, so enable only with more than 3 cores.
-	 */
-	bool enable_softrealtime = (strcmp(softrealtime, "on") == 0) || (get_nprocs() > 3);
-
-	/*
-	 * Actually apply the scheduler policy if not explicitly turned off
-	 */
-	if (!(strcmp(softrealtime, "off") == 0) && (enable_softrealtime)) {
-		const struct sched_param p = { .sched_priority = 0 };
-		if (sched_setscheduler(client, SCHED_ISO | SCHED_RESET_ON_FORK, &p)) {
-			const char *additional_message = "";
-			switch (errno) {
-			case EPERM: {
-				static int once = 0;
-				if (once++)
-					break;
-				additional_message =
-				    "    -- The error indicates that you may be running a resource management\n"
-				    "    -- daemon managing your game launcher and it leaks lower scheduling\n"
-				    "    -- classes into the games. This is likely a bug in the management daemon\n"
-				    "    -- and not a bug in GameMode, it should be reported upstream.\n"
-				    "    -- If unsure, please also look here:\n"
-				    "    -- https://github.com/FeralInteractive/gamemode/issues/68\n";
-				break;
-			}
-			case EINVAL: {
-				static int once = 0;
-				if (once++)
-					break;
-				additional_message =
-				    "    -- The error indicates that your kernel may not support this. If you\n"
-				    "    -- don't know what SCHED_ISO means, you can safely ignore this. If you\n"
-				    "    -- expected it to work, ensure you're running a kernel with MuQSS or\n"
-				    "    -- PDS scheduler.\n"
-				    "    -- For further technical reading on the topic start here:\n"
-				    "    -- https://lwn.net/Articles/720227/\n";
-				break;
-			}
-			}
-			LOG_ERROR(
-			    "Failed setting client [%d] into SCHED_ISO mode, ignoring error condition: %s\n%s",
-			    client,
-			    strerror(errno),
-			    additional_message);
-		}
-	} else {
-		LOG_ERROR("Skipped setting client [%d] into SCHED_ISO mode: softrealtime setting is '%s'\n",
-		          client,
-		          softrealtime);
-	}
-}
-
-/**
- * Apply io priorities
- *
- * This tries to change the io priority of the client to a value specified
- * and can possibly reduce lags or latency when a game has to load assets
- * on demand.
- */
-static void game_mode_apply_ioprio(GameModeContext *self, pid_t client)
-{
-	LOG_MSG("Setting scheduling policies...\n");
-
-	/*
-	 * read configuration "ioprio" (0..7)
-	 */
-	int ioprio = 0;
-	config_get_ioprio_value(self->config, &ioprio);
-	if (IOPRIO_RESET_DEFAULT == ioprio) {
-		LOG_MSG("IO priority will be reset to default behavior (based on CPU priority).\n");
-		ioprio = 0;
-	} else if (IOPRIO_DONT_SET == ioprio) {
-		return;
-	} else {
-		/* maybe clamp the value */
-		int invalid_ioprio = ioprio;
-		ioprio = CLAMP(0, 7, ioprio);
-		if (ioprio != invalid_ioprio)
-			LOG_ERROR("IO priority value %d invalid, clamping to %d\n", invalid_ioprio, ioprio);
-
-		/* We support only IOPRIO_CLASS_BE as IOPRIO_CLASS_RT required CAP_SYS_ADMIN */
-		ioprio = IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, ioprio);
-	}
-
-	/*
-	 * Actually apply the io priority
-	 */
-	int c = IOPRIO_PRIO_CLASS(ioprio), p = IOPRIO_PRIO_DATA(ioprio);
-	if (ioprio_set(IOPRIO_WHO_PROCESS, client, ioprio) == 0) {
-		if (0 == ioprio)
-			LOG_MSG("Resetting client [%d] IO priority.\n", client);
-		else
-			LOG_MSG("Setting client [%d] IO priority to (%d,%d).\n", client, c, p);
-	} else {
-		LOG_ERROR("Setting client [%d] IO priority to (%d,%d) failed with error %d, ignoring\n",
-		          client,
-		          c,
-		          p,
-		          errno);
-	}
-}
-
 /**
  * Pivot into game mode.
  *
@@ -517,16 +323,12 @@ bool game_mode_context_register(GameModeContext *self, pid_t client)
 	pthread_rwlock_rdlock(&self->rwlock); // ensure our pointer is sane
 	const GameModeClient *existing = game_mode_context_has_client(self, client);
 	if (existing) {
-		static int once = 0;
-		const char *additional_message =
-		    (once++
-		         ? ""
-		         : "    -- This may happen due to using exec or shell wrappers. You may want to\n"
-		           "    -- blacklist this client so GameMode can see its final name here.\n");
-		LOG_ERROR("Addition requested for already known client %d [%s].\n%s",
-		          existing->pid,
-		          existing->executable,
-		          additional_message);
+		LOG_HINTED(ERROR,
+		           "Addition requested for already known client %d [%s].\n",
+		           "    -- This may happen due to using exec or shell wrappers. You may want to\n"
+		           "    -- blacklist this client so GameMode can see its final name here.\n",
+		           existing->pid,
+		           existing->executable);
 		pthread_rwlock_unlock(&self->rwlock);
 		goto error_cleanup;
 	}
@@ -569,7 +371,8 @@ bool game_mode_context_register(GameModeContext *self, pid_t client)
 	}
 
 	/* Apply scheduler policies */
-	game_mode_apply_scheduler(self, client);
+	game_mode_apply_renice(self, client);
+	game_mode_apply_scheduling(self, client);
 
 	/* Apply io priorities */
 	game_mode_apply_ioprio(self, client);
@@ -616,15 +419,14 @@ bool game_mode_context_unregister(GameModeContext *self, pid_t client)
 	pthread_rwlock_unlock(&self->rwlock);
 
 	if (!found) {
-		static int once = 0;
-		const char *additional_message =
-		    (once++
-		         ? ""
-		         : "    -- The parent process probably forked and tries to unregister from the\n"
-		           "    -- wrong process now. We cannot work around this. This message will\n"
-		           "    -- likely be paired with a nearby 'Removing expired game' which means we\n"
-		           "    -- cleaned up properly (we will log this event).\n");
-		LOG_ERROR("Removal requested for unknown process [%d].\n%s", client, additional_message);
+		LOG_HINTED(
+		    ERROR,
+		    "Removal requested for unknown process [%d].\n",
+		    "    -- The parent process probably forked and tries to unregister from the wrong\n"
+		    "    -- process now. We cannot work around this. This message will likely be paired\n"
+		    "    -- with a nearby 'Removing expired game' which means we cleaned up properly\n"
+		    "    -- (we will log this event). This hint will be displayed only once.\n",
+		    client);
 		return false;
 	}
 
@@ -748,173 +550,9 @@ GameModeContext *game_mode_context_instance()
 	return &instance;
 }
 
-/**
- * Lookup the process environment for a specific variable or return NULL.
- * Requires an open directory FD from /proc/PID.
- */
-static char *game_mode_lookup_proc_env(int proc_fd, const char *var)
-{
-	char *environ = NULL;
-
-	int fd = openat(proc_fd, "environ", O_RDONLY | O_CLOEXEC);
-	if (fd != -1) {
-		FILE *stream = fdopen(fd, "r");
-		if (stream) {
-			/* Read every \0 terminated line from the environment */
-			char *line = NULL;
-			size_t len = 0;
-			size_t pos = strlen(var) + 1;
-			while (!environ && (getdelim(&line, &len, 0, stream) != -1)) {
-				/* Find a match including the "=" suffix */
-				if ((len > pos) && (strncmp(line, var, strlen(var)) == 0) && (line[pos - 1] == '='))
-					environ = strndup(line + pos, len - pos);
-			}
-			free(line);
-			fclose(stream);
-		} else
-			close(fd);
-	}
-
-	/* If found variable is empty, skip it */
-	if (environ && !strlen(environ)) {
-		free(environ);
-		environ = NULL;
-	}
-
-	return environ;
-}
-
-/**
- * Lookup the home directory of the user in a safe way.
- */
-static char *game_mode_lookup_user_home(void)
-{
-	/* Try loading env HOME first */
-	const char *home = secure_getenv("HOME");
-	if (!home) {
-		/* If HOME is not defined (or out of context), fall back to passwd */
-		struct passwd *pw = getpwuid(getuid());
-		if (!pw)
-			return NULL;
-		home = pw->pw_dir;
-	}
-
-	/* Try to allocate into our heap */
-	return home ? strdup(home) : NULL;
-}
-
-/**
- * Attempt to resolve the exe for wine-preloader.
- * This function is used if game_mode_context_find_exe() identified the
- * process as wine-preloader. Returns NULL when resolve fails.
- */
-static char *game_mode_resolve_wine_preloader(pid_t pid)
+GameModeConfig *game_mode_config_from_context(const GameModeContext *context)
 {
-	char buffer[PATH_MAX];
-	char *proc_path = NULL, *wine_exe = NULL, *wineprefix = NULL;
-	int proc_fd = -1;
-
-	/* We could use the buffered_snprintf() helper here but it may potentially
-	 * overwrite proc_path when the buffer is re-used later and usage of
-	 * proc_path has not been discarded yet (i.e., it's used in the fail path).
-	 * Let's not introduce this non-obvious pitfall.
-	 */
-	if (!(proc_path = safe_snprintf(buffer, "/proc/%d", pid)))
-		goto fail;
-
-	/* Open the directory, we are potentially reading multiple files from it */
-	if (-1 == (proc_fd = open(proc_path, O_RDONLY | O_CLOEXEC)))
-		goto fail_proc;
-
-	/* Open the command line */
-	int fd = openat(proc_fd, "cmdline", O_RDONLY | O_CLOEXEC);
-	if (fd != -1) {
-		FILE *stream = fdopen(fd, "r");
-		if (stream) {
-			char *argv = NULL;
-			size_t args = 0;
-			int argc = 0;
-			while (!wine_exe && (argc++ < 2) && (getdelim(&argv, &args, 0, stream) != -1)) {
-				/* If we see the wine loader here, we have to use the next argument */
-				if (strtail(argv, "/wine") || strtail(argv, "/wine64"))
-					continue;
-				free(wine_exe); // just in case
-				/* Check presence of the drive letter, we assume that below */
-				wine_exe = args > 2 && argv[1] == ':' ? strndup(argv, args) : NULL;
-			}
-			free(argv);
-			fclose(stream);
-		} else
-			close(fd);
-	}
-
-	/* Did we get wine exe from cmdline? */
-	if (wine_exe)
-		LOG_MSG("Detected wine exe for client %d [%s].\n", pid, wine_exe);
-	else
-		goto fail_cmdline;
-
-	/* Open the process environment and find the WINEPREFIX */
-	errno = 0;
-	if (!(wineprefix = game_mode_lookup_proc_env(proc_fd, "WINEPREFIX"))) {
-		/* Lookup user home instead only if there was no error */
-		char *home = NULL;
-		if (errno == 0)
-			home = game_mode_lookup_user_home();
-
-		/* Append "/.wine" if we found the user home */
-		if (home)
-			wineprefix = safe_snprintf(buffer, "%s/.wine", home);
-
-		/* Cleanup and check result */
-		free(home);
-		if (!wineprefix)
-			goto fail_env;
-	}
-
-	/* Wine prefix was detected, log this for diagnostics */
-	LOG_MSG("Detected wine prefix for client %d: '%s'\n", pid, wineprefix);
-
-	/* Convert Windows to Unix path separators */
-	char *ix = wine_exe;
-	while (ix != NULL)
-		(ix = strchr(ix, '\\')) && (*ix++ = '/');
-
-	/* Convert the drive letter to lcase because wine handles it this way in the prefix */
-	wine_exe[0] = (char)tolower(wine_exe[0]);
-
-	/* Convert relative wine exe path to full unix path */
-	char *wine_path = buffered_snprintf(buffer, "%s/dosdevices/%s", wineprefix, wine_exe);
-	free(wine_exe);
-	wine_exe = wine_path ? realpath(wine_path, NULL) : NULL;
-
-	/* Fine? Successo? Fortuna! */
-	if (wine_exe)
-		LOG_MSG("Successfully mapped wine client %d [%s].\n", pid, wine_exe);
-	else
-		goto fail;
-
-error_cleanup:
-	close(proc_fd);
-	free(wineprefix);
-	free(proc_path);
-	return wine_exe;
-
-fail:
-	LOG_ERROR("Unable to find wine executable for client %d: %s\n", pid, strerror(errno));
-	goto error_cleanup;
-
-fail_cmdline:
-	LOG_ERROR("Wine loader has no accepted cmdline for client %d yet, deferring.\n", pid);
-	goto error_cleanup;
-
-fail_env:
-	LOG_ERROR("Failed to access process environment in '%s': %s\n", proc_path, strerror(errno));
-	goto error_cleanup;
-
-fail_proc:
-	LOG_ERROR("Failed to access process data in '%s': %s\n", proc_path, strerror(errno));
-	goto error_cleanup;
+	return context ? context->config : NULL;
 }
 
 /**
@@ -935,11 +573,11 @@ static char *game_mode_context_find_exe(pid_t pid)
 		goto fail;
 
 	/* Detect if the process is a wine loader process */
-	if (strtail(exe, "/wine-preloader") || strtail(exe, "/wine64-preloader")) {
+	if (game_mode_detect_wine_preloader(exe)) {
 		LOG_MSG("Detected wine preloader for client %d [%s].\n", pid, exe);
 		goto wine_preloader;
 	}
-	if (strtail(exe, "/wine") || strtail(exe, "/wine64")) {
+	if (game_mode_detect_wine_loader(exe)) {
 		LOG_MSG("Detected wine loader for client %d [%s].\n", pid, exe);
 		goto wine_preloader;
 	}

+ 49 - 1
daemon/gamemode.h

@@ -34,10 +34,15 @@ POSSIBILITY OF SUCH DAMAGE.
 #include <stdbool.h>
 #include <sys/types.h>
 
+#define INVALID_PROCFD -1
+
+typedef int procfd_t;
+
 /**
- * Opaque context
+ * Opaque types
  */
 typedef struct GameModeContext GameModeContext;
+typedef struct GameModeConfig GameModeConfig;
 
 /**
  * Return the singleton instance
@@ -83,3 +88,46 @@ bool game_mode_context_unregister(GameModeContext *self, pid_t pid);
  *          2 if gamemode is active and the client is registered
  */
 int game_mode_context_query_status(GameModeContext *self, pid_t pid);
+
+/**
+ * Query the config of a gamemode context
+ *
+ * @param context A gamemode context
+ * @returns Configuration from the gamemode context
+ */
+GameModeConfig *game_mode_config_from_context(const GameModeContext *context);
+
+/** gamemode-env.c
+ * Provides internal API functions specific to working environment
+ * variables.
+ */
+char *game_mode_lookup_proc_env(const procfd_t proc_fd, const char *var);
+char *game_mode_lookup_user_home(void);
+
+/** gamemode-ioprio.c
+ * Provides internal API functions specific to adjusting process
+ * IO priorities.
+ */
+void game_mode_apply_ioprio(const GameModeContext *self, const pid_t client);
+
+/** gamemode-proc.c
+ * Provides internal API functions specific to working with process
+ * environments.
+ */
+procfd_t game_mode_open_proc(const pid_t pid);
+int game_mode_close_proc(const procfd_t procfd);
+
+/** gamemode-sched.c
+ * Provides internal API functions specific to adjusting process
+ * scheduling.
+ */
+void game_mode_apply_renice(const GameModeContext *self, const pid_t client);
+void game_mode_apply_scheduling(const GameModeContext *self, const pid_t client);
+
+/** gamemode-wine.c
+ * Provides internal API functions specific to handling wine
+ * prefixes.
+ */
+bool game_mode_detect_wine_loader(const char *exe);
+bool game_mode_detect_wine_preloader(const char *exe);
+char *game_mode_resolve_wine_preloader(const pid_t pid);

+ 64 - 0
daemon/helpers.h

@@ -0,0 +1,64 @@
+/*
+
+Copyright (c) 2017-2018, 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 <stdio.h>
+#include <string.h>
+#include <sys/param.h>
+
+/**
+ * Value clamping helper, works like MIN/MAX but constraints a value within the range.
+ */
+#define CLAMP(lbound, ubound, value) MIN(MIN(lbound, ubound), MAX(MAX(lbound, ubound), value))
+
+/**
+ * Little helper to safely print into a buffer, returns a pointer into the buffer
+ */
+#define buffered_snprintf(b, s, ...)                                                               \
+	(snprintf(b, sizeof(b), s, __VA_ARGS__) < (ssize_t)sizeof(b) ? b : NULL)
+
+/**
+ * Little helper to safely print into a buffer, returns a newly allocated string
+ */
+#define safe_snprintf(b, s, ...)                                                                   \
+	(snprintf(b, sizeof(b), s, __VA_ARGS__) < (ssize_t)sizeof(b) ? strndup(b, sizeof(b)) : NULL)
+
+/**
+ * Helper function: Test, if haystack ends with needle.
+ */
+static inline const char *strtail(const char *haystack, const char *needle)
+{
+	char *pos = strstr(haystack, needle);
+	if (pos && (strlen(pos) == strlen(needle)))
+		return pos;
+	return NULL;
+}

+ 27 - 0
daemon/logging.h

@@ -62,6 +62,13 @@ POSSIBILITY OF SUCH DAMAGE.
 		}                                                                                          \
 	} while (0)
 
+#define LOG_ONCE(type, ...)                                                                        \
+	do {                                                                                           \
+		static int __once = 0;                                                                     \
+		if (!__once++)                                                                             \
+			LOG_##type(__VA_ARGS__);                                                               \
+	} while (0)
+
 /* Fatal warnings trigger an exit */
 #define FATAL_ERRORNO(msg)                                                                         \
 	do {                                                                                           \
@@ -74,6 +81,26 @@ POSSIBILITY OF SUCH DAMAGE.
 		exit(EXIT_FAILURE);                                                                        \
 	} while (0)
 
+/* Hinting helpers */
+#define HINT_ONCE(name, hint)                                                                      \
+	do {                                                                                           \
+		static int __once = 0;                                                                     \
+		name = (!__once++ ? hint : "");                                                            \
+	} while (0)
+
+#define HINT_ONCE_ON(cond, ...)                                                                    \
+	do {                                                                                           \
+		if (cond)                                                                                  \
+			HINT_ONCE(__VA_ARGS__);                                                                \
+	} while (0);
+
+#define LOG_HINTED(type, msg, hint, ...)                                                           \
+	do {                                                                                           \
+		const char *__arg;                                                                         \
+		HINT_ONCE(__arg, hint);                                                                    \
+		LOG_##type(msg "%s", __VA_ARGS__, __arg);                                                  \
+	} while (0)
+
 /**
  * Control if and how how we use syslog
  */

+ 5 - 0
daemon/meson.build

@@ -18,6 +18,11 @@ link_daemon_common = declare_dependency(
 daemon_sources = [
     'main.c',
     'gamemode.c',
+    'gamemode-env.c',
+    'gamemode-ioprio.c',
+    'gamemode-proc.c',
+    'gamemode-sched.c',
+    'gamemode-wine.c',
     'daemonize.c',
     'dbus_messaging.c',
     'governors.c',

+ 1 - 1
scripts/format-check.sh

@@ -4,7 +4,7 @@
 # Ensure we are at the project root
 cd "$(dirname $0)"/..
 
-wget -Nq https://llvm.org/svn/llvm-project/cfe/trunk/tools/clang-format/git-clang-format
+wget -Nq -T3 -t1 https://llvm.org/svn/llvm-project/cfe/trunk/tools/clang-format/git-clang-format
 
 if chmod +x git-clang-format; then
   if [[ "$1" == "--pre-commit" ]]; then