@Override
        public void run()
        {
            // Prevent two tasks from running in parallel by using the plugin monitor itself as a mutex.
            synchronized ( PluginMonitor.this )
            {
                isTaskRunning = true;
                try
                {
                    // The directory that contains all plugins.
                    final Path pluginsDirectory = pluginManager.getPluginsDirectory();
                    if ( !Files.isDirectory( pluginsDirectory ) || !Files.isReadable( pluginsDirectory ) )
                    {
                        Log.error( "Unable to process plugins. The plugins directory does not exist (or is no directory): {}", pluginsDirectory );
                        return;
                    }

                    // Turn the list of JAR/WAR files into a set so that we can do lookups.
                    final Set<String> jarSet = new HashSet<>();

                    // Explode all plugin files that have not yet been exploded (or need to be re-exploded).
                    try ( final DirectoryStream<Path> ds = Files.newDirectoryStream( pluginsDirectory, path -> {
                        if (Files.isDirectory(path)) {
                            return false;
                        }

                        final String fileName = path.getFileName().toString().toLowerCase();
                        return ( fileName.endsWith( ".jar" ) || fileName.endsWith( ".war" ) );
                    }) )
                    {
                        for ( final Path jarFile : ds )
                        {
                            final String fileName = jarFile.getFileName().toString();
                            final String canonicalPluginName = fileName.substring( 0, fileName.length() - 4 ).toLowerCase(); // strip extension.

                            jarSet.add( canonicalPluginName );

                            // See if the JAR has already been exploded.
                            final Path dir = pluginsDirectory.resolve( canonicalPluginName );

                            // See if the JAR is newer than the directory. If so, the plugin needs to be unloaded and then reloaded.
                            if ( Files.exists( dir ) && Files.getLastModifiedTime( jarFile ).toMillis() > Files.getLastModifiedTime( dir ).toMillis() )
                            {
                                // If this is the first time that the monitor process is running, then plugins won't be loaded yet. Therefore, just delete the directory.
                                if ( !pluginManager.isExecuted() )
                                {
                                    int count = 0;
                                    // Attempt to delete the folder for up to 5 seconds.
                                    while ( !PluginManager.deleteDir( dir ) && count++ < 5 )
                                    {
                                        Thread.sleep( 1000 );
                                    }
                                }
                                else
                                {
                                    // Not the first time? Properly unload the plugin.
                                    pluginManager.unloadPlugin( canonicalPluginName );
                                }
                            }

                            // If the JAR needs to be exploded, do so.
                            if ( Files.notExists( dir ) )
                            {
                                if (!unzipPlugin( canonicalPluginName, jarFile, dir ) )
                                {
                                    // the 'continue' statement is strictly unneeded here, as this
                                    // is the last statement at the time of writing. It's left in
                                    // to avoid future additions to this code from progressing
                                    // beyond this point.
                                    continue;
                                }
                            }
                        }
                    }

                    // See if any currently running plugins need to be unloaded due to the JAR file being deleted. Note
                    // that unloading a parent plugin might cause more than one plugin to disappear. Don't reuse the
                    // directory stream afterward!
                    try ( final DirectoryStream<Path> ds = Files.newDirectoryStream( pluginsDirectory, path -> {
                        if (!Files.isDirectory(path)) {
                            return false;
                        }

                        final String pluginName = PluginMetadataHelper.getCanonicalName( path );
                        return !pluginName.equals( "admin" ) && !jarSet.contains( pluginName );
                    }) )
                    {
                        for ( final Path path : ds )
                        {
                            final String pluginName = PluginMetadataHelper.getCanonicalName( path );
                            Log.info( "Plugin '{}' was removed from the file system.", pluginName );
                            pluginManager.unloadPlugin( pluginName );
                        }
                    }

                    // Load all plugins that need to be loaded. Make sure that the admin plugin is loaded first (as that
                    // should be available as soon as possible), followed by all other plugins. Ensure that parent plugins
                    // are loaded before their children.
                    try ( final DirectoryStream<Path> ds = Files.newDirectoryStream( pluginsDirectory, Files::isDirectory) )
                    {
                        // Look for extra plugin directories specified as a system property.
                        final Set<Path> devPlugins = new HashSet<>();
                        final String devPluginDirs = System.getProperty( "pluginDirs" );
                        if ( devPluginDirs != null )
                        {
                            final StringTokenizer st = new StringTokenizer( devPluginDirs, "," );
                            while ( st.hasMoreTokens() )
                            {
                                try
                                {
                                    final String devPluginDir = st.nextToken().trim();
                                    final Path devPluginPath = Paths.get( devPluginDir );
                                    if ( Files.exists( devPluginPath ) && Files.isDirectory( devPluginPath ) )
                                    {
                                        devPlugins.add( devPluginPath );
                                    }
                                    else
                                    {
                                        Log.error( "Unable to load a dev plugin as its path (as supplied in the 'pluginDirs' system property) does not exist, or is not a directory. Offending path: [{}] (parsed from raw value [{}])", devPluginPath, devPluginDir );
                                    }
                                }
                                catch ( InvalidPathException ex )
                                {
                                    Log.error( "Unable to load a dev plugin as an invalid path was added to the 'pluginDirs' system property.", ex );
                                }
                            }
                        }

                        // Sort the list of directories so that the "admin" plugin is always first in the list, and 'parent'
                        // plugins always precede their children.
                        final Deque<List<Path>> dirs = sortPluginDirs( ds, devPlugins );

                        // Before running any plugin, make sure that the admin plugin is loaded. It is a dependency
                        // of all plugins that attempt to modify the admin panel.
                        if ( !pluginManager.isLoaded( "admin"))
                        {
                            pluginManager.loadPlugin( "admin", dirs.getFirst().get( 0 ) );
                        }

                        // Prevent trying to read properties from the database while we're still in setup mode.
                        if (!XMPPServer.getInstance().isSetupMode())
                        {
                            // Hierarchy processing could be parallel.
                            final Collection<Callable<Integer>> parallelProcesses = new ArrayList<>();
                            for ( final List<Path> hierarchy : dirs )
                            {
                                parallelProcesses.add(() -> {
                                    int loaded = 0;
                                    for (final Path path : hierarchy) {
                                        // If the plugin hasn't already been started, start it.
                                        final String name = PluginMetadataHelper.getName(path);
                                        if (pluginManager.getPluginByName(name).isEmpty()) {
                                            if (pluginManager.loadPlugin(PluginMetadataHelper.getCanonicalName(path), path)) {
                                                loaded++;
                                            }
                                        }
                                    }

                                    return loaded;
                                });
                            }

                            // Hierarchies could be processed in parallel. This is likely to be beneficial during the first
                            // execution of this monitor, as during later executions, most plugins will likely already be loaded.
                            final int parallelProcessMax = JiveGlobals.getIntProperty("plugins.loading.max-parallel", 4);
                            final int parallelProcessCount = (pluginManager.isExecuted() ? 1 : parallelProcessMax);
                            final ThreadFactory threadFactory = new NamedThreadFactory("PluginMonitorExec-", Executors.defaultThreadFactory(), false, Thread.NORM_PRIORITY);
                            final ExecutorService executorService = Executors.newFixedThreadPool(parallelProcessCount, threadFactory);
                            try {
                                // Blocks until ready
                                final List<Future<Integer>> futures = executorService.invokeAll(parallelProcesses);

                                // Unless nothing happened, report that we're done loading plugins.
                                int pluginsLoaded = 0;
                                for (Future<Integer> future : futures) {
                                    pluginsLoaded += future.get();
                                }
                                if (pluginsLoaded > 0) {
                                    Log.info("Finished processing all plugins.");
                                }
                            } finally {
                                executorService.shutdown();
                            }
                        }

                        // Trigger event that plugins have been monitored
                        pluginManager.firePluginsMonitored();
                    }
                }
                catch ( Throwable e )
                {
                    Log.error( "An unexpected exception occurred:", e );
                }
                finally
                {
                    isTaskRunning = false;
                }
            }
        }
Immediately run a check of the plugin directory.
@param blockUntilDone {code true} to wait until the check is complete, otherwise {@code false}
xmppserver/src/main/java/org/jivesoftware/openfire/container/PluginMonitor.java