Merge pull request #86 from kakra/modular-refactor

Tidy up the code and refactor into modules
This commit is contained in:
Alex Smith
2018-10-10 08:55:58 +01:00
committed by GitHub
11 changed files with 741 additions and 429 deletions

96
daemon/gamemode-env.c Normal file
View File

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

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

View File

@ -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.
*/
#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)
procfd_t game_mode_open_proc(const pid_t pid)
{
return (int)syscall(SYS_ioprio_set, which, who, ioprio);
char buffer[PATH_MAX];
const char *proc_path = buffered_snprintf(buffer, "/proc/%d", pid);
return proc_path ? open(proc_path, O_RDONLY | O_CLOEXEC) : INVALID_PROCFD;
}
/**
* Closes the process environment.
*/
int game_mode_close_proc(const procfd_t procfd)
{
return close(procfd);
}

149
daemon/gamemode-sched.c Normal file
View File

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

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

View File

@ -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",
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,
additional_message);
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)
GameModeConfig *game_mode_config_from_context(const GameModeContext *context)
{
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)
{
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;
}

View File

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

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

View File

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

View File

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

View File

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