From aca01715aa8cbf096b00db9dca25cca718f39b5c Mon Sep 17 00:00:00 2001 From: xp Date: Sat, 5 Mar 2022 13:56:26 -0800 Subject: [PATCH 1/7] Experimental new launcher --- .github/workflows/publish.yml | 12 +- launcher/pom.xml | 28 +++- .../java/gg/xp/xivsupport/gui/Update.java | 65 ++++++--- pom.xml | 46 +++++++ reevent/pom.xml | 6 - xivsupport/pom.xml | 5 - .../gg/xp/xivsupport/gui/GuiImportLaunch.java | 127 ++++++++++++++++++ .../xivsupport/gui/LaunchImportedActLog.java | 45 +++++++ .../xivsupport/gui/LaunchImportedSession.java | 41 ++++++ .../persistence/PersistenceTests.java | 1 - 10 files changed, 339 insertions(+), 37 deletions(-) create mode 100644 xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiImportLaunch.java create mode 100644 xivsupport/src/main/java/gg/xp/xivsupport/gui/LaunchImportedActLog.java create mode 100644 xivsupport/src/main/java/gg/xp/xivsupport/gui/LaunchImportedSession.java 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..0938431f085a 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -15,6 +15,8 @@ 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 @@ -93,6 +95,23 @@ + + copy-jar-secondary + + package + + copy-resources + + + ${basedir}/target/windows/deps + + + ${project.build.directory} + launcher-${project.version}.jar + + + + copy-jre @@ -146,8 +165,9 @@ ${mainImportClass} false - ./preload/*.jar;./deps/*.jar;./user/*.jar + ./preload/*.jar;./deps/*.jar;./user/*.jar;${launcherJar} + ./nonexistent.jar @@ -165,8 +185,9 @@ ${mainClass} false - ./preload/*.jar;./deps/*.jar;./user/*.jar + ./preload/*.jar;./deps/*.jar;./user/*.jar;${launcherJar} + ./nonexistent.jar @@ -181,6 +202,8 @@ ${mainUpdateClass} false + ${launcherJarRelativeToPom} + false @@ -193,7 +216,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..df74b8af9d7d 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,10 +22,11 @@ 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; @@ -39,9 +39,12 @@ public class Update { private static final String updaterUrlTemplate = "https://xpdota.github.io/event-trigger/%s/%s"; private static final String defaultBranch = "stable"; + private final boolean updateTheUpdaterItself; 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; @@ -97,7 +100,7 @@ private String getBranch() { } private Path getLocalFile(String name) { - return Paths.get(depsDir.toString(), name); + return Paths.get(installDir.toString(), name); } private final JFrame frame; @@ -106,13 +109,16 @@ private Path getLocalFile(String name) { private final StringBuilder logText = new StringBuilder(); private final HttpClient client = HttpClient.newHttpClient(); - private Update() { + private Update(boolean updateTheUpdaterItself) { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } - catch (Throwable e) { - // Ignore + this.updateTheUpdaterItself = updateTheUpdaterItself; + if (!updateTheUpdaterItself) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Throwable e) { + // Ignore + } } frame = new JFrame("Triggevent Updater"); frame.setSize(new Dimension(800, 500)); @@ -170,17 +176,33 @@ 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)); + } + } + List updaterFiles = List.of(updaterFilename, updaterFilenameBackup); + // Updater will not be able to update itself + if (updateTheUpdaterItself) { + actualFiles.keySet().retainAll(updaterFiles); + expectedFiles.keySet().retainAll(updaterFiles); } else { - actualFiles = Arrays.stream(depsFiles) - .parallel() - .filter(File::isFile) - .collect(Collectors.toMap(File::getName, Update::md5sum)); + actualFiles.keySet().removeAll(updaterFiles); + expectedFiles.keySet().remove(updaterFiles); } List allKeys = new ArrayList<>(); allKeys.addAll(actualFiles.keySet()); @@ -209,7 +231,7 @@ private void doUpdateCheck() { localFilesToDelete.forEach(name -> { boolean deleted; do { - deleted = Paths.get(depsDir.toString(), name).toFile().delete(); + deleted = Paths.get(installDir.toString(), name).toFile().delete(); if (deleted) { return; } @@ -255,7 +277,7 @@ private void doUpdateCheck() { public static void main(String[] args) { CatchFatalErrorInUpdater.run(() -> { - Update update = new Update(); + Update update = new Update(false); try { Thread.sleep(1000); } @@ -266,6 +288,11 @@ public static void main(String[] args) { }); } + @SuppressWarnings("unused") + public static void updateTheUpdater() { + new Update(true).doUpdateCheck(); + } + private static String md5sum(File file) { try (FileInputStream fis = new FileInputStream(file)) { MessageDigest md5 = MessageDigest.getInstance("MD5"); 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/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")); - } From 32e03227083185f815373f30f845a85b907e0d6b Mon Sep 17 00:00:00 2001 From: xp Date: Sat, 5 Mar 2022 15:09:56 -0800 Subject: [PATCH 2/7] Progress --- launcher/pom.xml | 5 +-- .../java/gg/xp/xivsupport/gui/Update.java | 31 +++++++++++++------ .../xp/xivsupport/gui/tabs/UpdatesPanel.java | 11 ++++++- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/launcher/pom.xml b/launcher/pom.xml index 0938431f085a..a5d0476b3fe8 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -17,6 +17,7 @@ gg.xp.xivsupport.gui.Update ./launcher-${project.version}.jar ./target/windows/launcher-${project.version}.jar + ./userdata;./preload/*.jar;./deps/*.jar;./user/*.jar;${launcherJar} @@ -165,7 +166,7 @@ ${mainImportClass} false - ./preload/*.jar;./deps/*.jar;./user/*.jar;${launcherJar} + ${preCp} ./nonexistent.jar @@ -185,7 +186,7 @@ ${mainClass} false - ./preload/*.jar;./deps/*.jar;./user/*.jar;${launcherJar} + ${preCp} ./nonexistent.jar 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 df74b8af9d7d..ff901028d119 100644 --- a/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java +++ b/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java @@ -22,7 +22,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -37,7 +36,7 @@ // ...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 boolean updateTheUpdaterItself; private String branch; @@ -143,15 +142,27 @@ private Update(boolean updateTheUpdaterItself) { 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(); + 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(Update.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); + if (jarLocation.isFile()) { + 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(); @@ -202,7 +213,7 @@ else if (depsFiles == null) { } else { actualFiles.keySet().removeAll(updaterFiles); - expectedFiles.keySet().remove(updaterFiles); + expectedFiles.keySet().removeAll(updaterFiles); } List allKeys = new ArrayList<>(); allKeys.addAll(actualFiles.keySet()); 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..ade20ff5387f 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 @@ -41,6 +41,15 @@ public UpdatesPanel() { c.weighty = 0; JButton button = new JButton("Check for Updates and Restart"); button.addActionListener(l -> { + // First, try to update the updater itself + try { + Class clazz = Class.forName("gg.xp.xivsupport.gui.Update"); + 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,7 +57,7 @@ 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); From a1b43df710cdac38c148ff8afe220cd5be852078 Mon Sep 17 00:00:00 2001 From: xp Date: Sat, 5 Mar 2022 15:20:19 -0800 Subject: [PATCH 3/7] Fix --- launcher/src/main/java/gg/xp/xivsupport/gui/Update.java | 4 ++++ 1 file changed, 4 insertions(+) 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 ff901028d119..97baff136b9e 100644 --- a/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java +++ b/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java @@ -152,6 +152,10 @@ private Update(boolean updateTheUpdaterItself) { 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) { From e0e862a5f9b0a5a6e6ef136383fbe8939c4683fd Mon Sep 17 00:00:00 2001 From: xp Date: Sat, 5 Mar 2022 16:21:08 -0800 Subject: [PATCH 4/7] Improved updater --- .../java/gg/xp/xivsupport/gui/Update.java | 171 +++++++++++------- .../xp/xivsupport/gui/tabs/UpdatesPanel.java | 37 +++- 2 files changed, 139 insertions(+), 69 deletions(-) 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 97baff136b9e..008e6b8c47db 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,7 @@ package gg.xp.xivsupport.gui; import gg.xp.xivsupport.gui.util.CatchFatalErrorInUpdater; +import groovyjarjarantlr4.v4.misc.Graph; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -30,6 +31,7 @@ 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 @@ -38,7 +40,9 @@ public class Update { 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"; @@ -47,7 +51,6 @@ public class Update { private final File installDir; private final File depsDir; private final File propsOverride; - private final JTextArea textArea; private URI makeUrl(String filename) { try { @@ -102,46 +105,12 @@ private Path getLocalFile(String 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(boolean updateTheUpdaterItself) { - + private Update(Consumer logging, boolean updateTheUpdaterItself, boolean onlyCheck) { + this.logging = logging; this.updateTheUpdaterItself = updateTheUpdaterItself; - if (!updateTheUpdaterItself) { - 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); + this.noop = onlyCheck; String override = System.getProperty("triggevent-update-override-dir"); if (override == null) { override = System.getenv("triggevent-update-override-dir"); @@ -170,20 +139,75 @@ private Update(boolean updateTheUpdaterItself) { } 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) { @@ -210,14 +234,17 @@ else if (depsFiles == null) { } } List updaterFiles = List.of(updaterFilename, updaterFilenameBackup); - // 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); + // 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()); @@ -225,7 +252,10 @@ else if (depsFiles == null) { 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.get(key); + String remoteHash = expectedFiles.get(key); + String separator = localHash.equals(remoteHash) ? "==" : "->"; + appendText("%32s %s %32s %s".formatted(localHash, separator, remoteHash, key)); }); appendText("Calculating update..."); List localFilesToDelete = new ArrayList<>(); @@ -274,16 +304,18 @@ else if (depsFiles == null) { 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()); - } - catch (IOException e) { - e.printStackTrace(); - } - })); + 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); @@ -292,20 +324,25 @@ else if (depsFiles == null) { public static void main(String[] args) { CatchFatalErrorInUpdater.run(() -> { - Update update = new Update(false); + GraphicalUpdater gupdate = new GraphicalUpdater(false); try { Thread.sleep(1000); } catch (InterruptedException e) { } - update.doUpdateCheck(); + gupdate.run(); }); } @SuppressWarnings("unused") public static void updateTheUpdater() { - new Update(true).doUpdateCheck(); + 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) { 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 ade20ff5387f..e13704e1a9bd 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,6 +44,11 @@ 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 @@ -62,10 +72,13 @@ public UpdatesPanel() { } 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++; @@ -76,4 +89,24 @@ 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 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."); + } + }); + } } From 97047154984b73ddb92bfecb35bd4d4ea0e8c2d7 Mon Sep 17 00:00:00 2001 From: xp Date: Sat, 5 Mar 2022 16:32:26 -0800 Subject: [PATCH 5/7] Updater updates --- .../java/gg/xp/xivsupport/gui/Update.java | 75 ++++++++++--------- .../xp/xivsupport/gui/tabs/UpdatesPanel.java | 2 +- 2 files changed, 39 insertions(+), 38 deletions(-) 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 008e6b8c47db..bc6e690fc981 100644 --- a/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java +++ b/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java @@ -1,7 +1,6 @@ package gg.xp.xivsupport.gui; import gg.xp.xivsupport.gui.util.CatchFatalErrorInUpdater; -import groovyjarjarantlr4.v4.misc.Graph; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -272,47 +271,49 @@ else if (depsFiles == null) { filesToDownload.add(name); } }); - 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) { + 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); - }); + } + } 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(() -> { + depsDir.mkdirs(); + depsDir.mkdir(); + AtomicInteger downloaded = new AtomicInteger(); + filesToDownload.parallelStream().forEach((name) -> { + HttpResponse.BodyHandler handler = HttpResponse.BodyHandlers.ofFile(getLocalFile(name)); try { - Runtime.getRuntime().exec(Paths.get(installDir.toString(), "triggevent.exe").toString()); + client.send(HttpRequest.newBuilder().GET().uri(makeUrl(name)).build(), handler); } - catch (IOException e) { - e.printStackTrace(); + 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(); 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 e13704e1a9bd..b543347124bb 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 @@ -105,7 +105,7 @@ private void doUpdateCheckInBackground() { } 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."); + checkingLabel.setText("Automatic Check Failed, but you can try updating anyway. Perhaps the branch does not exist?"); } }); } From 56ebf75798d9162fc9c12e6a9be3ee935878adb0 Mon Sep 17 00:00:00 2001 From: xp Date: Sat, 5 Mar 2022 16:48:20 -0800 Subject: [PATCH 6/7] Backup updater --- .../java/gg/xp/xivsupport/gui/Update.java | 4 +- .../gui/UpdateCopyForLegacyMigration.java | 374 ++++++++++++++++++ .../xp/xivsupport/gui/tabs/UpdatesPanel.java | 18 +- 3 files changed, 391 insertions(+), 5 deletions(-) create mode 100644 launcher/src/main/java/gg/xp/xivsupport/gui/UpdateCopyForLegacyMigration.java 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 bc6e690fc981..54179d67c1e2 100644 --- a/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java +++ b/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java @@ -251,8 +251,8 @@ else if (depsFiles == null) { allKeys.sort(String::compareTo); Set allKeysSet = new LinkedHashSet<>(allKeys); allKeysSet.forEach(key -> { - String localHash = actualFiles.get(key); - String remoteHash = expectedFiles.get(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)); }); 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/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java index b543347124bb..ccf2f09cb241 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 @@ -103,9 +103,21 @@ private void doUpdateCheckInBackground() { 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?"); + 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?"); + } } }); } From 3ed7bef8ca5e8355570923db4102325e251f0daf Mon Sep 17 00:00:00 2001 From: xp Date: Sat, 5 Mar 2022 17:04:14 -0800 Subject: [PATCH 7/7] Fixing corner case --- .../java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 ccf2f09cb241..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 @@ -53,8 +53,13 @@ public UpdatesPanel() { button.addActionListener(l -> { // First, try to update the updater itself try { - Class clazz = Class.forName("gg.xp.xivsupport.gui.Update"); - clazz.getMethod("updateTheUpdater").invoke(null); + 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);