Solved Hooking plugin loggers - events vs. manual hooking

Discussion in 'Plugin Development' started by 1Rogue, Jan 24, 2015.

Thread Status:
Not open for further replies.
  1. Offline

    1Rogue

    Overall, I'm looking for a solution to this that will hook all loggers once with absolute certainty.

    My initial approach was to have a single "hookBukkit" method that would be called after all plugins were loaded:

    Code:java
    1. /**
    2.   * Hooks into bukkit's plugin system and adds a handler to all plugin
    3.   * loggers to allow catching unreported exceptions. If already hooked,
    4.   * will do nothing.
    5.   *
    6.   * @since 0.1.0
    7.   * @version 0.1.0
    8.   *
    9.   * @throws IllegalAccessException If something other than
    10.   * {@link CodelanxLib} calls this method
    11.   */
    12. public static void hookBukkit() throws IllegalAccessException {
    13. if (Debugger.hooked) {
    14. return;
    15. }
    16. //Check to make sure CodelanxLib is calling it
    17. StackTraceElement[] elems = Thread.currentThread().getStackTrace();
    18. if (elems.length < 3) {
    19. return;
    20. }
    21. if (!elems[2].getClass().getName().equals(CodelanxLib.class.getName())) {
    22. throw new IllegalAccessException("Debugger#hookBukkit may only be called by CodelanxLib!");
    23. }
    24. //end check - add hook
    25. Debugger.hooked = true;
    26. for (Plugin p : Bukkit.getServer().getPluginManager().getPlugins()) {
    27. p.getLogger().addHandler(new ExceptionHandler(p));
    28. }
    29. }


    However, this wouldn't catch plugins that are dynamically loaded after a server start. So my followup idea was to hook on plugin enabling:

    Code:java
    1. static {
    2. Bukkit.getServer().getPluginManager().registerEvents(new Listener() {
    3.  
    4. @EventHandler
    5. public void onEnable(PluginEnableEvent event) {
    6. for (Handler h : event.getPlugin().getLogger().getHandlers()) {
    7. if (h instanceof ExceptionHandler) {
    8. return;
    9. }
    10. }
    11. event.getPlugin().getLogger().addHandler(new ExceptionHandler(event.getPlugin()));
    12. }
    13.  
    14. }, CodelanxLib.get()); //potentially would fail if reference is resolved too early
    15. }


    But since the internals for CraftBukkit are now no longer public, I have no way of knowing when the PluginManager is initialized, which raises two issues: I don't know when it is too early to call upon the PluginManager, and I can't register it too late or else miss the event being fired.

    A combination (with delayed listener registration and verification in the hook method) seems sloppy, but that's what I'm currently operating with. So I'm putting this out here for kudos if anyone can come up with a better solution.

    For anyone curious about the current solution:
    Show Spoiler
    Code:java
    1. /**
    2.   * Hooks into bukkit's plugin system and adds a handler to all plugin
    3.   * loggers to allow catching unreported exceptions. If already hooked,
    4.   * will do nothing.
    5.   *
    6.   * @since 0.1.0
    7.   * @version 0.1.0
    8.   *
    9.   * @throws IllegalAccessException If something other than
    10.   * {@link CodelanxLib} calls this method
    11.   */
    12. public static void hookBukkit() throws IllegalAccessException {
    13. //Check to make sure CodelanxLib is calling it
    14. StackTraceElement[] elems = Thread.currentThread().getStackTrace();
    15. if (elems.length < 3) {
    16. return;
    17. }
    18. if (!elems[2].getClass().getName().equals(CodelanxLib.class.getName())) {
    19. throw new IllegalAccessException("Debugger#hookBukkit may only be called by CodelanxLib!");
    20. }
    21. //end check - add hook
    22. if (!Debugger.listenerHooked) {
    23. Bukkit.getServer().getPluginManager().registerEvents(new Listener() {
    24.  
    25. @EventHandler
    26. public void onEnable(PluginEnableEvent event) {
    27. for (Handler h : event.getPlugin().getLogger().getHandlers()) {
    28. if (h instanceof ExceptionHandler) {
    29. return;
    30. }
    31. }
    32. event.getPlugin().getLogger().addHandler(new ExceptionHandler(event.getPlugin()));
    33. }
    34.  
    35. }, CodelanxLib.get());
    36. }
    37. Debugger.listenerHooked = true;
    38. pluginLoop:
    39. for (Plugin p : Bukkit.getServer().getPluginManager().getPlugins()) {
    40. for (Handler h : p.getLogger().getHandlers()) {
    41. if (h instanceof ExceptionHandler) {
    42. continue pluginLoop;
    43. }
    44. }
    45. p.getLogger().addHandler(new ExceptionHandler(p));
    46. }
    47. }
     
    Last edited: Jan 24, 2015
  2. Offline

    Rocoty

    To be completely frank with you I don't really see a combination as sloppy. It's really the best way I can think of.

    If I may comment on your style, though, I would create a method for adding the handler to a plugin's logger. This shortens your code, applies the DRY principle, creates an abstraction layer AND gets rid of that annoying label.

    Off-Topic: Love your reference to The Hitchhiker's Guide to the Galaxy in your signature :p
     
  3. Offline

    1Rogue

    Following up on this, this is my end solution:

    Code:java
    1. /**
    2. * Hooks into Bukkit's plugin system and adds a handler to all plugin
    3. * loggers to allow catching unreported exceptions. If already hooked, will
    4. * do nothing. This method will continue to hook new plugins via a listener
    5. *
    6. * @since 0.1.0
    7. * @version 0.1.0
    8. *
    9. * @throws IllegalPluginAccessException If something other than
    10. * {@link CodelanxLib} calls this
    11. * method
    12. */
    13. public static void hookBukkit() {
    14. //Check to make sure CodelanxLib is calling it
    15. Exceptions.illegalPluginAccess(Reflections.accessedFrom(CodelanxLib.class),
    16. "Debugger#hookBukkit may only be called by CodelanxLib!");
    17. Listener l = new BukkitPluginListener();
    18. if (!ListenerManager.isRegisteredToBukkit(CodelanxLib.get(), l)) {
    19. Bukkit.getServer().getPluginManager().registerEvents(l, CodelanxLib.get());
    20. }
    21. //Hook any current plugins without a handler
    22. for (Plugin p : Bukkit.getServer().getPluginManager().getPlugins()) { //boo arrays
    23. ExceptionHandler.apply(p);
    24. }
    25. }


    In the end it was a combination, additional changes:
    • Instead of using a static boolean, check bukkit's registered listeners
    • Applied DRY principle, because code shouldn't be wet
    • Lots of cleanup since other aspects of the API have developed as well (e.g. Reflections)
    The main thing is getting rid of the static boolean, as my primary concern was that the behavior would be undefined through a bukkit reload, and thus could potentially make the plugin not re-register a listener. Dynamically checking myself via:

    Code:java
    1. /**
    2. * Returns {@code true} if the passed {@link Listener} has another Listener
    3. * of the same class type already registered for bukkit. This should not be
    4. * used with any listeners that are from an anonymous class, as this will
    5. * return {@code true} for any other anonymous classes as well
    6. *
    7. * @since 0.1.0
    8. * @version 0.1.0
    9. *
    10. * @param p The {@link Plugin} that registers this {@link Listener}
    11. * @param l The {@link Listener} to check
    12. * @return {@code true} if registered to bukkit
    13. */
    14. public static boolean isRegisteredToBukkit(Plugin p, Listener l) {
    15. if (l.getClass().isAnonymousClass()) {
    16. StackTraceElement t = Reflections.getCaller();
    17. Logging.simple().here().print(Level.WARNING, "Passed an anonymous class from %s:%d", t.getClass().getName(), t.getLineNumber());
    18. }
    19. return HandlerList.getRegisteredListeners(p).stream().anyMatch(r -> r.getListener().getClass() == l.getClass());
    20. }


    Seems to have been the better idea.
     
    Last edited: Feb 10, 2015
Thread Status:
Not open for further replies.

Share This Page