From cdfe2e93069458e17fcee827d6633a0ae290d0c8 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Wed, 7 Aug 2019 03:43:41 -0600 Subject: [PATCH] runelite-client: Add fatal error dialog --- .../java/net/runelite/client/RuneLite.java | 16 +- .../runelite/client/RuneLiteProperties.java | 18 ++ .../net/runelite/client/rs/ClientLoader.java | 33 ++- .../client/rs/VerificationException.java | 2 +- .../java/net/runelite/client/ui/ClientUI.java | 10 +- .../runelite/client/ui/FatalErrorDialog.java | 240 ++++++++++++++++++ .../net/runelite/client/runelite.properties | 5 +- 7 files changed, 301 insertions(+), 23 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index e8b4d60a43..0decdb3c48 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -37,6 +37,7 @@ import java.util.Locale; import javax.annotation.Nullable; import javax.inject.Provider; import javax.inject.Singleton; +import javax.swing.SwingUtilities; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionParser; import joptsimple.OptionSet; @@ -60,6 +61,7 @@ import net.runelite.client.rs.ClientLoader; import net.runelite.client.rs.ClientUpdateCheckMode; import net.runelite.client.ui.ClientUI; import net.runelite.client.ui.DrawManager; +import net.runelite.client.ui.FatalErrorDialog; import net.runelite.client.ui.SplashScreen; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.OverlayRenderer; @@ -78,6 +80,7 @@ public class RuneLite public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles"); public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots"); + public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs"); @Getter private static Injector injector; @@ -219,7 +222,11 @@ public class RuneLite assert assertions = true; if (!assertions) { - throw new RuntimeException("Developers should enable assertions; Add `-ea` to your JVM arguments`"); + SwingUtilities.invokeLater(() -> + new FatalErrorDialog("Developers should enable assertions; Add `-ea` to your JVM arguments`") + .addBuildingGuide() + .open()); + return; } } @@ -238,6 +245,13 @@ public class RuneLite final long uptime = rb.getUptime(); log.info("Client initialization took {}ms. Uptime: {}ms", end - start, uptime); } + catch (Exception e) + { + log.warn("Failure during startup", e); + SwingUtilities.invokeLater(() -> + new FatalErrorDialog("RuneLite has encountered an unexpected error during startup.") + .open()); + } finally { SplashScreen.stop(); diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java index 4ff24fe690..a1ff15e2eb 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java @@ -40,6 +40,9 @@ public class RuneLiteProperties private static final String WIKI_LINK = "runelite.wiki.link"; private static final String PATREON_LINK = "runelite.patreon.link"; private static final String LAUNCHER_VERSION_PROPERTY = "runelite.launcher.version"; + private static final String TROUBLESHOOTING_LINK = "runelite.wiki.troubleshooting.link"; + private static final String BUILDING_LINK = "runelite.wiki.building.link"; + private static final String DNS_CHANGE_LINK = "runelite.dnschange.link"; private static final Properties properties = new Properties(); @@ -100,4 +103,19 @@ public class RuneLiteProperties { return System.getProperty(LAUNCHER_VERSION_PROPERTY); } + + public static String getTroubleshootingLink() + { + return properties.getProperty(TROUBLESHOOTING_LINK); + } + + public static String getBuildingLink() + { + return properties.getProperty(BUILDING_LINK); + } + + public static String getDNSChangeLink() + { + return properties.getProperty(DNS_CHANGE_LINK); + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java index f666703a66..43721b6f06 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java +++ b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java @@ -49,11 +49,13 @@ import java.util.Map; import java.util.function.Supplier; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; +import javax.swing.SwingUtilities; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import static net.runelite.client.rs.ClientUpdateCheckMode.AUTO; import static net.runelite.client.rs.ClientUpdateCheckMode.NONE; import static net.runelite.client.rs.ClientUpdateCheckMode.VANILLA; +import net.runelite.client.ui.FatalErrorDialog; import net.runelite.client.ui.SplashScreen; import net.runelite.http.api.RuneLiteAPI; import okhttp3.Request; @@ -64,7 +66,7 @@ import org.apache.commons.compress.compressors.CompressorException; public class ClientLoader implements Supplier { private ClientUpdateCheckMode updateCheckMode; - private Applet client = null; + private Object client = null; public ClientLoader(ClientUpdateCheckMode updateCheckMode) { @@ -78,10 +80,15 @@ public class ClientLoader implements Supplier { client = doLoad(); } - return client; + + if (client instanceof Throwable) + { + throw new RuntimeException((Throwable) client); + } + return (Applet) client; } - private Applet doLoad() + private Object doLoad() { if (updateCheckMode == NONE) { @@ -172,6 +179,15 @@ public class ClientLoader implements Supplier Map hashes; try (InputStream is = ClientLoader.class.getResourceAsStream("/patch/hashes.json")) { + if (is == null) + { + SwingUtilities.invokeLater(() -> + new FatalErrorDialog("The client-patch is missing from the classpath. If you are building " + + "the client you need to re-run maven") + .addBuildingGuide() + .open()); + throw new NullPointerException(); + } hashes = new Gson().fromJson(new InputStreamReader(is), new TypeToken>() { }.getType()); @@ -264,15 +280,10 @@ public class ClientLoader implements Supplier | CompressorException | InvalidHeaderException | CertificateException | VerificationException | SecurityException e) { - if (e instanceof ClassNotFoundException) - { - log.error("Unable to load client - class not found. This means you" - + " are not running RuneLite with Maven as the client patch" - + " is not in your classpath."); - } - log.error("Error loading RS!", e); - return null; + + SwingUtilities.invokeLater(() -> FatalErrorDialog.showNetErrorWindow("loading the client", e)); + return e; } } diff --git a/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java b/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java index 040370e8f0..4138a12fd3 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java +++ b/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java @@ -24,7 +24,7 @@ */ package net.runelite.client.rs; -class VerificationException extends Exception +public class VerificationException extends Exception { public VerificationException(String message) { diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java index 4fe2413c27..e50d312455 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java @@ -53,7 +53,6 @@ import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JOptionPane; -import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.INFORMATION_MESSAGE; import javax.swing.JPanel; import javax.swing.JRootPane; @@ -518,14 +517,7 @@ public class ClientUI }); // Show out of date dialog if needed - if (client == null) - { - SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(frame, - "Error loading client! Check your logs for more details.", - "Unable to load client", - ERROR_MESSAGE)); - } - else if (!(client instanceof Client)) + if (client != null && !(client instanceof Client)) { SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(frame, "RuneLite has not yet been updated to work with the latest\n" diff --git a/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java b/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java new file mode 100644 index 0000000000..746546fc10 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2019 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.ui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Desktop; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLite; +import net.runelite.client.RuneLiteProperties; +import net.runelite.client.rs.VerificationException; +import net.runelite.client.util.LinkBrowser; + +@Slf4j +public class FatalErrorDialog extends JDialog +{ + private static final AtomicBoolean alreadyOpen = new AtomicBoolean(false); + + private final JPanel rightColumn = new JPanel(); + private final Font font = new Font(Font.DIALOG, Font.PLAIN, 12); + + public FatalErrorDialog(String message) + { + if (alreadyOpen.getAndSet(true)) + { + throw new IllegalStateException("Fatal error during fatal error: " + message); + } + + try + { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } + catch (Exception e) + { + } + + UIManager.put("Button.select", ColorScheme.DARKER_GRAY_COLOR); + + try + { + BufferedImage logo = ImageIO.read(SplashScreen.class.getResourceAsStream("runelite_transparent.png")); + setIconImage(logo); + + JLabel runelite = new JLabel(); + runelite.setIcon(new ImageIcon(logo)); + runelite.setAlignmentX(Component.CENTER_ALIGNMENT); + runelite.setBackground(ColorScheme.DARK_GRAY_COLOR); + runelite.setOpaque(true); + rightColumn.add(runelite); + } + catch (IOException e) + { + } + + addWindowListener(new WindowAdapter() + { + @Override + public void windowClosing(WindowEvent e) + { + System.exit(-1); + } + }); + + setTitle("Fatal error starting RuneLite"); + setLayout(new BorderLayout()); + + Container pane = getContentPane(); + pane.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + JPanel leftPane = new JPanel(); + leftPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); + leftPane.setLayout(new BorderLayout()); + + JLabel title = new JLabel("There was a fatal error starting RuneLite"); + title.setForeground(Color.WHITE); + title.setFont(font.deriveFont(16.f)); + title.setBorder(new EmptyBorder(10, 10, 10, 10)); + leftPane.add(title, BorderLayout.NORTH); + + leftPane.setPreferredSize(new Dimension(400, 200)); + JTextArea textArea = new JTextArea(message); + textArea.setFont(font); + textArea.setBackground(ColorScheme.DARKER_GRAY_COLOR); + textArea.setForeground(Color.LIGHT_GRAY); + textArea.setLineWrap(true); + textArea.setWrapStyleWord(true); + textArea.setBorder(new EmptyBorder(10, 10, 10, 10)); + textArea.setEditable(false); + leftPane.add(textArea, BorderLayout.CENTER); + + pane.add(leftPane, BorderLayout.CENTER); + + rightColumn.setLayout(new BoxLayout(rightColumn, BoxLayout.Y_AXIS)); + rightColumn.setBackground(ColorScheme.DARK_GRAY_COLOR); + rightColumn.setMaximumSize(new Dimension(200, Integer.MAX_VALUE)); + + addButton("Open logs folder", () -> + { + try + { + Desktop.getDesktop().open(RuneLite.LOGS_DIR); + } + catch (IOException e) + { + log.warn("Unable to open logs", e); + } + }); + addButton("Get help on Discord", () -> LinkBrowser.browse(RuneLiteProperties.getDiscordInvite())); + addButton("Troubleshooting steps", () -> LinkBrowser.browse(RuneLiteProperties.getTroubleshootingLink())); + + pane.add(rightColumn, BorderLayout.EAST); + } + + public void open() + { + addButton("Exit", () -> System.exit(-1)); + + pack(); + SplashScreen.stop(); + setVisible(true); + } + + public FatalErrorDialog addButton(String message, Runnable action) + { + JButton button = new JButton(message); + button.addActionListener(e -> action.run()); + button.setFont(font); + button.setBackground(ColorScheme.DARK_GRAY_COLOR); + button.setForeground(Color.LIGHT_GRAY); + button.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, ColorScheme.DARK_GRAY_COLOR.brighter()), + new EmptyBorder(4, 4, 4, 4) + )); + button.setAlignmentX(Component.CENTER_ALIGNMENT); + button.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); + button.setFocusPainted(false); + button.addChangeListener(ev -> + { + if (button.getModel().isPressed()) + { + button.setBackground(ColorScheme.DARKER_GRAY_COLOR); + } + else if (button.getModel().isRollover()) + { + button.setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + } + else + { + button.setBackground(ColorScheme.DARK_GRAY_COLOR); + } + }); + + rightColumn.add(button); + rightColumn.revalidate(); + + return this; + } + + public FatalErrorDialog addBuildingGuide() + { + return addButton("Building guide", () -> LinkBrowser.browse(RuneLiteProperties.getBuildingLink())); + } + + public static void showNetErrorWindow(String action, Throwable err) + { + if (err instanceof VerificationException || err instanceof GeneralSecurityException) + { + new FatalErrorDialog("RuneLite was unable to verify the security of its connection to the internet while " + + action + ". You may have a misbehaving antivirus, internet service provider, a proxy, or an incomplete" + + " java installation.") + .open(); + return; + } + + if (err instanceof ConnectException) + { + new FatalErrorDialog("RuneLite is unable to connect to a required server while " + action + ". " + + "Please check your internet connection") + .open(); + return; + } + + if (err instanceof UnknownHostException) + { + new FatalErrorDialog("RuneLite is unable to resolve the address of a required server while " + action + ". " + + "Your DNS resolver may be misconfigured, pointing to an inaccurate resolver, or your internet connection may " + + "be down. ") + .addButton("Change your DNS resolver", () -> LinkBrowser.browse(RuneLiteProperties.getDNSChangeLink())) + .open(); + return; + } + + new FatalErrorDialog("RuneLite encountered a fatal error while " + action + ".").open(); + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/runelite.properties b/runelite-client/src/main/resources/net/runelite/client/runelite.properties index afeed8517f..c29c1e8e39 100644 --- a/runelite-client/src/main/resources/net/runelite/client/runelite.properties +++ b/runelite-client/src/main/resources/net/runelite/client/runelite.properties @@ -5,4 +5,7 @@ runelite.discord.appid=409416265891971072 runelite.discord.invite=https://discord.gg/R4BQ8tU runelite.github.link=https://github.com/runelite runelite.wiki.link=https://github.com/runelite/runelite/wiki -runelite.patreon.link=https://www.patreon.com/runelite \ No newline at end of file +runelite.patreon.link=https://www.patreon.com/runelite +runelite.wiki.troubleshooting.link=https://github.com/runelite/runelite/wiki/Troubleshooting-problems-with-the-client +runelite.wiki.building.link=https://github.com/runelite/runelite/wiki/Building-with-IntelliJ-IDEA#client-failing-to-start +runelite.dnschange.link=https://1.1.1.1/dns/