summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/plugins/PluginLoader.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/plugins/PluginLoader.java')
-rw-r--r--java/com/google/gerrit/server/plugins/PluginLoader.java732
1 files changed, 732 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
new file mode 100644
index 0000000000..57e7e4993f
--- /dev/null
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -0,0 +1,732 @@
+// 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.ComparisonChain;
+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.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.nio.file.Paths;
+import java.util.AbstractMap;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+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 UniversalServerPluginProvider serverPluginFactory;
+ private final GerritRuntime gerritRuntime;
+
+ @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,
+ 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);
+ this.gerritRuntime = gerritRuntime;
+
+ 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;
+ }
+
+ 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", plugin.getName());
+ }
+ } 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) {
+ 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;
+ }
+
+ 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 (Throwable 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 =
+ new DirectoryStream.Filter<Path>() {
+ @Override
+ public boolean accept(Path entry) throws IOException {
+ return 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()) {
+ System.gc();
+ 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);
+ logger.atInfo().log(
+ "Reloaded plugin %s, version %s", newPlugin.getName(), newPlugin.getVersion());
+ } catch (PluginInstallException e) {
+ logger.atWarning().withCause(e.getCause()).log("Cannot reload plugin %s", name);
+ throw e;
+ }
+ }
+
+ cleanInBackground();
+ }
+ }
+
+ public synchronized void rescan() {
+ SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
+ if (pluginsFiles.isEmpty()) {
+ return;
+ }
+
+ syncDisabledPlugins(pluginsFiles);
+
+ Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
+ for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(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)) {
+ continue;
+ }
+
+ if (active != null) {
+ logger.atInfo().log("Reloading plugin %s", active.getName());
+ }
+
+ try {
+ Plugin loadedPlugin = runPlugin(name, path, active);
+ if (!loadedPlugin.isDisabled()) {
+ logger.atInfo().log(
+ "%s plugin %s, version %s",
+ active == null ? "Loaded" : "Reloaded",
+ loadedPlugin.getName(),
+ loadedPlugin.getVersion());
+ }
+ } catch (PluginInstallException e) {
+ logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
+ }
+ }
+
+ cleanInBackground();
+ }
+
+ private void addAllEntries(Map<String, Path> from, TreeSet<Entry<String, Path>> to) {
+ Iterator<Entry<String, Path>> it = from.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, Path> entry = it.next();
+ to.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue()));
+ }
+ }
+
+ private TreeSet<Entry<String, Path>> jarsFirstSortedPluginsSet(Map<String, Path> activePlugins) {
+ TreeSet<Entry<String, Path>> sortedPlugins =
+ Sets.newTreeSet(
+ new Comparator<Entry<String, Path>>() {
+ @Override
+ public int compare(Entry<String, Path> e1, Entry<String, Path> e2) {
+ Path n1 = e1.getValue().getFileName();
+ Path n2 = e2.getValue().getFileName();
+ return ComparisonChain.start()
+ .compareTrueFirst(isJar(n1), isJar(n2))
+ .compare(n1, n2)
+ .result();
+ }
+
+ private boolean isJar(Path n1) {
+ return n1.toString().endsWith(".jar");
+ }
+ });
+
+ addAllEntries(activePlugins, sortedPlugins);
+ return sortedPlugins;
+ }
+
+ private void syncDisabledPlugins(SetMultimap<String, Path> jars) {
+ stopRemovedPlugins(jars);
+ dropRemovedDisabledPlugins(jars);
+ }
+
+ private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin)
+ throws PluginInstallException {
+ FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
+ try {
+ 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 (Throwable 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 (Throwable 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) {
+ 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);
+ }
+ }
+
+ 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.
+ Collection<Path> elementsToRemove = new ArrayList<>();
+ Collection<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 = Paths.get(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"));
+ }
+
+ 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") || isPlugin(name, "html");
+ }
+
+ 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");
+ }
+ }
+}