|
|
@@ -1,31 +1,55 @@
|
|
|
-// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
|
|
|
-// then press Enter. You can now see whitespace characters in your code.
|
|
|
-
|
|
|
package au.pfip;
|
|
|
|
|
|
-import java.util.Objects;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.LinkedHashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
|
/**
|
|
|
- * The code below will print the words Hello World
|
|
|
- * to the screen, and it is amazing
|
|
|
+ * Timekeeper enforcement: once a minute, look for the monitored students'
|
|
|
+ * games, ask the server how many play minutes are left, warn, and close the
|
|
|
+ * game when there are none.
|
|
|
+ *
|
|
|
+ * Changes from 4.0:
|
|
|
+ * - several students: the old loop kept ONE running game and ONE user, and
|
|
|
+ * checking the next student cleared what the previous check had found, so
|
|
|
+ * only the last student in UsersToMonitor was ever enforced (and only
|
|
|
+ * while they were playing). Each student now has their own state, is
|
|
|
+ * checked, charged and closed on their own
|
|
|
+ * - no shell: games are found and stopped with Java's process API, and
|
|
|
+ * pkill is restricted to the student's own processes (it used to stop
|
|
|
+ * anyone's process whose command line contained the game name)
|
|
|
+ * - an unreachable server or unusable reply is told apart from "0 minutes";
|
|
|
+ * the game is closed after OfflineChecksBeforeKill failed checks in a row
|
|
|
+ * (default 5, set 1 for the old close-at-once behaviour)
|
|
|
+ * - the games list is refreshed every GamesRefreshMinutes and cached, so
|
|
|
+ * starting offline doesn't leave nothing monitored
|
|
|
+ * - missing or blank settings no longer crash the program or switch
|
|
|
+ * enforcement off (ExemptNetwork empty used to match every network)
|
|
|
+ * - the 30 minute warning is only for a real 30 minutes (the server used to
|
|
|
+ * answer 30 for students with Timekeeper switched off; it answers 1440 now)
|
|
|
*/
|
|
|
public class Main {
|
|
|
|
|
|
- private static final double version = 4.0;
|
|
|
- public static final String GamesURL = "https://overlord.pfip.au/e107_plugins/timekeeper/timekeeperClientServices/games.json";
|
|
|
- public static final String PlaytimeURL = "https://overlord.pfip.au/e107_plugins/timekeeper/timekeeperClientServices/timer.php";
|
|
|
- private static String runningGame = "";
|
|
|
- private static String activeUser = "";
|
|
|
- private static int playTimeRemaining = 0;
|
|
|
- private static Boolean shutdownFlag = false;
|
|
|
- public static String[] GamesList;
|
|
|
- private static String[] Users;
|
|
|
+ public static final String VERSION = "4.1";
|
|
|
+ public static String GamesURL;
|
|
|
+ public static String PlaytimeURL;
|
|
|
+ public static String[] GamesList = new String[0];
|
|
|
+
|
|
|
+ private static final Pattern VALID_USER = Pattern.compile("[A-Za-z_][A-Za-z0-9_.-]{0,31}");
|
|
|
+
|
|
|
+ private static volatile boolean shutdownFlag = false;
|
|
|
+ private static String[] Users = new String[0];
|
|
|
+ private static final Map<String, String> runningGames = new HashMap<>();
|
|
|
+ private static final Map<String, Integer> failedChecks = new HashMap<>();
|
|
|
+ private static long lastGamesRefresh = 0;
|
|
|
|
|
|
/**
|
|
|
* @param args Commandline arguments
|
|
|
*/
|
|
|
public static void main(String[] args) {
|
|
|
-
|
|
|
// If we couldn't get/make settings just exit as no config available
|
|
|
if(!settings.initialiseSettings())
|
|
|
{
|
|
|
@@ -40,9 +64,12 @@ public class Main {
|
|
|
System.out.println("Logging engine failed to initialise, we can live without this.");
|
|
|
}
|
|
|
|
|
|
- logging.information("Starting Timekeeper " + version);
|
|
|
- boolean timekeeperActive = Boolean.parseBoolean(settings.getSetting("TimekeeperActive"));
|
|
|
+ logging.information("Starting Timekeeper " + VERSION);
|
|
|
|
|
|
+ GamesURL = settings.getSetting("GamesURL");
|
|
|
+ PlaytimeURL = settings.getSetting("PlaytimeURL");
|
|
|
+
|
|
|
+ boolean timekeeperActive = Boolean.parseBoolean(settings.getSetting("TimekeeperActive"));
|
|
|
if(!timekeeperActive)
|
|
|
{
|
|
|
logging.information("Timekeeper is set to not active, bailing out after a long delay.");
|
|
|
@@ -51,132 +78,198 @@ public class Main {
|
|
|
System.exit(0);
|
|
|
}
|
|
|
|
|
|
- Users = settings.getSetting("UsersToMonitor").split(",");
|
|
|
+ Users = parseUsers(settings.getSetting("UsersToMonitor"));
|
|
|
+ if(Users.length == 0)
|
|
|
+ {
|
|
|
+ logging.error("No valid UsersToMonitor in the settings file, bailing out after a long delay.");
|
|
|
+ System.out.println("No valid UsersToMonitor in the settings file, bailing out after a long delay.");
|
|
|
+ burnTime(600);
|
|
|
+ System.exit(0);
|
|
|
+ }
|
|
|
|
|
|
- if(Shell.check("/usr/bin/nmcli | /usr/bin/grep \"connected to \"",settings.getSetting("ExemptNetwork")))
|
|
|
+ if(isExemptNetwork())
|
|
|
{
|
|
|
- logging.information("Network " + settings.getSetting("ExemptNetwork") + "detected.");
|
|
|
+ logging.information("Network " + settings.getSetting("ExemptNetwork") + " detected.");
|
|
|
burnTime(600);
|
|
|
System.exit(0);
|
|
|
}
|
|
|
|
|
|
// Get list of games to be monitored.
|
|
|
communications.getGamesListFromServer();
|
|
|
+ lastGamesRefresh = System.currentTimeMillis();
|
|
|
|
|
|
// Inform users that the Timekeeper is starting up.
|
|
|
for (String user : Users) {
|
|
|
- communications.msgPopup("Starting up version " + version + ".",user);
|
|
|
+ communications.msgPopup("Starting up version " + VERSION + ".", user);
|
|
|
}
|
|
|
|
|
|
registerShutdownHook();
|
|
|
- while(!shutdownFlag) {
|
|
|
-
|
|
|
- startChecks();
|
|
|
-
|
|
|
- checkPlayTime();
|
|
|
-
|
|
|
- killGames();
|
|
|
|
|
|
+ while(!shutdownFlag) {
|
|
|
+ refreshGamesListIfDue();
|
|
|
+ for (String user : Users) {
|
|
|
+ checkUser(user);
|
|
|
+ }
|
|
|
burnTime(60);
|
|
|
}
|
|
|
|
|
|
// Inform users that the Timekeeper is shutting down.
|
|
|
for (String user : Users) {
|
|
|
- communications.msgPopup("Shutting down.",user);
|
|
|
+ communications.msgPopup("Shutting down.", user);
|
|
|
}
|
|
|
-
|
|
|
logging.information("Shutting down.");
|
|
|
}
|
|
|
|
|
|
+ public static boolean isValidUser(String user)
|
|
|
+ {
|
|
|
+ return user != null && VALID_USER.matcher(user).matches();
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * Kill running games if there is no playtime left.
|
|
|
+ * The monitored user names: trimmed, valid, no duplicates.
|
|
|
*/
|
|
|
- private static void killGames()
|
|
|
+ static String[] parseUsers(String setting)
|
|
|
{
|
|
|
- if(!Objects.equals(runningGame, "") && playTimeRemaining <= 0) {
|
|
|
- System.out.println("Killing " + runningGame + " for user " + activeUser);
|
|
|
- communications.msgPopup("Killing: " + runningGame + " as you are out of play time.",activeUser);
|
|
|
- killGame(runningGame,activeUser);
|
|
|
- //Shell.check("ps aux | grep " + user + " | grep " + game + " | grep -v \"grep\"", game)
|
|
|
- }else if(!Objects.equals(runningGame, "") && playTimeRemaining == 30){
|
|
|
- communications.msgPopup("Notice: You have 30 minutes remaining.",activeUser);
|
|
|
- }else if(!Objects.equals(runningGame, "") && playTimeRemaining == 5){
|
|
|
- communications.msgPopup("Notice: You have 5 minutes remaining.",activeUser);
|
|
|
- }else if(!Objects.equals(runningGame, "") && playTimeRemaining == 4){
|
|
|
- communications.msgPopup("Notice: You have 4 minutes remaining.",activeUser);
|
|
|
- }else if(!Objects.equals(runningGame, "") && playTimeRemaining == 3){
|
|
|
- communications.msgPopup("Notice: You have 3 minutes remaining.",activeUser);
|
|
|
- }else if(!Objects.equals(runningGame, "") && playTimeRemaining == 2){
|
|
|
- communications.msgPopup("Notice: You have 2 minutes remaining.",activeUser);
|
|
|
- }else if(!Objects.equals(runningGame, "") && playTimeRemaining == 1){
|
|
|
- communications.msgPopup("Notice: You have 1 minute remaining.",activeUser);
|
|
|
- }else if(!Objects.equals(activeUser, "")){
|
|
|
- System.out.println(activeUser + " still has playtime remaining (" + playTimeRemaining + " minutes).");
|
|
|
+ Set<String> users = new LinkedHashSet<>();
|
|
|
+ for (String user : setting.split(",")) {
|
|
|
+ user = user.trim();
|
|
|
+ if (user.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (isValidUser(user)) {
|
|
|
+ users.add(user);
|
|
|
+ } else {
|
|
|
+ logging.error("Ignoring invalid user name in UsersToMonitor: " + user);
|
|
|
+ }
|
|
|
}
|
|
|
+ return users.toArray(new String[0]);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Get remaining playtime from the server
|
|
|
+ * Is the computer connected to the network where enforcement is switched
|
|
|
+ * off? (Checked once at start-up. Anyone can name a hotspot after a
|
|
|
+ * network, so choose this one with that in mind.)
|
|
|
*/
|
|
|
- private static void checkPlayTime()
|
|
|
+ private static boolean isExemptNetwork()
|
|
|
{
|
|
|
- if(!Objects.equals(runningGame, "")) {
|
|
|
- System.out.println(runningGame);
|
|
|
- playTimeRemaining = communications.getPlaytimeFromServer(activeUser, runningGame);
|
|
|
- System.out.println(playTimeRemaining);
|
|
|
- }else{
|
|
|
- System.out.println("No games running.");
|
|
|
+ String exempt = settings.getSetting("ExemptNetwork");
|
|
|
+ if (exempt.isEmpty()) {
|
|
|
+ return false;
|
|
|
}
|
|
|
+ List<String> output = Shell.run(10, "/usr/bin/nmcli");
|
|
|
+ if (output == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (String line : output) {
|
|
|
+ if (line.contains("connected to ") && line.contains(exempt)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
- private static void startChecks()
|
|
|
+ /**
|
|
|
+ * Fetch the games list again now and then, and retry every minute while
|
|
|
+ * there is none at all.
|
|
|
+ */
|
|
|
+ private static void refreshGamesListIfDue()
|
|
|
{
|
|
|
- for (String user : Users) {
|
|
|
- System.out.println("Checking running games for " + user);
|
|
|
- checkRunningGames(user);
|
|
|
+ long refreshMillis = Math.max(1, settings.getInt("GamesRefreshMinutes", 60)) * 60_000L;
|
|
|
+ boolean due = System.currentTimeMillis() - lastGamesRefresh >= refreshMillis;
|
|
|
+
|
|
|
+ if (GamesList.length == 0 || due) {
|
|
|
+ communications.getGamesListFromServer();
|
|
|
+ lastGamesRefresh = System.currentTimeMillis();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void checkRunningGames(String user)
|
|
|
+ /**
|
|
|
+ * Check one student: find their game, ask for their minutes, act on the answer.
|
|
|
+ */
|
|
|
+ private static void checkUser(String user)
|
|
|
{
|
|
|
+ String game = findRunningGame(user);
|
|
|
+
|
|
|
+ if (game == null) {
|
|
|
+ if (runningGames.remove(user) != null) {
|
|
|
+ logging.information(user + " is no longer running a game.");
|
|
|
+ }
|
|
|
+ failedChecks.remove(user);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String previous = runningGames.put(user, game);
|
|
|
+ if (!game.equals(previous)) {
|
|
|
+ logging.information(game + " detected running for " + user + ".");
|
|
|
+ }
|
|
|
+
|
|
|
+ int remaining = communications.getPlaytimeFromServer(user, game);
|
|
|
+
|
|
|
+ if (remaining == communications.UNKNOWN) {
|
|
|
+ int failures = failedChecks.merge(user, 1, Integer::sum);
|
|
|
+ int limit = Math.max(1, settings.getInt("OfflineChecksBeforeKill", 5));
|
|
|
+ logging.information("Could not check play time for " + user + " (" + failures + " of " + limit + ").");
|
|
|
+ if (failures >= limit) {
|
|
|
+ communications.msgPopup("The Timekeeper server can not be reached, so " + game + " will be closed.", user);
|
|
|
+ killGame(user, game);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ failedChecks.remove(user);
|
|
|
+
|
|
|
+ if (remaining <= 0) {
|
|
|
+ communications.msgPopup("Closing " + game + " as you are out of play time.", user);
|
|
|
+ killGame(user, game);
|
|
|
+ } else if (remaining == 30 || remaining <= 5) {
|
|
|
+ communications.msgPopup("Notice: You have " + remaining + (remaining == 1 ? " minute" : " minutes") + " remaining.", user);
|
|
|
+ } else {
|
|
|
+ System.out.println(user + " still has playtime remaining (" + remaining + " minutes).");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The first known game that is running for the user, or null.
|
|
|
+ */
|
|
|
+ private static String findRunningGame(String user)
|
|
|
+ {
|
|
|
+ List<Processes.Proc> processes = Processes.snapshot(user);
|
|
|
for (String game : GamesList) {
|
|
|
- // ps aux | grep gregory | grep minecraft | grep -v grep
|
|
|
- if (Shell.check("/usr/bin/ps aux | /usr/bin/grep " + user + " | /usr/bin/grep " + game + " | /usr/bin/grep -v grep", game)) {
|
|
|
- runningGame = game;
|
|
|
- activeUser = user;
|
|
|
- //System.out.println("Dis true---------");
|
|
|
- System.out.println(game + " detected running.");
|
|
|
- return;
|
|
|
- } else {
|
|
|
- //System.out.println(game + " not running.");
|
|
|
- runningGame = "";
|
|
|
- activeUser = "";
|
|
|
+ if (!Processes.matching(processes, game).isEmpty()) {
|
|
|
+ return game;
|
|
|
}
|
|
|
}
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void killGame(String user, String game)
|
|
|
+ {
|
|
|
+ if (Processes.terminate(user, game)) {
|
|
|
+ runningGames.remove(user);
|
|
|
+ } else {
|
|
|
+ logging.error("Could not stop " + game + " for " + user + ", will try again next time.");
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Burn 60 seconds while watching for shutdown signal.
|
|
|
+ * Burn time while watching for the shutdown signal.
|
|
|
*/
|
|
|
private static void burnTime(int time)
|
|
|
{
|
|
|
- System.out.println("burning 60s.");
|
|
|
int timer = time;
|
|
|
- while(!shutdownFlag && timer!=0) {
|
|
|
+ while(!shutdownFlag && timer != 0) {
|
|
|
timer--;
|
|
|
try {
|
|
|
- //System.out.println("burning " + timer + "s.");
|
|
|
//noinspection BusyWait
|
|
|
Thread.sleep(1000);
|
|
|
} catch (InterruptedException e) {
|
|
|
- logging.error("RuntimeException(e) ~ " + e);
|
|
|
+ logging.error("InterruptedException(e) ~ " + e);
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
private static void registerShutdownHook() {
|
|
|
final Thread mainThread = Thread.currentThread();
|
|
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
|
@@ -188,45 +281,10 @@ public class Main {
|
|
|
} catch (InterruptedException ex) {
|
|
|
logging.error("RuntimeException(e) ~ " + ex);
|
|
|
}
|
|
|
-
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
static public void setShutdownProcess() {
|
|
|
shutdownFlag = true;
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- public static void killGame(String game, String user)
|
|
|
- {
|
|
|
-
|
|
|
- for (int i = 0; i <= 10; i++) {
|
|
|
- if (Shell.check("/usr/bin/ps aux | /usr/bin/grep " + user + " | /usr/bin/grep " + game + " | /usr/bin/grep -v grep", game)) {
|
|
|
- try{
|
|
|
- if(i >= 8) {
|
|
|
- Shell.runCmd("/usr/bin/pkill -9 -f " + game);
|
|
|
- logging.information("/usr/bin/pkill -9 -f " + game);
|
|
|
- logging.information("Killing " + game + " forcefully.");
|
|
|
- }else{
|
|
|
- Shell.runCmd("/usr/bin/pkill -15 -f " + game);
|
|
|
- logging.information("/usr/bin/pkill -15 -f " + game);
|
|
|
- logging.information("Killing " + game + " gracefully.");
|
|
|
- }
|
|
|
- Thread.sleep(1000);
|
|
|
- } catch (InterruptedException e) {
|
|
|
- logging.error("InterruptedException(e) ~ " + e);
|
|
|
- }
|
|
|
- } else {
|
|
|
- //System.out.println(game + " not running.");
|
|
|
- runningGame = "";
|
|
|
- activeUser = "";
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-// check for users logged in now.
|
|
|
-//ps -eo user,uid | awk 'NR>1 && $2 >= 1000 && ++seen[$2]==1{print $1}'
|
|
|
+}
|