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:

committed by
gerrit code review

parent
b4992582d6
commit
4c847cf912
@@ -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() {
|
||||
}
|
||||
}
|
@@ -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 + "]";
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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);
|
||||
}
|
Reference in New Issue
Block a user