Merge changes I688490ff,I267176cc,Ib0c1b40d,I8e211592,Iec165991

* changes:
  Unpack JARs for running servers in $site_path/tmp
  Support serving static/ and Documentation/ from plugins
  Allow plugins.checkFrequency = 0 to disable scanner
  Execute plugins from a copy in $site_path/tmp
  Atomic replace SSH and HTTP bindings during plugin reload
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index e7cc9e4..ddbddbe 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1680,6 +1680,22 @@
 By default, 1.
 
 
+[[plugins]]Section plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[plugins.checkFrequency]]plugins.checkFrequency::
++
+How often plugins should be examined for new plugins to load, removed
+plugins to be unloaded, or updated plugins to be reloaded.  Values can
+be specified using standard time unit abbreviations ('ms', 'sec',
+'min', etc.).
++
+If set to 0, automatic plugin reloading is disabled.  Administrators
+may force reloading with link:cmd-plugin.html[gerrit plugin reload].
++
+Default is 1 minute.
+
+
 [[receive]]Section receive
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 This section is used to set who can execute the 'receive-pack' and
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
index 1de330f..0ad90c2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.ServletModule;
@@ -21,10 +22,15 @@
 public class HttpPluginModule extends ServletModule {
   @Override
   protected void configureServlets() {
+    bind(HttpPluginServlet.class);
     serve("/plugins/*").with(HttpPluginServlet.class);
 
     bind(StartPluginListener.class)
       .annotatedWith(UniqueAnnotations.create())
       .to(HttpPluginServlet.class);
+
+    bind(ReloadPluginListener.class)
+      .annotatedWith(UniqueAnnotations.create())
+      .to(HttpPluginServlet.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index b73d6e6..86f886c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -17,18 +17,29 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.server.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.RegistrationHandle;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.GuiceFilter;
 
+import eu.medsea.mimeutil.MimeType;
+
+import org.eclipse.jgit.util.IO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.List;
 import java.util.concurrent.ConcurrentMap;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletConfig;
@@ -42,16 +53,22 @@
 
 @Singleton
 class HttpPluginServlet extends HttpServlet
-    implements StartPluginListener {
+    implements StartPluginListener, ReloadPluginListener {
   private static final long serialVersionUID = 1L;
   private static final Logger log
       = LoggerFactory.getLogger(HttpPluginServlet.class);
 
+  private final MimeUtilFileTypeRegistry mimeUtil;
   private List<Plugin> pending = Lists.newArrayList();
   private String base;
-  private final ConcurrentMap<String, GuiceFilter> plugins
+  private final ConcurrentMap<String, PluginHolder> plugins
       = Maps.newConcurrentMap();
 
+  @Inject
+  HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil) {
+    this.mimeUtil = mimeUtil;
+  }
+
   @Override
   public synchronized void init(ServletConfig config) throws ServletException {
     super.init(config);
@@ -59,7 +76,7 @@
     String path = config.getServletContext().getContextPath();
     base = Strings.nullToEmpty(path) + "/plugins/";
     for (Plugin plugin : pending) {
-      start(plugin);
+      install(plugin);
     }
     pending = null;
   }
@@ -69,11 +86,29 @@
     if (pending != null) {
       pending.add(plugin);
     } else {
-      start(plugin);
+      install(plugin);
     }
   }
 
-  private void start(Plugin plugin) {
+  @Override
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    install(newPlugin);
+  }
+
+  private void install(Plugin plugin) {
+    GuiceFilter filter = load(plugin);
+    final String name = plugin.getName();
+    final PluginHolder holder = new PluginHolder(plugin, filter);
+    plugin.add(new RegistrationHandle() {
+      @Override
+      public void remove() {
+        plugins.remove(name, holder);
+      }
+    });
+    plugins.put(name, holder);
+  }
+
+  private GuiceFilter load(Plugin plugin) {
     if (plugin.getHttpInjector() != null) {
       final String name = plugin.getName();
       final GuiceFilter filter;
@@ -81,7 +116,7 @@
         filter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
       } catch (RuntimeException e) {
         log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e);
-        return;
+        return null;
       }
 
       try {
@@ -89,44 +124,113 @@
         filter.init(new WrappedFilterConfig(ctx));
       } catch (ServletException e) {
         log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
-        return;
+        return null;
       }
 
       plugin.add(new RegistrationHandle() {
         @Override
         public void remove() {
-          try {
-            filter.destroy();
-          } finally {
-            plugins.remove(name, filter);
-          }
+          filter.destroy();
         }
       });
-      plugins.put(name, filter);
+      return filter;
     }
+    return null;
   }
 
   @Override
   public void service(HttpServletRequest req, HttpServletResponse res)
       throws IOException, ServletException {
     String name = extractName(req);
-    GuiceFilter filter = plugins.get(name);
-    if (filter == null) {
+    final PluginHolder holder = plugins.get(name);
+    if (holder == null) {
       noCache(res);
       res.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
     }
 
-    filter.doFilter(new WrappedRequest(req, base + name), res,
-        new FilterChain() {
-          @Override
-          public void doFilter(ServletRequest req, ServletResponse response)
-              throws IOException, ServletException {
-            HttpServletResponse res = (HttpServletResponse) response;
-            noCache(res);
-            res.sendError(HttpServletResponse.SC_NOT_FOUND);
+    WrappedRequest wr = new WrappedRequest(req, base + name);
+    FilterChain chain = new FilterChain() {
+      @Override
+      public void doFilter(ServletRequest req, ServletResponse res)
+          throws IOException {
+        onDefault(holder, (HttpServletRequest) req, (HttpServletResponse) res);
+      }
+    };
+    if (holder.filter != null) {
+      holder.filter.doFilter(wr, res, chain);
+    } else {
+      chain.doFilter(wr, res);
+    }
+  }
+
+  private void onDefault(PluginHolder holder,
+      HttpServletRequest req,
+      HttpServletResponse res) throws IOException {
+    String uri = req.getRequestURI();
+    String ctx = req.getContextPath();
+    String file = uri.substring(ctx.length() + 1);
+    if (file.startsWith("Documentation/") || file.startsWith("static/")) {
+      JarFile jar = holder.plugin.getJarFile();
+      JarEntry entry = jar.getJarEntry(file);
+      if (entry != null && entry.getSize() > 0) {
+        sendResource(jar, entry, res);
+        return;
+      }
+    }
+
+    noCache(res);
+    res.sendError(HttpServletResponse.SC_NOT_FOUND);
+  }
+
+  private void sendResource(JarFile jar, JarEntry entry, HttpServletResponse res)
+      throws IOException {
+    byte[] data = null;
+    if (entry.getSize() <= 128 * 1024) {
+      data = new byte[(int) entry.getSize()];
+      InputStream in = jar.getInputStream(entry);
+      try {
+        IO.readFully(in, data, 0, data.length);
+      } finally {
+        in.close();
+      }
+    }
+
+    String contentType = null;
+    Attributes atts = entry.getAttributes();
+    if (atts != null) {
+      contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
+    }
+    if (contentType == null) {
+      MimeType type = mimeUtil.getMimeType(entry.getName(), data);
+      contentType = type.toString();
+    }
+
+    long time = entry.getTime();
+    if (0 < time) {
+      res.setDateHeader("Last-Modified", time);
+    }
+    res.setContentType(contentType);
+    res.setHeader("Content-Length", Long.toString(entry.getSize()));
+    if (data != null) {
+      res.getOutputStream().write(data);
+    } else {
+      InputStream in = jar.getInputStream(entry);
+      try {
+        OutputStream out = res.getOutputStream();
+        try {
+          byte[] tmp = new byte[1024];
+          int n;
+          while ((n = in.read(tmp)) > 0) {
+            out.write(tmp, 0, n);
           }
-        });
+        } finally {
+          out.close();
+        }
+      } finally {
+        in.close();
+      }
+    }
   }
 
   private static String extractName(HttpServletRequest req) {
@@ -145,6 +249,16 @@
     res.setHeader("Content-Disposition", "attachment");
   }
 
+  private static class PluginHolder {
+    final Plugin plugin;
+    final GuiceFilter filter;
+
+    PluginHolder(Plugin plugin, GuiceFilter filter) {
+      this.plugin = plugin;
+      this.filter = filter;
+    }
+  }
+
   private static class WrappedRequest extends HttpServletRequestWrapper {
     private final String contextPath;
 
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 7f2007e..61bb52f 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -423,36 +423,42 @@
   }
 
   private static File tmproot() {
-    // Try to find the user's home directory. If we can't find it
-    // return null so the JVM's default temporary directory is used
-    // instead. This is probably /tmp or /var/tmp.
-    //
-    String userHome = System.getProperty("user.home");
-    if (userHome == null || "".equals(userHome)) {
-      userHome = System.getenv("HOME");
+    File tmp;
+    String gerritTemp = System.getenv("GERRIT_TMP");
+    if (gerritTemp != null && gerritTemp.length() > 0) {
+      tmp = new File(gerritTemp);
+    } else {
+      // Try to find the user's home directory. If we can't find it
+      // return null so the JVM's default temporary directory is used
+      // instead. This is probably /tmp or /var/tmp.
+      //
+      String userHome = System.getProperty("user.home");
       if (userHome == null || "".equals(userHome)) {
-        System.err.println("warning: cannot determine home directory");
-        System.err.println("warning: using system temporary directory instead");
-        return null;
+        userHome = System.getenv("HOME");
+        if (userHome == null || "".equals(userHome)) {
+          System.err.println("warning: cannot determine home directory");
+          System.err.println("warning: using system temporary directory instead");
+          return null;
+        }
       }
-    }
 
-    // Ensure the home directory exists. If it doesn't, try to make it.
-    //
-    final File home = new File(userHome);
-    if (!home.exists()) {
-      if (home.mkdirs()) {
-        System.err.println("warning: created " + home.getAbsolutePath());
-      } else {
-        System.err.println("warning: " + home.getAbsolutePath() + " not found");
-        System.err.println("warning: using system temporary directory instead");
-        return null;
+      // Ensure the home directory exists. If it doesn't, try to make it.
+      //
+      final File home = new File(userHome);
+      if (!home.exists()) {
+        if (home.mkdirs()) {
+          System.err.println("warning: created " + home.getAbsolutePath());
+        } else {
+          System.err.println("warning: " + home.getAbsolutePath() + " not found");
+          System.err.println("warning: using system temporary directory instead");
+          return null;
+        }
       }
-    }
 
-    // Use $HOME/.gerritcodereview/tmp for our temporary file area.
-    //
-    final File tmp = new File(new File(home, ".gerritcodereview"), "tmp");
+      // Use $HOME/.gerritcodereview/tmp for our temporary file area.
+      //
+      tmp = new File(new File(home, ".gerritcodereview"), "tmp");
+    }
     if (!tmp.exists() && !tmp.mkdirs()) {
       System.err.println("warning: cannot create " + tmp.getAbsolutePath());
       System.err.println("warning: using system temporary directory instead");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 3201139..d26d46e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -67,6 +67,7 @@
     mkdir(site.bin_dir);
     mkdir(site.etc_dir);
     mkdir(site.lib_dir);
+    mkdir(site.tmp_dir);
     mkdir(site.logs_dir);
     mkdir(site.mail_dir);
     mkdir(site.static_dir);
@@ -85,6 +86,7 @@
 
     extract(site.gerrit_sh, Init.class, "gerrit.sh");
     chmod(0755, site.gerrit_sh);
+    chmod(0700, site.tmp_dir);
 
     extractMailExample("Abandoned.vm");
     extractMailExample("ChangeFooter.vm");
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
index 51b7d1a..3857ebd 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
@@ -176,6 +176,8 @@
 
 GERRIT_PID="$GERRIT_SITE/logs/gerrit.pid"
 GERRIT_RUN="$GERRIT_SITE/logs/gerrit.run"
+GERRIT_TMP="$GERRIT_SITE/tmp"
+export GERRIT_TMP
 
 ##################################################
 # Check for JAVA_HOME
@@ -492,6 +494,7 @@
     echo "  GERRIT_SITE     =  $GERRIT_SITE"
     echo "  GERRIT_CONFIG   =  $GERRIT_CONFIG"
     echo "  GERRIT_PID      =  $GERRIT_PID"
+    echo "  GERRIT_TMP      =  $GERRIT_TMP"
     echo "  GERRIT_WAR      =  $GERRIT_WAR"
     echo "  GERRIT_FDS      =  $GERRIT_FDS"
     echo "  GERRIT_USER     =  $GERRIT_USER"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 9565831..4205420 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -28,6 +28,7 @@
   public final File bin_dir;
   public final File etc_dir;
   public final File lib_dir;
+  public final File tmp_dir;
   public final File logs_dir;
   public final File plugins_dir;
   public final File mail_dir;
@@ -63,6 +64,7 @@
     bin_dir = new File(site_path, "bin");
     etc_dir = new File(site_path, "etc");
     lib_dir = new File(site_path, "lib");
+    tmp_dir = new File(site_path, "tmp");
     plugins_dir = new File(site_path, "plugins");
     logs_dir = new File(site_path, "logs");
     mail_dir = new File(etc_dir, "mail");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
new file mode 100644
index 0000000..e18d840
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -0,0 +1,45 @@
+// 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 java.io.File;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.jar.JarFile;
+
+class CleanupHandle extends WeakReference<ClassLoader> {
+  private final File tmpFile;
+  private final JarFile jarFile;
+
+  CleanupHandle(File tmpFile,
+      JarFile jarFile,
+      ClassLoader ref,
+      ReferenceQueue<ClassLoader> queue) {
+    super(ref, queue);
+    this.tmpFile = tmpFile;
+    this.jarFile = jarFile;
+  }
+
+  void cleanup() {
+    try {
+      jarFile.close();
+    } catch (IOException err) {
+    }
+    if (!tmpFile.delete() && tmpFile.exists()) {
+      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index e9a6308..9e8da32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.lifecycle.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.inject.AbstractModule;
@@ -26,6 +27,7 @@
 
 import java.io.File;
 import java.util.jar.Attributes;
+import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 
 import javax.annotation.Nullable;
@@ -39,9 +41,10 @@
   }
 
   private final String name;
-  private final File jar;
-  private final Manifest manifest;
+  private final File srcJar;
   private final FileSnapshot snapshot;
+  private final JarFile jarFile;
+  private final Manifest manifest;
   private Class<? extends Module> sysModule;
   private Class<? extends Module> sshModule;
   private Class<? extends Module> httpModule;
@@ -52,23 +55,25 @@
   private LifecycleManager manager;
 
   public Plugin(String name,
-      File jar,
-      Manifest manifest,
+      File srcJar,
       FileSnapshot snapshot,
+      JarFile jarFile,
+      Manifest manifest,
       @Nullable Class<? extends Module> sysModule,
       @Nullable Class<? extends Module> sshModule,
       @Nullable Class<? extends Module> httpModule) {
     this.name = name;
-    this.jar = jar;
-    this.manifest = manifest;
+    this.srcJar = srcJar;
     this.snapshot = snapshot;
+    this.jarFile = jarFile;
+    this.manifest = manifest;
     this.sysModule = sysModule;
     this.sshModule = sshModule;
     this.httpModule = httpModule;
   }
 
-  File getJar() {
-    return jar;
+  File getSrcJar() {
+    return srcJar;
   }
 
   public String getName() {
@@ -80,6 +85,21 @@
     return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
   }
 
+  boolean canReload() {
+    Attributes main = manifest.getMainAttributes();
+    String v = main.getValue("Gerrit-ReloadMode");
+    if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
+      return true;
+    } else if ("restart".equalsIgnoreCase(v)) {
+      return false;
+    } else {
+      PluginLoader.log.warn(String.format(
+          "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart",
+          name, v));
+      return false;
+    }
+  }
+
   boolean isModified(File jar) {
     return snapshot.lastModified() != jar.lastModified();
   }
@@ -110,7 +130,6 @@
     }
 
     manager.start();
-    env.onStartPlugin(this);
   }
 
   private Injector newRootInjector(PluginGuiceEnvironment env) {
@@ -136,6 +155,10 @@
     }
   }
 
+  public JarFile getJarFile() {
+    return jarFile;
+  }
+
   @Nullable
   public Injector getSshInjector() {
     return sshInjector;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 4b6f497..0e8a95d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -42,7 +42,8 @@
 public class PluginGuiceEnvironment {
   private final Injector sysInjector;
   private final CopyConfigModule copyConfigModule;
-  private final List<StartPluginListener> listeners;
+  private final List<StartPluginListener> onStart;
+  private final List<ReloadPluginListener> onReload;
   private Module sysModule;
   private Module sshModule;
   private Module httpModule;
@@ -51,8 +52,12 @@
   PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) {
     this.sysInjector = sysInjector;
     this.copyConfigModule = ccm;
-    this.listeners = new CopyOnWriteArrayList<StartPluginListener>();
-    this.listeners.addAll(getListeners(sysInjector));
+
+    onStart = new CopyOnWriteArrayList<StartPluginListener>();
+    onStart.addAll(listeners(sysInjector, StartPluginListener.class));
+
+    onReload = new CopyOnWriteArrayList<ReloadPluginListener>();
+    onReload.addAll(listeners(sysInjector, ReloadPluginListener.class));
   }
 
   Module getSysModule() {
@@ -72,9 +77,10 @@
     };
   }
 
-  public void setSshInjector(Injector sshInjector) {
-    sshModule = copy(sshInjector);
-    listeners.addAll(getListeners(sshInjector));
+  public void setSshInjector(Injector injector) {
+    sshModule = copy(injector);
+    onStart.addAll(listeners(injector, StartPluginListener.class));
+    onReload.addAll(listeners(injector, ReloadPluginListener.class));
   }
 
   boolean hasSshModule() {
@@ -85,9 +91,10 @@
     return sshModule;
   }
 
-  public void setHttpInjector(Injector httpInjector) {
-    httpModule = copy(httpInjector);
-    listeners.addAll(getListeners(httpInjector));
+  public void setHttpInjector(Injector injector) {
+    httpModule = copy(injector);
+    onStart.addAll(listeners(injector, StartPluginListener.class));
+    onReload.addAll(listeners(injector, ReloadPluginListener.class));
   }
 
   boolean hasHttpModule() {
@@ -99,17 +106,21 @@
   }
 
   void onStartPlugin(Plugin plugin) {
-    for (StartPluginListener l : listeners) {
+    for (StartPluginListener l : onStart) {
       l.onStartPlugin(plugin);
     }
   }
 
-  private static List<StartPluginListener> getListeners(Injector src) {
-    List<Binding<StartPluginListener>> bindings =
-        src.findBindingsByType(new TypeLiteral<StartPluginListener>() {});
-    List<StartPluginListener> found =
-        Lists.newArrayListWithCapacity(bindings.size());
-    for (Binding<StartPluginListener> b : bindings) {
+  void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    for (ReloadPluginListener l : onReload) {
+      l.onReloadPlugin(oldPlugin, newPlugin);
+    }
+  }
+
+  private static <T> List<T> listeners(Injector src, Class<T> type) {
+    List<Binding<T>> bindings = src.findBindingsByType(TypeLiteral.get(type));
+    List<T> found = Lists.newArrayListWithCapacity(bindings.size());
+    for (Binding<T> b : bindings) {
       found.add(b.getProvider().get());
     }
     return found;
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 2ee6b04..330dc46 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
@@ -33,16 +33,21 @@
 
 import java.io.File;
 import java.io.FileFilter;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.ref.ReferenceQueue;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
@@ -50,12 +55,15 @@
 
 @Singleton
 public class PluginLoader implements LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
+  static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
   private final File pluginsDir;
+  private final File tmpDir;
   private final PluginGuiceEnvironment env;
   private final Map<String, Plugin> running;
   private final Map<String, FileSnapshot> broken;
+  private final ReferenceQueue<ClassLoader> cleanupQueue;
+  private final ConcurrentMap<CleanupHandle, Boolean> cleanupHandles;
   private final PluginScannerThread scanner;
 
   @Inject
@@ -63,14 +71,21 @@
       PluginGuiceEnvironment pe,
       @GerritServerConfig Config cfg) {
     pluginsDir = sitePaths.plugins_dir;
+    tmpDir = sitePaths.tmp_dir;
     env = pe;
     running = Maps.newHashMap();
     broken = Maps.newHashMap();
-    scanner = new PluginScannerThread(
-        this,
-        ConfigUtil.getTimeUnit(cfg,
-            "plugins", null, "checkFrequency",
-            TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS));
+    cleanupQueue = new ReferenceQueue<ClassLoader>();
+    cleanupHandles = Maps.newConcurrentMap();
+
+    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 synchronized List<Plugin> getPlugins() {
@@ -87,38 +102,41 @@
     name = nameOf(jar);
 
     File old = new File(pluginsDir, ".last_" + name + ".zip");
-    File tmp = copyToTemp(name, in);
-
+    File tmp = asTemp(in, ".next_" + name, ".zip", pluginsDir);
+    boolean clean = false;
     synchronized (this) {
       Plugin active = running.get(name);
       if (active != null) {
         log.info(String.format("Replacing plugin %s", name));
-        active.stop();
-        running.remove(name);
         old.delete();
         jar.renameTo(old);
       }
 
+      new File(pluginsDir, name + ".jar.disabled").delete();
       tmp.renameTo(jar);
-      FileSnapshot snapshot = FileSnapshot.save(jar);
-      Plugin next;
       try {
-        next = loadPlugin(name, snapshot, jar);
-        next.start(env);
-      } catch (Throwable err) {
+        runPlugin(name, jar, active);
+        if (active == null) {
+          log.info(String.format("Installed plugin %s", name));
+        } else {
+          clean = true;
+        }
+      } catch (PluginInstallException e) {
         jar.delete();
-        throw new PluginInstallException(err);
+        throw e;
       }
-      broken.remove(name);
-      running.put(name, next);
-      if (active == null) {
-        log.info(String.format("Installed plugin %s", name));
-      }
+    }
+
+    if (clean) {
+      System.gc();
+      processPendingCleanups();
     }
   }
 
-  private File copyToTemp(String name, InputStream in) throws IOException {
-    File tmp = File.createTempFile(".next_" + name, ".zip", pluginsDir);
+  private static File asTemp(InputStream in,
+      String prefix, String suffix,
+      File dir) throws IOException {
+    File tmp = File.createTempFile(prefix, suffix, dir);
     boolean keep = false;
     try {
       FileOutputStream out = new FileOutputStream(tmp);
@@ -140,45 +158,68 @@
     }
   }
 
-  public synchronized void disablePlugins(Set<String> names) {
-    for (String name : names) {
-      Plugin active = running.get(name);
-      if (active == null) {
-        continue;
+  public void disablePlugins(Set<String> names) {
+    boolean clean = false;
+    synchronized (this) {
+      for (String name : names) {
+        Plugin active = running.get(name);
+        if (active == null) {
+          continue;
+        }
+
+        log.info(String.format("Disabling plugin %s", name));
+        File off = new File(pluginsDir, active.getName() + ".jar.disabled");
+        active.getSrcJar().renameTo(off);
+
+        active.stop();
+        running.remove(name);
+        clean = true;
       }
-
-      log.info(String.format("Disabling plugin %s", name));
-      active.stop();
-      running.remove(name);
-
-      File off = new File(pluginsDir, active.getName() + ".jar.disabled");
-      active.getJar().renameTo(off);
+    }
+    if (clean) {
+      System.gc();
+      processPendingCleanups();
     }
   }
 
   @Override
   public synchronized void start() {
     log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
-    rescan();
-    scanner.start();
+    rescan(false);
+    if (scanner != null) {
+      scanner.start();
+    }
   }
 
   @Override
   public void stop() {
-    scanner.end();
+    if (scanner != null) {
+      scanner.end();
+    }
     synchronized (this) {
+      boolean clean = !running.isEmpty();
       for (Plugin p : running.values()) {
         p.stop();
       }
       running.clear();
       broken.clear();
+      if (clean) {
+        System.gc();
+        processPendingCleanups();
+      }
     }
   }
 
-  public synchronized void rescan() {
-    List<File> jars = scanJarsInPluginsDirectory();
+  public void rescan(boolean forceCleanup) {
+    if (rescanImp() || forceCleanup) {
+      System.gc();
+      processPendingCleanups();
+    }
+  }
 
-    stopRemovedPlugins(jars);
+  private synchronized boolean rescanImp() {
+    List<File> jars = scanJarsInPluginsDirectory();
+    boolean clean = stopRemovedPlugins(jars);
 
     for (File jar : jars) {
       String name = nameOf(jar);
@@ -193,35 +234,51 @@
       }
 
       if (active != null) {
-        log.warn(String.format(
-            "Detected %s was replaced/overwritten."
-            + " This is not a safe way to update a plugin.",
-            jar.getAbsolutePath()));
         log.info(String.format("Reloading plugin %s", name));
-        active.stop();
+      }
+
+      try {
+        runPlugin(name, jar, active);
+        if (active == null) {
+          log.info(String.format("Loaded plugin %s", name));
+        } else {
+          clean = true;
+        }
+      } catch (PluginInstallException e) {
+        log.warn(String.format("Cannot load plugin %s", name), e.getCause());
+      }
+    }
+    return clean;
+  }
+
+  private void runPlugin(String name, File jar, Plugin oldPlugin)
+      throws PluginInstallException {
+    FileSnapshot snapshot = FileSnapshot.save(jar);
+    try {
+      Plugin newPlugin = loadPlugin(name, jar, snapshot);
+      boolean reload = oldPlugin != null
+          && oldPlugin.canReload()
+          && newPlugin.canReload();
+      if (!reload && oldPlugin != null) {
+        oldPlugin.stop();
         running.remove(name);
       }
-
-      FileSnapshot snapshot = FileSnapshot.save(jar);
-      Plugin next;
-      try {
-        next = loadPlugin(name, snapshot, jar);
-        next.start(env);
-      } catch (Throwable err) {
-        log.warn(String.format("Cannot load plugin %s", name), err);
-        broken.put(name, snapshot);
-        continue;
+      newPlugin.start(env);
+      if (reload) {
+        env.onReloadPlugin(oldPlugin, newPlugin);
+        oldPlugin.stop();
+      } else {
+        env.onStartPlugin(newPlugin);
       }
+      running.put(name, newPlugin);
       broken.remove(name);
-      running.put(name, next);
-
-      if (active == null) {
-        log.info(String.format("Loaded plugin %s", name));
-      }
+    } catch (Throwable err) {
+      broken.put(name, snapshot);
+      throw new PluginInstallException(err);
     }
   }
 
-  private void stopRemovedPlugins(List<File> jars) {
+  private boolean stopRemovedPlugins(List<File> jars) {
     Set<String> unload = Sets.newHashSet(running.keySet());
     for (File jar : jars) {
       unload.remove(nameOf(jar));
@@ -230,6 +287,15 @@
       log.info(String.format("Unloading plugin %s", name));
       running.remove(name).stop();
     }
+    return !unload.isEmpty();
+  }
+
+  private synchronized void processPendingCleanups() {
+    CleanupHandle h;
+    while ((h = (CleanupHandle) cleanupQueue.poll()) != null) {
+      h.cleanup();
+      cleanupHandles.remove(h);
+    }
   }
 
   private static String nameOf(File jar) {
@@ -238,24 +304,50 @@
     return 0 < ext ? name.substring(0, ext) : name;
   }
 
-  private Plugin loadPlugin(String name, FileSnapshot snapshot, File jarFile)
+  private Plugin loadPlugin(String name, File srcJar, FileSnapshot snapshot)
       throws IOException, ClassNotFoundException {
-    Manifest manifest = new JarFile(jarFile).getManifest();
+    File tmp;
+    FileInputStream in = new FileInputStream(srcJar);
+    try {
+      tmp = asTemp(in, tempNameFor(name), ".jar", tmpDir);
+    } finally {
+      in.close();
+    }
 
-    Attributes main = manifest.getMainAttributes();
-    String sysName = main.getValue("Gerrit-Module");
-    String sshName = main.getValue("Gerrit-SshModule");
-    String httpName = main.getValue("Gerrit-HttpModule");
+    JarFile jarFile = new JarFile(tmp);
+    boolean keep = false;
+    try {
+      Manifest manifest = jarFile.getManifest();
+      Attributes main = manifest.getMainAttributes();
+      String sysName = main.getValue("Gerrit-Module");
+      String sshName = main.getValue("Gerrit-SshModule");
+      String httpName = main.getValue("Gerrit-HttpModule");
 
-    URL[] urls = {jarFile.toURI().toURL()};
-    ClassLoader parentLoader = PluginLoader.class.getClassLoader();
-    ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
+      URL[] urls = {tmp.toURI().toURL()};
+      ClassLoader parentLoader = PluginLoader.class.getClassLoader();
+      ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
+      cleanupHandles.put(
+          new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
+          Boolean.TRUE);
 
-    Class<? extends Module> sysModule = load(sysName, pluginLoader);
-    Class<? extends Module> sshModule = load(sshName, pluginLoader);
-    Class<? extends Module> httpModule = load(httpName, pluginLoader);
-    return new Plugin(name, jarFile, manifest, snapshot,
-        sysModule, sshModule, httpModule);
+      Class<? extends Module> sysModule = load(sysName, pluginLoader);
+      Class<? extends Module> sshModule = load(sshName, pluginLoader);
+      Class<? extends Module> httpModule = load(httpName, pluginLoader);
+      keep = true;
+      return new Plugin(name,
+          srcJar, snapshot,
+          jarFile, manifest,
+          sysModule, sshModule, httpModule);
+    } finally {
+      if (!keep) {
+        jarFile.close();
+      }
+    }
+  }
+
+  private static String tempNameFor(String name) {
+    SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
+    return "plugin_" + name + "_" + fmt.format(new Date()) + "_";
   }
 
   private Class<? extends Module> load(String name, ClassLoader pluginLoader)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
index a484c5d..b2e3fed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -38,7 +38,7 @@
         }
       } catch (InterruptedException e) {
       }
-      loader.rescan();
+      loader.rescan(false);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
new file mode 100644
index 0000000..72a499e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
@@ -0,0 +1,20 @@
+// 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;
+
+/** Broadcasts event indicating a plugin was reloaded. */
+public interface ReloadPluginListener {
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin);
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index 349cc79..3560d99 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -72,6 +72,18 @@
     };
   }
 
+  public RegistrationHandle replace(final CommandName name,
+      final Provider<Command> cmd) {
+    final ConcurrentMap<String, Provider<Command>> m = getMap();
+    m.put(name.value(), cmd);
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        m.remove(name.value(), cmd);
+      }
+    };
+  }
+
   private ConcurrentMap<String, Provider<Command>> getMap() {
     if (map == null) {
       synchronized (this) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 2637529..cd78796 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.ssh.SshInfo;
@@ -93,9 +94,15 @@
     install(new LifecycleModule() {
       @Override
       protected void configure() {
+        bind(SshPluginStarterCallback.class);
         bind(StartPluginListener.class)
           .annotatedWith(UniqueAnnotations.create())
           .to(SshPluginStarterCallback.class);
+
+        bind(ReloadPluginListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(SshPluginStarterCallback.class);
+
         listener().to(SshLog.class);
         listener().to(SshDaemon.class);
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index b82eb8f..4f9fe33 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -27,7 +28,8 @@
 import javax.inject.Inject;
 
 @Singleton
-class SshPluginStarterCallback implements StartPluginListener {
+class SshPluginStarterCallback
+    implements StartPluginListener, ReloadPluginListener {
   private static final Logger log = LoggerFactory
       .getLogger(SshPluginStarterCallback.class);
 
@@ -41,17 +43,31 @@
 
   @Override
   public void onStartPlugin(Plugin plugin) {
-    if (plugin.getSshInjector() != null) {
-      Key<Command> key = Commands.key(plugin.getName());
-      Provider<Command> cmd;
-      try {
-        cmd = plugin.getSshInjector().getProvider(key);
-      } catch (RuntimeException err) {
-        log.warn(String.format("Plugin %s does not define command",
-            plugin.getName()), err);
-        return;
-      }
+    Provider<Command> cmd = load(plugin);
+    if (cmd != null) {
       plugin.add(root.register(Commands.named(plugin.getName()), cmd));
     }
   }
+
+  @Override
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    Provider<Command> cmd = load(newPlugin);
+    if (cmd != null) {
+      newPlugin.add(root.replace(Commands.named(newPlugin.getName()), cmd));
+    }
+  }
+
+  private Provider<Command> load(Plugin plugin) {
+    if (plugin.getSshInjector() != null) {
+      Key<Command> key = Commands.key(plugin.getName());
+      try {
+        return plugin.getSshInjector().getProvider(key);
+      } catch (RuntimeException err) {
+        log.warn(String.format(
+            "Plugin %s did not define its top-level command",
+            plugin.getName()), err);
+      }
+    }
+    return null;
+  }
 }
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 5486698..4b76942 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
@@ -27,6 +27,6 @@
 
   @Override
   protected void run() {
-    loader.rescan();
+    loader.rescan(true);
   }
 }