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