Get registering and running task scheduler task working

This commit is contained in:
Nathan McRae 2025-01-27 14:50:43 -08:00
parent 4defb7d3a0
commit e5877c8a34
8 changed files with 223 additions and 61 deletions

View File

@ -7,6 +7,7 @@ $modules = $(
"com.fasterxml.jackson.core",
"com.fasterxml.jackson.dataformat.xml",
"com.fasterxml.jackson.datatype.jsr310",
"result",
"javafx.controls",
"javafx.fxml",
"org.apache.commons.cli"

View File

@ -1,5 +1,7 @@
package name.nathanmcrae.numbersstation;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -21,7 +23,7 @@ public class Main extends Application {
private static final String VERSION = "0.0.1";
private static Path statePath = null;
public Path getStatePath() {
public static Path getStatePath() {
if (statePath == null) {
String stateHome = System.getenv("XDG_STATE_HOME");
if (stateHome == null || stateHome.isEmpty() || !Paths.get(stateHome).isAbsolute()) {
@ -37,7 +39,6 @@ public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
setupLogger();
Parent root = FXMLLoader.load(getClass().getResource("MainView.fxml"));
primaryStage.setTitle("Numbers Station");
primaryStage.setScene(new Scene(root));
@ -45,7 +46,7 @@ public class Main extends Application {
logger.info("Application started");
}
private void setupLogger() {
private static void setupLogger() {
try {
Path logFile = getStatePath().resolve("main.log");
@ -61,7 +62,7 @@ public class Main extends Application {
}
}
private static void parseArguments(String[] args) {
private static String parseArguments(String[] args) {
Options options = new Options();
Option help = new Option("h", "help", false, "Show help");
@ -99,10 +100,12 @@ public class Main extends Application {
formatter.printHelp("numbers-station", options);
System.exit(1);
}
return stationName;
}
public static void main(String[] args) {
parseArguments(args);
String stationName = parseArguments(args);
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
logger.log(Level.SEVERE, "Unhandled exception caught", throwable);
@ -113,6 +116,41 @@ public class Main extends Application {
logger.log(Level.SEVERE, "Unhandled exception in JavaFX application thread", throwable);
});
setupLogger();
if (stationName != null) {
// TODO: errors in runStation should trigger a notification
runStation(stationName);
} else {
launch(args);
}
}
public static void runStation(String stationName) {
if (stationName == null || stationName == "") {
logger.log(Level.SEVERE, "Station name must be provided and not empty");
System.exit(1);
}
Result<MainSettings, Exception> result = MainSettings.load();
if (!result.hasSuccess()) {
logger.log(Level.SEVERE, "Unable to load settings");
System.exit(1);
}
MainSettings settings = result.getSuccess().get();
StationSettings loadedStation = settings.getStations().stream()
.filter(station -> station.getName().equals(stationName))
.findFirst()
.orElse(null);
if (loadedStation == null) {
logger.log(Level.SEVERE, "Unable to find station " + stationName);
System.exit(1);
}
logger.info("Loaded station " + stationName);
System.exit(0);
}
}

View File

@ -1,9 +1,7 @@
package name.nathanmcrae.numbersstation;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import java.io.File;
import java.io.IOException;
import java.net.URL;
@ -118,46 +116,24 @@ public class MainController implements Initializable {
}
}
private void loadSettings() throws IOException {
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(new JavaTimeModule());
String userHome = System.getProperty("user.home");
Path filePath = MainSettings.getSettingsFilePath();
Path directoryPath = filePath.getParent();
try {
if (!Files.exists(directoryPath)) {
Files.createDirectories(directoryPath);
}
if (!Files.exists(filePath)) {
settings = new MainSettings();
xmlMapper.writeValue(new File(filePath.toString()), settings);
} else {
settings = xmlMapper.readValue(new File(filePath.toString()), MainSettings.class);
for (StationSettings station : settings.getStations()) {
if (station.getDigitsPerGroup() == 0) {
station.setDigitsPerGroup(4);
}
}
}
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to load settings from " + filePath.toString(), e);
System.out.println("File contents: " + Files.readString(filePath));
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
stationNameField.textProperty().bindBidirectional(selectedStationName);
try {
loadSettings();
} catch (IOException e) {
e.printStackTrace();
Result<MainSettings, Exception> result = MainSettings.load();
if (!result.hasSuccess()) {
// TODO: on failure, prompt user to re-initialize settings
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Settings load error");
alert.setHeaderText(null);
alert.setContentText("Unable to load settings file");
alert.showAndWait();
return;
}
settings = result.getSuccess().get();
selectedStationName.set(settings.getSelectedStationName());
if (selectedStationName.get() == null || selectedStationName.get() == "") {

View File

@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@ -12,6 +14,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.logging.Level;
public class MainSettings {
private static final Logger logger = Logger.getLogger(Main.class.getName());
@ -71,6 +74,40 @@ public class MainSettings {
this.selectedStationName = selectedStationName;
}
public static Result<MainSettings, Exception> load() {
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(new JavaTimeModule());
String userHome = System.getProperty("user.home");
MainSettings settings;
Path filePath = MainSettings.getSettingsFilePath();
Path directoryPath = filePath.getParent();
try {
if (!Files.exists(directoryPath)) {
Files.createDirectories(directoryPath);
}
if (!Files.exists(filePath)) {
settings = new MainSettings();
xmlMapper.writeValue(new File(filePath.toString()), settings);
} else {
settings = xmlMapper.readValue(new File(filePath.toString()), MainSettings.class);
for (StationSettings station : settings.getStations()) {
if (station.getDigitsPerGroup() == 0) {
station.setDigitsPerGroup(4);
}
}
}
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to load settings from " + filePath.toString(), e);
return Results.failure(e);
}
return Results.success(settings);
}
public void save() {
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(new JavaTimeModule());

View File

@ -397,9 +397,18 @@ public class MainSettingsController {
@FXML
private void handleTestConnectionButtonPress() {
}
@FXML
private void handleTestRegisterScheduleButtonPress() {
WindowsScheduler.registerSchedule(settings);
}
@FXML
private void handleTestRunScheduleButtonPress() {
WindowsScheduler.runSchedule(settings);
}
public StringProperty stationAddressProperty() {
return stationAddress;
}

View File

@ -133,25 +133,27 @@
<AnchorPane prefHeight="152.0" prefWidth="578.0">
<children>
<CheckBox fx:id="manageScheduleExternallyCheckBox" layoutX="395.0" mnemonicParsing="false" text="Manage schedule externally" AnchorPane.rightAnchor="14.5" />
<RadioButton fx:id="dailyRadioButton" layoutX="14.0" layoutY="8.0" mnemonicParsing="false" text="Daily" disable="${manageScheduleExternallyCheckBox.selected}">
<RadioButton fx:id="dailyRadioButton" disable="${manageScheduleExternallyCheckBox.selected}" layoutX="14.0" layoutY="8.0" mnemonicParsing="false" text="Daily">
<toggleGroup>
<ToggleGroup fx:id="messagePeriodGroup" />
</toggleGroup>
</RadioButton>
<RadioButton fx:id="weeklyRadioButton" layoutX="14.0" layoutY="38.0" mnemonicParsing="false" text="Weekly" disable="${manageScheduleExternallyCheckBox.selected}">
<RadioButton fx:id="weeklyRadioButton" disable="${manageScheduleExternallyCheckBox.selected}" layoutX="14.0" layoutY="38.0" mnemonicParsing="false" text="Weekly">
<toggleGroup>
<fx:reference source="messagePeriodGroup" />
</toggleGroup>
</RadioButton>
<RadioButton fx:id="monthlyRadioButton" layoutX="14.0" layoutY="68.0" mnemonicParsing="false" text="Monthly" disable="${manageScheduleExternallyCheckBox.selected}">
<RadioButton fx:id="monthlyRadioButton" disable="${manageScheduleExternallyCheckBox.selected}" layoutX="14.0" layoutY="68.0" mnemonicParsing="false" text="Monthly">
<toggleGroup>
<fx:reference source="messagePeriodGroup" />
</toggleGroup>
</RadioButton>
<DatePicker fx:id="scheduleStartDatePicker" layoutX="115.0" layoutY="34.0" disable="${manageScheduleExternallyCheckBox.selected}" />
<DatePicker fx:id="scheduleStartDatePicker" disable="${manageScheduleExternallyCheckBox.selected}" layoutX="115.0" layoutY="34.0" />
<Label layoutX="115.0" layoutY="8.0" text="Starting from:" />
<TextField fx:id="scheduleStartTimeField" layoutX="115.0" layoutY="64.0" text="23:24:49" disable="${manageScheduleExternallyCheckBox.selected}" />
<TextField fx:id="scheduleStartTimeField" disable="${manageScheduleExternallyCheckBox.selected}" layoutX="115.0" layoutY="64.0" text="23:24:49" />
<Label layoutX="48.0" layoutY="102.0" text="TODO: Jitter" />
<Button layoutX="377.0" layoutY="73.0" mnemonicParsing="false" onMousePressed="#handleTestRegisterScheduleButtonPress" text="Test register scheduled task" />
<Button layoutX="377.0" layoutY="107.0" mnemonicParsing="false" onMousePressed="#handleTestRunScheduleButtonPress" text="Test running scheduled task" />
</children>
</AnchorPane>

View File

@ -1,5 +1,7 @@
package name.nathanmcrae.numbersstation;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
@ -7,8 +9,10 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.util.Pair;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@ -24,7 +28,7 @@ import org.w3c.dom.Element;
public class WindowsScheduler {
private static final Logger logger = Logger.getLogger(Main.class.getName());
public static Document generateScheduleDocument(String authorName, String userId) throws ParserConfigurationException {
public static Document generateScheduleDocument(String authorName, String userId, StationSettings station) throws ParserConfigurationException {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
@ -173,33 +177,82 @@ public class WindowsScheduler {
actions.appendChild(exec);
Element command = doc.createElement("Command");
command.appendChild(doc.createTextNode("notepad.exe"));
// TODO: need to figure out the real invocation
command.appendChild(doc.createTextNode("powershell"));
exec.appendChild(command);
Element arguments = doc.createElement("Arguments");
arguments.appendChild(doc.createTextNode("-Version 5 -NoProfile -File P:/personal_root/projects/number-station/src/main/java/run.ps1 --station \"" + station.getName() + "\""));
exec.appendChild(arguments);
return doc;
}
public static String getUserId() throws IOException, InterruptedException {
public static Result<String, String> getUserId() throws IOException, InterruptedException {
Process process = new ProcessBuilder("whoami", "/user").start();
process.waitFor();
if(!process.waitFor(5, TimeUnit.SECONDS)) {
String message = "Failed to get user id: process timed out";
logger.log(Level.SEVERE, message);
return Results.failure(message);
}
if(process.exitValue() != 0) {
Pair<String, String> output = captureProcessOutput(process);
String message = "Failed to get user id. Exit code: " + process.exitValue() + ". stdout: " + output.getKey() + "\n\tstderr: " + output.getValue();
logger.log(Level.SEVERE, message);
return Results.failure(message);
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("S-")) {
return line.split("\\s+")[1];
return Results.success(line.split("\\s+")[1]);
}
}
}
return null;
}
public static void registerSchedule(StationSettings settings) {
return Results.failure("Failed to get user id: no user id found");
}
public static Result<Boolean, String> registerSchedule(StationSettings settings) {
Path tempFile = null;
try {
String taskName = "numbers-station-main_" + settings.getName();
Document doc = generateScheduleDocument(System.getProperty("user.name"), getUserId());
Process queryTaskProcess = new ProcessBuilder("schtasks.exe", "/query", "/tn", taskName).start();
if (!queryTaskProcess.waitFor(5, TimeUnit.SECONDS)) {
String message = "Failed to query " + taskName + " task: process timed out";
logger.log(Level.SEVERE, message);
return Results.failure(message);
}
// Remove previous instance of task if it exists
if (queryTaskProcess.exitValue() == 0) {
Process removeTaskProcess = new ProcessBuilder("schtasks.exe", "/delete", "/f", "/tn", taskName).start();
if (!removeTaskProcess.waitFor(5, TimeUnit.SECONDS)) {
String message = "Failed to remove " + taskName + " task: process timed out";
logger.log(Level.SEVERE, message);
return Results.failure(message);
}
if (removeTaskProcess.exitValue() == 0) {
logger.info(taskName + " task removed successfully.");
} else {
Pair<String, String> output = captureProcessOutput(removeTaskProcess);
String message = "Failed to remove " + taskName + " task. Exit code: " + removeTaskProcess.exitValue() + ". stdout: " + output.getKey() + "\n\tstderr: " + output.getValue();
logger.log(Level.SEVERE, message);
return Results.failure(message);
}
}
Result<String, String> userIdResult = getUserId();
if (!userIdResult.hasSuccess()) {
return Results.failure(userIdResult.getFailure().get());
}
String userId = userIdResult.getSuccess().get();
Document doc = generateScheduleDocument(System.getProperty("user.name"), userId, settings);
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
@ -213,18 +266,30 @@ public class WindowsScheduler {
try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) {
writer.write("<?xml version=\"1.0\"?>\n");
transformer.transform(source, new StreamResult(writer));
} catch (IOException | TransformerException e) {
String message = "Failed to write schedule to temporary file";
logger.log(Level.SEVERE, message, e);
return Results.failure(message);
}
Process process = new ProcessBuilder("schtasks.exe", "/create", "/tn", taskName, "/xml", tempFile.toString()).start();
int exitCode = process.waitFor();
if (!process.waitFor(5, TimeUnit.SECONDS)) {
String message = "Failed to register " + taskName + " task: process timed out";
logger.log(Level.SEVERE, message);
return Results.failure(message);
}
if (exitCode == 0) {
if (process.exitValue() == 0) {
logger.info(taskName + " task registered successfully.");
} else {
logger.info("Failed to register " + taskName + " task. Exit code: " + exitCode);
String message = "Failed to register " + taskName + " task. Exit code: " + process.exitValue();
logger.log(Level.SEVERE, message);
return Results.failure(message);
}
} catch (ParserConfigurationException | TransformerException | IOException | InterruptedException e) {
logger.log(Level.SEVERE, "Exception while registering schedule", e);
String message = "Exception while registering schedule";
logger.log(Level.SEVERE, message, e);
return Results.failure(message);
} finally {
if (tempFile != null) {
try {
@ -234,5 +299,38 @@ public class WindowsScheduler {
}
}
}
return Results.success(true);
}
public static Result<Boolean, String> runSchedule(StationSettings settings) {
String taskName = "numbers-station-main_" + settings.getName();
try {
Process taskProcess = new ProcessBuilder("schtasks.exe", "/run", "/tn", taskName).start();
} catch (IOException e) {
String message = "Failed to run " + taskName + " task";
logger.log(Level.SEVERE, message, e);
return Results.failure(message);
}
return Results.success(true);
}
public static Pair<String, String> captureProcessOutput(Process process) throws IOException {
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line);
}
while ((line = errorReader.readLine()) != null) {
error.append(line);
}
}
return new Pair<>(output.toString(), error.toString());
}
}

View File

@ -8,6 +8,7 @@ $modules = $(
"com.fasterxml.jackson.core",
"com.fasterxml.jackson.dataformat.xml",
"com.fasterxml.jackson.datatype.jsr310",
"result",
"javafx.controls",
"javafx.fxml",
"org.apache.commons.cli"