Permit restarting a specific plugin

If a plugin's configuration is modified and the plugin needs to be
reloaded to pick up this new data, permit administrators to tickle the
plugin reload process by supplying the plugin name as an argument for
`gerrit plugin reload`.

Change-Id: If879f5bb8b4913fb6d801a5feae960c25874e4b3
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 54f0058..828a88d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -225,6 +227,40 @@
     }
   }
 
+  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 {
+          log.info(String.format("Reloading plugin %s", name));
+          runPlugin(name, active.getSrcJar(), active);
+        } catch (PluginInstallException e) {
+          log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
+          throw e;
+        }
+      }
+      System.gc();
+      processPendingCleanups();
+    }
+  }
+
   private synchronized boolean rescanImp() {
     List<File> jars = scanJarsInPluginsDirectory();
     boolean clean = stopRemovedPlugins(jars);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
index 4b76942..e01c6d8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -15,18 +15,37 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.PluginInstallException;
 import com.google.gerrit.server.plugins.PluginLoader;
 import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 final class PluginReloadCommand extends SshCommand {
+  @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
+  private List<String> names;
+
   @Inject
   private PluginLoader loader;
 
   @Override
-  protected void run() {
-    loader.rescan(true);
+  protected void run() throws UnloggedFailure {
+    if (names == null || names.isEmpty()) {
+      loader.rescan(true);
+    } else {
+      try {
+        loader.reload(names);
+      } catch (InvalidPluginException e) {
+        throw die(e.getMessage());
+      } catch (PluginInstallException e) {
+        throw die(e.getMessage());
+      }
+    }
   }
 }