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);
}
}