From 19bf0a56a0ee703d978da8e5f84999d1f5b20602 Mon Sep 17 00:00:00 2001
From: Joseph Hirschfeld <joe@ibj.io>
Date: Sat, 20 Feb 2016 20:07:46 -0500
Subject: [PATCH] Add exception reporting event


diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java
index 12d9232..f35bc09 100644
--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java
+++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java
@@ -17,6 +17,9 @@ import org.bukkit.Server;
 import org.bukkit.command.defaults.*;
 import org.bukkit.entity.Player;
 import org.bukkit.util.StringUtil;
+import org.github.paperspigot.event.ServerExceptionEvent;
+import org.github.paperspigot.exception.ServerCommandException;
+import org.github.paperspigot.exception.ServerTabCompleteException;
 
 public class SimpleCommandMap implements CommandMap {
     private static final Pattern PATTERN_ON_SPACE = Pattern.compile(" ", Pattern.LITERAL);
@@ -147,7 +150,9 @@ public class SimpleCommandMap implements CommandMap {
             throw ex;
         } catch (Throwable ex) {
             target.timings.stopTiming(); // Spigot
-            throw new CommandException("Unhandled exception executing '" + commandLine + "' in " + target, ex);
+            String msg = "Unhandled exception executing '" + commandLine + "' in " + target;
+            server.getPluginManager().callEvent(new ServerExceptionEvent(new ServerCommandException(ex, target, sender, args))); // Paper
+            throw new CommandException(msg, ex);
         }
 
         // return true as command was handled
@@ -224,7 +229,9 @@ public class SimpleCommandMap implements CommandMap {
         } catch (CommandException ex) {
             throw ex;
         } catch (Throwable ex) {
-            throw new CommandException("Unhandled exception executing tab-completer for '" + cmdLine + "' in " + target, ex);
+            String msg = "Unhandled exception executing tab-completer for '" + cmdLine + "' in " + target;
+            server.getPluginManager().callEvent(new ServerExceptionEvent(new ServerTabCompleteException(msg, ex, target, sender, args))); // Paper
+            throw new CommandException(msg, ex);
         }
     }
     // PaperSpigot end
diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
index 1325b03..ce9839e 100644
--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
@@ -33,6 +33,9 @@ import org.bukkit.permissions.PermissionDefault;
 import org.bukkit.util.FileUtil;
 
 import com.google.common.collect.ImmutableSet;
