From 553f091693969259bf9f0d60792be960e0ce19ac Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Thu, 15 Jan 2015 21:55:26 -0500 Subject: [PATCH] Added ErrorHandler class to handle all critical errors. This mostly replaces the Slick2D Log class for error reporting. Most game errors will now trigger the error popup. Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/ErrorHandler.java | 161 ++++++++++++++++++ src/itdelatrisu/opsu/GameImage.java | 7 +- src/itdelatrisu/opsu/GameMod.java | 3 +- src/itdelatrisu/opsu/Opsu.java | 47 +---- src/itdelatrisu/opsu/OsuParser.java | 4 +- src/itdelatrisu/opsu/OszUnpacker.java | 6 +- src/itdelatrisu/opsu/Utils.java | 4 +- .../opsu/audio/MusicController.java | 8 +- .../opsu/audio/SoundController.java | 4 +- src/itdelatrisu/opsu/states/Game.java | 8 +- src/itdelatrisu/opsu/states/Options.java | 11 +- 11 files changed, 192 insertions(+), 71 deletions(-) create mode 100644 src/itdelatrisu/opsu/ErrorHandler.java diff --git a/src/itdelatrisu/opsu/ErrorHandler.java b/src/itdelatrisu/opsu/ErrorHandler.java new file mode 100644 index 00000000..8b5886fd --- /dev/null +++ b/src/itdelatrisu/opsu/ErrorHandler.java @@ -0,0 +1,161 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ + +package itdelatrisu.opsu; + +import itdelatrisu.opsu.states.Options; + +import java.awt.Cursor; +import java.awt.Desktop; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; + +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.UIManager; + +import org.newdawn.slick.util.Log; + +/** + * Error handler to log and display errors. + */ +public class ErrorHandler { + /** + * Error popup title. + */ + private static final String title = "Error"; + + /** + * Error popup description text. + */ + private static final String + desc = "opsu! has encountered an error.", + descR = "opsu! has encountered an error. Please report this!"; + + /** + * Error popup button options. + */ + private static final String[] + options = {"View Error Log", "Close"}, + optionsR = {"Send Report", "View Error Log", "Close"}; + + /** + * Text area for Exception. + */ + private static final JTextArea textArea = new JTextArea(7, 30); + static { + textArea.setEditable(false); + textArea.setBackground(UIManager.getColor("Panel.background")); + textArea.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + textArea.setTabSize(2); + textArea.setLineWrap(true); + } + + /** + * Scroll pane holding JTextArea. + */ + private static final JScrollPane scroll = new JScrollPane(textArea); + + /** + * Error popup objects. + */ + private static final Object[] + message = { desc, scroll }, + messageR = { descR, scroll }; + + /** + * Address to report issues. + */ + private static URI uri; + static { + try { + uri = new URI("https://github.com/itdelatrisu/opsu/issues/new"); + } catch (URISyntaxException e) { + Log.error("Problem with error URI.", e); + } + } + + // This class should not be instantiated. + private ErrorHandler() {} + + /** + * Displays an error popup and logs the given error. + * @param error a descR of the error + * @param e the exception causing the error + * @param report whether to ask to report the error + */ + public static void error(String error, Throwable e, boolean report) { + if (error == null && e == null) + return; + + // log the error + if (error == null) + Log.error(e); + else if (e == null) + Log.error(error); + else + Log.error(error, e); + + // set the textArea to the error message + textArea.setText(null); + if (error != null) { + textArea.append(error); + textArea.append("\n"); + } + if (e != null) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + textArea.append(sw.toString()); + } + + // display popup + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + if (Desktop.isDesktopSupported()) { + // try to open the log file and/or issues webpage + if (report) { + // ask to report the error + int n = JOptionPane.showOptionDialog(null, messageR, title, + JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, + null, optionsR, optionsR[2]); + if (n == 0) { + Desktop.getDesktop().browse(uri); + Desktop.getDesktop().open(Options.LOG_FILE); + } else if (n == 1) + Desktop.getDesktop().open(Options.LOG_FILE); + } else { + // don't report the error + int n = JOptionPane.showOptionDialog(null, message, title, + JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, + null, options, optionsR[1]); + if (n == 0) + Desktop.getDesktop().open(Options.LOG_FILE); + } + } else { + // display error only + JOptionPane.showMessageDialog(null, report ? messageR : message, + title, JOptionPane.ERROR_MESSAGE); + } + } catch (Exception e1) { + Log.error("Error opening crash popup.", e1); + } + } +} diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index c99d95c1..e7926623 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -26,7 +26,6 @@ import java.util.List; import org.newdawn.slick.Image; import org.newdawn.slick.SlickException; -import org.newdawn.slick.util.Log; /** * Game images. @@ -471,7 +470,7 @@ public enum GameImage { continue; } } - Log.error(String.format("Failed to set default image '%s'.", filename)); + ErrorHandler.error(String.format("Failed to set default image '%s'.", filename), null, false); } /** @@ -512,7 +511,7 @@ public enum GameImage { } skinImage = null; if (errorFile != null) - Log.error(String.format("Failed to set skin image '%s'.", errorFile)); + ErrorHandler.error(String.format("Failed to set skin image '%s'.", errorFile), null, false); return false; } @@ -533,7 +532,7 @@ public enum GameImage { skinImage.destroy(); skinImage = null; } catch (SlickException e) { - Log.error(String.format("Failed to destroy skin image for '%s'.", this.name()), e); + ErrorHandler.error(String.format("Failed to destroy skin image for '%s'.", this.name()), e, true); } } diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index d8469393..77db4a71 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -23,7 +23,6 @@ import java.util.Collections; import org.newdawn.slick.Image; import org.newdawn.slick.SlickException; -import org.newdawn.slick.util.Log; /** * Game mods. @@ -131,7 +130,7 @@ public enum GameMod { this.button = new MenuButton(img, x + (offsetX * id), y); this.button.setHoverScale(1.15f); } catch (SlickException e) { - Log.error(String.format("Failed to initialize game mod '%s'.", this), e); + ErrorHandler.error(String.format("Failed to initialize game mod '%s'.", this), e, false); } } diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index cc755936..2afb6c14 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -28,17 +28,12 @@ import itdelatrisu.opsu.states.Options; import itdelatrisu.opsu.states.SongMenu; import itdelatrisu.opsu.states.Splash; -import java.awt.Desktop; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.net.ServerSocket; -import java.net.URI; - -import javax.swing.JOptionPane; -import javax.swing.UIManager; import org.newdawn.slick.AppGameContainer; import org.newdawn.slick.Color; @@ -108,8 +103,7 @@ public class Opsu extends StateBasedGame { @Override public void uncaughtException(Thread t, Throwable e) { if (!(e instanceof ThreadDeath)) { // TODO: see MusicController - Log.error("** Uncaught Exception! **", e); - openErrorPopup(); + ErrorHandler.error("** Uncaught Exception! **", e, true); } } }); @@ -121,7 +115,7 @@ public class Opsu extends StateBasedGame { try { SERVER_SOCKET = new ServerSocket(Options.getPort()); } catch (IOException e) { - Log.error(String.format("Another program is already running on port %d.", Options.getPort()), e); + ErrorHandler.error(String.format("Another program is already running on port %d.", Options.getPort()), e, false); System.exit(1); } @@ -159,9 +153,9 @@ public class Opsu extends StateBasedGame { // JARs will not run properly inside directories containing '!' // http://bugs.java.com/view_bug.do?bug_id=4523159 if (new File("").getAbsolutePath().indexOf('!') != -1) - Log.error("Cannot run JAR from path containing '!'."); + ErrorHandler.error("Cannot run JAR from path containing '!'.", null, false); else - Log.error("Error while creating game container.", e); + ErrorHandler.error("Error while creating game container.", e, true); } } @@ -191,38 +185,7 @@ public class Opsu extends StateBasedGame { try { SERVER_SOCKET.close(); } catch (IOException e) { - Log.error("Failed to close server socket.", e); - } - } - - /** - * Opens the error popup. - */ - private static void openErrorPopup() { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - if (Desktop.isDesktopSupported()) { - // try to open the log file and/or issues webpage - String[] options = {"Send Report", "View Error Log", "Close"}; - int n = JOptionPane.showOptionDialog(null, - "opsu! has encountered an error.\nPlease report this!", - "Error", JOptionPane.DEFAULT_OPTION, - JOptionPane.ERROR_MESSAGE, null, options, - options[2]); - if (n == 0) { - Desktop.getDesktop().browse( - new URI("https://github.com/itdelatrisu/opsu/issues/new")); - Desktop.getDesktop().open(Options.LOG_FILE); - } else if (n == 1) - Desktop.getDesktop().open(Options.LOG_FILE); - } else { - // display error only - JOptionPane.showMessageDialog(null, - "opsu! has encountered an error.\nPlease report this!", - "Error", JOptionPane.ERROR_MESSAGE); - } - } catch (Exception e) { - Log.error("Error opening crash popup.", e); + ErrorHandler.error("Failed to close server socket.", e, false); } } } diff --git a/src/itdelatrisu/opsu/OsuParser.java b/src/itdelatrisu/opsu/OsuParser.java index 9b1f576d..b5c9e04c 100644 --- a/src/itdelatrisu/opsu/OsuParser.java +++ b/src/itdelatrisu/opsu/OsuParser.java @@ -465,7 +465,7 @@ public class OsuParser { } } } catch (IOException e) { - Log.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e); + ErrorHandler.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e, false); } // if no custom colors, use the default color scheme @@ -546,7 +546,7 @@ public class OsuParser { } } } catch (IOException e) { - Log.error(String.format("Failed to read file '%s'.", osu.getFile().getAbsolutePath()), e); + ErrorHandler.error(String.format("Failed to read file '%s'.", osu.getFile().getAbsolutePath()), e, false); } } diff --git a/src/itdelatrisu/opsu/OszUnpacker.java b/src/itdelatrisu/opsu/OszUnpacker.java index 82c7a8a6..533487ed 100644 --- a/src/itdelatrisu/opsu/OszUnpacker.java +++ b/src/itdelatrisu/opsu/OszUnpacker.java @@ -24,8 +24,6 @@ import java.io.FilenameFilter; import net.lingala.zip4j.core.ZipFile; import net.lingala.zip4j.exception.ZipException; -import org.newdawn.slick.util.Log; - /** * Unpacker for OSZ (ZIP) archives. */ @@ -85,8 +83,8 @@ public class OszUnpacker { ZipFile zipFile = new ZipFile(file); zipFile.extractAll(dest.getAbsolutePath()); } catch (ZipException e) { - Log.error(String.format("Failed to unzip file %s to dest %s.", - file.getAbsolutePath(), dest.getAbsolutePath()), e); + ErrorHandler.error(String.format("Failed to unzip file %s to dest %s.", + file.getAbsolutePath(), dest.getAbsolutePath()), e, false); } } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 97d6fe21..4e148cac 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -149,7 +149,7 @@ public class Utils { Cursor emptyCursor = new Cursor(min, min, min/2, min/2, 1, tmp, null); container.setMouseCursor(emptyCursor, 0, 0); } catch (LWJGLException e) { - Log.error("Failed to set the cursor.", e); + ErrorHandler.error("Failed to set the cursor.", e, true); } loadCursor(); @@ -181,7 +181,7 @@ public class Utils { loadFont(FONT_MEDIUM, 3, colorEffect); loadFont(FONT_SMALL, 1, colorEffect); } catch (Exception e) { - Log.error("Failed to load fonts.", e); + ErrorHandler.error("Failed to load fonts.", e, true); } // initialize game images diff --git a/src/itdelatrisu/opsu/audio/MusicController.java b/src/itdelatrisu/opsu/audio/MusicController.java index e1217041..fd730c1a 100644 --- a/src/itdelatrisu/opsu/audio/MusicController.java +++ b/src/itdelatrisu/opsu/audio/MusicController.java @@ -18,6 +18,7 @@ package itdelatrisu.opsu.audio; +import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuParser; import itdelatrisu.opsu.states.Options; @@ -35,7 +36,6 @@ import org.newdawn.slick.Music; import org.newdawn.slick.SlickException; import org.newdawn.slick.openal.Audio; import org.newdawn.slick.openal.SoundStore; -import org.newdawn.slick.util.Log; /** * Controller for all music. @@ -132,7 +132,7 @@ public class MusicController { player = new Music(file.getPath()); playAt((previewTime > 0) ? previewTime : 0, loop); } catch (Exception e) { - Log.error(String.format("Could not play track '%s'.", file.getName()), e); + ErrorHandler.error(String.format("Could not play track '%s'.", file.getName()), e, false); } } @@ -163,7 +163,7 @@ public class MusicController { converter.convert(file.getPath(), wavFile.getPath()); return wavFile; } catch (Exception e) { - Log.error(String.format("Failed to play file '%s'.", file.getAbsolutePath()), e); + ErrorHandler.error(String.format("Failed to play file '%s'.", file.getAbsolutePath()), e, false); } return wavFile; } @@ -362,7 +362,7 @@ public class MusicController { player = null; } catch (Exception e) { - Log.error("Failed to destroy OpenAL.", e); + ErrorHandler.error("Failed to destroy OpenAL.", e, false); } } } \ No newline at end of file diff --git a/src/itdelatrisu/opsu/audio/SoundController.java b/src/itdelatrisu/opsu/audio/SoundController.java index 95007128..c9e43eb4 100644 --- a/src/itdelatrisu/opsu/audio/SoundController.java +++ b/src/itdelatrisu/opsu/audio/SoundController.java @@ -18,6 +18,7 @@ package itdelatrisu.opsu.audio; +import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.audio.HitSound.SampleSet; import itdelatrisu.opsu.states.Options; @@ -34,7 +35,6 @@ import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; -import org.newdawn.slick.util.Log; import org.newdawn.slick.util.ResourceLoader; /** @@ -90,7 +90,7 @@ public class SoundController { clip.open(audioIn); return clip; } catch (UnsupportedAudioFileException | IOException | LineUnavailableException | RuntimeException e) { - Log.error(String.format("Failed to load file '%s'.", ref), e); + ErrorHandler.error(String.format("Failed to load file '%s'.", ref), e, true); } return null; } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index d839098b..fafaaad8 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -18,6 +18,7 @@ package itdelatrisu.opsu.states; +import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameScore; @@ -52,7 +53,6 @@ import org.newdawn.slick.state.StateBasedGame; import org.newdawn.slick.state.transition.EmptyTransition; import org.newdawn.slick.state.transition.FadeInTransition; import org.newdawn.slick.state.transition.FadeOutTransition; -import org.newdawn.slick.util.Log; /** * "Game" state. @@ -643,7 +643,7 @@ public class Game extends BasicGameState { enter(container, game); skipIntro(); } catch (SlickException e) { - Log.error("Failed to restart game.", e); + ErrorHandler.error("Failed to restart game.", e, false); } } break; @@ -681,7 +681,7 @@ public class Game extends BasicGameState { ; objectIndex--; } catch (SlickException e) { - Log.error("Failed to load checkpoint.", e); + ErrorHandler.error("Failed to load checkpoint.", e, false); } } break; @@ -948,7 +948,7 @@ public class Game extends BasicGameState { score.setDrainRate(HPDrainRate); score.setDifficulty(overallDifficulty); } catch (SlickException e) { - Log.error("Error while setting map modifiers.", e); + ErrorHandler.error("Error while setting map modifiers.", e, true); } } diff --git a/src/itdelatrisu/opsu/states/Options.java b/src/itdelatrisu/opsu/states/Options.java index a0278662..72de49b8 100644 --- a/src/itdelatrisu/opsu/states/Options.java +++ b/src/itdelatrisu/opsu/states/Options.java @@ -18,6 +18,7 @@ package itdelatrisu.opsu.states; +import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.MenuButton; @@ -226,7 +227,7 @@ public class Options extends BasicGameState { try { Utils.loadCursor(); } catch (SlickException e) { - Log.error("Failed to load cursor.", e); + ErrorHandler.error("Failed to load cursor.", e, true); } } }, @@ -1290,7 +1291,7 @@ public class Options extends BasicGameState { public static OsuFile getOsuTheme() { String[] tokens = themeString.split(","); if (tokens.length != 4) { - Log.error("Theme song string is malformed."); + ErrorHandler.error("Theme song string is malformed.", null, false); return null; } @@ -1301,7 +1302,7 @@ public class Options extends BasicGameState { try { osu.endTime = Integer.parseInt(tokens[3]); } catch (NumberFormatException e) { - Log.error("Theme song length is not a valid integer", e); + ErrorHandler.error("Theme song length is not a valid integer", e, false); return null; } @@ -1468,7 +1469,7 @@ public class Options extends BasicGameState { } } } catch (IOException e) { - Log.error(String.format("Failed to read file '%s'.", OPTIONS_FILE.getAbsolutePath()), e); + ErrorHandler.error(String.format("Failed to read file '%s'.", OPTIONS_FILE.getAbsolutePath()), e, false); } catch (NumberFormatException e) { Log.warn("Format error in options file.", e); return; @@ -1562,7 +1563,7 @@ public class Options extends BasicGameState { writer.newLine(); writer.close(); } catch (IOException e) { - Log.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e); + ErrorHandler.error(String.format("Failed to write to file '%s'.", OPTIONS_FILE.getAbsolutePath()), e, false); } } }