SSH commands gerrit plugin {ls|install|remove|reload}

Administrators can use these commands to view loaded plugins, install
new plugins from any URL source the server can reach (including HTTP),
and disable plugins that might not required or are misbehaving.

When installing plugins the JAR can be supplied as stdin to the SSH
command, or as a URL. Either way the server copies the JAR into a
temporary file in the $site_dir/plugins directory, then safely does
a reload of the plugin by stopping the running plugin, replacing the
JAR on disk, and then starting the plugin back up. This is safer than
the rescan thread detecting changes made in the background.

Removing a plugin doesn't actually delete it, rather the plugin will
be renamed to have a ".disabled" suffix, preventing the scanner from
considering that plugin during a future server restart.

Change-Id: Iaa4cb9d7c28294fa616975093a0d4ffb1fcb2788
This commit is contained in:
Shawn O. Pearce
2012-05-08 22:44:25 -07:00
committed by gerrit code review
parent 1c748e2564
commit 5a99e04ccf
8 changed files with 364 additions and 1 deletions

View File

@@ -24,11 +24,15 @@ import com.google.inject.Module;
import org.eclipse.jgit.storage.file.FileSnapshot;
import java.io.File;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
public class Plugin {
private final String name;
private final File jar;
private final Manifest manifest;
private final FileSnapshot snapshot;
private Class<? extends Module> sysModule;
private Class<? extends Module> sshModule;
@@ -38,19 +42,32 @@ public class Plugin {
private LifecycleManager manager;
public Plugin(String name,
File jar,
Manifest manifest,
FileSnapshot snapshot,
@Nullable Class<? extends Module> sysModule,
@Nullable Class<? extends Module> sshModule) {
this.name = name;
this.jar = jar;
this.manifest = manifest;
this.snapshot = snapshot;
this.sysModule = sysModule;
this.sshModule = sshModule;
}
File getJar() {
return jar;
}
public String getName() {
return name;
}
public String getVersion() {
Attributes main = manifest.getMainAttributes();
return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
}
boolean isModified(File jar) {
return snapshot.lastModified() != jar.lastModified();
}

View File

@@ -0,0 +1,23 @@
// 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;
public class PluginInstallException extends Exception {
private static final long serialVersionUID = 1L;
public PluginInstallException(Throwable why) {
super(why.getMessage(), why);
}
}

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.server.plugins;
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.lifecycle.LifecycleListener;
@@ -32,7 +33,9 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
@@ -70,6 +73,89 @@ public class PluginLoader implements LifecycleListener {
TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS));
}
public synchronized List<Plugin> getPlugins() {
return Lists.newArrayList(running.values());
}
public void installPluginFromStream(String name, InputStream in)
throws IOException, PluginInstallException {
if (!name.endsWith(".jar")) {
name += ".jar";
}
File jar = new File(pluginsDir, name);
name = nameOf(jar);
File old = new File(pluginsDir, ".last_" + name + ".zip");
File tmp = copyToTemp(name, in);
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);
}
tmp.renameTo(jar);
FileSnapshot snapshot = FileSnapshot.save(jar);
Plugin next;
try {
next = loadPlugin(name, snapshot, jar);
next.start(env);
} catch (Throwable err) {
jar.delete();
throw new PluginInstallException(err);
}
broken.remove(name);
running.put(name, next);
if (active == null) {
log.info(String.format("Installed plugin %s", name));
}
}
}
private File copyToTemp(String name, InputStream in) throws IOException {
File tmp = File.createTempFile(".next_" + name, ".zip", pluginsDir);
boolean keep = false;
try {
FileOutputStream out = new FileOutputStream(tmp);
try {
byte[] data = new byte[8192];
int n;
while ((n = in.read(data)) > 0) {
out.write(data, 0, n);
}
keep = true;
return tmp;
} finally {
out.close();
}
} finally {
if (!keep) {
tmp.delete();
}
}
}
public synchronized void disablePlugins(Set<String> names) {
for (String name : names) {
Plugin active = running.get(name);
if (active == null) {
continue;
}
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);
}
}
@Override
public synchronized void start() {
log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
@@ -166,7 +252,7 @@ public class PluginLoader implements LifecycleListener {
Class<? extends Module> sysModule = load(sysName, pluginLoader);
Class<? extends Module> sshModule = load(sshName, pluginLoader);
return new Plugin(name, snapshot, sysModule, sshModule);
return new Plugin(name, jarFile, manifest, snapshot, sysModule, sshModule);
}
private Class<? extends Module> load(String name, ClassLoader pluginLoader)