diff --git a/app/src/main/java/io/github/hizumiaoba/mctimemachine/MainController.java b/app/src/main/java/io/github/hizumiaoba/mctimemachine/MainController.java index 4b7d684..a8f528b 100644 --- a/app/src/main/java/io/github/hizumiaoba/mctimemachine/MainController.java +++ b/app/src/main/java/io/github/hizumiaoba/mctimemachine/MainController.java @@ -9,6 +9,7 @@ import io.github.hizumiaoba.mctimemachine.internal.ApplicationConfig; import io.github.hizumiaoba.mctimemachine.internal.concurrent.ConcurrentThreadFactory; import io.github.hizumiaoba.mctimemachine.internal.fs.BackupUtils; +import io.github.hizumiaoba.mctimemachine.internal.natives.NativeHandleUtil; import io.github.hizumiaoba.mctimemachine.internal.version.VersionObj; import java.awt.Desktop; import java.io.File; @@ -17,6 +18,7 @@ import java.net.URL; import java.nio.file.Paths; import java.text.SimpleDateFormat; +import java.util.Optional; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -37,6 +39,7 @@ import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory; import javafx.scene.control.TabPane; import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; import javafx.stage.FileChooser; import javafx.stage.Modality; import javafx.stage.Stage; @@ -116,6 +119,9 @@ public class MainController { @FXML private TabPane mainTabPane; + @FXML + public CheckBox enableAutoExitOnQuittingGamesChkbox; + private Config mainConfig; private static final ExecutorService es; private static final ThreadFactory internalControllerThreadFactory = new ConcurrentThreadFactory( @@ -145,6 +151,8 @@ void initialize() { backupNowWithShortcutChkbox.isSelected() ? "true" : "false"); mainConfig.set("special_backup_on_shortcut", specialBackupNowWithShortcutChkbox.isSelected() ? "true" : "false"); + mainConfig.set("exit_on_quitting_minecraft", + enableAutoExitOnQuittingGamesChkbox.isSelected() ? "true" : "false"); mainConfig.save(); es.shutdownNow(); backupSchedulerExecutors.shutdownNow(); @@ -173,6 +181,11 @@ void initialize() { Boolean.parseBoolean(mainConfig.load("normal_backup_on_shortcut"))); specialBackupNowWithShortcutChkbox.setSelected( Boolean.parseBoolean(mainConfig.load("special_backup_on_shortcut"))); + enableAutoExitOnQuittingGamesChkbox.setSelected( + Boolean.parseBoolean(mainConfig.load("exit_on_quitting_minecraft"))); + Tooltip backupNowTooltip = new Tooltip("ランチャーをここから起動した際、Minecraft終了を自動で検知してこのアプリを終了します。"); + backupNowTooltip.setStyle("-fx-font-size: 12px;"); + enableAutoExitOnQuittingGamesChkbox.setTooltip(backupNowTooltip); backupUtils = new BackupUtils(backupSavingFolderPathField.getText()); } @@ -286,6 +299,19 @@ void onOpenLauncherBtnClick() { log.trace("launcher path: {}", launcherExePathField.getText()); try { Runtime.getRuntime().exec(launcherExePathField.getText()); + if(enableAutoExitOnQuittingGamesChkbox.isSelected()) { + boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows"); + log.debug("Scheduling to kill the program after 3 minutes."); + Executors + .newSingleThreadScheduledExecutor(new ConcurrentThreadFactory("MainController", "process-killer-scheduled", true)) + .schedule(() -> { + Optional handle = isWindows ? NativeHandleUtil.getMinecraftProcessId() : NativeHandleUtil.getMinecraftProcess(); + handle.ifPresentOrElse(h -> { + log.debug("scheduling to kill the program"); + h.onExit().thenRun(() -> System.exit(0)); + }, () -> log.warn("No Minecraft process was found.")); + }, 3, TimeUnit.MINUTES); + } } catch (IOException e) { ExceptionPopup popup = new ExceptionPopup(e, "外部プロセスを開始できませんでした。", "MainController#onOpenLauncherBtnClick()$lambda"); popup.pop(); diff --git a/app/src/main/java/io/github/hizumiaoba/mctimemachine/internal/ApplicationConfig.java b/app/src/main/java/io/github/hizumiaoba/mctimemachine/internal/ApplicationConfig.java index e6e4eb7..b8c5e4d 100644 --- a/app/src/main/java/io/github/hizumiaoba/mctimemachine/internal/ApplicationConfig.java +++ b/app/src/main/java/io/github/hizumiaoba/mctimemachine/internal/ApplicationConfig.java @@ -29,7 +29,8 @@ public class ApplicationConfig implements Config { "backup_schedule_duration", "20", "backup_count", "5", "normal_backup_on_shortcut", "true", - "special_backup_on_shortcut", "true" + "special_backup_on_shortcut", "true", + "exit_on_quitting_minecraft", "false" ); } @@ -42,11 +43,28 @@ public static ApplicationConfig getInstance(String configFile) { return instances.get(configFile); } else { ApplicationConfig instance = new ApplicationConfig(configFile); + updateConfigSkeleton(instance); instances.put(configFile, instance); return instance; } } + /** + * Update current config when there is update in default config skeleton + *

+ * This is only for migration from old version to new version + * + * @param instance current config instance + */ + private static void updateConfigSkeleton(Config instance) { + instance.load(); + for(Map.Entry entry : defaultConfigSkeleton.entrySet()) { + if(instance.load(entry.getKey()) == null) { + instance.set(entry.getKey(), entry.getValue()); + } + } + } + @Override public void load() { if(properties != null) { diff --git a/app/src/main/java/io/github/hizumiaoba/mctimemachine/internal/natives/NativeHandleUtil.java b/app/src/main/java/io/github/hizumiaoba/mctimemachine/internal/natives/NativeHandleUtil.java new file mode 100644 index 0000000..7bb0e4c --- /dev/null +++ b/app/src/main/java/io/github/hizumiaoba/mctimemachine/internal/natives/NativeHandleUtil.java @@ -0,0 +1,52 @@ +package io.github.hizumiaoba.mctimemachine.internal.natives; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class NativeHandleUtil { + + private NativeHandleUtil() {} + + public static Optional getMinecraftProcessId() { + ProcessBuilder pwsh = new ProcessBuilder("powershell", "-Command", "\"Get-Process -Name javaw | Where-Object { $_.MainWindowTitle -like 'Minecraft*' } | Select-Object -ExpandProperty Id | Out-File -Encoding ASCII .\\.mctm-mc.id.txt \""); + final String idFile = ".mctm-mc.id.txt"; + List lines; + try { + Process p = pwsh.start(); + p.onExit().join(); + Path path = Paths.get(idFile); + if (Files.notExists(path)) { + log.warn("File {} cannot be found", idFile); + return Optional.empty(); + } + lines = Files.readAllLines(path, StandardCharsets.UTF_8); + Files.deleteIfExists(path); + } catch (IOException e) { + log.error("Failed to start shell process to find Minecraft process id", e); + return Optional.empty(); + } + if(lines.size() != 1) { + log.warn("It seems that there are multiple Minecraft processes running? Found {} processes: ", lines.size()); + lines.forEach(log::warn); + log.warn("No process id will be returned"); + return Optional.empty(); + } + log.trace("Found Minecraft process id: {}", lines.get(0)); + int pid = Integer.parseInt(lines.get(0).trim()); + return ProcessHandle.of(pid); + } + + @Deprecated + public static Optional getMinecraftProcess() { + Optional handle = ProcessHandle.allProcesses().parallel().filter(h -> h.info().command().isPresent()).filter(h -> h.info().command().get().contains("javaw")).findFirst(); + return handle; + } + +} diff --git a/app/src/main/resources/io/github/hizumiaoba/mctimemachine/main.fxml b/app/src/main/resources/io/github/hizumiaoba/mctimemachine/main.fxml index 7216e0e..a23bf59 100644 --- a/app/src/main/resources/io/github/hizumiaoba/mctimemachine/main.fxml +++ b/app/src/main/resources/io/github/hizumiaoba/mctimemachine/main.fxml @@ -44,10 +44,7 @@ - + @@ -60,20 +57,14 @@