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 ac6a8665ae..0c1ab0fdaf 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 @@ -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 sysModule; private Class sshModule; @@ -38,19 +42,32 @@ public class Plugin { private LifecycleManager manager; public Plugin(String name, + File jar, + Manifest manifest, FileSnapshot snapshot, @Nullable Class sysModule, @Nullable Class 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(); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java new file mode 100644 index 0000000000..77fa7028e3 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java @@ -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); + } +} 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 5bb1b36eca..44b2f12aee 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 @@ -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 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 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 sysModule = load(sysName, pluginLoader); Class sshModule = load(sshName, pluginLoader); - return new Plugin(name, snapshot, sysModule, sshModule); + return new Plugin(name, jarFile, manifest, snapshot, sysModule, sshModule); } private Class load(String name, ClassLoader pluginLoader) diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java index 4d7c93e02a..64e7289aef 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java @@ -28,6 +28,7 @@ public class DefaultCommandModule extends CommandModule { protected void configure() { final CommandName git = Commands.named("git"); final CommandName gerrit = Commands.named("gerrit"); + final CommandName plugin = Commands.named(gerrit, "plugin"); // The following commands can be ran on a server in either Master or Slave // mode. If a command should only be used on a server in one mode, but not @@ -46,6 +47,14 @@ public class DefaultCommandModule extends CommandModule { command(gerrit, "stream-events").to(StreamEvents.class); command(gerrit, "version").to(VersionCommand.class); + command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin)); + command(plugin, "ls").to(PluginLsCommand.class); + command(plugin, "install").to(PluginInstallCommand.class); + command(plugin, "reload").to(PluginReloadCommand.class); + command(plugin, "remove").to(PluginRemoveCommand.class); + command(plugin, "add").to(Commands.key(plugin, "install")); + command(plugin, "rm").to(Commands.key(plugin, "remove")); + command(git).toProvider(new DispatchCommandProvider(git)); command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack")); command(git, "upload-pack").to(Upload.class); diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java new file mode 100644 index 0000000000..2328847661 --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java @@ -0,0 +1,103 @@ +// 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.sshd.commands; + +import com.google.common.base.Strings; +import com.google.gerrit.common.data.GlobalCapability; +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 org.kohsuke.args4j.Option; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER) +final class PluginInstallCommand extends SshCommand { + @Option(name = "--name", aliases = {"-n"}, usage = "install under name") + private String name; + + @Option(name = "-") + void useInput(boolean on) { + source = "-"; + } + + @Argument(index = 0, metaVar = "-|URL", usage = "JAR to load") + private String source; + + @Inject + private PluginLoader loader; + + @Override + protected void run() throws UnloggedFailure { + if (Strings.isNullOrEmpty(source)) { + throw die("Argument \"-|URL\" is required"); + } + if (Strings.isNullOrEmpty(name) && "-".equalsIgnoreCase(source)) { + throw die("--name required when source is stdin"); + } + + if (Strings.isNullOrEmpty(name)) { + int s = source.lastIndexOf('/'); + if (0 <= s) { + name = source.substring(s + 1); + } else { + name = source; + } + } + + InputStream data; + if ("-".equalsIgnoreCase(source)) { + data = in; + } else if (new File(source).isFile() + && source.equals(new File(source).getAbsolutePath())) { + try { + data = new FileInputStream(new File(source)); + } catch (FileNotFoundException e) { + throw die("cannot read " + source); + } + } else { + try { + data = new URL(source).openStream(); + } catch (MalformedURLException e) { + throw die("invalid url " + source); + } catch (IOException e) { + throw die("cannot read " + source); + } + } + try { + loader.installPluginFromStream(name, data); + } catch (IOException e) { + throw die("cannot install plugin"); + } catch (PluginInstallException e) { + e.printStackTrace(stderr); + throw die("plugin failed to install"); + } finally { + try { + data.close(); + } catch (IOException err) { + } + } + } +} diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java new file mode 100644 index 0000000000..6044151ddc --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java @@ -0,0 +1,51 @@ +// 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.sshd.commands; + +import com.google.common.base.Strings; +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.server.plugins.Plugin; +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 java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER) +final class PluginLsCommand extends SshCommand { + @Inject + private PluginLoader loader; + + @Override + protected void run() { + List running = loader.getPlugins(); + Collections.sort(running, new Comparator() { + @Override + public int compare(Plugin a, Plugin b) { + return a.getName().compareTo(b.getName()); + } + }); + + stdout.format("%-30s %-10s\n", "Name", "Version"); + stdout.print("----------------------------------------------------------------------\n"); + for (Plugin p : running) { + stdout.format("%-30s %-10s\n", p.getName(), + Strings.nullToEmpty(p.getVersion())); + } + } +} 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 new file mode 100644 index 0000000000..548669866b --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java @@ -0,0 +1,32 @@ +// 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.sshd.commands; + +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.server.plugins.PluginLoader; +import com.google.gerrit.sshd.RequiresCapability; +import com.google.gerrit.sshd.SshCommand; +import com.google.inject.Inject; + +@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER) +final class PluginReloadCommand extends SshCommand { + @Inject + private PluginLoader loader; + + @Override + protected void run() { + loader.rescan(); + } +} diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java new file mode 100644 index 0000000000..6444e71ee7 --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java @@ -0,0 +1,42 @@ +// 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.sshd.commands; + +import com.google.common.collect.Sets; +import com.google.gerrit.common.data.GlobalCapability; +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 PluginRemoveCommand extends SshCommand { + @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove") + List names; + + @Inject + private PluginLoader loader; + + @Override + protected void run() { + if (names != null && !names.isEmpty()) { + loader.disablePlugins(Sets.newHashSet(names)); + } + } +}