Dynamically load plugins in new private injectors

Each plugin is given its own Guice injector that is isolated from the
main server, and from each other. Explicit bindings in the main server
are copied down into the plugin's private environment, making any
object that is bound in a module (e.g. GerritGlobalModule)
automatically available, but hiding anything that is loaded by a
just-in-time implicit binding.

These private injectors ensure plugins can't accidentally load a
just-in-time binding into the sysInjector and cause them to be unable
to garbage collect, or to confuse another plugin with a bogus binding.

Change-Id: I7bc54c84fba30381cfb58d24b88871b2714c335a
This commit is contained in:
Shawn O. Pearce
2012-05-08 19:16:30 -07:00
committed by gerrit code review
parent b4992582d6
commit 4c847cf912
15 changed files with 549 additions and 109 deletions

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,120 @@
// 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 javax.annotation.Nullable;
public class Plugin {
private final String name;
private final ClassLoader loader;
private Class<? extends Module> sysModule;
private Class<? extends Module> sshModule;
private Injector sysInjector;
private Injector sshInjector;
private LifecycleManager manager;
public Plugin(String name, ClassLoader loader,
@Nullable Class<? extends Module> sysModule,
@Nullable Class<? extends Module> sshModule) {
this.name = name;
this.loader = loader;
this.sysModule = sysModule;
this.sshModule = sshModule;
}
public String getName() {
return name;
}
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();
sysInjector = null;
sshInjector = null;
manager = 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,159 @@
// 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.gerrit.lifecycle.LifecycleListener;
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.List;
import java.util.Map;
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;
@Inject
public PluginLoader(SitePaths sitePaths, PluginGuiceEnvironment pe) {
pluginsDir = sitePaths.plugins_dir;
env = pe;
running = Maps.newHashMap();
}
@Override
public synchronized void start() {
log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
for (Plugin p : scanPlugins()) {
if (running.containsKey(p.getName())) {
log.error("Skipping duplicate plugin " + p.getName());
continue;
}
try {
p.start(env);
} catch (Exception err) {
log.error("Cannot start plugin " + p.getName(), err);
continue;
}
running.put(p.getName(), p);
}
}
@Override
public synchronized void stop() {
for (Plugin p : running.values()) {
p.stop();
}
running.clear();
}
private Collection<Plugin> scanPlugins() {
Collection<File> pluginJars;
try {
pluginJars = getPluginFiles();
} catch (IOException e) {
log.error("Cannot scan Gerrit plugins directory looking for jar files", e);
return Collections.emptyList();
}
List<Plugin> all = Lists.newArrayListWithCapacity(pluginJars.size());
for (File jarFile : pluginJars) {
try {
all.add(loadPlugin(jarFile));
} catch (IOException e) {
log.error("Cannot access plugin jar " + jarFile, e);
} catch (ClassNotFoundException e) {
log.error("Cannot load plugin class module from " + jarFile, e);
}
}
return all;
}
@SuppressWarnings("unchecked")
private Plugin loadPlugin(File jarFile) throws IOException,
ClassNotFoundException {
Manifest jarManifest = new JarFile(jarFile).getManifest();
ClassLoader parentLoader = PluginLoader.class.getClassLoader();
ClassLoader pluginLoader =
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<? extends Module> moduleClass =
(Class<? extends Module>) Class
.forName(moduleName, false, pluginLoader);
if (!Module.class.isAssignableFrom(moduleClass)) {
throw new ClassNotFoundException(String.format(
"Gerrit-SshModule %s is not a Guice Module", moduleClass.getName()));
}
return new Plugin(pluginName, pluginLoader, null, 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,27 @@
// 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.LifecycleModule;
public class PluginModule extends LifecycleModule {
@Override
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,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);
}