diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 4997c576bbfa..96aa39a876a6 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -26,14 +26,20 @@ jobs:
run: xvfb-run mvn -T5 clean install
- name: Copy release files
run: |
- pushd launcher/target/windows/deps/
- md5sum *.jar > manifest
+ pushd launcher/target/windows/
+ mkdir -p updater_data/v2
+ md5sum *.exe deps/*.jar > updater_data/v2/manifest
+ cp -r *.exe deps/ updater_data/v2/
+ # Legacy updater
+ cd deps
+ md5sum *.jar > ../updater_data/manifest
+ cp *.jar ../updater_data/
popd
- name: Deploy release files
uses: JamesIves/github-pages-deploy-action@4.1.7
with:
- folder: ./launcher/target/windows/deps
+ folder: ./launcher/target/windows/updater_data
branch: gh-pages
target-folder: ${{ github.ref_name }}
diff --git a/launcher/pom.xml b/launcher/pom.xml
index 5011e32694ab..a5d0476b3fe8 100644
--- a/launcher/pom.xml
+++ b/launcher/pom.xml
@@ -15,6 +15,9 @@
gg.xp.xivsupport.gui.GuiLaunch
gg.xp.xivsupport.gui.GuiImportLaunch
gg.xp.xivsupport.gui.Update
+ ./launcher-${project.version}.jar
+ ./target/windows/launcher-${project.version}.jar
+ ./userdata;./preload/*.jar;./deps/*.jar;./user/*.jar;${launcherJar}
@@ -93,6 +96,23 @@
+
+ copy-jar-secondary
+
+ package
+
+ copy-resources
+
+
+ ${basedir}/target/windows/deps
+
+
+ ${project.build.directory}
+ launcher-${project.version}.jar
+
+
+
+
copy-jre
@@ -146,8 +166,9 @@
${mainImportClass}
false
- ./preload/*.jar;./deps/*.jar;./user/*.jar
+ ${preCp}
+ ./nonexistent.jar
@@ -165,8 +186,9 @@
${mainClass}
false
- ./preload/*.jar;./deps/*.jar;./user/*.jar
+ ${preCp}
+ ./nonexistent.jar
@@ -181,6 +203,8 @@
${mainUpdateClass}
false
+ ${launcherJarRelativeToPom}
+ false
@@ -193,7 +217,6 @@
@args.txt
gui
- ./launcher-${project.version}.jar
Triggevent
true
.
diff --git a/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java b/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java
index c8a6b7692dc9..54179d67c1e2 100644
--- a/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java
+++ b/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java
@@ -1,6 +1,5 @@
package gg.xp.xivsupport.gui;
-import gg.xp.xivsupport.gui.util.CatchFatalError;
import gg.xp.xivsupport.gui.util.CatchFatalErrorInUpdater;
import javax.swing.*;
@@ -23,29 +22,34 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import java.util.stream.Collectors;
// This one will NOT be launched with the full classpath - it NEEDS to be self-sufficient
// ...which is also why the code is complete shit, no external libraries.
public class Update {
- private static final String updaterUrlTemplate = "https://xpdota.github.io/event-trigger/%s/%s";
+ private static final String updaterUrlTemplate = "https://xpdota.github.io/event-trigger/%s/v2/%s";
private static final String defaultBranch = "stable";
+ private final Consumer logging;
+ private final boolean updateTheUpdaterItself;
+ private final boolean noop;
private String branch;
private static final String manifestFile = "manifest";
private static final String propsOverrideFileName = "update.properties";
+ private static final String updaterFilename = "triggevent-upd.exe";
+ private static final String updaterFilenameBackup = "triggevent-upd.bak";
private final File installDir;
private final File depsDir;
private final File propsOverride;
- private final JTextArea textArea;
private URI makeUrl(String filename) {
try {
@@ -97,72 +101,112 @@ private String getBranch() {
}
private Path getLocalFile(String name) {
- return Paths.get(depsDir.toString(), name);
+ return Paths.get(installDir.toString(), name);
}
- private final JFrame frame;
- private final JPanel content;
- private final JButton button;
- private final StringBuilder logText = new StringBuilder();
private final HttpClient client = HttpClient.newHttpClient();
- private Update() {
-
- try {
- UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ private Update(Consumer logging, boolean updateTheUpdaterItself, boolean onlyCheck) {
+ this.logging = logging;
+ this.updateTheUpdaterItself = updateTheUpdaterItself;
+ this.noop = onlyCheck;
+ String override = System.getProperty("triggevent-update-override-dir");
+ if (override == null) {
+ override = System.getenv("triggevent-update-override-dir");
}
- catch (Throwable e) {
- // Ignore
- }
- frame = new JFrame("Triggevent Updater");
- frame.setSize(new Dimension(800, 500));
- frame.setLocationRelativeTo(null);
- content = new JPanel();
- content.setBorder(new EmptyBorder(10, 10, 10, 10));
- content.setLayout(new BorderLayout());
- frame.add(content);
- textArea = new JTextArea();
- textArea.setEditable(false);
- textArea.setLineWrap(true);
- textArea.setWrapStyleWord(true);
- textArea.setCaretPosition(0);
- textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
- JScrollPane scroll = new JScrollPane(textArea);
- scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
- scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
- content.add(scroll, BorderLayout.CENTER);
- button = new JButton("Wait");
- button.setPreferredSize(new Dimension(80, button.getPreferredSize().height));
- button.addActionListener(l -> System.exit(0));
- JPanel buttonHolder = new JPanel();
- buttonHolder.add(button);
- content.add(buttonHolder, BorderLayout.PAGE_END);
- try {
- File jarLocation = new File(Update.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
- if (jarLocation.isFile()) {
- jarLocation = jarLocation.getParentFile();
+ if (override == null) {
+ try {
+ File jarLocation = new File(Update.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
+ if (jarLocation.isFile()) {
+ jarLocation = jarLocation.getParentFile();
+ }
+ // Special case for updating the updater itself
+ if (jarLocation.getName().equals("deps") && updateTheUpdaterItself) {
+ jarLocation = jarLocation.getParentFile();
+ }
+ this.installDir = jarLocation;
+ }
+ catch (URISyntaxException e) {
+ throw new RuntimeException(e);
}
- this.installDir = jarLocation;
}
- catch (URISyntaxException e) {
- throw new RuntimeException(e);
+ else {
+ this.installDir = new File(override);
+ if (!installDir.isDirectory()) {
+ throw new RuntimeException("Not a directory: " + installDir);
+ }
}
depsDir = Paths.get(installDir.toString(), "deps").toFile();
propsOverride = Paths.get(installDir.toString(), propsOverrideFileName).toFile();
- frame.setVisible(true);
- frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
- button.setEnabled(false);
appendText("Install dir: " + installDir);
appendText("Starting update check...");
}
+ private static class GraphicalUpdater {
+ private final JFrame frame;
+ private final JPanel content;
+ private final JButton button;
+ private final StringBuilder logText = new StringBuilder();
+ private final JTextArea textArea;
+ private final Update updater;
+
+ GraphicalUpdater(boolean updateTheUpdater) {
+ if (!updateTheUpdater) {
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ }
+ catch (Throwable e) {
+ // Ignore
+ }
+ }
+ frame = new JFrame("Triggevent Updater");
+ frame.setSize(new Dimension(800, 500));
+ frame.setLocationRelativeTo(null);
+ content = new JPanel();
+ content.setBorder(new EmptyBorder(10, 10, 10, 10));
+ content.setLayout(new BorderLayout());
+ frame.add(content);
+ textArea = new JTextArea();
+ textArea.setEditable(false);
+ textArea.setLineWrap(true);
+ textArea.setWrapStyleWord(true);
+ textArea.setCaretPosition(0);
+ textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ JScrollPane scroll = new JScrollPane(textArea);
+ scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
+ scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
+ content.add(scroll, BorderLayout.CENTER);
+ button = new JButton("Wait");
+ button.setPreferredSize(new Dimension(80, button.getPreferredSize().height));
+ button.addActionListener(l -> System.exit(0));
+ JPanel buttonHolder = new JPanel();
+ buttonHolder.add(button);
+ content.add(buttonHolder, BorderLayout.PAGE_END);
+ frame.setVisible(true);
+ frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ button.setEnabled(false);
+ updater = new Update(this::logText, updateTheUpdater, false);
+ }
+
+ public void run() {
+ updater.doUpdateCheck();
+ button.setText("Close");
+ button.setEnabled(true);
+ }
+
+ void logText(String text) {
+ logText.append(text).append('\n');
+ textArea.setText(logText.toString());
+ textArea.setCaretPosition(textArea.getDocument().getLength());
+ }
+ }
+
+
private synchronized void appendText(String text) {
- logText.append(text).append('\n');
- textArea.setText(logText.toString());
- textArea.setCaretPosition(textArea.getDocument().getLength());
+ logging.accept(text);
}
- private void doUpdateCheck() {
+ private boolean doUpdateCheck() {
try {
HttpResponse manifestResponse = client.send(HttpRequest.newBuilder().GET().uri(makeUrl(manifestFile)).build(), HttpResponse.BodyHandlers.ofString());
if (manifestResponse.statusCode() != 200) {
@@ -170,17 +214,36 @@ private void doUpdateCheck() {
}
String body = manifestResponse.body();
Map expectedFiles = body.lines().map(line -> line.split("\s+")).collect(Collectors.toMap(s -> s[1], s -> s[0]));
- File[] depsFiles = depsDir.listFiles();
- Map actualFiles;
+ Map actualFiles = new HashMap<>();
appendText("Hashing Local Files...");
- if (depsFiles == null) {
- actualFiles = Collections.emptyMap();
+ {
+ File[] mainFiles = installDir.listFiles((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".exe"));
+ File[] depsFiles = depsDir.listFiles();
+ if (mainFiles == null) {
+ throw new RuntimeException("Error checking local main files. Try reinstalling.");
+ }
+ else if (depsFiles == null) {
+ throw new RuntimeException("Error checking local deps files. Try reinstalling.");
+ }
+ for (File mainFile : mainFiles) {
+ actualFiles.put(mainFile.getName(), md5sum(mainFile));
+ }
+ for (File depsFile : depsFiles) {
+ actualFiles.put("deps/" + depsFile.getName(), md5sum(depsFile));
+ }
}
- else {
- actualFiles = Arrays.stream(depsFiles)
- .parallel()
- .filter(File::isFile)
- .collect(Collectors.toMap(File::getName, Update::md5sum));
+ List updaterFiles = List.of(updaterFilename, updaterFilenameBackup);
+ // For a no-op (i.e. just check for updates without applying anything), then we should check everything
+ if (!noop) {
+ // Updater will not be able to update itself
+ if (updateTheUpdaterItself) {
+ actualFiles.keySet().retainAll(updaterFiles);
+ expectedFiles.keySet().retainAll(updaterFiles);
+ }
+ else {
+ actualFiles.keySet().removeAll(updaterFiles);
+ expectedFiles.keySet().removeAll(updaterFiles);
+ }
}
List allKeys = new ArrayList<>();
allKeys.addAll(actualFiles.keySet());
@@ -188,7 +251,10 @@ private void doUpdateCheck() {
allKeys.sort(String::compareTo);
Set allKeysSet = new LinkedHashSet<>(allKeys);
allKeysSet.forEach(key -> {
- appendText("%32s -> %32s %s".formatted(actualFiles.get(key), expectedFiles.get(key), key));
+ String localHash = actualFiles.getOrDefault(key, "null");
+ String remoteHash = expectedFiles.getOrDefault(key, "null");
+ String separator = localHash.equals(remoteHash) ? "==" : "->";
+ appendText("%32s %s %32s %s".formatted(localHash, separator, remoteHash, key));
});
appendText("Calculating update...");
List localFilesToDelete = new ArrayList<>();
@@ -205,48 +271,52 @@ private void doUpdateCheck() {
filesToDownload.add(name);
}
});
- appendText(String.format("Updating %s files...", filesToDownload.size()));
- localFilesToDelete.forEach(name -> {
- boolean deleted;
- do {
- deleted = Paths.get(depsDir.toString(), name).toFile().delete();
- if (deleted) {
- return;
- }
- appendText("Could not delete file %s. Make sure the app is not running.".formatted(name));
+ if (!noop) {
+ appendText(String.format("Updating %s files...", filesToDownload.size()));
+ localFilesToDelete.forEach(name -> {
+ boolean deleted;
+ do {
+ deleted = Paths.get(installDir.toString(), name).toFile().delete();
+ if (deleted) {
+ return;
+ }
+ appendText("Could not delete file %s. Make sure the app is not running.".formatted(name));
+ try {
+ Thread.sleep(5000);
+ }
+ catch (InterruptedException e) {
+
+ }
+ } while (true);
+ });
+
+ depsDir.mkdirs();
+ depsDir.mkdir();
+ AtomicInteger downloaded = new AtomicInteger();
+ filesToDownload.parallelStream().forEach((name) -> {
+ HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofFile(getLocalFile(name));
try {
- Thread.sleep(5000);
+ client.send(HttpRequest.newBuilder().GET().uri(makeUrl(name)).build(), handler);
}
- catch (InterruptedException e) {
-
+ catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
}
- } while (true);
- });
-
- depsDir.mkdirs();
- depsDir.mkdir();
- AtomicInteger downloaded = new AtomicInteger();
- filesToDownload.parallelStream().forEach((name) -> {
- HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofFile(getLocalFile(name));
- try {
- client.send(HttpRequest.newBuilder().GET().uri(makeUrl(name)).build(), handler);
- }
- catch (IOException | InterruptedException e) {
- throw new RuntimeException(e);
- }
- appendText(String.format("Downloaded %s / %s files", downloaded.incrementAndGet(), filesToDownload.size()));
- });
- appendText("Update finished! %s files needed to be updated.".formatted(filesToDownload.size()));
- button.setText("Close");
- button.setEnabled(true);
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- try {
- Runtime.getRuntime().exec(Paths.get(installDir.toString(), "triggevent.exe").toString());
+ appendText(String.format("Downloaded %s / %s files", downloaded.incrementAndGet(), filesToDownload.size()));
+ });
+ appendText("Update finished! %s files needed to be updated.".formatted(filesToDownload.size()));
+ if (!updateTheUpdaterItself) {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ Runtime.getRuntime().exec(Paths.get(installDir.toString(), "triggevent.exe").toString());
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }));
}
- catch (IOException e) {
- e.printStackTrace();
- }
- }));
+ }
+ // Chances of a file being deleted without anything else being touched are essentially zero
+ return !filesToDownload.isEmpty();
}
catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
@@ -255,17 +325,27 @@ private void doUpdateCheck() {
public static void main(String[] args) {
CatchFatalErrorInUpdater.run(() -> {
- Update update = new Update();
+ GraphicalUpdater gupdate = new GraphicalUpdater(false);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
}
- update.doUpdateCheck();
+ gupdate.run();
});
}
+ @SuppressWarnings("unused")
+ public static void updateTheUpdater() {
+ new GraphicalUpdater(true).run();
+ }
+
+ @SuppressWarnings("unused")
+ public static boolean justCheck(Consumer logging) {
+ return new Update(logging, true, true).doUpdateCheck();
+ }
+
private static String md5sum(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
MessageDigest md5 = MessageDigest.getInstance("MD5");
diff --git a/launcher/src/main/java/gg/xp/xivsupport/gui/UpdateCopyForLegacyMigration.java b/launcher/src/main/java/gg/xp/xivsupport/gui/UpdateCopyForLegacyMigration.java
new file mode 100644
index 000000000000..d133a9c1d357
--- /dev/null
+++ b/launcher/src/main/java/gg/xp/xivsupport/gui/UpdateCopyForLegacyMigration.java
@@ -0,0 +1,374 @@
+package gg.xp.xivsupport.gui;
+
+import gg.xp.xivsupport.gui.util.CatchFatalErrorInUpdater;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import java.awt.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+// This one will NOT be launched with the full classpath - it NEEDS to be self-sufficient
+// ...which is also why the code is complete shit, no external libraries.
+public class UpdateCopyForLegacyMigration {
+
+ private static final String updaterUrlTemplate = "https://xpdota.github.io/event-trigger/%s/v2/%s";
+ private static final String defaultBranch = "stable";
+ private final Consumer logging;
+ private final boolean updateTheUpdaterItself;
+ private final boolean noop;
+ private String branch;
+ private static final String manifestFile = "manifest";
+ private static final String propsOverrideFileName = "update.properties";
+ private static final String updaterFilename = "triggevent-upd.exe";
+ private static final String updaterFilenameBackup = "triggevent-upd.bak";
+ private final File installDir;
+ private final File depsDir;
+ private final File propsOverride;
+
+ private URI makeUrl(String filename) {
+ try {
+ return new URI(updaterUrlTemplate.formatted(getBranch(), filename));
+ }
+ catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String getBranch() {
+ if (this.branch != null) {
+ return branch;
+ }
+ Properties props = new Properties();
+ String branch;
+ if (propsOverride.exists()) {
+ appendText("Properties file exists, loading it...");
+ try {
+ props.load(new FileInputStream(propsOverride));
+ }
+ catch (IOException e) {
+ appendText("ERROR: Could not read properties!");
+ appendText(e.toString());
+ appendText(getStackTrace(e));
+ }
+ branch = props.getProperty("branch");
+ if (branch == null) {
+ appendText("Branch not specified in properties file, assuming default of " + branch);
+ branch = defaultBranch;
+ }
+ }
+ else {
+ appendText("Properties file does not exist, creating one with defaults");
+ props.setProperty("branch", defaultBranch);
+ branch = defaultBranch;
+ try {
+ props.store(new FileOutputStream(propsOverride), "Created by updater");
+ }
+ catch (IOException e) {
+ appendText("ERROR: Could not save properties!");
+ appendText(e.toString());
+ appendText(getStackTrace(e));
+ }
+ }
+ this.branch = branch;
+ appendText("Using branch: " + branch);
+ return branch;
+ }
+
+ private Path getLocalFile(String name) {
+ return Paths.get(installDir.toString(), name);
+ }
+
+ private final HttpClient client = HttpClient.newHttpClient();
+
+ private UpdateCopyForLegacyMigration(Consumer logging, boolean updateTheUpdaterItself, boolean onlyCheck) {
+ this.logging = logging;
+ this.updateTheUpdaterItself = updateTheUpdaterItself;
+ this.noop = onlyCheck;
+ String override = System.getProperty("triggevent-update-override-dir");
+ if (override == null) {
+ override = System.getenv("triggevent-update-override-dir");
+ }
+ if (override == null) {
+ try {
+ File jarLocation = new File(UpdateCopyForLegacyMigration.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
+ if (jarLocation.isFile()) {
+ jarLocation = jarLocation.getParentFile();
+ }
+ // Special case for updating the updater itself
+ if (jarLocation.getName().equals("deps") && updateTheUpdaterItself) {
+ jarLocation = jarLocation.getParentFile();
+ }
+ this.installDir = jarLocation;
+ }
+ catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ else {
+ this.installDir = new File(override);
+ if (!installDir.isDirectory()) {
+ throw new RuntimeException("Not a directory: " + installDir);
+ }
+ }
+ depsDir = Paths.get(installDir.toString(), "deps").toFile();
+ propsOverride = Paths.get(installDir.toString(), propsOverrideFileName).toFile();
+ appendText("Install dir: " + installDir);
+ appendText("Starting update check...");
+ }
+
+ private static class GraphicalUpdater {
+ private final JFrame frame;
+ private final JPanel content;
+ private final JButton button;
+ private final StringBuilder logText = new StringBuilder();
+ private final JTextArea textArea;
+ private final UpdateCopyForLegacyMigration updater;
+
+ GraphicalUpdater(boolean updateTheUpdater) {
+ if (!updateTheUpdater) {
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ }
+ catch (Throwable e) {
+ // Ignore
+ }
+ }
+ frame = new JFrame("Triggevent Updater");
+ frame.setSize(new Dimension(800, 500));
+ frame.setLocationRelativeTo(null);
+ content = new JPanel();
+ content.setBorder(new EmptyBorder(10, 10, 10, 10));
+ content.setLayout(new BorderLayout());
+ frame.add(content);
+ textArea = new JTextArea();
+ textArea.setEditable(false);
+ textArea.setLineWrap(true);
+ textArea.setWrapStyleWord(true);
+ textArea.setCaretPosition(0);
+ textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ JScrollPane scroll = new JScrollPane(textArea);
+ scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
+ scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
+ content.add(scroll, BorderLayout.CENTER);
+ button = new JButton("Wait");
+ button.setPreferredSize(new Dimension(80, button.getPreferredSize().height));
+ button.addActionListener(l -> System.exit(0));
+ JPanel buttonHolder = new JPanel();
+ buttonHolder.add(button);
+ content.add(buttonHolder, BorderLayout.PAGE_END);
+ frame.setVisible(true);
+ frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ button.setEnabled(false);
+ updater = new UpdateCopyForLegacyMigration(this::logText, updateTheUpdater, false);
+ }
+
+ public void run() {
+ updater.doUpdateCheck();
+ button.setText("Close");
+ button.setEnabled(true);
+ }
+
+ void logText(String text) {
+ logText.append(text).append('\n');
+ textArea.setText(logText.toString());
+ textArea.setCaretPosition(textArea.getDocument().getLength());
+ }
+ }
+
+
+ private synchronized void appendText(String text) {
+ logging.accept(text);
+ }
+
+ private boolean doUpdateCheck() {
+ try {
+ HttpResponse manifestResponse = client.send(HttpRequest.newBuilder().GET().uri(makeUrl(manifestFile)).build(), HttpResponse.BodyHandlers.ofString());
+ if (manifestResponse.statusCode() != 200) {
+ throw new RuntimeException("Bad response: %s: %s".formatted(manifestResponse.statusCode(), manifestResponse));
+ }
+ String body = manifestResponse.body();
+ Map expectedFiles = body.lines().map(line -> line.split("\s+")).collect(Collectors.toMap(s -> s[1], s -> s[0]));
+ Map actualFiles = new HashMap<>();
+ appendText("Hashing Local Files...");
+ {
+ File[] mainFiles = installDir.listFiles((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".exe"));
+ File[] depsFiles = depsDir.listFiles();
+ if (mainFiles == null) {
+ throw new RuntimeException("Error checking local main files. Try reinstalling.");
+ }
+ else if (depsFiles == null) {
+ throw new RuntimeException("Error checking local deps files. Try reinstalling.");
+ }
+ for (File mainFile : mainFiles) {
+ actualFiles.put(mainFile.getName(), md5sum(mainFile));
+ }
+ for (File depsFile : depsFiles) {
+ actualFiles.put("deps/" + depsFile.getName(), md5sum(depsFile));
+ }
+ }
+ List updaterFiles = List.of(updaterFilename, updaterFilenameBackup);
+ // For a no-op (i.e. just check for updates without applying anything), then we should check everything
+ if (!noop) {
+ // Updater will not be able to update itself
+ if (updateTheUpdaterItself) {
+ actualFiles.keySet().retainAll(updaterFiles);
+ expectedFiles.keySet().retainAll(updaterFiles);
+ }
+ else {
+ actualFiles.keySet().removeAll(updaterFiles);
+ expectedFiles.keySet().removeAll(updaterFiles);
+ }
+ }
+ List allKeys = new ArrayList<>();
+ allKeys.addAll(actualFiles.keySet());
+ allKeys.addAll(expectedFiles.keySet());
+ allKeys.sort(String::compareTo);
+ Set allKeysSet = new LinkedHashSet<>(allKeys);
+ allKeysSet.forEach(key -> {
+ String localHash = actualFiles.getOrDefault(key, "null");
+ String remoteHash = expectedFiles.getOrDefault(key, "null");
+ String separator = localHash.equals(remoteHash) ? "==" : "->";
+ appendText("%32s %s %32s %s".formatted(localHash, separator, remoteHash, key));
+ });
+ appendText("Calculating update...");
+ List localFilesToDelete = new ArrayList<>();
+ List filesToDownload = new ArrayList<>();
+ actualFiles.forEach((name, md5) -> {
+ String expected = expectedFiles.get(name);
+ if (!md5.equals(expected)) {
+ localFilesToDelete.add(name);
+ }
+ });
+ expectedFiles.forEach((name, md5) -> {
+ String actual = actualFiles.get(name);
+ if (!md5.equals(actual)) {
+ filesToDownload.add(name);
+ }
+ });
+ if (!noop) {
+ appendText(String.format("Updating %s files...", filesToDownload.size()));
+ localFilesToDelete.forEach(name -> {
+ boolean deleted;
+ do {
+ deleted = Paths.get(installDir.toString(), name).toFile().delete();
+ if (deleted) {
+ return;
+ }
+ appendText("Could not delete file %s. Make sure the app is not running.".formatted(name));
+ try {
+ Thread.sleep(5000);
+ }
+ catch (InterruptedException e) {
+
+ }
+ } while (true);
+ });
+
+ depsDir.mkdirs();
+ depsDir.mkdir();
+ AtomicInteger downloaded = new AtomicInteger();
+ filesToDownload.parallelStream().forEach((name) -> {
+ HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofFile(getLocalFile(name));
+ try {
+ client.send(HttpRequest.newBuilder().GET().uri(makeUrl(name)).build(), handler);
+ }
+ catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ appendText(String.format("Downloaded %s / %s files", downloaded.incrementAndGet(), filesToDownload.size()));
+ });
+ appendText("Update finished! %s files needed to be updated.".formatted(filesToDownload.size()));
+ if (!updateTheUpdaterItself) {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ Runtime.getRuntime().exec(Paths.get(installDir.toString(), "triggevent.exe").toString());
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }));
+ }
+ }
+ // Chances of a file being deleted without anything else being touched are essentially zero
+ return !filesToDownload.isEmpty();
+ }
+ catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void main(String[] args) {
+ CatchFatalErrorInUpdater.run(() -> {
+ GraphicalUpdater gupdate = new GraphicalUpdater(false);
+ try {
+ Thread.sleep(1000);
+ }
+ catch (InterruptedException e) {
+
+ }
+ gupdate.run();
+ });
+ }
+
+ @SuppressWarnings("unused")
+ public static void updateTheUpdater() {
+ new GraphicalUpdater(true).run();
+ }
+
+ @SuppressWarnings("unused")
+ public static boolean justCheck(Consumer logging) {
+ return new UpdateCopyForLegacyMigration(logging, true, true).doUpdateCheck();
+ }
+
+ private static String md5sum(File file) {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ try (DigestInputStream dis = new DigestInputStream(fis, md5)) {
+ dis.readAllBytes();
+ }
+ byte[] md5sum = md5.digest();
+ StringBuilder md5String = new StringBuilder();
+ for (byte b : md5sum) {
+ md5String.append(String.format("%02x", b & 0xff));
+ }
+ return md5String.toString();
+ }
+ catch (IOException | NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String getStackTrace(final Throwable throwable) {
+ final StringWriter sw = new StringWriter();
+ final PrintWriter pw = new PrintWriter(sw, true);
+ throwable.printStackTrace(pw);
+ return sw.getBuffer().toString();
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 769fcb2bcf45..4f7c0cb3c1b9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,6 +19,42 @@
pom
+
+
+
+ org.testng
+ testng
+ 7.3.0
+
+
+ ch.qos.logback
+ logback-classic
+ 1.2.6
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.32
+
+
+ org.jetbrains
+ annotations
+ 22.0.0
+
+
+ org.reflections
+ reflections
+ 0.10.2
+
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+ compile
+
+
+
+
@@ -42,6 +78,16 @@
maven-assembly-plugin
3.3.0
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+ methods
+ 2
+ true
+
+
diff --git a/reevent/pom.xml b/reevent/pom.xml
index 4b15f9a28742..814ec828d18a 100644
--- a/reevent/pom.xml
+++ b/reevent/pom.xml
@@ -15,33 +15,27 @@
ch.qos.logback
logback-classic
- 1.2.6
org.slf4j
slf4j-api
- 1.7.32
org.jetbrains
annotations
- 22.0.0
org.testng
testng
- 7.4.0
test
org.reflections
reflections
- 0.10.2
org.apache.commons
commons-lang3
- 3.12.0
compile
diff --git a/xivsupport/pom.xml b/xivsupport/pom.xml
index f5066775d2f5..192f4ba379ba 100644
--- a/xivsupport/pom.xml
+++ b/xivsupport/pom.xml
@@ -20,11 +20,6 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.0.0-M5
-
- methods
- 8
-
diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiImportLaunch.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiImportLaunch.java
new file mode 100644
index 000000000000..1586e813982d
--- /dev/null
+++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiImportLaunch.java
@@ -0,0 +1,127 @@
+package gg.xp.xivsupport.gui;
+
+import gg.xp.xivsupport.eventstorage.EventReader;
+import gg.xp.xivsupport.gui.components.ReadOnlyText;
+import gg.xp.xivsupport.gui.util.CatchFatalError;
+import gg.xp.xivsupport.persistence.Platform;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public final class GuiImportLaunch {
+ private static final Logger log = LoggerFactory.getLogger(GuiImportLaunch.class);
+
+ private GuiImportLaunch() {
+ }
+
+ public static void main(String[] args) {
+ log.info("GUI Import Init");
+ CatchFatalError.run(CommonGuiSetup::setup);
+ SwingUtilities.invokeLater(() -> CatchFatalError.run(() -> {
+ JFrame frame = new JFrame("Triggevent Import");
+ frame.setLocationByPlatform(true);
+ JPanel panel = new TitleBorderFullsizePanel("Import");
+
+ JCheckBox decompressCheckbox = new JCheckBox("Decompress events (uses more memory)");
+ Path sessionsDir = Paths.get(Platform.getTriggeventDir().toString(), "sessions");
+ JFileChooser sessionChooser = new JFileChooser(sessionsDir.toString());
+ sessionChooser.setPreferredSize(new Dimension(800, 600));
+ JButton importSessionButton = new JButton("Import Session");
+ importSessionButton.addActionListener(e -> {
+ int result = sessionChooser.showOpenDialog(panel);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ File file = sessionChooser.getSelectedFile();
+ // TODO: this should be async
+ CatchFatalError.run(() -> {
+ LaunchImportedSession.fromEvents(EventReader.readEventsFromFile(file), decompressCheckbox.isSelected());
+ });
+ frame.setVisible(false);
+ }
+ });
+
+
+ Path actLogDir = Platform.getActDir();
+ JFileChooser actLogChooser = new JFileChooser(actLogDir.toString());
+ actLogChooser.setPreferredSize(new Dimension(800, 600));
+ JButton importActLogButton = new JButton("Import ACT Log");
+ importActLogButton.addActionListener(e -> {
+ int result = actLogChooser.showOpenDialog(panel);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ File file = actLogChooser.getSelectedFile();
+ // TODO: this should be async
+ CatchFatalError.run(() -> {
+ LaunchImportedActLog.fromEvents(EventReader.readActLogFile(file), decompressCheckbox.isSelected());
+ });
+ frame.setVisible(false);
+ }
+ });
+
+ panel.setLayout(new GridBagLayout());
+ GridBagConstraints c = new GridBagConstraints();
+
+ c.gridx = 0;
+ c.gridy = 0;
+ c.insets = new Insets(10, 10, 10, 10);
+ c.fill = GridBagConstraints.HORIZONTAL;
+ {
+ c.weightx = 1;
+ c.gridwidth = GridBagConstraints.REMAINDER;
+ JLabel importLabel = new JLabel("Please select a file to import");
+ panel.add(importLabel, c);
+ }
+ {
+ c.anchor = GridBagConstraints.LINE_START;
+ c.gridy++;
+ c.gridwidth = 1;
+ c.weightx = 0;
+ panel.add(importSessionButton, c);
+ c.gridx++;
+ c.weightx = 1;
+ panel.add(new JLabel("Import a Triggevent Session"), c);
+ }
+ {
+ c.gridy++;
+ c.gridx = 0;
+ c.gridwidth = 1;
+ c.weightx = 0;
+ panel.add(importActLogButton, c);
+ c.gridx++;
+ c.weightx = 1;
+ panel.add(new JLabel("Import an ACT Log file"), c);
+ }
+ {
+ c.gridy++;
+ c.weighty = 0;
+ c.gridx = 0;
+ c.gridwidth = GridBagConstraints.REMAINDER;
+
+ panel.add(decompressCheckbox, c);
+ }
+ {
+ c.gridy++;
+ c.weighty = 0;
+ c.gridx = 0;
+ c.gridwidth = GridBagConstraints.REMAINDER;
+ panel.add(new ReadOnlyText("In replay mode, the program will use your existing settings, but any changes you make will not be saved."), c);
+ }
+ {
+ c.gridy++;
+ c.weighty = 1;
+ // Filler
+ panel.add(new JPanel(), c);
+ }
+
+
+ frame.add(panel);
+ frame.setSize(new Dimension(400, 400));
+ frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ frame.setVisible(true);
+ }));
+
+ }
+}
diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/LaunchImportedActLog.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/LaunchImportedActLog.java
new file mode 100644
index 000000000000..98212a418277
--- /dev/null
+++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/LaunchImportedActLog.java
@@ -0,0 +1,45 @@
+package gg.xp.xivsupport.gui;
+
+import gg.xp.reevent.events.AutoEventDistributor;
+import gg.xp.reevent.events.Event;
+import gg.xp.reevent.events.EventMaster;
+import gg.xp.reevent.events.InitEvent;
+import gg.xp.xivsupport.events.actlines.parsers.FakeACTTimeSource;
+import gg.xp.xivsupport.events.misc.RawEventStorage;
+import gg.xp.xivsupport.events.state.XivStateImpl;
+import gg.xp.xivsupport.persistence.PersistenceProvider;
+import gg.xp.xivsupport.replay.ReplayController;
+import gg.xp.xivsupport.sys.XivMain;
+import org.picocontainer.MutablePicoContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public final class LaunchImportedActLog {
+ private static final Logger log = LoggerFactory.getLogger(LaunchImportedActLog.class);
+
+ private LaunchImportedActLog() {
+ }
+ public static void fromEvents(List extends Event> events) {
+ fromEvents(events, false);
+ }
+
+ public static void fromEvents(List extends Event> events, boolean decompress) {
+ CommonGuiSetup.setup();
+ MutablePicoContainer pico = XivMain.importInit();
+ pico.addComponent(FakeACTTimeSource.class);
+ AutoEventDistributor dist = pico.getComponent(AutoEventDistributor.class);
+ PersistenceProvider pers = pico.getComponent(PersistenceProvider.class);
+ EventMaster master = pico.getComponent(EventMaster.class);
+ ReplayController replayController = new ReplayController(master, events, decompress);
+ pico.addComponent(replayController);
+ dist.acceptEvent(new InitEvent());
+ pico.getComponent(XivStateImpl.class).setActImport(true);
+ RawEventStorage raw = pico.getComponent(RawEventStorage.class);
+ raw.getMaxEventsStoredSetting().set(1_000_000);
+ pico.addComponent(GuiMain.class);
+ pico.getComponent(GuiMain.class);
+// FailOnThreadViolationRepaintManager.install();
+ }
+}
diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/LaunchImportedSession.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/LaunchImportedSession.java
new file mode 100644
index 000000000000..161f4469fd3a
--- /dev/null
+++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/LaunchImportedSession.java
@@ -0,0 +1,41 @@
+package gg.xp.xivsupport.gui;
+
+import gg.xp.reevent.events.AutoEventDistributor;
+import gg.xp.reevent.events.Event;
+import gg.xp.reevent.events.EventMaster;
+import gg.xp.reevent.events.InitEvent;
+import gg.xp.xivsupport.events.misc.RawEventStorage;
+import gg.xp.xivsupport.replay.ReplayController;
+import gg.xp.xivsupport.sys.XivMain;
+import org.picocontainer.MutablePicoContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public final class LaunchImportedSession {
+ private static final Logger log = LoggerFactory.getLogger(LaunchImportedSession.class);
+
+ private LaunchImportedSession() {
+ }
+
+ public static void fromEvents(List extends Event> events) {
+ fromEvents(events, false);
+ }
+
+ public static void fromEvents(List extends Event> events, boolean decompress) {
+ CommonGuiSetup.setup();
+ MutablePicoContainer pico = XivMain.importInit();
+ AutoEventDistributor dist = pico.getComponent(AutoEventDistributor.class);
+ EventMaster master = pico.getComponent(EventMaster.class);
+ ReplayController replayController = new ReplayController(master, events, decompress);
+ pico.addComponent(replayController);
+ pico.getComponent(RawEventStorage.class);
+ dist.acceptEvent(new InitEvent());
+ RawEventStorage raw = pico.getComponent(RawEventStorage.class);
+ raw.getMaxEventsStoredSetting().set(1_000_000);
+ pico.addComponent(GuiMain.class);
+ pico.getComponent(GuiMain.class);
+// FailOnThreadViolationRepaintManager.install();
+ }
+}
diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java
index fc288689821d..e3c135b04a09 100644
--- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java
+++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java
@@ -7,18 +7,23 @@
import gg.xp.xivsupport.persistence.SimplifiedPropertiesFilePersistenceProvider;
import gg.xp.xivsupport.persistence.gui.StringSettingGui;
import gg.xp.xivsupport.persistence.settings.StringSetting;
+import gg.xp.xivsupport.sys.Threading;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.io.File;
-import java.io.IOException;
import java.nio.file.Paths;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
public class UpdatesPanel extends TitleBorderFullsizePanel {
private static final Logger log = LoggerFactory.getLogger(UpdatesPanel.class);
private static final String propsOverrideFileName = "update.properties";
+ private static final ExecutorService exs = Executors.newCachedThreadPool(Threading.namedDaemonThreadFactory("UpdateCheck"));
+ private JLabel checkingLabel;
private File installDir;
private File propsOverride;
private PersistenceProvider updatePropsFilePers;
@@ -39,8 +44,27 @@ public UpdatesPanel() {
GridBagConstraints c = new GridBagConstraints();
c.gridy = 0;
c.weighty = 0;
+ {
+ checkingLabel = new JLabel("Update Status");
+ doUpdateCheckInBackground();
+ add(checkingLabel, c);
+ }
JButton button = new JButton("Check for Updates and Restart");
button.addActionListener(l -> {
+ // First, try to update the updater itself
+ try {
+ try {
+ Class> clazz = Class.forName("gg.xp.xivsupport.gui.Update");
+ clazz.getMethod("updateTheUpdater").invoke(null);
+ } catch (Throwable e) {
+ Class> clazz = Class.forName("gg.xp.xivsupport.gui.UpdateCopyForLegacyMigration");
+ clazz.getMethod("updateTheUpdater").invoke(null);
+ }
+ }
+ catch (Throwable e) {
+ log.error("Error updating the updater - you may not have a recent enough version.", e);
+ JOptionPane.showMessageDialog(SwingUtilities.getRoot(button), "There was an error updating the updater. This may fix itself after updates. ");
+ }
try {
// Desktop.open seems to open it in such a way that when we exit, we release the mutex, so the updater
// can relaunch the application correctly.
@@ -48,15 +72,18 @@ public UpdatesPanel() {
}
catch (Throwable e) {
log.error("Error launching updater", e);
- JOptionPane.showMessageDialog(SwingUtilities.getRoot(button), "There was an error launching the updater. You can try running the updater manually by running triggevent-upd.exe.");
+ JOptionPane.showMessageDialog(SwingUtilities.getRoot(button), "There was an error launching the updater. You can try running the updater manually by running triggevent-upd.exe, or reinstall if that doesn't work.");
return;
}
System.exit(0);
});
+ c.gridy++;
add(new JLabel("Install Dir: " + installDir), c);
c.gridy++;
JPanel content = new JPanel();
- content.add(new StringSettingGui(new StringSetting(updatePropsFilePers, "branch", "stable"), "Branch").getComponent());
+ StringSetting setting = new StringSetting(updatePropsFilePers, "branch", "stable");
+ content.add(new StringSettingGui(setting, "Branch").getComponent());
+ setting.addListener(this::doUpdateCheckInBackground);
content.add(button);
add(content, c);
c.gridy++;
@@ -67,4 +94,36 @@ public UpdatesPanel() {
c.weighty = 1;
add(new JPanel(), c);
}
+
+ private void doUpdateCheckInBackground() {
+ exs.submit(() -> {
+ checkingLabel.setText("Checking for updates...");
+ try {
+ Class> clazz = Class.forName("gg.xp.xivsupport.gui.Update");
+ boolean result = (boolean) clazz.getMethod("justCheck", Consumer.class).invoke(null, (Consumer) s -> log.info("From Updater: {}", s));
+ if (result) {
+ checkingLabel.setText("There are updates available!");
+ }
+ else {
+ checkingLabel.setText("It looks like you are up to date.");
+ }
+ }
+ catch (Throwable firstError) {
+ log.error("Error updating, will try backup updater", firstError);
+ try {
+ Class> clazz = Class.forName("gg.xp.xivsupport.gui.UpdateCopyForLegacyMigration");
+ boolean result = (boolean) clazz.getMethod("justCheck", Consumer.class).invoke(null, (Consumer) s -> log.info("From Updater: {}", s));
+ if (result) {
+ checkingLabel.setText("There are updates available!");
+ }
+ else {
+ checkingLabel.setText("It looks like you are up to date.");
+ }
+ } catch (Throwable e) {
+ log.error("Error checking for updates - you may not have a recent enough version.", e);
+ checkingLabel.setText("Automatic Check Failed, but you can try updating anyway. Perhaps the branch does not exist?");
+ }
+ }
+ });
+ }
}
diff --git a/xivsupport/src/test/java/gg/xp/xivsupport/persistence/PersistenceTests.java b/xivsupport/src/test/java/gg/xp/xivsupport/persistence/PersistenceTests.java
index 5dad1985a401..31982bd07aef 100644
--- a/xivsupport/src/test/java/gg/xp/xivsupport/persistence/PersistenceTests.java
+++ b/xivsupport/src/test/java/gg/xp/xivsupport/persistence/PersistenceTests.java
@@ -77,7 +77,6 @@ public void testDefaultPropsLocation() throws InterruptedException {
// Wait for file flush
Thread.sleep(200);
testPersistenceRead(PropertiesFilePersistenceProvider.inUserDataFolder("integration-test"));
-
}