PluginFaultHandler 5.46.0
Isolates failures of plugin code so that a buggy plugin cannot crash the whole server.
Every place where the core calls into plugin code (event handlers, scheduled tasks, commands, async task completions, form responses, promise callbacks, chunk listeners, plugin-defined blocks/entities, plugin load/enable/disable...) catches \Throwable and passes it to PluginFaultHandler::isolate(), which:
- Attributes the failure to a plugin: first the registered owner of the entry point (the plugin which registered the listener / owns the task scheduler / owns the command), then the file in which the called object's class or closure was declared, then the files of the stack trace (innermost first), like the crash dump does.
- If no plugin can be blamed, the throwable is rethrown: core bugs must never be hidden, so they still crash the server as before.
- Otherwise a
PluginErrorEventis called, the failure is logged at ERROR level as "[Plugin vX] Unhandled exception in <entry point>" with the full stack trace, and the server carries on. Whatever the plugin did before failing stays done (e.g. an event keeps the state the failing handler left it in). - If the plugin fails
PluginFaultHandler::getDisableAfterErrors()times withinPluginFaultHandler::getErrorWindowSeconds()seconds (by default 10 failures in 60 seconds), it is scheduled to be disabled, with a CRITICAL log.
Disabling is deferred: the plugin is only marked as pending, and actually disabled by PluginFaultHandler::processPendingDisables() at the end of the current server tick. Disabling a plugin from inside its own failing callback (while the core is iterating a handler list, a scheduler heap, a task queue...) is therefore never needed. Until then, the core skips the plugin's remaining event handlers and scheduled tasks (PluginFaultHandler::isPendingDisable()).
Exceptions used for control flow by the core (task cancellation, command usage errors, bad packets) are always rethrown. A DisablePluginException thrown from any entry point disables the plugin that threw it.
Uncatchable fatal errors (memory exhaustion, compile errors, segfaults...) cannot be isolated and still crash the server.
The old behaviour (any plugin exception crashes the server) can be restored by setting plugins.crash-on-unhandled-exception: true in pocketmine.yml.
Constants
DEFAULT_DISABLE_AFTER_ERRORS
public const DEFAULT_DISABLE_AFTER_ERRORS = 10DEFAULT_ERROR_WINDOW_SECONDS
public const DEFAULT_ERROR_WINDOW_SECONDS = 60Methods
__construct()
public function __construct(\Logger $logger, \Closure $pluginsProvider, \Closure $pluginDisabler, bool $crashOnUnhandledException = false, int $disableAfterErrors = self::DEFAULT_DISABLE_AFTER_ERRORS, float $errorWindowSeconds = self::DEFAULT_ERROR_WINDOW_SECONDS, ?\Closure $clock = null)attribute()
public function attribute(\Throwable $throwable, ?\pocketmine\plugin\Plugin $owner = null, ?object $origin = null) : ?\pocketmine\plugin\PluginDetermines which plugin is responsible for the given throwable, or null if it doesn't look like a plugin's fault.
findPluginByFile()
public function findPluginByFile(string $file) : ?\pocketmine\plugin\PluginReturns the plugin whose files contain the given source file, if any. Only works for plugins extending PluginBase (like the crash dump).
findPluginByObject()
public function findPluginByObject(object $object) : ?\pocketmine\plugin\PluginReturns the plugin which declared the given closure or the class of the given object, if any.
getDisableAfterErrors()
public function getDisableAfterErrors() : intNumber of failures within the error window after which a plugin is disabled. 0 or less means never.
getErrorWindowSeconds()
public function getErrorWindowSeconds() : floatgetInstance()
public static function getInstance() : ?selfReturns the fault handler used by the server, or null if none is installed (in which case plugin failures propagate as they used to).
getRecentErrorCount()
public function getRecentErrorCount(\pocketmine\plugin\Plugin $plugin) : intReturns the number of failures counted for the given plugin within the error window.
handle()
public function handle(\Throwable $throwable, ?\pocketmine\plugin\Plugin $owner, string $context, ?object $origin = null) : voidisControlFlowException()
public static function isControlFlowException(\Throwable $throwable) : boolReturns whether the given throwable is used by the core for control flow and must therefore never be swallowed.
isCrashOnUnhandledException()
public function isCrashOnUnhandledException() : boolWhether plugin failures crash the server like they used to (pmmp behaviour).
isPendingDisable()
public function isPendingDisable(\pocketmine\plugin\Plugin $plugin) : boolisPluginPendingDisable()
public static function isPluginPendingDisable(\pocketmine\plugin\Plugin $plugin) : boolReturns whether the plugin has been scheduled to be disabled because of its failures and must not be called any more until then (the core skips its remaining event handlers and scheduled tasks).
processPendingDisables()
public function processPendingDisables() : voidDisables the plugins scheduled to be disabled. Called by the server at the end of every tick, when no plugin code is on the stack.
