| // Copyright (C) 2012 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.plugins; |
| |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.LinkedHashMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.SetMultimap; |
| import com.google.common.collect.Sets; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.extensions.events.LifecycleListener; |
| import com.google.gerrit.extensions.restapi.MethodNotAllowedException; |
| import com.google.gerrit.extensions.systemstatus.ServerInformation; |
| import com.google.gerrit.server.PluginUser; |
| import com.google.gerrit.server.cache.PersistentCacheFactory; |
| import com.google.gerrit.server.config.CanonicalWebUrl; |
| import com.google.gerrit.server.config.ConfigUtil; |
| import com.google.gerrit.server.config.GerritRuntime; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.config.SitePaths; |
| import com.google.gerrit.server.plugins.ServerPluginProvider.PluginDescription; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.AbstractMap; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Queue; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.TimeUnit; |
| import org.eclipse.jgit.internal.storage.file.FileSnapshot; |
| import org.eclipse.jgit.lib.Config; |
| |
| @Singleton |
| public class PluginLoader implements LifecycleListener { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| public String getPluginName(Path srcPath) { |
| return MoreObjects.firstNonNull(getGerritPluginName(srcPath), PluginUtil.nameOf(srcPath)); |
| } |
| |
| private final Path pluginsDir; |
| private final Path dataDir; |
| private final Path tempDir; |
| private final PluginGuiceEnvironment env; |
| private final ServerInformationImpl srvInfoImpl; |
| private final PluginUser.Factory pluginUserFactory; |
| private final ConcurrentMap<String, Plugin> running = Maps.newConcurrentMap(); |
| private final ConcurrentMap<String, Plugin> disabled = Maps.newConcurrentMap(); |
| private final Map<String, FileSnapshot> broken = Maps.newHashMap(); |
| private final Map<Plugin, CleanupHandle> cleanupHandles = Maps.newConcurrentMap(); |
| private final Queue<Plugin> toCleanup = new ArrayDeque<>(); |
| private final Provider<PluginCleanerTask> cleaner; |
| private final PluginScannerThread scanner; |
| private final Provider<String> urlProvider; |
| private final PersistentCacheFactory persistentCacheFactory; |
| private final boolean remoteAdmin; |
| private final MandatoryPluginsCollection mandatoryPlugins; |
| private final UniversalServerPluginProvider serverPluginFactory; |
| private final GerritRuntime gerritRuntime; |
| private final PluginOrderComparator pluginOrderComparator; |
| |
| @Inject |
| public PluginLoader( |
| SitePaths sitePaths, |
| PluginGuiceEnvironment pe, |
| ServerInformationImpl sii, |
| PluginUser.Factory puf, |
| Provider<PluginCleanerTask> pct, |
| @GerritServerConfig Config cfg, |
| @CanonicalWebUrl Provider<String> provider, |
| PersistentCacheFactory cacheFactory, |
| UniversalServerPluginProvider pluginFactory, |
| MandatoryPluginsCollection mpc, |
| GerritRuntime gerritRuntime) { |
| pluginsDir = sitePaths.plugins_dir; |
| dataDir = sitePaths.data_dir; |
| tempDir = sitePaths.tmp_dir; |
| env = pe; |
| srvInfoImpl = sii; |
| pluginUserFactory = puf; |
| cleaner = pct; |
| urlProvider = provider; |
| persistentCacheFactory = cacheFactory; |
| serverPluginFactory = pluginFactory; |
| |
| remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false); |
| mandatoryPlugins = mpc; |
| this.gerritRuntime = gerritRuntime; |
| |
| ImmutableList<String> pluginOrderOverrides = |
| ImmutableList.copyOf(cfg.getStringList("plugins", null, "loadPriority")); |
| pluginOrderComparator = new PluginOrderComparator(pluginOrderOverrides); |
| |
| long checkFrequency = |
| ConfigUtil.getTimeUnit( |
| cfg, |
| "plugins", |
| null, |
| "checkFrequency", |
| TimeUnit.MINUTES.toMillis(1), |
| TimeUnit.MILLISECONDS); |
| if (checkFrequency > 0) { |
| scanner = new PluginScannerThread(this, checkFrequency); |
| } else { |
| scanner = null; |
| } |
| } |
| |
| public boolean isRemoteAdminEnabled() { |
| return remoteAdmin; |
| } |
| |
| public void checkRemoteAdminEnabled() throws MethodNotAllowedException { |
| if (!remoteAdmin) { |
| throw new MethodNotAllowedException("remote plugin administration is disabled"); |
| } |
| } |
| |
| public Plugin get(String name) { |
| Plugin p = running.get(name); |
| if (p != null) { |
| return p; |
| } |
| return disabled.get(name); |
| } |
| |
| public Iterable<Plugin> getPlugins(boolean all) { |
| if (!all) { |
| return running.values(); |
| } |
| List<Plugin> plugins = new ArrayList<>(running.values()); |
| plugins.addAll(disabled.values()); |
| return plugins; |
| } |
| |
| @CanIgnoreReturnValue |
| public String installPluginFromStream(String originalName, InputStream in) |
| throws IOException, PluginInstallException { |
| checkRemoteInstall(); |
| |
| String fileName = originalName; |
| Path tmp = PluginUtil.asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir); |
| String name = MoreObjects.firstNonNull(getGerritPluginName(tmp), PluginUtil.nameOf(fileName)); |
| if (!originalName.equals(name)) { |
| logger.atWarning().log( |
| "Plugin provides its own name: <%s>, use it instead of the input name: <%s>", |
| name, originalName); |
| } |
| |
| String fileExtension = getExtension(fileName); |
| Path dst = pluginsDir.resolve(name + fileExtension); |
| synchronized (this) { |
| Plugin active = running.get(name); |
| if (active != null) { |
| fileName = active.getSrcFile().getFileName().toString(); |
| logger.atInfo().log("Replacing plugin %s", active.getName()); |
| Path old = pluginsDir.resolve(".last_" + fileName); |
| Files.deleteIfExists(old); |
| Files.move(active.getSrcFile(), old); |
| } |
| |
| Files.deleteIfExists(pluginsDir.resolve(fileName + ".disabled")); |
| Files.move(tmp, dst); |
| try { |
| Plugin plugin = runPlugin(name, dst, active); |
| if (active == null) { |
| logger.atInfo().log( |
| "Installed plugin %s%s", |
| plugin.getName(), plugin.getApiModule().isPresent() ? " (w/ ApiModule)" : ""); |
| } |
| } catch (PluginInstallException e) { |
| Files.deleteIfExists(dst); |
| throw e; |
| } |
| |
| cleanInBackground(); |
| } |
| |
| return name; |
| } |
| |
| private synchronized void unloadPlugin(Plugin plugin) { |
| persistentCacheFactory.onStop(plugin.getName()); |
| String name = plugin.getName(); |
| logger.atInfo().log("Unloading plugin %s, version %s", name, plugin.getVersion()); |
| plugin.stop(env); |
| env.onStopPlugin(plugin); |
| running.remove(name); |
| disabled.remove(name); |
| toCleanup.add(plugin); |
| } |
| |
| public void disablePlugins(Set<String> names) throws PluginInstallException { |
| if (!isRemoteAdminEnabled()) { |
| logger.atWarning().log( |
| "Remote plugin administration is disabled, ignoring disablePlugins(%s)", names); |
| return; |
| } |
| |
| synchronized (this) { |
| for (String name : names) { |
| Plugin active = running.get(name); |
| if (active == null) { |
| continue; |
| } |
| |
| if (active.getApiModule().isPresent()) { |
| throw new PluginInstallException( |
| String.format( |
| "Plugin %s has registered an ApiModule therefore it cannot be disabled", name)); |
| } |
| |
| if (mandatoryPlugins.contains(name)) { |
| logger.atWarning().log("Mandatory plugin %s cannot be disabled", name); |
| continue; |
| } |
| |
| logger.atInfo().log("Disabling plugin %s", active.getName()); |
| Path off = |
| active.getSrcFile().resolveSibling(active.getSrcFile().getFileName() + ".disabled"); |
| try { |
| Files.move(active.getSrcFile(), off); |
| } catch (IOException e) { |
| logger.atSevere().withCause(e).log("Failed to disable plugin"); |
| // In theory we could still unload the plugin even if the rename |
| // failed. However, it would be reloaded on the next server startup, |
| // which is probably not what the user expects. |
| continue; |
| } |
| |
| unloadPlugin(active); |
| try { |
| FileSnapshot snapshot = FileSnapshot.save(off.toFile()); |
| Plugin offPlugin = loadPlugin(name, off, snapshot); |
| disabled.put(name, offPlugin); |
| } catch (Exception e) { |
| // This shouldn't happen, as the plugin was loaded earlier. |
| logger.atWarning().withCause(e.getCause()).log( |
| "Cannot load disabled plugin %s", active.getName()); |
| } |
| } |
| cleanInBackground(); |
| } |
| } |
| |
| public void enablePlugins(Set<String> names) throws PluginInstallException { |
| if (!isRemoteAdminEnabled()) { |
| logger.atWarning().log( |
| "Remote plugin administration is disabled, ignoring enablePlugins(%s)", names); |
| return; |
| } |
| |
| synchronized (this) { |
| for (String name : names) { |
| Plugin off = disabled.get(name); |
| if (off == null) { |
| continue; |
| } |
| |
| logger.atInfo().log("Enabling plugin %s", name); |
| String n = off.getSrcFile().toFile().getName(); |
| if (n.endsWith(".disabled")) { |
| n = n.substring(0, n.lastIndexOf('.')); |
| } |
| Path on = pluginsDir.resolve(n); |
| try { |
| Files.move(off.getSrcFile(), on); |
| } catch (IOException e) { |
| logger.atSevere().withCause(e).log("Failed to move plugin %s into place", name); |
| continue; |
| } |
| disabled.remove(name); |
| runPlugin(name, on, null); |
| } |
| cleanInBackground(); |
| } |
| } |
| |
| private void removeStalePluginFiles() { |
| DirectoryStream.Filter<Path> filter = |
| entry -> entry.getFileName().toString().startsWith("plugin_"); |
| try (DirectoryStream<Path> files = Files.newDirectoryStream(tempDir, filter)) { |
| for (Path file : files) { |
| logger.atInfo().log("Removing stale plugin file: %s", file.toFile().getName()); |
| try { |
| Files.delete(file); |
| } catch (IOException e) { |
| logger.atSevere().log( |
| "Failed to remove stale plugin file %s: %s", file.toFile().getName(), e.getMessage()); |
| } |
| } |
| } catch (IOException e) { |
| logger.atWarning().log("Unable to discover stale plugin files: %s", e.getMessage()); |
| } |
| } |
| |
| @Override |
| public synchronized void start() { |
| removeStalePluginFiles(); |
| Path absolutePath = pluginsDir.toAbsolutePath(); |
| if (!Files.exists(absolutePath)) { |
| logger.atInfo().log("%s does not exist; creating", absolutePath); |
| try { |
| Files.createDirectories(absolutePath); |
| } catch (IOException e) { |
| logger.atSevere().log("Failed to create %s: %s", absolutePath, e.getMessage()); |
| } |
| } |
| logger.atInfo().log("Loading plugins from %s", absolutePath); |
| srvInfoImpl.state = ServerInformation.State.STARTUP; |
| rescan(); |
| srvInfoImpl.state = ServerInformation.State.RUNNING; |
| if (scanner != null) { |
| scanner.start(); |
| } |
| } |
| |
| @Override |
| public void stop() { |
| if (scanner != null) { |
| scanner.end(); |
| } |
| srvInfoImpl.state = ServerInformation.State.SHUTDOWN; |
| synchronized (this) { |
| for (Plugin p : running.values()) { |
| unloadPlugin(p); |
| } |
| running.clear(); |
| disabled.clear(); |
| broken.clear(); |
| if (!toCleanup.isEmpty()) { |
| processPendingCleanups(); |
| } |
| } |
| } |
| |
| public void reload(List<String> names) throws InvalidPluginException, PluginInstallException { |
| synchronized (this) { |
| List<Plugin> reload = Lists.newArrayListWithCapacity(names.size()); |
| List<String> bad = Lists.newArrayListWithExpectedSize(4); |
| for (String name : names) { |
| Plugin active = running.get(name); |
| if (active != null) { |
| reload.add(active); |
| } else { |
| bad.add(name); |
| } |
| } |
| if (!bad.isEmpty()) { |
| throw new InvalidPluginException( |
| String.format("Plugin(s) \"%s\" not running", Joiner.on("\", \"").join(bad))); |
| } |
| |
| for (Plugin active : reload) { |
| String name = active.getName(); |
| try { |
| logger.atInfo().log("Reloading plugin %s", name); |
| Plugin newPlugin = runPlugin(name, active.getSrcFile(), active); |
| |
| if (newPlugin != active) { |
| logger.atInfo().log( |
| "Reloaded plugin %s%s, version %s", |
| newPlugin.getName(), |
| newPlugin.getApiModule().isPresent() ? " (w/ ApiModule)" : "", |
| newPlugin.getVersion()); |
| } |
| } catch (PluginInstallException e) { |
| logger.atWarning().withCause(e.getCause()).log("Cannot reload plugin %s", name); |
| throw e; |
| } |
| } |
| |
| cleanInBackground(); |
| } |
| } |
| |
| public synchronized void rescan() { |
| Set<String> loadedPlugins = new HashSet<>(); |
| SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir); |
| |
| if (!pluginsFiles.isEmpty()) { |
| syncDisabledPlugins(pluginsFiles); |
| |
| Map<String, Path> activePlugins = filterDisabled(pluginsFiles); |
| for (Map.Entry<String, Path> entry : jarsApiFirstSortedPluginsSet(activePlugins)) { |
| String name = entry.getKey(); |
| Path path = entry.getValue(); |
| String fileName = path.getFileName().toString(); |
| if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) { |
| logger.atWarning().log( |
| "No Plugin provider was found that handles this file format: %s", fileName); |
| continue; |
| } |
| |
| FileSnapshot brokenTime = broken.get(name); |
| if (brokenTime != null && !brokenTime.isModified(path.toFile())) { |
| continue; |
| } |
| |
| Plugin active = running.get(name); |
| if (active != null && !active.isModified(path)) { |
| loadedPlugins.add(name); |
| continue; |
| } |
| |
| if (active != null) { |
| logger.atInfo().log("Reloading plugin %s", active.getName()); |
| } |
| |
| try { |
| Plugin loadedPlugin = runPlugin(name, path, active); |
| if (!loadedPlugin.isDisabled()) { |
| loadedPlugins.add(name); |
| logger.atInfo().log( |
| "%s plugin %s%s, version %s", |
| active == null ? "Loaded" : "Reloaded", |
| loadedPlugin.getName(), |
| loadedPlugin.getApiModule().isPresent() ? " (w/ ApiModule)" : "", |
| loadedPlugin.getVersion()); |
| } |
| } catch (PluginInstallException e) { |
| logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name); |
| } |
| } |
| } |
| |
| Set<String> missingMandatory = Sets.difference(mandatoryPlugins.asSet(), loadedPlugins); |
| if (!missingMandatory.isEmpty()) { |
| throw new MissingMandatoryPluginsException(missingMandatory); |
| } |
| |
| cleanInBackground(); |
| } |
| |
| private void addAllEntries(Map<String, Path> from, TreeSet<Map.Entry<String, Path>> to) { |
| Iterator<Map.Entry<String, Path>> it = from.entrySet().iterator(); |
| while (it.hasNext()) { |
| Map.Entry<String, Path> entry = it.next(); |
| to.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue())); |
| } |
| } |
| |
| private TreeSet<Map.Entry<String, Path>> jarsApiFirstSortedPluginsSet( |
| Map<String, Path> activePlugins) { |
| TreeSet<Map.Entry<String, Path>> sortedPlugins = Sets.newTreeSet(pluginOrderComparator); |
| |
| addAllEntries(activePlugins, sortedPlugins); |
| return sortedPlugins; |
| } |
| |
| private void syncDisabledPlugins(SetMultimap<String, Path> jars) { |
| stopRemovedPlugins(jars); |
| dropRemovedDisabledPlugins(jars); |
| } |
| |
| @CanIgnoreReturnValue |
| private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin) |
| throws PluginInstallException { |
| FileSnapshot snapshot = FileSnapshot.save(plugin.toFile()); |
| try { |
| boolean restartRequired = oldPlugin != null && !oldPlugin.canReload(); |
| if (restartRequired && mandatoryPlugins.contains(name)) { |
| logger.atWarning().log("Restarting mandatory plugin %s not allowed", name); |
| return oldPlugin; |
| } |
| |
| if (oldPlugin != null && oldPlugin.getApiModule().isPresent()) { |
| throw new PluginInstallException( |
| String.format( |
| "Plugin %s has registered an ApiModule therefore its restart/reload is not allowed", |
| name)); |
| } |
| |
| Plugin newPlugin = loadPlugin(name, plugin, snapshot); |
| if (newPlugin.getCleanupHandle() != null) { |
| cleanupHandles.put(newPlugin, newPlugin.getCleanupHandle()); |
| } |
| /* |
| * Pluggable plugin provider may have assigned a plugin name that could be |
| * actually different from the initial one assigned during scan. It is |
| * safer then to reassign it. |
| */ |
| name = newPlugin.getName(); |
| boolean reload = oldPlugin != null && oldPlugin.canReload() && newPlugin.canReload(); |
| if (!reload && oldPlugin != null) { |
| unloadPlugin(oldPlugin); |
| } |
| if (!newPlugin.isDisabled()) { |
| try { |
| newPlugin.start(env); |
| } catch (Exception e) { |
| newPlugin.stop(env); |
| throw e; |
| } |
| } |
| if (reload) { |
| env.onReloadPlugin(oldPlugin, newPlugin); |
| unloadPlugin(oldPlugin); |
| } else if (!newPlugin.isDisabled()) { |
| env.onStartPlugin(newPlugin); |
| } |
| if (!newPlugin.isDisabled()) { |
| running.put(name, newPlugin); |
| } else { |
| disabled.put(name, newPlugin); |
| } |
| broken.remove(name); |
| return newPlugin; |
| } catch (Exception err) { |
| broken.put(name, snapshot); |
| throw new PluginInstallException(err); |
| } |
| } |
| |
| private void stopRemovedPlugins(SetMultimap<String, Path> jars) { |
| Set<String> unload = Sets.newHashSet(running.keySet()); |
| for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) { |
| for (Path path : entry.getValue()) { |
| if (!path.getFileName().toString().endsWith(".disabled")) { |
| unload.remove(entry.getKey()); |
| } |
| } |
| } |
| for (String name : unload) { |
| Plugin runningPlugin = running.get(name); |
| |
| if (runningPlugin.getApiModule().isPresent()) { |
| logger.atWarning().log("Cannot remove plugin %s as it has registered an ApiModule", name); |
| } else { |
| unloadPlugin(running.get(name)); |
| } |
| } |
| } |
| |
| private void dropRemovedDisabledPlugins(SetMultimap<String, Path> jars) { |
| Set<String> unload = Sets.newHashSet(disabled.keySet()); |
| for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) { |
| for (Path path : entry.getValue()) { |
| if (path.getFileName().toString().endsWith(".disabled")) { |
| unload.remove(entry.getKey()); |
| } |
| } |
| } |
| for (String name : unload) { |
| disabled.remove(name); |
| } |
| } |
| |
| @CanIgnoreReturnValue |
| synchronized int processPendingCleanups() { |
| Iterator<Plugin> iterator = toCleanup.iterator(); |
| while (iterator.hasNext()) { |
| Plugin plugin = iterator.next(); |
| iterator.remove(); |
| |
| CleanupHandle cleanupHandle = cleanupHandles.remove(plugin); |
| if (cleanupHandle != null) { |
| cleanupHandle.cleanup(); |
| } |
| } |
| return toCleanup.size(); |
| } |
| |
| private void cleanInBackground() { |
| int cnt = toCleanup.size(); |
| if (0 < cnt) { |
| cleaner.get().clean(cnt); |
| } |
| } |
| |
| private String getExtension(String name) { |
| int ext = name.lastIndexOf('.'); |
| return 0 < ext ? name.substring(ext) : ""; |
| } |
| |
| private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot) |
| throws InvalidPluginException { |
| String pluginName = srcPlugin.getFileName().toString(); |
| if (isUiPlugin(pluginName)) { |
| return loadJsPlugin(name, srcPlugin, snapshot); |
| } else if (serverPluginFactory.handles(srcPlugin)) { |
| return loadServerPlugin(srcPlugin, snapshot); |
| } else { |
| throw new InvalidPluginException( |
| String.format("Unsupported plugin type: %s", srcPlugin.getFileName())); |
| } |
| } |
| |
| private Path getPluginDataDir(String name) { |
| return dataDir.resolve(name); |
| } |
| |
| private String getPluginCanonicalWebUrl(String name) { |
| String canonicalWebUrl = urlProvider.get(); |
| if (Strings.isNullOrEmpty(canonicalWebUrl)) { |
| return "/plugins/" + name; |
| } |
| |
| String url = |
| String.format( |
| "%s/plugins/%s/", CharMatcher.is('/').trimTrailingFrom(canonicalWebUrl), name); |
| return url; |
| } |
| |
| private Plugin loadJsPlugin(String name, Path srcJar, FileSnapshot snapshot) { |
| return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot); |
| } |
| |
| private ServerPlugin loadServerPlugin(Path scriptFile, FileSnapshot snapshot) |
| throws InvalidPluginException { |
| String name = serverPluginFactory.getPluginName(scriptFile); |
| return serverPluginFactory.get( |
| scriptFile, |
| snapshot, |
| new PluginDescription( |
| pluginUserFactory.create(name), |
| getPluginCanonicalWebUrl(name), |
| getPluginDataDir(name), |
| gerritRuntime)); |
| } |
| |
| // Only one active plugin per plugin name can exist for each plugin name. |
| // Filter out disabled plugins and transform the multimap to a map |
| private Map<String, Path> filterDisabled(SetMultimap<String, Path> pluginPaths) { |
| Map<String, Path> activePlugins = Maps.newHashMapWithExpectedSize(pluginPaths.keys().size()); |
| for (String name : pluginPaths.keys()) { |
| for (Path pluginPath : pluginPaths.asMap().get(name)) { |
| if (!pluginPath.getFileName().toString().endsWith(".disabled")) { |
| assert !activePlugins.containsKey(name); |
| activePlugins.put(name, pluginPath); |
| } |
| } |
| } |
| return activePlugins; |
| } |
| |
| // Scan the $site_path/plugins directory and fetch all files and directories. |
| // The Key in returned multimap is the plugin name initially assigned from its filename. |
| // Values are the files. Plugins can optionally provide their name in MANIFEST file. |
| // If multiple plugin files provide the same plugin name, then only |
| // the first plugin remains active and all other plugins with the same |
| // name are disabled. |
| // |
| // NOTE: Bear in mind that the plugin name can be reassigned after load by the |
| // Server plugin provider. |
| public SetMultimap<String, Path> prunePlugins(Path pluginsDir) { |
| List<Path> pluginPaths = scanPathsInPluginsDirectory(pluginsDir); |
| SetMultimap<String, Path> map; |
| map = asMultimap(pluginPaths); |
| for (String plugin : map.keySet()) { |
| Collection<Path> files = map.asMap().get(plugin); |
| if (files.size() == 1) { |
| continue; |
| } |
| // retrieve enabled plugins |
| Iterable<Path> enabled = filterDisabledPlugins(files); |
| // If we have only one (the winner) plugin, nothing to do |
| if (!Iterables.skip(enabled, 1).iterator().hasNext()) { |
| continue; |
| } |
| Path winner = Iterables.getFirst(enabled, null); |
| assert winner != null; |
| // Disable all loser plugins by renaming their file names to |
| // "file.disabled" and replace the disabled files in the multimap. |
| List<Path> elementsToRemove = new ArrayList<>(); |
| List<Path> elementsToAdd = new ArrayList<>(); |
| for (Path loser : Iterables.skip(enabled, 1)) { |
| logger.atWarning().log( |
| "Plugin <%s> was disabled, because" |
| + " another plugin <%s>" |
| + " with the same name <%s> already exists", |
| loser, winner, plugin); |
| Path disabledPlugin = Path.of(loser + ".disabled"); |
| elementsToAdd.add(disabledPlugin); |
| elementsToRemove.add(loser); |
| try { |
| Files.move(loser, disabledPlugin); |
| } catch (IOException e) { |
| logger.atWarning().withCause(e).log("Failed to fully disable plugin %s", loser); |
| } |
| } |
| Iterables.removeAll(files, elementsToRemove); |
| Iterables.addAll(files, elementsToAdd); |
| } |
| return map; |
| } |
| |
| private List<Path> scanPathsInPluginsDirectory(Path pluginsDir) { |
| try { |
| return PluginUtil.listPlugins(pluginsDir); |
| } catch (IOException e) { |
| logger.atSevere().withCause(e).log("Cannot list %s", pluginsDir.toAbsolutePath()); |
| return ImmutableList.of(); |
| } |
| } |
| |
| private Iterable<Path> filterDisabledPlugins(Collection<Path> paths) { |
| return Iterables.filter(paths, p -> !p.getFileName().toString().endsWith(".disabled")); |
| } |
| |
| @Nullable |
| public String getGerritPluginName(Path srcPath) { |
| String fileName = srcPath.getFileName().toString(); |
| if (isUiPlugin(fileName)) { |
| return fileName.substring(0, fileName.lastIndexOf('.')); |
| } |
| if (serverPluginFactory.handles(srcPath)) { |
| return serverPluginFactory.getPluginName(srcPath); |
| } |
| return null; |
| } |
| |
| private SetMultimap<String, Path> asMultimap(List<Path> plugins) { |
| SetMultimap<String, Path> map = LinkedHashMultimap.create(); |
| for (Path srcPath : plugins) { |
| map.put(getPluginName(srcPath), srcPath); |
| } |
| return map; |
| } |
| |
| private boolean isUiPlugin(String name) { |
| return isPlugin(name, "js"); |
| } |
| |
| private boolean isPlugin(String fileName, String ext) { |
| String fullExt = "." + ext; |
| return fileName.endsWith(fullExt) || fileName.endsWith(fullExt + ".disabled"); |
| } |
| |
| private void checkRemoteInstall() throws PluginInstallException { |
| if (!isRemoteAdminEnabled()) { |
| throw new PluginInstallException("remote installation is disabled"); |
| } |
| } |
| } |