package name.nathanmcrae.numbersstation; import com.leakyabstractions.result.api.Result; import com.leakyabstractions.result.core.Results; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.management.ManagementFactory; import java.lang.ProcessHandle; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javafx.util.Pair; public class LinuxScheduler { private static final Logger logger = Logger.getLogger(Main.class.getName()); public static Result registerSchedule(StationSettings settings) { try { String taskName = "numbers-station-main_" + settings.getName(); Process listProcess = new ProcessBuilder("crontab", "-l").start(); if (!listProcess.waitFor(5, TimeUnit.SECONDS)) { String message = "Failed to query " + taskName + " task: process timed out"; logger.log(Level.SEVERE, message); return Results.failure(message); } Pair output = WindowsScheduler.captureProcessOutput(listProcess); if(listProcess.exitValue() != 0) { String message = "Failed to get user id. Exit code: " + listProcess.exitValue() + ". stdout: " + output.getKey() + "\n\tstderr: " + output.getValue(); logger.log(Level.SEVERE, message); return Results.failure(message); } String currentCrontab = output.getKey(); String cronEntry = "# " + taskName + "\n" + cronExpression(settings) + "\n\n"; Pattern pattern = Pattern.compile("# " + taskName + "\\n[^\\n]+\\n\\n"); Matcher matcher = pattern.matcher(currentCrontab); StringBuilder sb = new StringBuilder(); if (matcher.find()) { matcher.appendReplacement(sb, cronEntry); boolean foundMultiple = false; while (matcher.find()) { foundMultiple = true; matcher.appendReplacement(sb, ""); } if (foundMultiple) { logger.log(Level.WARNING, "Found multiple instances of '" + taskName + "' prior to replacement"); } matcher.appendTail(sb); } else { sb.append(currentCrontab); sb.append(cronEntry); } String newCrontab = sb.toString(); Process addProcess = new ProcessBuilder("crontab", "-").start(); Writer w = new OutputStreamWriter(addProcess.getOutputStream(), "UTF-8"); System.out.println(newCrontab); w.write(newCrontab); w.flush(); w.close(); if (!addProcess.waitFor(5, TimeUnit.SECONDS)) { String message = "Failed to register " + taskName + " task: process timed out"; logger.log(Level.SEVERE, message); return Results.failure(message); } if(addProcess.exitValue() != 0) { Pair addOutput = WindowsScheduler.captureProcessOutput(addProcess); String message = "Failed to get user id. Exit code: " + addProcess.exitValue() + ". stdout: " + addOutput.getKey() + ". stderr: " + addOutput.getValue(); logger.log(Level.SEVERE, message); return Results.failure(message); } } catch (CronExpressionException | IOException | InterruptedException e) { String message = "Exception while registering schedule"; logger.log(Level.SEVERE, message, e); return Results.failure(message); } return Results.success(true); } public static Result scheduleExists(StationSettings settings) { try { String taskName = "numbers-station-main_" + settings.getName(); Process listProcess = new ProcessBuilder("crontab", "-l").start(); if (!listProcess.waitFor(5, TimeUnit.SECONDS)) { String message = "Failed to query " + taskName + " task: process timed out"; logger.log(Level.SEVERE, message); return Results.failure(message); } Pair output = WindowsScheduler.captureProcessOutput(listProcess); if(listProcess.exitValue() != 0) { String message = "Failed to get user id. Exit code: " + listProcess.exitValue() + ". stdout: " + output.getKey() + "\n\tstderr: " + output.getValue(); logger.log(Level.SEVERE, message); return Results.failure(message); } String currentCrontab = output.getKey(); String cronEntry = "# " + taskName + "\n" + cronExpression(settings) + "\n\n"; Pattern pattern = Pattern.compile("# " + taskName + "\\n[^\\n]+\\n\\n"); Matcher matcher = pattern.matcher(currentCrontab); if (matcher.find()) { return Results.success(true); } else { return Results.success(true); } } catch (CronExpressionException | IOException | InterruptedException e) { String message = "Exception while checking for schedule"; logger.log(Level.SEVERE, message, e); return Results.failure(message); } } public static Result removeSchedule(StationSettings settings) { try { String taskName = "numbers-station-main_" + settings.getName(); Process listProcess = new ProcessBuilder("crontab", "-l").start(); if (!listProcess.waitFor(5, TimeUnit.SECONDS)) { String message = "Failed to query " + taskName + " task: process timed out"; logger.log(Level.SEVERE, message); return Results.failure(message); } Pair output = WindowsScheduler.captureProcessOutput(listProcess); if(listProcess.exitValue() != 0) { String message = "Failed to get user id. Exit code: " + listProcess.exitValue() + ". stdout: " + output.getKey() + "\n\tstderr: " + output.getValue(); logger.log(Level.SEVERE, message); return Results.failure(message); } String currentCrontab = output.getKey(); String cronEntry = "# " + taskName + "\n" + cronExpression(settings) + "\n\n"; Pattern pattern = Pattern.compile("# " + taskName + "\\n[^\\n]+\\n\\n"); Matcher matcher = pattern.matcher(currentCrontab); StringBuilder sb = new StringBuilder(); while (matcher.find()) { matcher.appendReplacement(sb, ""); } String newCrontab = sb.toString(); Process addProcess = new ProcessBuilder("crontab", "-").start(); Writer w = new OutputStreamWriter(addProcess.getOutputStream(), "UTF-8"); System.out.println(newCrontab); w.write(newCrontab); w.flush(); w.close(); if (!addProcess.waitFor(5, TimeUnit.SECONDS)) { String message = "Failed to register " + taskName + " task: process timed out"; logger.log(Level.SEVERE, message); return Results.failure(message); } if(addProcess.exitValue() != 0) { Pair addOutput = WindowsScheduler.captureProcessOutput(addProcess); String message = "Failed to get user id. Exit code: " + addProcess.exitValue() + ". stdout: " + addOutput.getKey() + ". stderr: " + addOutput.getValue(); logger.log(Level.SEVERE, message); return Results.failure(message); } } catch (CronExpressionException | IOException | InterruptedException e) { String message = "Exception while removing schedule"; logger.log(Level.SEVERE, message, e); return Results.failure(message); } return Results.success(true); } /** * * * * * * {command to execute} * | | | | | * | | | | day of the week (0–6) (Sunday to Saturday; * | | | month (1–12) 7 is also Sunday on some systems) * | | day of the month (1–31) * | hour (0–23) * minute (0–59) */ public static String cronExpression(StationSettings settings) throws CronExpressionException { LocalDateTime scheduleDateTime = settings.getScheduleStart().withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); String minute = Integer.toString(scheduleDateTime.getMinute()); String hour = Integer.toString(scheduleDateTime.getHour()); String dayOfMonth = Integer.toString(scheduleDateTime.getDayOfMonth()); String dayOfWeek = Integer.toString(scheduleDateTime.getDayOfMonth()); String scheduleString = ""; switch(settings.getMessagePeriod()) { case DAILY: scheduleString = minute + " " + hour + " * * *"; break; case WEEKLY: scheduleString = minute + " " + hour + " * * " + dayOfWeek; break; case MONTHLY: scheduleString = minute + " " + hour + " " + dayOfMonth + " * *"; break; default: throw new CronExpressionException("Message period not implemented: '" + settings.getMessagePeriod() + "'"); } String processName = ManagementFactory.getRuntimeMXBean().getName(); long pid = Long.parseLong(processName.split("@")[0]); ProcessHandle currentProcess = ProcessHandle.of(pid).orElseThrow(); Path executablePath = currentProcess.info().command().map(Paths::get).orElseThrow(); logger.info("Executable Path: " + executablePath); return scheduleString + " " + executablePath.toString() + " --station \"" + settings.getName() + "\""; } public static class CronExpressionException extends Exception { public CronExpressionException(String message) { super(message); } } }