LCOV - code coverage report
Current view: top level - server/plugins - PluginLoader.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 257 410 62.7 %
Date: 2022-11-19 15:00:39 Functions: 31 44 70.5 %

          Line data    Source code
       1             : // Copyright (C) 2012 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.plugins;
      16             : 
      17             : import com.google.common.base.CharMatcher;
      18             : import com.google.common.base.Joiner;
      19             : import com.google.common.base.MoreObjects;
      20             : import com.google.common.base.Strings;
      21             : import com.google.common.collect.ComparisonChain;
      22             : import com.google.common.collect.ImmutableList;
      23             : import com.google.common.collect.Iterables;
      24             : import com.google.common.collect.LinkedHashMultimap;
      25             : import com.google.common.collect.Lists;
      26             : import com.google.common.collect.Maps;
      27             : import com.google.common.collect.SetMultimap;
      28             : import com.google.common.collect.Sets;
      29             : import com.google.common.flogger.FluentLogger;
      30             : import com.google.gerrit.common.Nullable;
      31             : import com.google.gerrit.extensions.events.LifecycleListener;
      32             : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
      33             : import com.google.gerrit.extensions.systemstatus.ServerInformation;
      34             : import com.google.gerrit.server.PluginUser;
      35             : import com.google.gerrit.server.cache.PersistentCacheFactory;
      36             : import com.google.gerrit.server.config.CanonicalWebUrl;
      37             : import com.google.gerrit.server.config.ConfigUtil;
      38             : import com.google.gerrit.server.config.GerritRuntime;
      39             : import com.google.gerrit.server.config.GerritServerConfig;
      40             : import com.google.gerrit.server.config.SitePaths;
      41             : import com.google.gerrit.server.plugins.ServerPluginProvider.PluginDescription;
      42             : import com.google.inject.Inject;
      43             : import com.google.inject.Provider;
      44             : import com.google.inject.Singleton;
      45             : import java.io.IOException;
      46             : import java.io.InputStream;
      47             : import java.nio.file.DirectoryStream;
      48             : import java.nio.file.Files;
      49             : import java.nio.file.Path;
      50             : import java.nio.file.Paths;
      51             : import java.util.AbstractMap;
      52             : import java.util.ArrayDeque;
      53             : import java.util.ArrayList;
      54             : import java.util.Collection;
      55             : import java.util.Comparator;
      56             : import java.util.HashSet;
      57             : import java.util.Iterator;
      58             : import java.util.List;
      59             : import java.util.Map;
      60             : import java.util.Queue;
      61             : import java.util.Set;
      62             : import java.util.TreeSet;
      63             : import java.util.concurrent.ConcurrentMap;
      64             : import java.util.concurrent.TimeUnit;
      65             : import org.eclipse.jgit.internal.storage.file.FileSnapshot;
      66             : import org.eclipse.jgit.lib.Config;
      67             : 
      68         149 : @Singleton
      69             : public class PluginLoader implements LifecycleListener {
      70         149 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      71             : 
      72             :   public String getPluginName(Path srcPath) {
      73           0 :     return MoreObjects.firstNonNull(getGerritPluginName(srcPath), PluginUtil.nameOf(srcPath));
      74             :   }
      75             : 
      76             :   private final Path pluginsDir;
      77             :   private final Path dataDir;
      78             :   private final Path tempDir;
      79             :   private final PluginGuiceEnvironment env;
      80             :   private final ServerInformationImpl srvInfoImpl;
      81             :   private final PluginUser.Factory pluginUserFactory;
      82         149 :   private final ConcurrentMap<String, Plugin> running = Maps.newConcurrentMap();
      83         149 :   private final ConcurrentMap<String, Plugin> disabled = Maps.newConcurrentMap();
      84         149 :   private final Map<String, FileSnapshot> broken = Maps.newHashMap();
      85         149 :   private final Map<Plugin, CleanupHandle> cleanupHandles = Maps.newConcurrentMap();
      86         149 :   private final Queue<Plugin> toCleanup = new ArrayDeque<>();
      87             :   private final Provider<PluginCleanerTask> cleaner;
      88             :   private final PluginScannerThread scanner;
      89             :   private final Provider<String> urlProvider;
      90             :   private final PersistentCacheFactory persistentCacheFactory;
      91             :   private final boolean remoteAdmin;
      92             :   private final MandatoryPluginsCollection mandatoryPlugins;
      93             :   private final UniversalServerPluginProvider serverPluginFactory;
      94             :   private final GerritRuntime gerritRuntime;
      95             : 
      96             :   @Inject
      97             :   public PluginLoader(
      98             :       SitePaths sitePaths,
      99             :       PluginGuiceEnvironment pe,
     100             :       ServerInformationImpl sii,
     101             :       PluginUser.Factory puf,
     102             :       Provider<PluginCleanerTask> pct,
     103             :       @GerritServerConfig Config cfg,
     104             :       @CanonicalWebUrl Provider<String> provider,
     105             :       PersistentCacheFactory cacheFactory,
     106             :       UniversalServerPluginProvider pluginFactory,
     107             :       MandatoryPluginsCollection mpc,
     108         149 :       GerritRuntime gerritRuntime) {
     109         149 :     pluginsDir = sitePaths.plugins_dir;
     110         149 :     dataDir = sitePaths.data_dir;
     111         149 :     tempDir = sitePaths.tmp_dir;
     112         149 :     env = pe;
     113         149 :     srvInfoImpl = sii;
     114         149 :     pluginUserFactory = puf;
     115         149 :     cleaner = pct;
     116         149 :     urlProvider = provider;
     117         149 :     persistentCacheFactory = cacheFactory;
     118         149 :     serverPluginFactory = pluginFactory;
     119             : 
     120         149 :     remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
     121         149 :     mandatoryPlugins = mpc;
     122         149 :     this.gerritRuntime = gerritRuntime;
     123             : 
     124         149 :     long checkFrequency =
     125         149 :         ConfigUtil.getTimeUnit(
     126             :             cfg,
     127             :             "plugins",
     128             :             null,
     129             :             "checkFrequency",
     130         149 :             TimeUnit.MINUTES.toMillis(1),
     131             :             TimeUnit.MILLISECONDS);
     132         149 :     if (checkFrequency > 0) {
     133          13 :       scanner = new PluginScannerThread(this, checkFrequency);
     134             :     } else {
     135         138 :       scanner = null;
     136             :     }
     137         149 :   }
     138             : 
     139             :   public boolean isRemoteAdminEnabled() {
     140           3 :     return remoteAdmin;
     141             :   }
     142             : 
     143             :   public void checkRemoteAdminEnabled() throws MethodNotAllowedException {
     144           3 :     if (!remoteAdmin) {
     145           1 :       throw new MethodNotAllowedException("remote plugin administration is disabled");
     146             :     }
     147           3 :   }
     148             : 
     149             :   public Plugin get(String name) {
     150           3 :     Plugin p = running.get(name);
     151           3 :     if (p != null) {
     152           3 :       return p;
     153             :     }
     154           2 :     return disabled.get(name);
     155             :   }
     156             : 
     157             :   public Iterable<Plugin> getPlugins(boolean all) {
     158           2 :     if (!all) {
     159           2 :       return running.values();
     160             :     }
     161           1 :     List<Plugin> plugins = new ArrayList<>(running.values());
     162           1 :     plugins.addAll(disabled.values());
     163           1 :     return plugins;
     164             :   }
     165             : 
     166             :   public String installPluginFromStream(String originalName, InputStream in)
     167             :       throws IOException, PluginInstallException {
     168           3 :     checkRemoteInstall();
     169             : 
     170           3 :     String fileName = originalName;
     171           3 :     Path tmp = PluginUtil.asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
     172           3 :     String name = MoreObjects.firstNonNull(getGerritPluginName(tmp), PluginUtil.nameOf(fileName));
     173           3 :     if (!originalName.equals(name)) {
     174           3 :       logger.atWarning().log(
     175             :           "Plugin provides its own name: <%s>, use it instead of the input name: <%s>",
     176             :           name, originalName);
     177             :     }
     178             : 
     179           3 :     String fileExtension = getExtension(fileName);
     180           3 :     Path dst = pluginsDir.resolve(name + fileExtension);
     181           3 :     synchronized (this) {
     182           3 :       Plugin active = running.get(name);
     183           3 :       if (active != null) {
     184           1 :         fileName = active.getSrcFile().getFileName().toString();
     185           1 :         logger.atInfo().log("Replacing plugin %s", active.getName());
     186           1 :         Path old = pluginsDir.resolve(".last_" + fileName);
     187           1 :         Files.deleteIfExists(old);
     188           1 :         Files.move(active.getSrcFile(), old);
     189             :       }
     190             : 
     191           3 :       Files.deleteIfExists(pluginsDir.resolve(fileName + ".disabled"));
     192           3 :       Files.move(tmp, dst);
     193             :       try {
     194           3 :         Plugin plugin = runPlugin(name, dst, active);
     195           3 :         if (active == null) {
     196           3 :           logger.atInfo().log("Installed plugin %s", plugin.getName());
     197             :         }
     198           1 :       } catch (PluginInstallException e) {
     199           1 :         Files.deleteIfExists(dst);
     200           1 :         throw e;
     201           3 :       }
     202             : 
     203           3 :       cleanInBackground();
     204           3 :     }
     205             : 
     206           3 :     return name;
     207             :   }
     208             : 
     209             :   private synchronized void unloadPlugin(Plugin plugin) {
     210           3 :     persistentCacheFactory.onStop(plugin.getName());
     211           3 :     String name = plugin.getName();
     212           3 :     logger.atInfo().log("Unloading plugin %s, version %s", name, plugin.getVersion());
     213           3 :     plugin.stop(env);
     214           3 :     env.onStopPlugin(plugin);
     215           3 :     running.remove(name);
     216           3 :     disabled.remove(name);
     217           3 :     toCleanup.add(plugin);
     218           3 :   }
     219             : 
     220             :   public void disablePlugins(Set<String> names) {
     221           2 :     if (!isRemoteAdminEnabled()) {
     222           0 :       logger.atWarning().log(
     223             :           "Remote plugin administration is disabled, ignoring disablePlugins(%s)", names);
     224           0 :       return;
     225             :     }
     226             : 
     227           2 :     synchronized (this) {
     228           2 :       for (String name : names) {
     229           2 :         Plugin active = running.get(name);
     230           2 :         if (active == null) {
     231           0 :           continue;
     232             :         }
     233             : 
     234           2 :         if (mandatoryPlugins.contains(name)) {
     235           0 :           logger.atWarning().log("Mandatory plugin %s cannot be disabled", name);
     236           0 :           continue;
     237             :         }
     238             : 
     239           2 :         logger.atInfo().log("Disabling plugin %s", active.getName());
     240           2 :         Path off =
     241           2 :             active.getSrcFile().resolveSibling(active.getSrcFile().getFileName() + ".disabled");
     242             :         try {
     243           1 :           Files.move(active.getSrcFile(), off);
     244           1 :         } catch (IOException e) {
     245           1 :           logger.atSevere().withCause(e).log("Failed to disable plugin");
     246             :           // In theory we could still unload the plugin even if the rename
     247             :           // failed. However, it would be reloaded on the next server startup,
     248             :           // which is probably not what the user expects.
     249           1 :           continue;
     250           1 :         }
     251             : 
     252           1 :         unloadPlugin(active);
     253             :         try {
     254           1 :           FileSnapshot snapshot = FileSnapshot.save(off.toFile());
     255           1 :           Plugin offPlugin = loadPlugin(name, off, snapshot);
     256           1 :           disabled.put(name, offPlugin);
     257           0 :         } catch (Exception e) {
     258             :           // This shouldn't happen, as the plugin was loaded earlier.
     259           0 :           logger.atWarning().withCause(e.getCause()).log(
     260           0 :               "Cannot load disabled plugin %s", active.getName());
     261           1 :         }
     262           1 :       }
     263           2 :       cleanInBackground();
     264           2 :     }
     265           2 :   }
     266             : 
     267             :   public void enablePlugins(Set<String> names) throws PluginInstallException {
     268           2 :     if (!isRemoteAdminEnabled()) {
     269           0 :       logger.atWarning().log(
     270             :           "Remote plugin administration is disabled, ignoring enablePlugins(%s)", names);
     271           0 :       return;
     272             :     }
     273             : 
     274           2 :     synchronized (this) {
     275           2 :       for (String name : names) {
     276           2 :         Plugin off = disabled.get(name);
     277           2 :         if (off == null) {
     278           1 :           continue;
     279             :         }
     280             : 
     281           1 :         logger.atInfo().log("Enabling plugin %s", name);
     282           1 :         String n = off.getSrcFile().toFile().getName();
     283           1 :         if (n.endsWith(".disabled")) {
     284           1 :           n = n.substring(0, n.lastIndexOf('.'));
     285             :         }
     286           1 :         Path on = pluginsDir.resolve(n);
     287             :         try {
     288           1 :           Files.move(off.getSrcFile(), on);
     289           0 :         } catch (IOException e) {
     290           0 :           logger.atSevere().withCause(e).log("Failed to move plugin %s into place", name);
     291           0 :           continue;
     292           1 :         }
     293           1 :         disabled.remove(name);
     294           1 :         runPlugin(name, on, null);
     295           1 :       }
     296           2 :       cleanInBackground();
     297           2 :     }
     298           2 :   }
     299             : 
     300             :   private void removeStalePluginFiles() {
     301         138 :     DirectoryStream.Filter<Path> filter =
     302           0 :         entry -> entry.getFileName().toString().startsWith("plugin_");
     303          15 :     try (DirectoryStream<Path> files = Files.newDirectoryStream(tempDir, filter)) {
     304          15 :       for (Path file : files) {
     305           0 :         logger.atInfo().log("Removing stale plugin file: %s", file.toFile().getName());
     306             :         try {
     307           0 :           Files.delete(file);
     308           0 :         } catch (IOException e) {
     309           0 :           logger.atSevere().log(
     310           0 :               "Failed to remove stale plugin file %s: %s", file.toFile().getName(), e.getMessage());
     311           0 :         }
     312           0 :       }
     313         132 :     } catch (IOException e) {
     314         132 :       logger.atWarning().log("Unable to discover stale plugin files: %s", e.getMessage());
     315          15 :     }
     316         138 :   }
     317             : 
     318             :   @Override
     319             :   public synchronized void start() {
     320         138 :     removeStalePluginFiles();
     321         138 :     Path absolutePath = pluginsDir.toAbsolutePath();
     322         138 :     if (!Files.exists(absolutePath)) {
     323         132 :       logger.atInfo().log("%s does not exist; creating", absolutePath);
     324             :       try {
     325         132 :         Files.createDirectories(absolutePath);
     326           0 :       } catch (IOException e) {
     327           0 :         logger.atSevere().log("Failed to create %s: %s", absolutePath, e.getMessage());
     328         132 :       }
     329             :     }
     330         138 :     logger.atInfo().log("Loading plugins from %s", absolutePath);
     331         138 :     srvInfoImpl.state = ServerInformation.State.STARTUP;
     332         138 :     rescan();
     333         138 :     srvInfoImpl.state = ServerInformation.State.RUNNING;
     334         138 :     if (scanner != null) {
     335           0 :       scanner.start();
     336             :     }
     337         138 :   }
     338             : 
     339             :   @Override
     340             :   public void stop() {
     341         138 :     if (scanner != null) {
     342           0 :       scanner.end();
     343             :     }
     344         138 :     srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
     345         138 :     synchronized (this) {
     346         138 :       for (Plugin p : running.values()) {
     347           3 :         unloadPlugin(p);
     348           3 :       }
     349         138 :       running.clear();
     350         138 :       disabled.clear();
     351         138 :       broken.clear();
     352         138 :       if (!toCleanup.isEmpty()) {
     353           3 :         System.gc();
     354           3 :         processPendingCleanups();
     355             :       }
     356         138 :     }
     357         138 :   }
     358             : 
     359             :   public void reload(List<String> names) throws InvalidPluginException, PluginInstallException {
     360           1 :     synchronized (this) {
     361           1 :       List<Plugin> reload = Lists.newArrayListWithCapacity(names.size());
     362           1 :       List<String> bad = Lists.newArrayListWithExpectedSize(4);
     363           1 :       for (String name : names) {
     364           1 :         Plugin active = running.get(name);
     365           1 :         if (active != null) {
     366           1 :           reload.add(active);
     367             :         } else {
     368           0 :           bad.add(name);
     369             :         }
     370           1 :       }
     371           1 :       if (!bad.isEmpty()) {
     372           0 :         throw new InvalidPluginException(
     373           0 :             String.format("Plugin(s) \"%s\" not running", Joiner.on("\", \"").join(bad)));
     374             :       }
     375             : 
     376           1 :       for (Plugin active : reload) {
     377           1 :         String name = active.getName();
     378             :         try {
     379           1 :           logger.atInfo().log("Reloading plugin %s", name);
     380           1 :           Plugin newPlugin = runPlugin(name, active.getSrcFile(), active);
     381           1 :           logger.atInfo().log(
     382           1 :               "Reloaded plugin %s, version %s", newPlugin.getName(), newPlugin.getVersion());
     383           0 :         } catch (PluginInstallException e) {
     384           0 :           logger.atWarning().withCause(e.getCause()).log("Cannot reload plugin %s", name);
     385           0 :           throw e;
     386           1 :         }
     387           1 :       }
     388             : 
     389           1 :       cleanInBackground();
     390           1 :     }
     391           1 :   }
     392             : 
     393             :   public synchronized void rescan() {
     394         138 :     Set<String> loadedPlugins = new HashSet<>();
     395         138 :     SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
     396             : 
     397         138 :     if (!pluginsFiles.isEmpty()) {
     398           0 :       syncDisabledPlugins(pluginsFiles);
     399             : 
     400           0 :       Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
     401           0 :       for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
     402           0 :         String name = entry.getKey();
     403           0 :         Path path = entry.getValue();
     404           0 :         String fileName = path.getFileName().toString();
     405           0 :         if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
     406           0 :           logger.atWarning().log(
     407             :               "No Plugin provider was found that handles this file format: %s", fileName);
     408           0 :           continue;
     409             :         }
     410             : 
     411           0 :         FileSnapshot brokenTime = broken.get(name);
     412           0 :         if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
     413           0 :           continue;
     414             :         }
     415             : 
     416           0 :         Plugin active = running.get(name);
     417           0 :         if (active != null && !active.isModified(path)) {
     418           0 :           loadedPlugins.add(name);
     419           0 :           continue;
     420             :         }
     421             : 
     422           0 :         if (active != null) {
     423           0 :           logger.atInfo().log("Reloading plugin %s", active.getName());
     424             :         }
     425             : 
     426             :         try {
     427           0 :           Plugin loadedPlugin = runPlugin(name, path, active);
     428           0 :           if (!loadedPlugin.isDisabled()) {
     429           0 :             loadedPlugins.add(name);
     430           0 :             logger.atInfo().log(
     431             :                 "%s plugin %s, version %s",
     432           0 :                 active == null ? "Loaded" : "Reloaded",
     433           0 :                 loadedPlugin.getName(),
     434           0 :                 loadedPlugin.getVersion());
     435             :           }
     436           0 :         } catch (PluginInstallException e) {
     437           0 :           logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
     438           0 :         }
     439           0 :       }
     440             :     }
     441             : 
     442         138 :     Set<String> missingMandatory = Sets.difference(mandatoryPlugins.asSet(), loadedPlugins);
     443         138 :     if (!missingMandatory.isEmpty()) {
     444           1 :       throw new MissingMandatoryPluginsException(missingMandatory);
     445             :     }
     446             : 
     447         138 :     cleanInBackground();
     448         138 :   }
     449             : 
     450             :   private void addAllEntries(Map<String, Path> from, TreeSet<Map.Entry<String, Path>> to) {
     451           0 :     Iterator<Map.Entry<String, Path>> it = from.entrySet().iterator();
     452           0 :     while (it.hasNext()) {
     453           0 :       Map.Entry<String, Path> entry = it.next();
     454           0 :       to.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue()));
     455           0 :     }
     456           0 :   }
     457             : 
     458             :   private TreeSet<Map.Entry<String, Path>> jarsFirstSortedPluginsSet(
     459             :       Map<String, Path> activePlugins) {
     460           0 :     TreeSet<Map.Entry<String, Path>> sortedPlugins =
     461           0 :         Sets.newTreeSet(
     462           0 :             new Comparator<Map.Entry<String, Path>>() {
     463             :               @Override
     464             :               public int compare(Map.Entry<String, Path> e1, Map.Entry<String, Path> e2) {
     465           0 :                 Path n1 = e1.getValue().getFileName();
     466           0 :                 Path n2 = e2.getValue().getFileName();
     467           0 :                 return ComparisonChain.start()
     468           0 :                     .compareTrueFirst(isJar(n1), isJar(n2))
     469           0 :                     .compare(n1, n2)
     470           0 :                     .result();
     471             :               }
     472             : 
     473             :               private boolean isJar(Path n1) {
     474           0 :                 return n1.toString().endsWith(".jar");
     475             :               }
     476             :             });
     477             : 
     478           0 :     addAllEntries(activePlugins, sortedPlugins);
     479           0 :     return sortedPlugins;
     480             :   }
     481             : 
     482             :   private void syncDisabledPlugins(SetMultimap<String, Path> jars) {
     483           0 :     stopRemovedPlugins(jars);
     484           0 :     dropRemovedDisabledPlugins(jars);
     485           0 :   }
     486             : 
     487             :   private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin)
     488             :       throws PluginInstallException {
     489           3 :     FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
     490             :     try {
     491           3 :       boolean restartRequired = oldPlugin != null && !oldPlugin.canReload();
     492           3 :       if (restartRequired && mandatoryPlugins.contains(name)) {
     493           0 :         logger.atWarning().log("Restarting mandatory plugin %s not allowed", name);
     494           0 :         return oldPlugin;
     495             :       }
     496             : 
     497           3 :       Plugin newPlugin = loadPlugin(name, plugin, snapshot);
     498           3 :       if (newPlugin.getCleanupHandle() != null) {
     499           1 :         cleanupHandles.put(newPlugin, newPlugin.getCleanupHandle());
     500             :       }
     501             :       /*
     502             :        * Pluggable plugin provider may have assigned a plugin name that could be
     503             :        * actually different from the initial one assigned during scan. It is
     504             :        * safer then to reassign it.
     505             :        */
     506           3 :       name = newPlugin.getName();
     507           3 :       boolean reload = oldPlugin != null && oldPlugin.canReload() && newPlugin.canReload();
     508           3 :       if (!reload && oldPlugin != null) {
     509           0 :         unloadPlugin(oldPlugin);
     510             :       }
     511           3 :       if (!newPlugin.isDisabled()) {
     512             :         try {
     513           3 :           newPlugin.start(env);
     514           0 :         } catch (Exception e) {
     515           0 :           newPlugin.stop(env);
     516           0 :           throw e;
     517           3 :         }
     518             :       }
     519           3 :       if (reload) {
     520           1 :         env.onReloadPlugin(oldPlugin, newPlugin);
     521           1 :         unloadPlugin(oldPlugin);
     522           3 :       } else if (!newPlugin.isDisabled()) {
     523           3 :         env.onStartPlugin(newPlugin);
     524             :       }
     525           3 :       if (!newPlugin.isDisabled()) {
     526           3 :         running.put(name, newPlugin);
     527             :       } else {
     528           0 :         disabled.put(name, newPlugin);
     529             :       }
     530           3 :       broken.remove(name);
     531           3 :       return newPlugin;
     532           1 :     } catch (Exception err) {
     533           1 :       broken.put(name, snapshot);
     534           1 :       throw new PluginInstallException(err);
     535             :     }
     536             :   }
     537             : 
     538             :   private void stopRemovedPlugins(SetMultimap<String, Path> jars) {
     539           0 :     Set<String> unload = Sets.newHashSet(running.keySet());
     540           0 :     for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
     541           0 :       for (Path path : entry.getValue()) {
     542           0 :         if (!path.getFileName().toString().endsWith(".disabled")) {
     543           0 :           unload.remove(entry.getKey());
     544             :         }
     545           0 :       }
     546           0 :     }
     547           0 :     for (String name : unload) {
     548           0 :       unloadPlugin(running.get(name));
     549           0 :     }
     550           0 :   }
     551             : 
     552             :   private void dropRemovedDisabledPlugins(SetMultimap<String, Path> jars) {
     553           0 :     Set<String> unload = Sets.newHashSet(disabled.keySet());
     554           0 :     for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
     555           0 :       for (Path path : entry.getValue()) {
     556           0 :         if (path.getFileName().toString().endsWith(".disabled")) {
     557           0 :           unload.remove(entry.getKey());
     558             :         }
     559           0 :       }
     560           0 :     }
     561           0 :     for (String name : unload) {
     562           0 :       disabled.remove(name);
     563           0 :     }
     564           0 :   }
     565             : 
     566             :   synchronized int processPendingCleanups() {
     567           3 :     Iterator<Plugin> iterator = toCleanup.iterator();
     568           3 :     while (iterator.hasNext()) {
     569           3 :       Plugin plugin = iterator.next();
     570           3 :       iterator.remove();
     571             : 
     572           3 :       CleanupHandle cleanupHandle = cleanupHandles.remove(plugin);
     573           3 :       if (cleanupHandle != null) {
     574           1 :         cleanupHandle.cleanup();
     575             :       }
     576           3 :     }
     577           3 :     return toCleanup.size();
     578             :   }
     579             : 
     580             :   private void cleanInBackground() {
     581         138 :     int cnt = toCleanup.size();
     582         138 :     if (0 < cnt) {
     583           2 :       cleaner.get().clean(cnt);
     584             :     }
     585         138 :   }
     586             : 
     587             :   private String getExtension(String name) {
     588           3 :     int ext = name.lastIndexOf('.');
     589           3 :     return 0 < ext ? name.substring(ext) : "";
     590             :   }
     591             : 
     592             :   private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot)
     593             :       throws InvalidPluginException {
     594           3 :     String pluginName = srcPlugin.getFileName().toString();
     595           3 :     if (isUiPlugin(pluginName)) {
     596           3 :       return loadJsPlugin(name, srcPlugin, snapshot);
     597           2 :     } else if (serverPluginFactory.handles(srcPlugin)) {
     598           1 :       return loadServerPlugin(srcPlugin, snapshot);
     599             :     } else {
     600           1 :       throw new InvalidPluginException(
     601           1 :           String.format("Unsupported plugin type: %s", srcPlugin.getFileName()));
     602             :     }
     603             :   }
     604             : 
     605             :   private Path getPluginDataDir(String name) {
     606           1 :     return dataDir.resolve(name);
     607             :   }
     608             : 
     609             :   private String getPluginCanonicalWebUrl(String name) {
     610           1 :     String canonicalWebUrl = urlProvider.get();
     611           1 :     if (Strings.isNullOrEmpty(canonicalWebUrl)) {
     612           0 :       return "/plugins/" + name;
     613             :     }
     614             : 
     615           1 :     String url =
     616           1 :         String.format(
     617           1 :             "%s/plugins/%s/", CharMatcher.is('/').trimTrailingFrom(canonicalWebUrl), name);
     618           1 :     return url;
     619             :   }
     620             : 
     621             :   private Plugin loadJsPlugin(String name, Path srcJar, FileSnapshot snapshot) {
     622           3 :     return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot);
     623             :   }
     624             : 
     625             :   private ServerPlugin loadServerPlugin(Path scriptFile, FileSnapshot snapshot)
     626             :       throws InvalidPluginException {
     627           1 :     String name = serverPluginFactory.getPluginName(scriptFile);
     628           1 :     return serverPluginFactory.get(
     629             :         scriptFile,
     630             :         snapshot,
     631             :         new PluginDescription(
     632           1 :             pluginUserFactory.create(name),
     633           1 :             getPluginCanonicalWebUrl(name),
     634           1 :             getPluginDataDir(name),
     635             :             gerritRuntime));
     636             :   }
     637             : 
     638             :   // Only one active plugin per plugin name can exist for each plugin name.
     639             :   // Filter out disabled plugins and transform the multimap to a map
     640             :   private Map<String, Path> filterDisabled(SetMultimap<String, Path> pluginPaths) {
     641           0 :     Map<String, Path> activePlugins = Maps.newHashMapWithExpectedSize(pluginPaths.keys().size());
     642           0 :     for (String name : pluginPaths.keys()) {
     643           0 :       for (Path pluginPath : pluginPaths.asMap().get(name)) {
     644           0 :         if (!pluginPath.getFileName().toString().endsWith(".disabled")) {
     645           0 :           assert !activePlugins.containsKey(name);
     646           0 :           activePlugins.put(name, pluginPath);
     647             :         }
     648           0 :       }
     649           0 :     }
     650           0 :     return activePlugins;
     651             :   }
     652             : 
     653             :   // Scan the $site_path/plugins directory and fetch all files and directories.
     654             :   // The Key in returned multimap is the plugin name initially assigned from its filename.
     655             :   // Values are the files. Plugins can optionally provide their name in MANIFEST file.
     656             :   // If multiple plugin files provide the same plugin name, then only
     657             :   // the first plugin remains active and all other plugins with the same
     658             :   // name are disabled.
     659             :   //
     660             :   // NOTE: Bear in mind that the plugin name can be reassigned after load by the
     661             :   //       Server plugin provider.
     662             :   public SetMultimap<String, Path> prunePlugins(Path pluginsDir) {
     663         138 :     List<Path> pluginPaths = scanPathsInPluginsDirectory(pluginsDir);
     664             :     SetMultimap<String, Path> map;
     665         138 :     map = asMultimap(pluginPaths);
     666         138 :     for (String plugin : map.keySet()) {
     667           0 :       Collection<Path> files = map.asMap().get(plugin);
     668           0 :       if (files.size() == 1) {
     669           0 :         continue;
     670             :       }
     671             :       // retrieve enabled plugins
     672           0 :       Iterable<Path> enabled = filterDisabledPlugins(files);
     673             :       // If we have only one (the winner) plugin, nothing to do
     674           0 :       if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
     675           0 :         continue;
     676             :       }
     677           0 :       Path winner = Iterables.getFirst(enabled, null);
     678           0 :       assert winner != null;
     679             :       // Disable all loser plugins by renaming their file names to
     680             :       // "file.disabled" and replace the disabled files in the multimap.
     681           0 :       Collection<Path> elementsToRemove = new ArrayList<>();
     682           0 :       Collection<Path> elementsToAdd = new ArrayList<>();
     683           0 :       for (Path loser : Iterables.skip(enabled, 1)) {
     684           0 :         logger.atWarning().log(
     685             :             "Plugin <%s> was disabled, because"
     686             :                 + " another plugin <%s>"
     687             :                 + " with the same name <%s> already exists",
     688             :             loser, winner, plugin);
     689           0 :         Path disabledPlugin = Paths.get(loser + ".disabled");
     690           0 :         elementsToAdd.add(disabledPlugin);
     691           0 :         elementsToRemove.add(loser);
     692             :         try {
     693           0 :           Files.move(loser, disabledPlugin);
     694           0 :         } catch (IOException e) {
     695           0 :           logger.atWarning().withCause(e).log("Failed to fully disable plugin %s", loser);
     696           0 :         }
     697           0 :       }
     698           0 :       Iterables.removeAll(files, elementsToRemove);
     699           0 :       Iterables.addAll(files, elementsToAdd);
     700           0 :     }
     701         138 :     return map;
     702             :   }
     703             : 
     704             :   private List<Path> scanPathsInPluginsDirectory(Path pluginsDir) {
     705             :     try {
     706         138 :       return PluginUtil.listPlugins(pluginsDir);
     707           0 :     } catch (IOException e) {
     708           0 :       logger.atSevere().withCause(e).log("Cannot list %s", pluginsDir.toAbsolutePath());
     709           0 :       return ImmutableList.of();
     710             :     }
     711             :   }
     712             : 
     713             :   private Iterable<Path> filterDisabledPlugins(Collection<Path> paths) {
     714           0 :     return Iterables.filter(paths, p -> !p.getFileName().toString().endsWith(".disabled"));
     715             :   }
     716             : 
     717             :   @Nullable
     718             :   public String getGerritPluginName(Path srcPath) {
     719           3 :     String fileName = srcPath.getFileName().toString();
     720           3 :     if (isUiPlugin(fileName)) {
     721           0 :       return fileName.substring(0, fileName.lastIndexOf('.'));
     722             :     }
     723           3 :     if (serverPluginFactory.handles(srcPath)) {
     724           0 :       return serverPluginFactory.getPluginName(srcPath);
     725             :     }
     726           3 :     return null;
     727             :   }
     728             : 
     729             :   private SetMultimap<String, Path> asMultimap(List<Path> plugins) {
     730         138 :     SetMultimap<String, Path> map = LinkedHashMultimap.create();
     731         138 :     for (Path srcPath : plugins) {
     732           0 :       map.put(getPluginName(srcPath), srcPath);
     733           0 :     }
     734         138 :     return map;
     735             :   }
     736             : 
     737             :   private boolean isUiPlugin(String name) {
     738           3 :     return isPlugin(name, "js");
     739             :   }
     740             : 
     741             :   private boolean isPlugin(String fileName, String ext) {
     742           3 :     String fullExt = "." + ext;
     743           3 :     return fileName.endsWith(fullExt) || fileName.endsWith(fullExt + ".disabled");
     744             :   }
     745             : 
     746             :   private void checkRemoteInstall() throws PluginInstallException {
     747           3 :     if (!isRemoteAdminEnabled()) {
     748           0 :       throw new PluginInstallException("remote installation is disabled");
     749             :     }
     750           3 :   }
     751             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750