Skip to content

Commit

Permalink
feat: auto exit software when user quits minecraft (#107)
Browse files Browse the repository at this point in the history
* Add util for finding minecraft process

* Add trace log

* Add Native process id handle method

* Add test for onExit method

* Add scheduler for auto quitting

* Add setting for auto exit

* Add setting for auto exit and migration logic

* make auto-exit feature compatible for any other oses rather than windows
  • Loading branch information
hizumiaoba authored Aug 31, 2024
1 parent 7faa8eb commit 4312103
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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<ProcessHandle> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}

Expand All @@ -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
* <p>
* 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<String, String> entry : defaultConfigSkeleton.entrySet()) {
if(instance.load(entry.getKey()) == null) {
instance.set(entry.getKey(), entry.getValue());
}
}
}

@Override
public void load() {
if(properties != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ProcessHandle> 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<String> 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<ProcessHandle> getMinecraftProcess() {
Optional<ProcessHandle> handle = ProcessHandle.allProcesses().parallel().filter(h -> h.info().command().isPresent()).filter(h -> h.info().command().get().contains("javaw")).findFirst();
return handle;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<TabPane xmlns:fx="http://javafx.com/fxml/1" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
minWidth="-Infinity" prefHeight="190.0" prefWidth="920.0" tabClosingPolicy="UNAVAILABLE"
xmlns="http://javafx.com/javafx/19" fx:controller="io.github.hizumiaoba.mctimemachine.MainController"
fx:id="mainTabPane">
<TabPane fx:id="mainTabPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="190.0" prefWidth="920.0" tabClosingPolicy="UNAVAILABLE" xmlns="http://javafx.com/javafx/20.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.hizumiaoba.mctimemachine.MainController">
<tabs>
<Tab text="メインツール">
<content>
Expand All @@ -60,20 +57,14 @@
<Button fx:id="backupNowBtn" mnemonicParsing="false" onAction="#onBackupNowBtnClick" prefHeight="40.0" prefWidth="170.0" text="いますぐバックアップ" />
<Button fx:id="backupScheduledBtn" layoutX="385.0" layoutY="20.0" mnemonicParsing="false" onAction="#onBackupScheduledBtnClick" prefHeight="40.0" prefWidth="170.0" text="定期バックアップ開始" />
<Button fx:id="specialBackupNowBtn" layoutX="475.0" layoutY="20.0" mnemonicParsing="false" onAction="#onSpecialBackupNowBtnClick" prefHeight="40.0" prefWidth="170.0" text="いますぐ特殊バックアップ" />
<Button layoutX="565.0" layoutY="20.0"
mnemonicParsing="false" onAction="#onOpenBackupListBtnClick"
prefHeight="40.0" prefWidth="170.0"
text="バックアップリスト" fx:id="openBackupListBtn"/>
<Button fx:id="openBackupListBtn" layoutX="565.0" layoutY="20.0" mnemonicParsing="false" onAction="#onOpenBackupListBtnClick" prefHeight="40.0" prefWidth="170.0" text="バックアップリスト" />
<Button fx:id="openLauncherBtn" layoutX="655.0" layoutY="20.0" mnemonicParsing="false" onAction="#onOpenLauncherBtnClick" prefHeight="40.0" prefWidth="170.0" text="ランチャー起動" />
</children>
</HBox>
<HBox alignment="CENTER_LEFT" prefHeight="40.0" prefWidth="920.0" spacing="30.0">
<children>
<CheckBox mnemonicParsing="false" text="Ctrl + Shift + B でいますぐバックアップを実行(一定時間後)"
fx:id="backupNowWithShortcutChkbox"/>
<CheckBox layoutX="30.0" layoutY="22.0"
mnemonicParsing="false" text="Ctrl + Shift + Z でいますぐ特別バックアップを実行(一定時間後)"
fx:id="specialBackupNowWithShortcutChkbox"/>
<CheckBox fx:id="backupNowWithShortcutChkbox" mnemonicParsing="false" text="Ctrl + Shift + B でいますぐバックアップを実行(一定時間後)" />
<CheckBox fx:id="specialBackupNowWithShortcutChkbox" layoutX="30.0" layoutY="22.0" mnemonicParsing="false" text="Ctrl + Shift + Z でいますぐ特別バックアップを実行(一定時間後)" />
</children>
<opaqueInsets>
<Insets />
Expand Down Expand Up @@ -106,16 +97,9 @@
</HBox>
<HBox alignment="CENTER_LEFT" prefHeight="63.0" prefWidth="920.0" spacing="15.0">
<children>
<Button disable="true" mnemonicParsing="false"
onAction="#onOpenConfigEditorBtnClick" prefHeight="40.0"
prefWidth="170.0" text="コンフィグエディター" fx:id="openConfigEditorBtn"/>
<Button disable="true" layoutX="30.0" layoutY="17.0"
mnemonicParsing="false" onAction="#onOpenRelatedFolderBtnClick"
prefHeight="40.0" prefWidth="170.0"
text="各種フォルダを開く" fx:id="openRelatedFolderBtn"/>
<Button layoutX="215.0" layoutY="17.0" mnemonicParsing="false"
onAction="#onOpenReleasePageOnWebBtnClick" prefHeight="40.0"
prefWidth="170.0" text="最新リリースをwebで確認" fx:id="checkVersionUpdateBtn"/>
<Button fx:id="openConfigEditorBtn" disable="true" mnemonicParsing="false" onAction="#onOpenConfigEditorBtnClick" prefHeight="40.0" prefWidth="170.0" text="コンフィグエディター" />
<Button fx:id="openRelatedFolderBtn" disable="true" layoutX="30.0" layoutY="17.0" mnemonicParsing="false" onAction="#onOpenRelatedFolderBtnClick" prefHeight="40.0" prefWidth="170.0" text="各種フォルダを開く" />
<Button fx:id="checkVersionUpdateBtn" layoutX="215.0" layoutY="17.0" mnemonicParsing="false" onAction="#onOpenReleasePageOnWebBtnClick" prefHeight="40.0" prefWidth="170.0" text="最新リリースをwebで確認" />
</children>
<padding>
<Insets left="20.0" />
Expand Down Expand Up @@ -173,6 +157,7 @@
<Spinner fx:id="backupCountSpinner" editable="true" prefHeight="25.0" prefWidth="70.0" />
<Label layoutX="10.0" layoutY="10.0" text="自動バックアップ間隔(分)" />
<Spinner fx:id="backupScheduleDurationSpinner" editable="true" layoutX="38.0" layoutY="10.0" prefHeight="25.0" prefWidth="70.0" />
<CheckBox fx:id="enableAutoExitOnQuittingGamesChkbox" mnemonicParsing="false" text="自動で終了する" />
</children>
<padding>
<Insets left="20.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.github.hizumiaoba.mctimemachine;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import io.github.hizumiaoba.mctimemachine.internal.natives.NativeHandleUtil;
import java.util.Optional;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

public class NativeHandleTest {

@Test
@Disabled("This test is already completed under development environment")
public void testGetMinecraftProcessId() {
Optional<ProcessHandle> processHandle = NativeHandleUtil.getMinecraftProcessId();
processHandle.ifPresentOrElse(
handle -> {
assertThat(handle.pid()).isGreaterThan(0);
assertThat(handle.isAlive()).isTrue();
},
() -> fail("Failed to get Minecraft process id")
);
}

@Test
@Disabled("This test is already completed under development environment")
public void onExitEventSettingTest() {
Optional<ProcessHandle> mcProcess = NativeHandleUtil.getMinecraftProcessId();
mcProcess.ifPresentOrElse(
handle -> handle.onExit().thenRun(() -> assertThat(true).isTrue()).join(),
() -> fail("Failed to get Minecraft process id")
);
}
}

0 comments on commit 4312103

Please sign in to comment.