#!/bin/php user = trim(exec('whoami')); } public function test() { $this->message('test title', 'test message', ''); } /** * Send a message to the user. * * Example usage: * $this->message('Alert','this is an important message.',1); * * @param string $title The title to be displayed for the message. * @param string $messageText The body text of the message to be displayed. * @param int $urgency 1 = High, 2 = Medium, 3 = Low * * @return void */ public function message($title, $messageText, $urgency = '') { switch ($urgency) { case 1: $urgencyText = ' --urgency=critical'; break; case 2: $urgencyText = ' --urgency=normal'; break; case 3: $urgencyText = ' --urgency=low'; break; default: $urgencyText = ''; break; } exec('notify-send' . $urgencyText . ' ' . escapeshellarg((string) $title) . ' ' . escapeshellarg(substr((string) $messageText, 0, 300))); } /** * Ask the server. Returns the trimmed reply, or null if it can't be reached. */ private function fetch($path) { $context = stream_context_create(['http' => ['timeout' => self::HTTP_TIMEOUT]]); $reply = @file_get_contents(self::BASE_URL . $path, false, $context); return ($reply === false) ? null : trim($reply); } private function timerUrl($game, $peek = false) { return 'timer.php?user=' . rawurlencode($this->user) . '&game=' . rawurlencode($game) . ($peek ? '&peek=1' : ''); } /** * The list of game process names. Waits until the server answers. */ public function loadGames() { while (true) { $json = $this->fetch('games.json'); $games = ($json === null) ? null : json_decode($json, true); if (is_array($games)) { return $games; } $this->Logging('Could not load games.json, trying again in 30 seconds.'); sleep(30); } } /** * All of this user's processes as [pid => command line], without this script. */ private function processList() { $lines = []; exec('ps -u ' . escapeshellarg($this->user) . ' -o pid=,args=', $lines); $self = getmypid(); $processes = []; foreach ($lines as $line) { if (!preg_match('/^\s*(\d+)\s+(.*)$/', $line, $match)) { continue; } if ((int) $match[1] === $self) { continue; } $processes[(int) $match[1]] = $match[2]; } return $processes; } /** * Pids of the processes that belong to $game. */ private function gamePids($game, $processes) { $pids = []; foreach ($processes as $pid => $commandLine) { if (strpos($commandLine, $game) !== false && $this->cmdExceptions($commandLine)) { $pids[] = $pid; } } return $pids; } public function burnTime($startupTime, $burnTimeDuration) { if ($this->lastActiveGame == "") { sleep(20); return; } $remaining = ($startupTime + $burnTimeDuration) - microtime(true); if ($remaining > 0) { usleep((int) ($remaining * 1000000)); } } public function CheckForGames($listOfKnownGames) { $processes = $this->processList(); foreach ($listOfKnownGames as $gameName) { if (!is_string($gameName) || $gameName === '') { continue; } if (count($this->gamePids($gameName, $processes)) > 0) { $this->AnounceGameDetected($gameName); return; } } $this->AnounceGameNotDetected(); } private function cmdExceptions($cmdLineReturn) { if (strpos($cmdLineReturn, 'grep') !== false) { return false; } elseif (strpos($cmdLineReturn, 'kiosk') !== false) { return false; } elseif (strpos($cmdLineReturn, 'contentproc') !== false) { return false; } elseif (strpos($cmdLineReturn, 'new-window') !== false) { //opera helper exception return false; } elseif (strpos($cmdLineReturn, 'type=gpu-process') !== false) { // opera helper exception return false; } return true; } private function AnounceGameDetected($game) { if ($this->lastActiveGame == "") { $this->lastActiveGame = $game; $this->failedChecks = 0; // peek=1: report the minutes remaining without using one up. $reply = $this->fetch($this->timerUrl($game, true)); if ($reply !== null && is_numeric($reply)) { $text = ucfirst($game) . " detected running." . PHP_EOL . "You have " . $reply . " minutes remaining."; } elseif ($reply !== null) { $text = ucfirst($game) . " detected running." . PHP_EOL . $reply; } else { $text = ucfirst($game) . " detected running." . PHP_EOL . "The Timekeeper server could not be reached."; } $this->message("Timekeeper", $text); $this->Logging(ucfirst($game) . " detected running. " . ($reply === null ? 'No reply from server.' : $reply . ' minutes remaining.')); } } private function AnounceGameNotDetected() { if ($this->lastActiveGame != "") { $this->lastActiveGame = ""; $this->failedChecks = 0; $this->message("Timekeeper", "No games detected running."); $this->Logging("No games detected running."); } } public function CheckTimeRemaining() { if ($this->lastActiveGame == "") { return; } $reply = $this->fetch($this->timerUrl($this->lastActiveGame)); if ($reply === null) { $this->failedChecks++; $this->Logging('Could not reach the Timekeeper server (' . $this->failedChecks . ' in a row).'); if ($this->failedChecks >= self::MAX_FAILED_CHECKS) { $this->message("Timekeeper", "The Timekeeper server can not be reached, so the game will be closed.", 1); $this->KillGame(); } return; } $this->failedChecks = 0; $this->TimeLimitWarning($reply); } private function TimeLimitWarning($reply) { if (!is_numeric($reply)) { // Anything that is not a number is the reason play is not allowed. $this->message("Timekeeper", $reply); $this->KillGame(); return; } $minutes = (int) $reply; if ($minutes <= 0) { $this->message("Timekeeper", "Your play time has expired."); $this->KillGame(); } elseif ($minutes == 5) { $this->message("Timekeeper", "You only have 5 minutes remaining."); } elseif ($minutes == 1) { $this->message("Timekeeper", "You only have 1 minute remaining."); } } private function KillGame() { $game = $this->lastActiveGame; if ($game == "") { return; } $pids = $this->gamePids($game, $this->processList()); if (count($pids) == 0) { $this->Logging('Game is supposedly dead, bailing out now.'); return; } if ($this->debug) { $this->message("Timekeeper", "Game PID-" . implode(',', $pids) . " should\nhave just been killed."); return; } foreach ([15, 9] as $signal) { $this->Logging('Killing ' . $game . ' (pid ' . implode(',', $pids) . ') with signal ' . $signal . '.'); foreach ($pids as $pid) { $this->signal($pid, $signal); } sleep($signal === 15 ? 10 : 3); $pids = $this->gamePids($game, $this->processList()); if (count($pids) == 0) { return; } } // Last resort. $this->Logging('Still running, using pkill on ' . $game . '.'); exec('pkill -9 -u ' . escapeshellarg($this->user) . ' -f ' . escapeshellarg($game)); } private function signal($pid, $signal) { if (function_exists('posix_kill')) { @posix_kill((int) $pid, (int) $signal); } else { exec('kill -' . (int) $signal . ' ' . (int) $pid); } } /** * Write to the common log file (next to this script, capped at 1 MB). * * @param string $message The message to be written into the log file. * @param bool $append Set to false to overwrite the log file. * * @return void */ public function Logging($message, $append = true) { $filename = __DIR__ . '/timekeeper.log'; if (is_file($filename) && filesize($filename) > self::MAX_LOG_BYTES) { @rename($filename, $filename . '.1'); } if (@file_put_contents($filename, $message . PHP_EOL, $append ? FILE_APPEND : 0) === false) { $this->message("Error", "Logging can not be initialised.", 1); } } } $TKL = new timekeeperLocal(); if (isset($argv[1]) && $argv[1] == 'debug') { echo "Debug mode active."; $TKL->debug = true; $TKL->message("Timekeeper " . timekeeperLocal::VERSION, "Timekeeper is running. (In debug mode)"); } else { $TKL->message("Timekeeper " . timekeeperLocal::VERSION, "Timekeeper is running."); } // Only one copy per user: the lock goes away by itself when this script ends. $lock = fopen(sys_get_temp_dir() . '/timekeeper-' . preg_replace('/\W/', '_', $TKL->user) . '.lock', 'c'); if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) { die("Found another instance." . PHP_EOL); } set_time_limit(0); $gamesList = $TKL->loadGames(); $TKL->Logging("Timekeeper " . timekeeperLocal::VERSION . " initialised for " . $TKL->user, false); while (true) { $startupTime = microtime(true); $TKL->CheckForGames($gamesList); $TKL->CheckTimeRemaining(); $TKL->burnTime($startupTime, 60); }