package com.github.games647.lagmonitor.threading; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.util.Map; import java.util.Set; import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.event.Listener; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; public class BlockingActionManager implements Listener { //feel free to improve the wording of this: private static final String THREAD_SAFETY_NOTICE = "As threads **can** run concurrently or in parallel " + "shared data access has to be synchronized (thread-safety) in order to prevent " + "unexpected behavior or crashes. "; private static final String SAFETY_METHODS = "You can guarantee thread-safety by " + "running the data access always on the same thread, using atomic operations, " + "locks (ex: a synchronized block), immutable objects, thread local data " + "or something similar. "; private static final String COMMON_SAFE = "Common things that are thread-safe: Logging, Bukkit Scheduler, " + "Concurrent collections (ex: ConcurrentHashMap or Collections.synchronized*), ... "; private static final String BLOCKING_ACTION_MESSAGE = "Plugin {0} is performing a blocking I/O operation ({1}) " + "on the main thread. " + "This could affect the server performance, because the thread pauses until it gets the response. " + "Such operations should be performed asynchronous from the main thread. " + "Besides gameplay performance it could also improve startup time. " + "Keep in mind to keep the code thread-safe. "; private final Plugin plugin; private final Set<PluginViolation> violations = Sets.newConcurrentHashSet(); private final Set<String> violatedPlugins = Sets.newConcurrentHashSet(); public BlockingActionManager(Plugin plugin) { this.plugin = plugin; } public void checkBlockingAction(String event) { if (!Bukkit.isPrimaryThread()) { return; } logCurrentStack(BLOCKING_ACTION_MESSAGE, event); } public void checkThreadSafety(String eventName) { if (Bukkit.isPrimaryThread()) { return; } logCurrentStack("Plugin {0} triggered an synchronous event {1} from an asynchronous Thread. ", eventName); plugin.getLogger().info(THREAD_SAFETY_NOTICE); plugin.getLogger().info("Use runTask* (no Async*), scheduleSync* or callSyncMethod to run on the main thread."); } public void logCurrentStack(String format, String eventName) { IllegalAccessException stackTraceCreator = new IllegalAccessException(); StackTraceElement[] stackTrace = stackTraceCreator.getStackTrace(); Map.Entry<String, StackTraceElement> foundPlugin = findPlugin(stackTrace); PluginViolation violation = new PluginViolation(eventName); if (foundPlugin != null) { String pluginName = foundPlugin.getKey(); violation = new PluginViolation(pluginName, foundPlugin.getValue(), eventName); if (!violatedPlugins.add(violation.getPluginName()) && plugin.getConfig().getBoolean("oncePerPlugin")) { return; } } if (!violations.add(violation)) { return; } plugin.getLogger().log(Level.WARNING, format + "Report it to the plugin author" , new Object[]{violation.getPluginName(), eventName}); if (plugin.getConfig().getBoolean("hideStacktrace")) { plugin.getLogger().log(Level.WARNING, "Source: {0}, method {1}, line {2}" , new Object[]{violation.getSourceFile(), violation.getMethodName(), violation.getLineNumber()}); } else { plugin.getLogger().log(Level.WARNING, "The following exception is not an error. " + "It's a hint for the plugin developer to find the source. " + plugin.getName() + " doesn't prevent this action. It just warns you about it. ", stackTraceCreator); } } public Map.Entry<String, StackTraceElement> findPlugin(StackTraceElement[] stacktrace) { boolean skipping = true; for (StackTraceElement elem : stacktrace) { try { Class<?> clazz = Class.forName(elem.getClassName()); if (clazz.getName().endsWith("VanillaCommandWrapper")) { //explicit use getName instead of SimpleName because getSimpleBinaryName causes a //StringIndexOutOfBoundsException for obfuscated plugins return Maps.immutableEntry("Vanilla", elem); } Plugin plugin; try { plugin = JavaPlugin.getProvidingPlugin(clazz); if (plugin == this.plugin) { continue; } return Maps.immutableEntry(plugin.getName(), elem); } catch (IllegalArgumentException illegalArgumentEx) { //ignore } } catch (ClassNotFoundException ex) { //if this class cannot be loaded then it could be something native so we ignore it } } return null; } }