Merge "Add a new permission to permit rebasing changes in the web UI"
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
index 4d1c1f5..1d4d3e2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
@@ -65,7 +65,7 @@
PatchSetInfoNotAvailableException, RepositoryNotFoundException,
IOException {
final ReviewResult result =
- abandonChangeFactory.create(patchSetId, message).call();
+ abandonChangeFactory.create(patchSetId.getParentKey(), message).call();
if (result.getErrors().size() > 0) {
throw new NoSuchChangeException(result.getChangeId());
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
index bcb03b0..5d7fe32 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
@@ -64,7 +64,7 @@
PatchSetInfoNotAvailableException, RepositoryNotFoundException,
IOException {
final ReviewResult result =
- restoreChangeFactory.create(patchSetId, message).call();
+ restoreChangeFactory.create(patchSetId.getParentKey(), message).call();
if (result.getErrors().size() > 0) {
throw new NoSuchChangeException(result.getChangeId());
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 8cef4d7..85b1012 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -46,11 +46,12 @@
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.plugins.PluginModule;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gerrit.server.ssh.NoSshModule;
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.commands.MasterCommandModule;
-import com.google.gerrit.sshd.commands.MasterPluginsModule;
import com.google.gerrit.sshd.commands.SlaveCommandModule;
import com.google.inject.Injector;
import com.google.inject.Module;
@@ -141,6 +142,8 @@
dbInjector = createDbInjector(MULTI_USER);
cfgInjector = createCfgInjector();
sysInjector = createSysInjector();
+ sysInjector.getInstance(PluginGuiceEnvironment.class)
+ .setCfgInjector(cfgInjector);
manager.add(dbInjector, cfgInjector, sysInjector);
if (sshd) {
@@ -209,6 +212,7 @@
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
modules.add(new PushReplication.Module());
+ modules.add(new PluginModule());
if (httpd) {
modules.add(new CanonicalWebUrlModule() {
@Override
@@ -232,6 +236,8 @@
private void initSshd() {
sshInjector = createSshInjector();
+ sysInjector.getInstance(PluginGuiceEnvironment.class)
+ .setSshInjector(sshInjector);
manager.add(sshInjector);
}
@@ -243,7 +249,6 @@
modules.add(new SlaveCommandModule());
} else {
modules.add(new MasterCommandModule());
- modules.add(cfgInjector.getInstance(MasterPluginsModule.class));
}
} else {
modules.add(new NoSshModule());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/common/PluginLoader.java
deleted file mode 100644
index ae428e1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/PluginLoader.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// 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.common;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Singleton;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.jar.Attributes;
-import java.util.jar.JarFile;
-import java.util.jar.Manifest;
-
-@Singleton
-public class PluginLoader {
- private static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
-
- private final File pluginsDir;
- private Map<String, Plugin> pluginCache;
-
- @Inject
- public PluginLoader(SitePaths sitePaths) {
- pluginsDir = sitePaths.plugins_dir;
- }
-
- private synchronized void initialize() {
- if (pluginCache != null) {
- return;
- }
-
- pluginCache = new HashMap<String, Plugin>();
- loadPlugins();
- }
-
- public Plugin get(String pluginName) {
- initialize();
- return pluginCache.get(pluginName);
- }
-
- public Collection<Plugin> getPlugins() {
- initialize();
- return pluginCache.values();
- }
-
- private void loadPlugins() {
- Collection<File> pluginJars;
- try {
- pluginJars = getPluginFiles();
- } catch (IOException e) {
- log.error("Cannot scan Gerrit plugins directory looking for jar files", e);
- return;
- }
-
- for (File jarFile : pluginJars) {
- Plugin plugin;
- try {
- plugin = loadPlugin(jarFile);
- pluginCache.put(plugin.name, plugin);
- } catch (IOException e) {
- log.error("Cannot access plugin jar " + jarFile, e);
- } catch (ClassNotFoundException e) {
- log.error("Cannot load plugin class module from " + jarFile, e);
- }
- }
- }
-
- @SuppressWarnings("unchecked")
- private Plugin loadPlugin(File jarFile) throws IOException,
- ClassNotFoundException {
- Manifest jarManifest = new JarFile(jarFile).getManifest();
- ClassLoader parentLoader = PluginLoader.class.getClassLoader();
- ClassLoader jarClassLoader =
- new URLClassLoader(getPluginURLs(jarFile), parentLoader);
-
- Attributes attrs = jarManifest.getMainAttributes();
- String pluginName = attrs.getValue("Gerrit-Plugin");
- if (Strings.isNullOrEmpty(pluginName)) {
- throw new IOException("No Gerrit-Plugin attribute in manifest");
- }
-
- String moduleName = attrs.getValue("Gerrit-SshModule");
- if (Strings.isNullOrEmpty(moduleName)) {
- throw new IOException("No Gerrit-SshModule attribute in manifest");
- }
-
- Class<?> moduleClass = Class.forName(moduleName, false, jarClassLoader);
- if (!Module.class.isAssignableFrom(moduleClass)) {
- throw new ClassNotFoundException(String.format(
- "Gerrit-SshModule %s is not a Guice Module",
- moduleClass.getName()));
- }
-
- return new Plugin(pluginName, (Class<? extends Module>) moduleClass);
- }
-
- private URL[] getPluginURLs(File jarFile) throws MalformedURLException {
- return new URL[] {jarFile.toURI().toURL()};
- }
-
- private List<File> getPluginFiles() throws IOException {
- if (pluginsDir == null || !pluginsDir.exists()) {
- return Collections.emptyList();
- }
-
- File[] plugins = pluginsDir.listFiles(new FileFilter() {
- @Override
- public boolean accept(File pathname) {
- return pathname.isFile() && pathname.getName().endsWith(".jar");
- }
- });
- if (plugins == null) {
- log.error("Cannot list " + pluginsDir.getAbsolutePath());
- return Collections.emptyList();
- }
-
- return Arrays.asList(plugins);
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
index 1fac8c5..83fa671 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
@@ -35,10 +35,12 @@
import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
+
public class AbandonChange implements Callable<ReviewResult> {
public interface Factory {
- AbandonChange create(PatchSet.Id patchSetId, String changeComment);
+ AbandonChange create(Change.Id changeId, String changeComment);
}
private final AbandonedSender.Factory abandonedSenderFactory;
@@ -47,22 +49,22 @@
private final IdentifiedUser currentUser;
private final ChangeHooks hooks;
- private final PatchSet.Id patchSetId;
+ private final Change.Id changeId;
private final String changeComment;
@Inject
AbandonChange(final AbandonedSender.Factory abandonedSenderFactory,
final ChangeControl.Factory changeControlFactory, final ReviewDb db,
final IdentifiedUser currentUser, final ChangeHooks hooks,
- @Assisted final PatchSet.Id patchSetId,
- @Assisted final String changeComment) {
+ @Assisted final Change.Id changeId,
+ @Assisted @Nullable final String changeComment) {
this.abandonedSenderFactory = abandonedSenderFactory;
this.changeControlFactory = changeControlFactory;
this.db = db;
this.currentUser = currentUser;
this.hooks = hooks;
- this.patchSetId = patchSetId;
+ this.changeId = changeId;
this.changeComment = changeComment;
}
@@ -70,10 +72,11 @@
public ReviewResult call() throws EmailException,
InvalidChangeOperationException, NoSuchChangeException, OrmException {
final ReviewResult result = new ReviewResult();
-
- final Change.Id changeId = patchSetId.getParentKey();
result.setChangeId(changeId);
+
final ChangeControl control = changeControlFactory.validateFor(changeId);
+ final Change change = db.changes().get(changeId);
+ final PatchSet.Id patchSetId = change.currentPatchSetId();
final PatchSet patch = db.patchSets().get(patchSetId);
if (!control.canAbandon()) {
result.addError(new ReviewResult.Error(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
index 7e52564..966efce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
@@ -41,10 +41,12 @@
import java.io.IOException;
import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
+
public class RestoreChange implements Callable<ReviewResult> {
public interface Factory {
- RestoreChange create(PatchSet.Id patchSetId, String changeComment);
+ RestoreChange create(Change.Id changeId, String changeComment);
}
private final RestoredSender.Factory restoredSenderFactory;
@@ -54,15 +56,15 @@
private final IdentifiedUser currentUser;
private final ChangeHooks hooks;
- private final PatchSet.Id patchSetId;
+ private final Change.Id changeId;
private final String changeComment;
@Inject
RestoreChange(final RestoredSender.Factory restoredSenderFactory,
final ChangeControl.Factory changeControlFactory, final ReviewDb db,
final GitRepositoryManager repoManager, final IdentifiedUser currentUser,
- final ChangeHooks hooks, @Assisted final PatchSet.Id patchSetId,
- @Assisted final String changeComment) {
+ final ChangeHooks hooks, @Assisted final Change.Id changeId,
+ @Assisted @Nullable final String changeComment) {
this.restoredSenderFactory = restoredSenderFactory;
this.changeControlFactory = changeControlFactory;
this.db = db;
@@ -70,7 +72,7 @@
this.currentUser = currentUser;
this.hooks = hooks;
- this.patchSetId = patchSetId;
+ this.changeId = changeId;
this.changeComment = changeComment;
}
@@ -79,10 +81,11 @@
InvalidChangeOperationException, NoSuchChangeException, OrmException,
RepositoryNotFoundException, IOException {
final ReviewResult result = new ReviewResult();
-
- final Change.Id changeId = patchSetId.getParentKey();
result.setChangeId(changeId);
+
final ChangeControl control = changeControlFactory.validateFor(changeId);
+ final Change change = db.changes().get(changeId);
+ final PatchSet.Id patchSetId = change.currentPatchSetId();
if (!control.canRestore()) {
result.addError(new ReviewResult.Error(
ReviewResult.Error.Type.RESTORE_NOT_PERMITTED));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
new file mode 100644
index 0000000..f34826d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -0,0 +1,102 @@
+// 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 com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+
+/**
+ * Copies critical objects from the {@code dbInjector} into a plugin.
+ * <p>
+ * Most explicit bindings are copied automatically from the cfgInjector and
+ * sysInjector to be made available to a plugin's private world. This module is
+ * necessary to get things bound in the dbInjector that are not otherwise easily
+ * available, but that a plugin author might expect to exist.
+ */
+@Singleton
+class CopyConfigModule extends AbstractModule {
+ @Inject
+ @SitePath
+ private File sitePath;
+
+ @Provides
+ @SitePath
+ File getSitePath() {
+ return sitePath;
+ }
+
+ @Inject
+ private SitePaths sitePaths;
+
+ @Provides
+ SitePaths getSitePaths() {
+ return sitePaths;
+ }
+
+ @Inject
+ private TrackingFooters trackingFooters;
+
+ @Provides
+ TrackingFooters getTrackingFooters() {
+ return trackingFooters;
+ }
+
+ @Inject
+ @GerritServerConfig
+ private Config gerritServerConfig;
+
+ @Provides
+ @GerritServerConfig
+ Config getGerritServerConfig() {
+ return gerritServerConfig;
+ }
+
+ @Inject
+ private SchemaFactory<ReviewDb> schemaFactory;
+
+ @Provides
+ SchemaFactory<ReviewDb> getSchemaFactory() {
+ return schemaFactory;
+ }
+
+ @Inject
+ private GitRepositoryManager gitRepositoryManager;
+
+ @Provides
+ GitRepositoryManager getGitRepositoryManager() {
+ return gitRepositoryManager;
+ }
+
+ @Inject
+ CopyConfigModule() {
+ }
+
+ @Override
+ protected void configure() {
+ }
+}
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
new file mode 100644
index 0000000..0c1ab0f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -0,0 +1,145 @@
+// 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 com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+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;
+
+ private Injector sysInjector;
+ private Injector sshInjector;
+ 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();
+ }
+
+ public void start(PluginGuiceEnvironment env) throws Exception {
+ Injector root = newRootInjector(env);
+ manager = new LifecycleManager();
+
+ if (sysModule != null) {
+ sysInjector = root.createChildInjector(root.getInstance(sysModule));
+ manager.add(sysInjector);
+ } else {
+ sysInjector = root;
+ }
+
+ if (sshModule != null && env.hasSshModule()) {
+ sshInjector = sysInjector.createChildInjector(
+ env.getSshModule(),
+ sysInjector.getInstance(sshModule));
+ manager.add(sshInjector);
+ }
+
+ manager.start();
+ env.onStartPlugin(this);
+ }
+
+ private Injector newRootInjector(PluginGuiceEnvironment env) {
+ return Guice.createInjector(
+ env.getSysModule(),
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(String.class)
+ .annotatedWith(PluginName.class)
+ .toInstance(name);
+ }
+ });
+ }
+
+ public void stop() {
+ if (manager != null) {
+ manager.stop();
+ manager = null;
+ sysInjector = null;
+ sshInjector = null;
+ }
+ }
+
+ @Nullable
+ public Injector getSshInjector() {
+ return sshInjector;
+ }
+
+ public void add(final RegistrationHandle handle) {
+ add(new LifecycleListener() {
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void stop() {
+ handle.remove();
+ }
+ });
+ }
+
+ public void add(LifecycleListener listener) {
+ manager.add(listener);
+ }
+
+ @Override
+ public String toString() {
+ return "Plugin [" + name + "]";
+ }
+}
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
new file mode 100644
index 0000000..418fbf2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -0,0 +1,140 @@
+// 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 com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.inject.Inject;
+
+/**
+ * Tracks Guice bindings that should be exposed to loaded plugins.
+ * <p>
+ * This is an internal implementation detail of how the main server is able to
+ * export its explicit Guice bindings to tightly coupled plugins, giving them
+ * access to singletons and request scoped resources just like any core code.
+ */
+@Singleton
+public class PluginGuiceEnvironment {
+ private final Injector sysInjector;
+ private final CopyConfigModule copyConfigModule;
+ private final List<StartPluginListener> listeners;
+ private Module sysModule;
+ private Module sshModule;
+
+ @Inject
+ PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) {
+ this.sysInjector = sysInjector;
+ this.copyConfigModule = ccm;
+ this.listeners = new CopyOnWriteArrayList<StartPluginListener>();
+ this.listeners.addAll(getListeners(sysInjector));
+ }
+
+ Module getSysModule() {
+ return sysModule;
+ }
+
+ public void setCfgInjector(Injector cfgInjector) {
+ final Module cm = copy(cfgInjector);
+ final Module sm = copy(sysInjector);
+ sysModule = new AbstractModule() {
+ @Override
+ protected void configure() {
+ install(copyConfigModule);
+ install(cm);
+ install(sm);
+ }
+ };
+ }
+
+ public void setSshInjector(Injector sshInjector) {
+ sshModule = copy(sshInjector);
+ listeners.addAll(getListeners(sshInjector));
+ }
+
+ boolean hasSshModule() {
+ return sshModule != null;
+ }
+
+ Module getSshModule() {
+ return sshModule;
+ }
+
+ void onStartPlugin(Plugin plugin) {
+ for (StartPluginListener l : listeners) {
+ 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) {
+ found.add(b.getProvider().get());
+ }
+ return found;
+ }
+
+ private static Module copy(Injector src) {
+ final Map<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap();
+ for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+ if (shouldCopy(e.getKey())) {
+ bindings.put(e.getKey(), e.getValue());
+ }
+ }
+ bindings.remove(Key.get(Injector.class));
+ bindings.remove(Key.get(java.util.logging.Logger.class));
+
+ return new AbstractModule() {
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void configure() {
+ for (Map.Entry<Key<?>, Binding<?>> e : bindings.entrySet()) {
+ Key<Object> k = (Key<Object>) e.getKey();
+ Binding<Object> b = (Binding<Object>) e.getValue();
+ bind(k).toProvider(b.getProvider());
+ }
+ }
+ };
+ }
+
+ private static boolean shouldCopy(Key<?> key) {
+ Class<?> type = key.getTypeLiteral().getRawType();
+ if (type == LifecycleListener.class) {
+ return false;
+ }
+ if (type == StartPluginListener.class) {
+ return false;
+ }
+ if ("org.apache.sshd.server.Command".equals(type.getName())) {
+ return false;
+ }
+ return true;
+ }
+}
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 0000000..77fa702
--- /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
new file mode 100644
index 0000000..44b2f12
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -0,0 +1,291 @@
+// 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 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;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileSnapshot;
+import org.slf4j.Logger;
+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;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+@Singleton
+public class PluginLoader implements LifecycleListener {
+ private static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
+
+ private final File pluginsDir;
+ private final PluginGuiceEnvironment env;
+ private final Map<String, Plugin> running;
+ private final Map<String, FileSnapshot> broken;
+ private final PluginScannerThread scanner;
+
+ @Inject
+ public PluginLoader(SitePaths sitePaths,
+ PluginGuiceEnvironment pe,
+ @GerritServerConfig Config cfg) {
+ pluginsDir = sitePaths.plugins_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));
+ }
+
+ 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());
+ rescan();
+ scanner.start();
+ }
+
+ @Override
+ public void stop() {
+ scanner.end();
+ synchronized (this) {
+ for (Plugin p : running.values()) {
+ p.stop();
+ }
+ running.clear();
+ broken.clear();
+ }
+ }
+
+ public synchronized void rescan() {
+ List<File> jars = scanJarsInPluginsDirectory();
+
+ stopRemovedPlugins(jars);
+
+ for (File jar : jars) {
+ String name = nameOf(jar);
+ FileSnapshot brokenTime = broken.get(name);
+ if (brokenTime != null && !brokenTime.isModified(jar)) {
+ continue;
+ }
+
+ Plugin active = running.get(name);
+ if (active != null && !active.isModified(jar)) {
+ continue;
+ }
+
+ 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();
+ 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;
+ }
+ broken.remove(name);
+ running.put(name, next);
+
+ if (active == null) {
+ log.info(String.format("Loaded plugin %s", name));
+ }
+ }
+ }
+
+ private void stopRemovedPlugins(List<File> jars) {
+ Set<String> unload = Sets.newHashSet(running.keySet());
+ for (File jar : jars) {
+ unload.remove(nameOf(jar));
+ }
+ for (String name : unload){
+ log.info(String.format("Unloading plugin %s", name));
+ running.remove(name).stop();
+ }
+ }
+
+ private static String nameOf(File jar) {
+ String name = jar.getName();
+ int ext = name.lastIndexOf('.');
+ return 0 < ext ? name.substring(0, ext) : name;
+ }
+
+ private Plugin loadPlugin(String name, FileSnapshot snapshot, File jarFile)
+ throws IOException, ClassNotFoundException {
+ Manifest manifest = new JarFile(jarFile).getManifest();
+
+ Attributes main = manifest.getMainAttributes();
+ String sysName = main.getValue("Gerrit-Module");
+ String sshName = main.getValue("Gerrit-SshModule");
+
+ URL[] urls = {jarFile.toURI().toURL()};
+ ClassLoader parentLoader = PluginLoader.class.getClassLoader();
+ ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
+
+ Class<? extends Module> sysModule = load(sysName, pluginLoader);
+ Class<? extends Module> sshModule = load(sshName, pluginLoader);
+ return new Plugin(name, jarFile, manifest, snapshot, sysModule, sshModule);
+ }
+
+ private Class<? extends Module> load(String name, ClassLoader pluginLoader)
+ throws ClassNotFoundException {
+ if (Strings.isNullOrEmpty(name)) {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ Class<? extends Module> clazz =
+ (Class<? extends Module>) Class.forName(name, false, pluginLoader);
+ if (!Module.class.isAssignableFrom(clazz)) {
+ throw new ClassCastException(String.format(
+ "Class %s does not implement %s",
+ name, Module.class.getName()));
+ }
+ return clazz;
+ }
+
+ private List<File> scanJarsInPluginsDirectory() {
+ if (pluginsDir == null || !pluginsDir.exists()) {
+ return Collections.emptyList();
+ }
+ File[] matches = pluginsDir.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return pathname.getName().endsWith(".jar") && pathname.isFile();
+ }
+ });
+ if (matches == null) {
+ log.error("Cannot list " + pluginsDir.getAbsolutePath());
+ return Collections.emptyList();
+ }
+ return Arrays.asList(matches);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
similarity index 61%
rename from gerrit-server/src/main/java/com/google/gerrit/common/Plugin.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
index 36f4311..0431ee1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -12,21 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.common;
+package com.google.gerrit.server.plugins;
-import com.google.inject.Module;
+import com.google.gerrit.lifecycle.LifecycleModule;
-public class Plugin {
- public final String name;
- public final Class<? extends Module> sshModule;
-
- public Plugin(String name, Class<? extends Module> sshModule) {
- this.name = name;
- this.sshModule = sshModule;
- }
-
+public class PluginModule extends LifecycleModule {
@Override
- public String toString() {
- return "Plugin [" + name + "; SshModule=" + sshModule.getName() + "]";
+ protected void configure() {
+ bind(PluginGuiceEnvironment.class);
+ bind(PluginLoader.class);
+ bind(CopyConfigModule.class);
+ listener().to(PluginLoader.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java
new file mode 100644
index 0000000..6a47b93
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java
@@ -0,0 +1,29 @@
+// 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 static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginName {
+}
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
new file mode 100644
index 0000000..a484c5d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -0,0 +1,52 @@
+// 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.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+class PluginScannerThread extends Thread {
+ private final CountDownLatch done = new CountDownLatch(1);
+ private final PluginLoader loader;
+ private final long checkFrequencyMillis;
+
+ PluginScannerThread(PluginLoader loader, long checkFrequencyMillis) {
+ this.loader = loader;
+ this.checkFrequencyMillis = checkFrequencyMillis;
+ setDaemon(true);
+ setName("PluginScanner");
+ }
+
+ @Override
+ public void run() {
+ for (;;) {
+ try {
+ if (done.await(checkFrequencyMillis, TimeUnit.MILLISECONDS)) {
+ return;
+ }
+ } catch (InterruptedException e) {
+ }
+ loader.rescan();
+ }
+ }
+
+ void end() {
+ done.countDown();
+ try {
+ join();
+ } catch (InterruptedException e) {
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
new file mode 100644
index 0000000..cd0b334
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
@@ -0,0 +1,21 @@
+// 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;
+
+/** Handle for registered information. */
+public interface RegistrationHandle {
+ /** Delete this registration. */
+ public void remove();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
new file mode 100644
index 0000000..aaad370
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.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 loaded. */
+public interface StartPluginListener {
+ public void onStartPlugin(Plugin plugin);
+}
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 0b69228..349cc79 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
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.plugins.RegistrationHandle;
import com.google.inject.Binding;
import com.google.inject.Inject;
import com.google.inject.Injector;
@@ -23,11 +25,8 @@
import org.apache.sshd.server.Command;
import java.lang.annotation.Annotation;
-import java.util.Collections;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
+import java.util.concurrent.ConcurrentMap;
/**
* Creates DispatchCommand using commands registered by {@link CommandModule}.
@@ -42,7 +41,7 @@
private final String dispatcherName;
private final CommandName parent;
- private volatile Map<String, Provider<Command>> map;
+ private volatile ConcurrentMap<String, Provider<Command>> map;
public DispatchCommandProvider(final CommandName cn) {
this(Commands.nameOf(cn), cn);
@@ -59,7 +58,21 @@
return factory.create(dispatcherName, getMap());
}
- private Map<String, Provider<Command>> getMap() {
+ public RegistrationHandle register(final CommandName name,
+ final Provider<Command> cmd) {
+ final ConcurrentMap<String, Provider<Command>> m = getMap();
+ if (m.putIfAbsent(name.value(), cmd) != null) {
+ throw new IllegalArgumentException(name.value() + " exists");
+ }
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ m.remove(name.value(), cmd);
+ }
+ };
+ }
+
+ private ConcurrentMap<String, Provider<Command>> getMap() {
if (map == null) {
synchronized (this) {
if (map == null) {
@@ -71,10 +84,8 @@
}
@SuppressWarnings("unchecked")
- private Map<String, Provider<Command>> createMap() {
- final Map<String, Provider<Command>> m =
- new TreeMap<String, Provider<Command>>();
-
+ private ConcurrentMap<String, Provider<Command>> createMap() {
+ ConcurrentMap<String, Provider<Command>> m = Maps.newConcurrentMap();
for (final Binding<?> b : allCommands()) {
final Annotation annotation = b.getKey().getAnnotation();
if (annotation instanceof CommandName) {
@@ -84,9 +95,7 @@
}
}
}
-
- return Collections.unmodifiableMap(
- new LinkedHashMap<String, Provider<Command>>(m));
+ return m;
}
private static final TypeLiteral<Command> type =
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 558707b..2637529 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
@@ -19,6 +19,7 @@
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -30,12 +31,14 @@
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.StartPluginListener;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
import com.google.gerrit.sshd.args4j.AccountGroupUUIDHandler;
import com.google.gerrit.sshd.args4j.AccountIdHandler;
+import com.google.gerrit.sshd.args4j.ChangeIdHandler;
import com.google.gerrit.sshd.args4j.ObjectIdHandler;
import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
import com.google.gerrit.sshd.args4j.ProjectControlHandler;
@@ -44,6 +47,7 @@
import com.google.gerrit.sshd.commands.QueryShell;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gerrit.util.cli.OptionHandlerUtil;
+import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.RequestScoped;
import org.apache.sshd.common.KeyPairProvider;
@@ -89,6 +93,9 @@
install(new LifecycleModule() {
@Override
protected void configure() {
+ bind(StartPluginListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(SshPluginStarterCallback.class);
listener().to(SshLog.class);
listener().to(SshDaemon.class);
}
@@ -120,6 +127,7 @@
registerOptionHandler(Account.Id.class, AccountIdHandler.class);
registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
+ registerOptionHandler(Change.Id.class, ChangeIdHandler.class);
registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
registerOptionHandler(ProjectControl.class, ProjectControlHandler.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
new file mode 100644
index 0000000..b82eb8f
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -0,0 +1,57 @@
+// 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;
+
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.apache.sshd.server.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+
+@Singleton
+class SshPluginStarterCallback implements StartPluginListener {
+ private static final Logger log = LoggerFactory
+ .getLogger(SshPluginStarterCallback.class);
+
+ private final DispatchCommandProvider root;
+
+ @Inject
+ SshPluginStarterCallback(
+ @CommandName(Commands.ROOT) DispatchCommandProvider root) {
+ this.root = root;
+ }
+
+ @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;
+ }
+ plugin.add(root.register(Commands.named(plugin.getName()), cmd));
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java
new file mode 100644
index 0000000..0194b91
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java
@@ -0,0 +1,77 @@
+// 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.args4j;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class ChangeIdHandler extends OptionHandler<Change.Id> {
+
+ @Inject
+ private ReviewDb db;
+
+ @Inject
+ public ChangeIdHandler(
+ final ReviewDb db,
+ @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
+ @Assisted final Setter<Change.Id> setter) {
+ super(parser, option, setter);
+ this.db = db;
+ }
+
+ @Override
+ public final int parseArguments(final Parameters params)
+ throws CmdLineException {
+ final String token = params.getParameter(0);
+ final String[] tokens = token.split(",");
+ if (tokens.length != 3) {
+ throw new CmdLineException(owner, "change should be specified as "
+ + "<project>,<branch>,<change-id>");
+ }
+
+ try {
+ final Change.Key key = Change.Key.parse(tokens[2]);
+ final Project.NameKey project = new Project.NameKey(tokens[0]);
+ final Branch.NameKey branch = new Branch.NameKey(project, tokens[1]);
+ for (final Change change : db.changes().byBranchKey(branch, key)) {
+ setter.addValue(change.getId());
+ return 1;
+ }
+ } catch (IllegalArgumentException e) {
+ throw new CmdLineException(owner, "Change-Id is not valid");
+ } catch (OrmException e) {
+ throw new CmdLineException(owner, "Database error: " + e.getMessage());
+ }
+
+ throw new CmdLineException(owner, "\"" + token + "\": change not found");
+ }
+
+ @Override
+ public final String getDefaultMetaVariable() {
+ return "CHANGE";
+ }
+}
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 4d7c93e..64e7289 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 @@
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 @@
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/MasterPluginsModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterPluginsModule.java
deleted file mode 100644
index 76eebce..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterPluginsModule.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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.Plugin;
-import com.google.gerrit.common.PluginLoader;
-import com.google.gerrit.sshd.CommandModule;
-import com.google.inject.Inject;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Collection;
-
-public class MasterPluginsModule extends CommandModule {
- private static final Logger log =
- LoggerFactory.getLogger(MasterPluginsModule.class);
-
- private PluginLoader pluginLoader;
-
- @Inject
- MasterPluginsModule(PluginLoader loader) {
- pluginLoader = loader;
- }
-
- @Override
- protected void configure() {
- Collection<Plugin> plugins = pluginLoader.getPlugins();
- for (Plugin p : plugins) {
- if (PluginCommandModule.class.isAssignableFrom(p.sshModule)) {
- @SuppressWarnings("unchecked")
- Class<PluginCommandModule> c = (Class<PluginCommandModule>) p.sshModule;
- try {
- PluginCommandModule module = c.newInstance();
- module.initSshModule(p.name);
- install(module);
- } catch (InstantiationException e) {
- log.warn("Initialization of plugin module '" + p.name + "' failed");
- } catch (IllegalAccessException e) {
- log.warn("Initialization of plugin module '" + p.name + "' failed");
- }
- }
- }
- }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
index 788dfa1..d9015c6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd.commands;
+import com.google.common.base.Preconditions;
+import com.google.gerrit.server.plugins.PluginName;
import com.google.gerrit.sshd.CommandName;
import com.google.gerrit.sshd.Commands;
import com.google.gerrit.sshd.DispatchCommandProvider;
@@ -22,20 +24,24 @@
import org.apache.sshd.server.Command;
+import javax.inject.Inject;
+
public abstract class PluginCommandModule extends AbstractModule {
private CommandName command;
- public void initSshModule(String pluginName) {
- command = Commands.named(pluginName);
+ @Inject
+ void setPluginName(@PluginName String name) {
+ this.command = Commands.named(name);
}
@Override
protected final void configure() {
+ Preconditions.checkState(command != null, "@PluginName must be provided");
bind(Commands.key(command)).toProvider(new DispatchCommandProvider(command));
- configureCmds();
+ configureCommands();
}
- protected abstract void configureCmds();
+ protected abstract void configureCommands();
protected LinkedBindingBuilder<Command> command(String subCmd) {
return bind(Commands.key(command, subCmd));
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 0000000..2328847
--- /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 0000000..6044151
--- /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<Plugin> running = loader.getPlugins();
+ Collections.sort(running, new Comparator<Plugin>() {
+ @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 0000000..5486698
--- /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 0000000..6444e71
--- /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<String> names;
+
+ @Inject
+ private PluginLoader loader;
+
+ @Override
+ protected void run() {
+ if (names != null && !names.isEmpty()) {
+ loader.disablePlugins(Sets.newHashSet(names));
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 74782ed..f38e17e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -202,11 +202,11 @@
if (abandonChange) {
final ReviewResult result = abandonChangeFactory.create(
- patchSetId, changeComment).call();
+ patchSetId.getParentKey(), changeComment).call();
handleReviewResultErrors(result);
} else if (restoreChange) {
final ReviewResult result = restoreChangeFactory.create(
- patchSetId, changeComment).call();
+ patchSetId.getParentKey(), changeComment).call();
handleReviewResultErrors(result);
}
if (submitChange) {
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 8ffc531..0c15dfd 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -37,13 +37,14 @@
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.plugins.PluginModule;
import com.google.gerrit.server.schema.DataSourceProvider;
import com.google.gerrit.server.schema.DatabaseModule;
import com.google.gerrit.server.schema.SchemaModule;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.commands.MasterCommandModule;
-import com.google.gerrit.sshd.commands.MasterPluginsModule;
import com.google.inject.AbstractModule;
import com.google.inject.CreationException;
import com.google.inject.Guice;
@@ -113,6 +114,10 @@
sshInjector = createSshInjector();
webInjector = createWebInjector();
+ PluginGuiceEnvironment env = sysInjector.getInstance(PluginGuiceEnvironment.class);
+ env.setCfgInjector(cfgInjector);
+ env.setSshInjector(sshInjector);
+
// Push the Provider<HttpServletRequest> down into the canonical
// URL provider. Its optional for that provider, but since we can
// supply one we should do so, in case the administrator has not
@@ -198,6 +203,7 @@
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
modules.add(new PushReplication.Module());
+ modules.add(new PluginModule());
modules.add(new CanonicalWebUrlModule() {
@Override
protected Class<? extends Provider<String>> provider() {
@@ -212,7 +218,6 @@
final List<Module> modules = new ArrayList<Module>();
modules.add(new SshModule());
modules.add(new MasterCommandModule());
- modules.add(cfgInjector.getInstance(MasterPluginsModule.class));
return sysInjector.createChildInjector(modules);
}