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 8cef4d71c3..85b10126c0 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.ReceiveCommitsExecutorModule; 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 @@ public class Daemon extends SiteProgram { 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 @@ public class Daemon extends SiteProgram { 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 @@ public class Daemon extends SiteProgram { private void initSshd() { sshInjector = createSshInjector(); + sysInjector.getInstance(PluginGuiceEnvironment.class) + .setSshInjector(sshInjector); manager.add(sshInjector); } @@ -243,7 +249,6 @@ public class Daemon extends SiteProgram { 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 ae428e1e98..0000000000 --- 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 pluginCache; - - @Inject - public PluginLoader(SitePaths sitePaths) { - pluginsDir = sitePaths.plugins_dir; - } - - private synchronized void initialize() { - if (pluginCache != null) { - return; - } - - pluginCache = new HashMap(); - loadPlugins(); - } - - public Plugin get(String pluginName) { - initialize(); - return pluginCache.get(pluginName); - } - - public Collection getPlugins() { - initialize(); - return pluginCache.values(); - } - - private void loadPlugins() { - Collection 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) moduleClass); - } - - private URL[] getPluginURLs(File jarFile) throws MalformedURLException { - return new URL[] {jarFile.toURI().toURL()}; - } - - private List 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/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java new file mode 100644 index 0000000000..f34826daca --- /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. + *

+ * 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 schemaFactory; + + @Provides + SchemaFactory 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 0000000000..ac6a8665ae --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java @@ -0,0 +1,128 @@ +// 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 javax.annotation.Nullable; + +public class Plugin { + private final String name; + private final FileSnapshot snapshot; + private Class sysModule; + private Class sshModule; + + private Injector sysInjector; + private Injector sshInjector; + private LifecycleManager manager; + + public Plugin(String name, + FileSnapshot snapshot, + @Nullable Class sysModule, + @Nullable Class sshModule) { + this.name = name; + this.snapshot = snapshot; + this.sysModule = sysModule; + this.sshModule = sshModule; + } + + public String getName() { + return name; + } + + 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 0000000000..418fbf29b4 --- /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. + *

+ * 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 listeners; + private Module sysModule; + private Module sshModule; + + @Inject + PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) { + this.sysInjector = sysInjector; + this.copyConfigModule = ccm; + this.listeners = new CopyOnWriteArrayList(); + 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 getListeners(Injector src) { + List> bindings = + src.findBindingsByType(new TypeLiteral() {}); + List found = + Lists.newArrayListWithCapacity(bindings.size()); + for (Binding b : bindings) { + found.add(b.getProvider().get()); + } + return found; + } + + private static Module copy(Injector src) { + final Map, Binding> bindings = Maps.newLinkedHashMap(); + for (Map.Entry, 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, Binding> e : bindings.entrySet()) { + Key k = (Key) e.getKey(); + Binding b = (Binding) 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/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java new file mode 100644 index 0000000000..5bb1b36eca --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java @@ -0,0 +1,205 @@ +// 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.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.IOException; +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 running; + private final Map 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)); + } + + @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 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 jars) { + Set 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 sysModule = load(sysName, pluginLoader); + Class sshModule = load(sshName, pluginLoader); + return new Plugin(name, snapshot, sysModule, sshModule); + } + + private Class load(String name, ClassLoader pluginLoader) + throws ClassNotFoundException { + if (Strings.isNullOrEmpty(name)) { + return null; + } + + @SuppressWarnings("unchecked") + Class clazz = + (Class) 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 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 36f4311015..0431ee16a9 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; - -public class Plugin { - public final String name; - public final Class sshModule; - - public Plugin(String name, Class sshModule) { - this.name = name; - this.sshModule = sshModule; - } +import com.google.gerrit.lifecycle.LifecycleModule; +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 0000000000..6a47b935c3 --- /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 0000000000..a484c5d44b --- /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 0000000000..cd0b334275 --- /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 0000000000..aaad370009 --- /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 0b69228b85..349cc7915b 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 com.google.inject.TypeLiteral; 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 @@ public class DispatchCommandProvider implements Provider { private final String dispatcherName; private final CommandName parent; - private volatile Map> map; + private volatile ConcurrentMap> map; public DispatchCommandProvider(final CommandName cn) { this(Commands.nameOf(cn), cn); @@ -59,7 +58,21 @@ public class DispatchCommandProvider implements Provider { return factory.create(dispatcherName, getMap()); } - private Map> getMap() { + public RegistrationHandle register(final CommandName name, + final Provider cmd) { + final ConcurrentMap> 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> getMap() { if (map == null) { synchronized (this) { if (map == null) { @@ -71,10 +84,8 @@ public class DispatchCommandProvider implements Provider { } @SuppressWarnings("unchecked") - private Map> createMap() { - final Map> m = - new TreeMap>(); - + private ConcurrentMap> createMap() { + ConcurrentMap> m = Maps.newConcurrentMap(); for (final Binding b : allCommands()) { final Annotation annotation = b.getKey().getAnnotation(); if (annotation instanceof CommandName) { @@ -84,9 +95,7 @@ public class DispatchCommandProvider implements Provider { } } } - - return Collections.unmodifiableMap( - new LinkedHashMap>(m)); + return m; } private static final TypeLiteral 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 85d7f3e38e..26375292fc 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.FactoryModule; 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; @@ -46,6 +47,7 @@ import com.google.gerrit.sshd.commands.DefaultCommandModule; 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; @@ -91,6 +93,9 @@ public class SshModule extends FactoryModule { install(new LifecycleModule() { @Override protected void configure() { + bind(StartPluginListener.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 new file mode 100644 index 0000000000..b82eb8f56f --- /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 key = Commands.key(plugin.getName()); + Provider 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/commands/MasterPluginsModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterPluginsModule.java deleted file mode 100644 index 76eebce02b..0000000000 --- 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 plugins = pluginLoader.getPlugins(); - for (Plugin p : plugins) { - if (PluginCommandModule.class.isAssignableFrom(p.sshModule)) { - @SuppressWarnings("unchecked") - Class c = (Class) 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 788dfa14b0..d9015c6a72 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 com.google.inject.binder.LinkedBindingBuilder; 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(String subCmd) { return bind(Commands.key(command, subCmd)); 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 8ffc531bae..0c15dfdd00 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.ReceiveCommitsExecutorModule; 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 @@ public class WebAppInitializer extends GuiceServletContextListener { sshInjector = createSshInjector(); webInjector = createWebInjector(); + PluginGuiceEnvironment env = sysInjector.getInstance(PluginGuiceEnvironment.class); + env.setCfgInjector(cfgInjector); + env.setSshInjector(sshInjector); + // Push the Provider 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 @@ public class WebAppInitializer extends GuiceServletContextListener { 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> provider() { @@ -212,7 +218,6 @@ public class WebAppInitializer extends GuiceServletContextListener { final List modules = new ArrayList(); modules.add(new SshModule()); modules.add(new MasterCommandModule()); - modules.add(cfgInjector.getInstance(MasterPluginsModule.class)); return sysInjector.createChildInjector(modules); }