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 events) { + fromEvents(events, false); + } + + public static void fromEvents(List 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 events) { + fromEvents(events, false); + } + + public static void fromEvents(List 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")); - }