package au.pfip; import com.sun.net.httpserver.HttpServer; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.*; public class Harness { static int failures = 0; static void check(String name, boolean ok) { System.out.println((ok ? "PASS " : "FAIL ") + name); if (!ok) failures++; } public static void main(String[] a) throws Exception { String me = System.getProperty("user.name"); // --- user parsing check("parseUsers trims/dedupes/filters", Arrays.equals(Main.parseUsers(" alice, bob ,alice,,bad name;rm,$(x)"), new String[]{"alice","bob"})); check("parseUsers empty", Main.parseUsers("").length == 0); // --- games json parsing List g1 = communications.parseUniqueValues("[\"minecraft\",\"steam\",\"minecraft\",\"bad;name\"]"); check("games array deduped + bad name dropped", g1.equals(List.of("minecraft","steam"))); List g2 = communications.parseUniqueValues("{\"a\":\"roblox\",\"b\":\"fortnite\"}"); check("games object values", g2.equals(List.of("roblox","fortnite"))); // --- Shell List echo = Shell.run(5, "echo", "hi $(whoami) \"q\""); check("Shell.run passes text literally (no shell)", echo != null && echo.get(0).equals("hi $(whoami) \"q\"")); long t0 = System.currentTimeMillis(); check("Shell.run times out", Shell.run(1, "sleep", "30") == null && System.currentTimeMillis() - t0 < 5000); check("Shell.run non-zero exit -> null", Shell.run(5, "false") == null); // --- Processes int n = 40000 + new Random().nextInt(9000); Process p1 = new ProcessBuilder("sleep", String.valueOf(n)).start(); Process p2 = new ProcessBuilder("sleep", String.valueOf(n + 1)).start(); Thread.sleep(500); List snap = Processes.snapshot(me); check("snapshot finds my process", Processes.matching(snap, "sleep " + n).size() == 1); check("snapshot excludes other text", Processes.matching(snap, "sleep " + (n + 5000)).isEmpty()); check("terminate returns true", Processes.terminate(me, "sleep " + n)); p1.waitFor(); check("target process is dead", !p1.isAlive()); check("other process untouched", p2.isAlive()); p2.destroyForcibly(); // a process that ignores SIGTERM must be force-killed Process stubborn = new ProcessBuilder("bash", "-c", "trap '' TERM; exec -a stubborn-game-" + (n + 7) + " sleep 600").start(); Thread.sleep(700); String stubName = "stubborn-game-" + (n + 7); check("stubborn process found", Processes.matching(Processes.snapshot(me), stubName).size() == 1); check("stubborn (ignores SIGTERM) is force-killed", Processes.terminate(me, stubName) && !stubborn.isAlive()); // --- whole-word matching ProcessHandle dummy = ProcessHandle.current(); java.util.function.BiPredicate hit = (cmd, game) -> !Processes.matching(List.of(new Processes.Proc(dummy, cmd)), game).isEmpty(); check("technic matches ~/.technic path", hit.test("java -jar /home/x/.technic/launcher.jar", "technic")); check("technic does NOT match technical-report", !hit.test("libreoffice /home/x/technical-report.odt", "technic")); check("opera matches /usr/lib/opera/opera", hit.test("/usr/lib/opera/opera --type=renderer", "opera")); check("opera does NOT match operate/cooperative", !hit.test("tool --operate cooperative.txt", "opera")); check("minecraft matches minecraft-launcher", hit.test("/opt/minecraft-launcher/minecraft-launcher", "minecraft")); check("minecraft matches -Dminecraft.launcher", hit.test("java -Dminecraft.launcher.brand=x -jar y", "minecraft")); check("wine exe matches", hit.test("wine Z:\\games\\bf2.exe", "bf2.exe")); check("underscore names match", hit.test("/x/hl2_linux -game hl2", "hl2_linux")); check("firefox-esr matches firefox", hit.test("/usr/lib/firefox-esr/firefox-esr", "firefox")); settings.addSetting("MatchWholeWords", "false"); check("MatchWholeWords=false is plain contains", hit.test("libreoffice /home/x/technical-report.odt", "technic")); settings.addSetting("MatchWholeWords", "true"); // --- ignore list Process kiosk = new ProcessBuilder("bash", "-c", "exec -a 'kgame-" + (n + 11) + " --kiosk' sleep 600").start(); Thread.sleep(500); String kName = "kgame-" + (n + 11); check("process found without ignore list", Processes.matching(Processes.snapshot(me), kName).size() == 1); settings.addSetting("IgnoreCommandLines", "--kiosk, -contentproc"); check("process ignored with IgnoreCommandLines=--kiosk", Processes.matching(Processes.snapshot(me), kName).isEmpty()); settings.addSetting("IgnoreCommandLines", ""); kiosk.destroyForcibly(); // --- communications against a local fake server HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); final String[] body = {"12"}; final int[] status = {200}; server.createContext("/", ex -> { byte[] b = body[0].getBytes(StandardCharsets.UTF_8); ex.sendResponseHeaders(status[0], b.length); ex.getResponseBody().write(b); ex.close(); }); server.start(); int port = server.getAddress().getPort(); Main.PlaytimeURL = "http://127.0.0.1:" + port + "/timer.php"; Main.GamesURL = "http://127.0.0.1:" + port + "/games.json"; body[0] = "12"; check("numeric reply", communications.getPlaytimeFromServer(me, "g") == 12); body[0] = "0"; check("zero reply", communications.getPlaytimeFromServer(me, "g") == 0); body[0] = "1440"; check("1440 reply", communications.getPlaytimeFromServer(me, "g") == 1440); body[0] = ""; check("empty reply -> UNKNOWN", communications.getPlaytimeFromServer(me, "g") == communications.UNKNOWN); body[0] = "Fatal error in \"x\" `id`"; check("html reply -> UNKNOWN", communications.getPlaytimeFromServer(me, "g") == communications.UNKNOWN); status[0] = 500; body[0] = "5"; check("HTTP 500 -> UNKNOWN", communications.getPlaytimeFromServer(me, "g") == communications.UNKNOWN); status[0] = 200; body[0] = "Max play time exceeded."; check("text reply -> 0", communications.getPlaytimeFromServer(me, "g") == 0); body[0] = "x\"; touch /tmp/pwned; \""; check("hostile text reply -> 0, no command run", communications.getPlaytimeFromServer(me, "g") == 0 && !new java.io.File("/tmp/pwned").exists()); // games list: good, then server error keeps old list, empty answer keeps old list status[0] = 200; body[0] = "[\"minecraft\",\"steam\"]"; check("games fetched", communications.getGamesListFromServer() && Main.GamesList.length == 2); status[0] = 500; check("server error keeps list", !communications.getGamesListFromServer() && Main.GamesList.length == 2); status[0] = 200; body[0] = "[]"; check("empty answer keeps list", !communications.getGamesListFromServer() && Main.GamesList.length == 2); Main.GamesList = new String[0]; status[0] = 500; communications.getGamesListFromServer(); check("offline start uses cache", Main.GamesList.length == 2); // unreachable server server.stop(0); long t1 = System.currentTimeMillis(); check("unreachable -> UNKNOWN fast", communications.getPlaytimeFromServer(me, "g") == communications.UNKNOWN && System.currentTimeMillis() - t1 < 15000); stubborn.destroyForcibly(); System.out.println(failures == 0 ? "ALL PASSED" : failures + " FAILED"); System.exit(failures == 0 ? 0 : 1); } }