+import org.github.paperspigot.event.ServerExceptionEvent;
+import org.github.paperspigot.exception.ServerEventException;
+import org.github.paperspigot.exception.ServerPluginEnableDisableException;
 
 /**
  * Handles all plugin management from the Server
@@ -403,7 +406,8 @@ public final class SimplePluginManager implements PluginManager {
             try {
                 plugin.getPluginLoader().enablePlugin(plugin);
             } catch (Throwable ex) {
-                server.getLogger().log(Level.SEVERE, "Error occurred (in the plugin loader) while enabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
+                handlePluginException("Error occurred (in the plugin loader) while enabling "
+                        + plugin.getDescription().getFullName() + " (Is it up to date?)", ex, plugin);
             }
 
             HandlerList.bakeAll();
@@ -422,36 +426,48 @@ public final class SimplePluginManager implements PluginManager {
             try {
                 plugin.getPluginLoader().disablePlugin(plugin);
             } catch (Throwable ex) {
-                server.getLogger().log(Level.SEVERE, "Error occurred (in the plugin loader) while disabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
+                handlePluginException("Error occurred (in the plugin loader) while disabling "
+                        + plugin.getDescription().getFullName() + " (Is it up to date?)", ex, plugin); // Paper
             }
 
             try {
                 server.getScheduler().cancelTasks(plugin);
             } catch (Throwable ex) {
-                server.getLogger().log(Level.SEVERE, "Error occurred (in the plugin loader) while cancelling tasks for " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
+                handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
+                        + plugin.getDescription().getFullName() + " (Is it up to date?)", ex, plugin); // Paper
             }
 
             try {
                 server.getServicesManager().unregisterAll(plugin);
             } catch (Throwable ex) {
-                server.getLogger().log(Level.SEVERE, "Error occurred (in the plugin loader) while unregistering services for " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
+                handlePluginException("Error occurred (in the plugin loader) while unregistering services for "
+                        + plugin.getDescription().getFullName() + " (Is it up to date?)", ex, plugin); // Paper
             }
 
             try {
                 HandlerList.unregisterAll(plugin);
             } catch (Throwable ex) {
-                server.getLogger().log(Level.SEVERE, "Error occurred (in the plugin loader) while unregistering events for " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
+                handlePluginException("Error occurred (in the plugin loader) while unregistering events for "
+                        + plugin.getDescription().getFullName() + " (Is it up to date?)", ex, plugin); // Paper
             }
 
             try {
                 server.getMessenger().unregisterIncomingPluginChannel(plugin);
                 server.getMessenger().unregisterOutgoingPluginChannel(plugin);
             } catch(Throwable ex) {
-                server.getLogger().log(Level.SEVERE, "Error occurred (in the plugin loader) while unregistering plugin channels for " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
+                handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for "
+                        + plugin.getDescription().getFullName() + " (Is it up to date?)", ex, plugin); // Paper
             }
         }
     }
 
+    // Paper start
+    private void handlePluginException(String msg, Throwable ex, Plugin plugin) {
+        server.getLogger().log(Level.SEVERE, msg, ex);
+        callEvent(new ServerExceptionEvent(new ServerPluginEnableDisableException(msg, ex, plugin)));
+    }
+    // Paper end
+
     public void clearPlugins() {
         synchronized (this) {
             disablePlugins();
@@ -513,7 +529,13 @@ public final class SimplePluginManager implements PluginManager {
                             ));
                 }
             } catch (Throwable ex) {
-                server.getLogger().log(Level.SEVERE, "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getDescription().getFullName(), ex);
+                // Paper start - error reporting
+                String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getDescription().getFullName();
+                server.getLogger().log(Level.SEVERE, msg, ex);
+                if (!(event instanceof ServerExceptionEvent)) { // We don't want to cause an endless event loop
+                    callEvent(new ServerExceptionEvent(new ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event)));
+                }
+                // Paper end
             }
         }
     }
diff --git a/src/main/java/org/bukkit/plugin/messaging/StandardMessenger.java b/src/main/java/org/bukkit/plugin/messaging/StandardMessenger.java
index 4c171e8..9ef6525 100644
--- a/src/main/java/org/bukkit/plugin/messaging/StandardMessenger.java
+++ b/src/main/java/org/bukkit/plugin/messaging/StandardMessenger.java
@@ -8,6 +8,8 @@ import java.util.Map;
 import java.util.Set;
 import org.bukkit.entity.Player;
 import org.bukkit.plugin.Plugin;
+import org.github.paperspigot.event.ServerExceptionEvent;
+import org.github.paperspigot.exception.ServerPluginMessageException;
 
 /**
  * Standard implementation to {@link Messenger}
@@ -427,7 +429,13 @@ public class StandardMessenger implements Messenger {
                 registration.getListener().onPluginMessageReceived( channel, source, message );
             } catch ( Throwable t )
             {
-                org.bukkit.Bukkit.getLogger().log( java.util.logging.Level.WARNING, "Could not pass incoming plugin message to " + registration.getPlugin(), t );
+                // Paper start
+                String msg = "Could not pass incoming plugin message to " + registration.getPlugin();
+                org.bukkit.Bukkit.getLogger().log( java.util.logging.Level.WARNING, msg, t );
+                source.getServer().getPluginManager().callEvent(new ServerExceptionEvent(
+                        new ServerPluginMessageException(msg, t, registration.getPlugin(), source, channel, message)
+                ));
+                // Paper end
             }
             // Spigot End
         }
diff --git a/src/main/java/org/github/paperspigot/event/ServerExceptionEvent.java b/src/main/java/org/github/paperspigot/event/ServerExceptionEvent.java
new file mode 100644
index 0000000..317e9d2
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/event/ServerExceptionEvent.java
@@ -0,0 +1,36 @@
+package org.github.paperspigot.event;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.lang.Validate;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.github.paperspigot.exception.ServerException;
+
+/**
+ * Called whenever an exception is thrown in a recoverable section of the server.
+ */
+public class ServerExceptionEvent extends Event {
+    private static final HandlerList handlers = new HandlerList();
+    private ServerException exception;
+
+    public ServerExceptionEvent (ServerException exception) {
+        this.exception = Preconditions.checkNotNull(exception, "exception");
+    }
+
+    /**
+     * Gets the wrapped exception that was thrown.
+     * @return Exception thrown
+     */
+    public ServerException getException() {
+        return exception;
+    }
+
+    @Override
+    public HandlerList getHandlers() {
+        return handlers;
+    }
+
+    public static HandlerList getHandlerList() {
+        return handlers;
+    }
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerCommandException.java b/src/main/java/org/github/paperspigot/exception/ServerCommandException.java
new file mode 100644
index 0000000..aae647a
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerCommandException.java
@@ -0,0 +1,64 @@
+package org.github.paperspigot.exception;
+
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Thrown when a command throws an exception
+ */
+public class ServerCommandException extends ServerException {
+
+    private final Command command;
+    private final CommandSender commandSender;
+    private final String[] arguments;
+
+    public ServerCommandException(String message, Throwable cause, Command command, CommandSender commandSender, String[] arguments) {
+        super(message, cause);
+        this.commandSender = checkNotNull(commandSender, "commandSender");
+        this.arguments = checkNotNull(arguments, "arguments");
+        this.command = checkNotNull(command, "command");
+    }
+
+    public ServerCommandException(Throwable cause, Command command, CommandSender commandSender, String[] arguments) {
+        super(cause);
+        this.commandSender = checkNotNull(commandSender, "commandSender");
+        this.arguments = checkNotNull(arguments, "arguments");
+        this.command = checkNotNull(command, "command");
+    }
+
+    protected ServerCommandException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Command command, CommandSender commandSender, String[] arguments) {
+        super(message, cause, enableSuppression, writableStackTrace);
+        this.commandSender = checkNotNull(commandSender, "commandSender");
+        this.arguments = checkNotNull(arguments, "arguments");
+        this.command = checkNotNull(command, "command");
+    }
+
+    /**
+     * Gets the command which threw the exception
+     *
+     * @return exception throwing command
+     */
+    public Command getCommand() {
+        return command;
+    }
+
+    /**
+     * Gets the command sender which executed the command request
+     *
+     * @return command sender of exception thrown command request
+     */
+    public CommandSender getCommandSender() {
+        return commandSender;
+    }
+
+    /**
+     * Gets the arguments which threw the exception for the command
+     *
+     * @return arguments of exception thrown command request
+     */
+    public String[] getArguments() {
+        return arguments;
+    }
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerEventException.java b/src/main/java/org/github/paperspigot/exception/ServerEventException.java
new file mode 100644
index 0000000..d99cab8
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerEventException.java
@@ -0,0 +1,52 @@
+package org.github.paperspigot.exception;
+
+import org.bukkit.event.Event;
+import org.bukkit.event.Listener;
+import org.bukkit.plugin.Plugin;
+
+import static com.google.common.base.Preconditions.*;
+
+/**
+ * Exception thrown when a server event listener throws an exception
+ */
+public class ServerEventException extends ServerPluginException {
+
+    private final Listener listener;
+    private final Event event;
+
+    public ServerEventException(String message, Throwable cause, Plugin responsiblePlugin, Listener listener, Event event) {
+        super(message, cause, responsiblePlugin);
+        this.listener = checkNotNull(listener, "listener");
+        this.event = checkNotNull(event, "event");
+    }
+
+    public ServerEventException(Throwable cause, Plugin responsiblePlugin, Listener listener, Event event) {
+        super(cause, responsiblePlugin);
+        this.listener = checkNotNull(listener, "listener");
+        this.event = checkNotNull(event, "event");
+    }
+
+    protected ServerEventException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Plugin responsiblePlugin, Listener listener, Event event) {
+        super(message, cause, enableSuppression, writableStackTrace, responsiblePlugin);
+        this.listener = checkNotNull(listener, "listener");
+        this.event = checkNotNull(event, "event");
+    }
+
+    /**
+     * Gets the listener which threw the exception
+     *
+     * @return event listener
+     */
+    public Listener getListener() {
+        return listener;
+    }
+
+    /**
+     * Gets the event which caused the exception
+     *
+     * @return event
+     */
+    public Event getEvent() {
+        return event;
+    }
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerException.java b/src/main/java/org/github/paperspigot/exception/ServerException.java
new file mode 100644
index 0000000..f7aad05
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerException.java
@@ -0,0 +1,23 @@
+package org.github.paperspigot.exception;
+
+/**
+ * Wrapper exception for all exceptions that are thrown by the server.
+ */
+public class ServerException extends Exception {
+
+    public ServerException(String message) {
+        super(message);
+    }
+
+    public ServerException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ServerException(Throwable cause) {
+        super(cause);
+    }
+
+    protected ServerException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerInternalException.java b/src/main/java/org/github/paperspigot/exception/ServerInternalException.java
new file mode 100644
index 0000000..17ad4cb
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerInternalException.java
@@ -0,0 +1,35 @@
+package org.github.paperspigot.exception;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.ThrownExpBottle;
+import org.github.paperspigot.event.ServerExceptionEvent;
+
+/**
+ * Thrown when the internal server throws a recoverable exception.
+ */
+public class ServerInternalException extends ServerException {
+
+    public ServerInternalException(String message) {
+        super(message);
+    }
+
+    public ServerInternalException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ServerInternalException(Throwable cause) {
+        super(cause);
+    }
+
+    protected ServerInternalException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+    public static void reportInternalException(Throwable cause) {
+        try {
+            Bukkit.getPluginManager().callEvent(new ServerExceptionEvent(new ServerInternalException(cause)));;
+        } catch (Throwable t) {
+            t.printStackTrace(); // Don't want to rethrow!
+        }
+    }
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerPluginEnableDisableException.java b/src/main/java/org/github/paperspigot/exception/ServerPluginEnableDisableException.java
new file mode 100644
index 0000000..8c938d0
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerPluginEnableDisableException.java
@@ -0,0 +1,20 @@
+package org.github.paperspigot.exception;
+
+import org.bukkit.plugin.Plugin;
+
+/**
+ * Thrown whenever there is an exception with any enabling or disabling of plugins.
+ */
+public class ServerPluginEnableDisableException extends ServerPluginException {
+    public ServerPluginEnableDisableException(String message, Throwable cause, Plugin responsiblePlugin) {
+        super(message, cause, responsiblePlugin);
+    }
+
+    public ServerPluginEnableDisableException(Throwable cause, Plugin responsiblePlugin) {
+        super(cause, responsiblePlugin);
+    }
+
+    protected ServerPluginEnableDisableException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Plugin responsiblePlugin) {
+        super(message, cause, enableSuppression, writableStackTrace, responsiblePlugin);
+    }
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerPluginException.java b/src/main/java/org/github/paperspigot/exception/ServerPluginException.java
new file mode 100644
index 0000000..f9241ad
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerPluginException.java
@@ -0,0 +1,39 @@
+package org.github.paperspigot.exception;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.lang.Validate;
+import org.bukkit.plugin.Plugin;
+
+import static com.google.common.base.Preconditions.*;
+
+/**
+ * Wrapper exception for all cases to which a plugin can be immediately blamed for
+ */
+public class ServerPluginException extends ServerException {
+    public ServerPluginException(String message, Throwable cause, Plugin responsiblePlugin) {
+        super(message, cause);
+        this.responsiblePlugin = checkNotNull(responsiblePlugin, "responsiblePlugin");
+    }
+
+    public ServerPluginException(Throwable cause, Plugin responsiblePlugin) {
+        super(cause);
+        this.responsiblePlugin = checkNotNull(responsiblePlugin, "responsiblePlugin");
+    }
+
+    protected ServerPluginException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Plugin responsiblePlugin) {
+        super(message, cause, enableSuppression, writableStackTrace);
+        this.responsiblePlugin = checkNotNull(responsiblePlugin, "responsiblePlugin");
+    }
+
+    private final Plugin responsiblePlugin;
+
+    /**
+     * Gets the plugin which is directly responsible for the exception being thrown
+     *
+     * @return plugin which is responsible for the exception throw
+     */
+    public Plugin getResponsiblePlugin() {
+        return responsiblePlugin;
+    }
+
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerPluginMessageException.java b/src/main/java/org/github/paperspigot/exception/ServerPluginMessageException.java
new file mode 100644
index 0000000..b71303b
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerPluginMessageException.java
@@ -0,0 +1,61 @@
+package org.github.paperspigot.exception;
+
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.Plugin;
+
+import static com.google.common.base.Preconditions.*;
+
+/**
+ * Thrown when an incoming plugin message channel throws an exception
+ */
+public class ServerPluginMessageException extends ServerPluginException {
+
+    private final Player player;
+    private final String channel;
+    private final byte[] data;
+
+    public ServerPluginMessageException(String message, Throwable cause, Plugin responsiblePlugin, Player player, String channel, byte[] data) {
+        super(message, cause, responsiblePlugin);
+        this.player = checkNotNull(player, "player");
+        this.channel = checkNotNull(channel, "channel");
+        this.data = checkNotNull(data, "data");
+    }
+
+    public ServerPluginMessageException(Throwable cause, Plugin responsiblePlugin, Player player, String channel, byte[] data) {
+        super(cause, responsiblePlugin);
+        this.player = checkNotNull(player, "player");
+        this.channel = checkNotNull(channel, "channel");
+        this.data = checkNotNull(data, "data");
+    }
+
+    protected ServerPluginMessageException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Plugin responsiblePlugin, Player player, String channel, byte[] data) {
+        super(message, cause, enableSuppression, writableStackTrace, responsiblePlugin);
+        this.player = checkNotNull(player, "player");
+        this.channel = checkNotNull(channel, "channel");
+        this.data = checkNotNull(data, "data");
+    }
+
+    /**
+     * Gets the channel to which the error occurred from recieving data from
+     * @return exception channel
+     */
+    public String getChannel() {
+        return channel;
+    }
+
+    /**
+     * Gets the data to which the error occurred from
+     * @return exception data
+     */
+    public byte[] getData() {
+        return data;
+    }
+
+    /**
+     * Gets the player which the plugin message causing the exception originated from
+     * @return  exception player
+     */
+    public Player getPlayer() {
+        return player;
+    }
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerSchedulerException.java b/src/main/java/org/github/paperspigot/exception/ServerSchedulerException.java
new file mode 100644
index 0000000..99757ee
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerSchedulerException.java
@@ -0,0 +1,37 @@
+package org.github.paperspigot.exception;
+
+import org.bukkit.scheduler.BukkitTask;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Thrown when a plugin's scheduler fails with an exception
+ */
+public class ServerSchedulerException extends ServerPluginException {
+
+    private final BukkitTask task;
+
+    public ServerSchedulerException(String message, Throwable cause, BukkitTask task) {
+        super(message, cause, task.getOwner());
+        this.task = checkNotNull(task, "task");
+    }
+
+    public ServerSchedulerException(Throwable cause, BukkitTask task) {
+        super(cause, task.getOwner());
+        this.task = checkNotNull(task, "task");
+    }
+
+    protected ServerSchedulerException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, BukkitTask task) {
+        super(message, cause, enableSuppression, writableStackTrace, task.getOwner());
+        this.task = checkNotNull(task, "task");
+    }
+
+    /**
+     * Gets the task which threw the exception
+     * @return exception throwing task
+     */
+    public BukkitTask getTask() {
+        return task;
+    }
+
+}
diff --git a/src/main/java/org/github/paperspigot/exception/ServerTabCompleteException.java b/src/main/java/org/github/paperspigot/exception/ServerTabCompleteException.java
new file mode 100644
index 0000000..6e1e2ab
--- /dev/null
+++ b/src/main/java/org/github/paperspigot/exception/ServerTabCompleteException.java
@@ -0,0 +1,22 @@
+package org.github.paperspigot.exception;
+
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+
+/**
+ * Called when a tab-complete request throws an exception
+ */
+public class ServerTabCompleteException extends ServerCommandException {
+
+    public ServerTabCompleteException(String message, Throwable cause, Command command, CommandSender commandSender, String[] arguments) {
+        super(message, cause, command, commandSender, arguments);
+    }
+
+    public ServerTabCompleteException(Throwable cause, Command command, CommandSender commandSender, String[] arguments) {
+        super(cause, command, commandSender, arguments);
+    }
+
+    protected ServerTabCompleteException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Command command, CommandSender commandSender, String[] arguments) {
+        super(message, cause, enableSuppression, writableStackTrace, command, commandSender, arguments);
+    }
+}
-- 
2.5.0