Merge changes I7afb6192,I7bc54c84,I8e60aa0f

* changes:
  Automatically load/unload/reload plugins
  Dynamically load plugins in new private injectors
  Allow SSH commands to be registered dynamically
This commit is contained in:
Martin Fick
2012-05-09 19:41:00 -07:00
committed by gerrit code review
17 changed files with 813 additions and 237 deletions

View File

@@ -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());

View File

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

View File

@@ -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() {
}
}

View File

@@ -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<? extends Module> sysModule;
private Class<? extends Module> sshModule;
private Injector sysInjector;
private Injector sshInjector;
private LifecycleManager manager;
public Plugin(String name,
FileSnapshot snapshot,
@Nullable Class<? extends Module> sysModule,
@Nullable Class<? extends Module> 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 + "]";
}
}

View File

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

View File

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

View File

@@ -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<? extends Module> sshModule;
public Plugin(String name, Class<? extends Module> 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);
}
}

View File

@@ -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 {
}

View File

@@ -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) {
}
}
}

View File

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

View File

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

View File

@@ -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<DispatchCommand> {
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 @@ public class DispatchCommandProvider implements Provider<DispatchCommand> {
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 @@ public class DispatchCommandProvider implements Provider<DispatchCommand> {
}
@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 @@ public class DispatchCommandProvider implements Provider<DispatchCommand> {
}
}
}
return Collections.unmodifiableMap(
new LinkedHashMap<String, Provider<Command>>(m));
return m;
}
private static final TypeLiteral<Command> type =

View File

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

View File

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

View File

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

View File

@@ -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> command(String subCmd) {
return bind(Commands.key(command, subCmd));

View File

@@ -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<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 @@ 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<? extends Provider<String>> provider() {
@@ -212,7 +218,6 @@ public class WebAppInitializer extends GuiceServletContextListener {
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);
}