diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/server/ServerExceptionEvent.java b/paper-api/src/main/java/com/destroystokyo/paper/event/server/ServerExceptionEvent.java new file mode 100644 index 0000000000..95a5a59e6b --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/event/server/ServerExceptionEvent.java @@ -0,0 +1,43 @@ +package com.destroystokyo.paper.event.server; + +import com.destroystokyo.paper.exception.ServerException; +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +/** + * Called whenever an exception is thrown in a recoverable section of the server. + */ +@NullMarked +public class ServerExceptionEvent extends Event { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final ServerException exception; + + @ApiStatus.Internal + public ServerExceptionEvent(final ServerException exception) { + super(!Bukkit.isPrimaryThread()); + this.exception = exception; + } + + /** + * Gets the wrapped exception that was thrown. + * + * @return Exception thrown + */ + public ServerException getException() { + return this.exception; + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } +} diff --git a/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerCommandException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerCommandException.java new file mode 100644 index 0000000000..6fb39af047 --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerCommandException.java @@ -0,0 +1,64 @@ +package com.destroystokyo.paper.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/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerEventException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerEventException.java new file mode 100644 index 0000000000..410b241395 --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerEventException.java @@ -0,0 +1,52 @@ +package com.destroystokyo.paper.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/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerException.java new file mode 100644 index 0000000000..c06ea39424 --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerException.java @@ -0,0 +1,23 @@ +package com.destroystokyo.paper.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/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerInternalException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerInternalException.java new file mode 100644 index 0000000000..2c3effca7c --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerInternalException.java @@ -0,0 +1,36 @@ +package com.destroystokyo.paper.exception; + +import java.util.logging.Level; +import org.bukkit.Bukkit; +import com.destroystokyo.paper.event.server.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) { + Bukkit.getLogger().log(Level.WARNING, "Exception posting ServerExceptionEvent", t); // Don't want to rethrow! + } + } +} diff --git a/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginEnableDisableException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginEnableDisableException.java new file mode 100644 index 0000000000..f016ba3b1b --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginEnableDisableException.java @@ -0,0 +1,20 @@ +package com.destroystokyo.paper.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); + } +} \ No newline at end of file diff --git a/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginException.java new file mode 100644 index 0000000000..be3f92e3c6 --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginException.java @@ -0,0 +1,36 @@ +package com.destroystokyo.paper.exception; + +import org.bukkit.plugin.Plugin; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * 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/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginMessageException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginMessageException.java new file mode 100644 index 0000000000..2faef4cb35 --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerPluginMessageException.java @@ -0,0 +1,64 @@ +package com.destroystokyo.paper.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 receiving 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/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerSchedulerException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerSchedulerException.java new file mode 100644 index 0000000000..2d0b2d4a9b --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerSchedulerException.java @@ -0,0 +1,37 @@ +package com.destroystokyo.paper.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/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerTabCompleteException.java b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerTabCompleteException.java new file mode 100644 index 0000000000..5582999fe9 --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/exception/ServerTabCompleteException.java @@ -0,0 +1,22 @@ +package com.destroystokyo.paper.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); + } +} diff --git a/paper-api/src/main/java/org/bukkit/command/SimpleCommandMap.java b/paper-api/src/main/java/org/bukkit/command/SimpleCommandMap.java index 36fc2c3539..c7fa1d235c 100644 --- a/paper-api/src/main/java/org/bukkit/command/SimpleCommandMap.java +++ b/paper-api/src/main/java/org/bukkit/command/SimpleCommandMap.java @@ -156,11 +156,14 @@ public class SimpleCommandMap implements CommandMap { target.execute(sender, sentCommandLabel, Arrays.copyOfRange(args, 1, args.length)); } // target.timings.stopTiming(); // Spigot // Paper } catch (CommandException ex) { + server.getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerCommandException(ex, target, sender, args))); // Paper //target.timings.stopTiming(); // Spigot // Paper throw ex; } catch (Throwable ex) { //target.timings.stopTiming(); // Spigot // Paper - throw new CommandException("Unhandled exception executing '" + commandLine + "' in " + target, ex); + String msg = "Unhandled exception executing '" + commandLine + "' in " + target; + server.getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerCommandException(ex, target, sender, args))); // Paper + throw new CommandException(msg, ex); } // return true as command was handled @@ -239,7 +242,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 com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerTabCompleteException(msg, ex, target, sender, args))); // Paper + throw new CommandException(msg, ex); } } diff --git a/paper-api/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/paper-api/src/main/java/org/bukkit/plugin/SimplePluginManager.java index 48b6605491..001465eeda 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/SimplePluginManager.java +++ b/paper-api/src/main/java/org/bukkit/plugin/SimplePluginManager.java @@ -528,7 +528,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(); @@ -551,32 +552,37 @@ 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 } try { @@ -589,6 +595,13 @@ public final class SimplePluginManager implements PluginManager { } } + // Paper start + private void handlePluginException(String msg, Throwable ex, Plugin plugin) { + server.getLogger().log(Level.SEVERE, msg, ex); + callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin))); + } + // Paper end + @Override public void clearPlugins() { if (true) {this.paperPluginManager.clearPlugins(); return;} // Paper @@ -654,7 +667,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 com.destroystokyo.paper.event.server.ServerExceptionEvent)) { // We don't want to cause an endless event loop + callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event))); + } + // Paper end } } }