728 lines
24 KiB
Java
728 lines
24 KiB
Java
// 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.CharMatcher;
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.base.MoreObjects;
|
|
import com.google.common.base.Strings;
|
|
import com.google.common.collect.ComparisonChain;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.common.collect.LinkedHashMultimap;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.collect.SetMultimap;
|
|
import com.google.common.collect.Sets;
|
|
import com.google.common.flogger.FluentLogger;
|
|
import com.google.gerrit.extensions.events.LifecycleListener;
|
|
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
|
|
import com.google.gerrit.extensions.systemstatus.ServerInformation;
|
|
import com.google.gerrit.server.PluginUser;
|
|
import com.google.gerrit.server.cache.PersistentCacheFactory;
|
|
import com.google.gerrit.server.config.CanonicalWebUrl;
|
|
import com.google.gerrit.server.config.ConfigUtil;
|
|
import com.google.gerrit.server.config.GerritRuntime;
|
|
import com.google.gerrit.server.config.GerritServerConfig;
|
|
import com.google.gerrit.server.config.SitePaths;
|
|
import com.google.gerrit.server.plugins.ServerPluginProvider.PluginDescription;
|
|
import com.google.inject.Inject;
|
|
import com.google.inject.Provider;
|
|
import com.google.inject.Singleton;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.nio.file.DirectoryStream;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.AbstractMap;
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Comparator;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.Queue;
|
|
import java.util.Set;
|
|
import java.util.TreeSet;
|
|
import java.util.concurrent.ConcurrentMap;
|
|
import java.util.concurrent.TimeUnit;
|
|
import org.eclipse.jgit.internal.storage.file.FileSnapshot;
|
|
import org.eclipse.jgit.lib.Config;
|
|
|
|
@Singleton
|
|
public class PluginLoader implements LifecycleListener {
|
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
|
|
public String getPluginName(Path srcPath) {
|
|
return MoreObjects.firstNonNull(getGerritPluginName(srcPath), PluginUtil.nameOf(srcPath));
|
|
}
|
|
|
|
private final Path pluginsDir;
|
|
private final Path dataDir;
|
|
private final Path tempDir;
|
|
private final PluginGuiceEnvironment env;
|
|
private final ServerInformationImpl srvInfoImpl;
|
|
private final PluginUser.Factory pluginUserFactory;
|
|
private final ConcurrentMap<String, Plugin> running = Maps.newConcurrentMap();
|
|
private final ConcurrentMap<String, Plugin> disabled = Maps.newConcurrentMap();
|
|
private final Map<String, FileSnapshot> broken = Maps.newHashMap();
|
|
private final Map<Plugin, CleanupHandle> cleanupHandles = Maps.newConcurrentMap();
|
|
private final Queue<Plugin> toCleanup = new ArrayDeque<>();
|
|
private final Provider<PluginCleanerTask> cleaner;
|
|
private final PluginScannerThread scanner;
|
|
private final Provider<String> urlProvider;
|
|
private final PersistentCacheFactory persistentCacheFactory;
|
|
private final boolean remoteAdmin;
|
|
private final UniversalServerPluginProvider serverPluginFactory;
|
|
private final GerritRuntime gerritRuntime;
|
|
|
|
@Inject
|
|
public PluginLoader(
|
|
SitePaths sitePaths,
|
|
PluginGuiceEnvironment pe,
|
|
ServerInformationImpl sii,
|
|
PluginUser.Factory puf,
|
|
Provider<PluginCleanerTask> pct,
|
|
@GerritServerConfig Config cfg,
|
|
@CanonicalWebUrl Provider<String> provider,
|
|
PersistentCacheFactory cacheFactory,
|
|
UniversalServerPluginProvider pluginFactory,
|
|
GerritRuntime gerritRuntime) {
|
|
pluginsDir = sitePaths.plugins_dir;
|
|
dataDir = sitePaths.data_dir;
|
|
tempDir = sitePaths.tmp_dir;
|
|
env = pe;
|
|
srvInfoImpl = sii;
|
|
pluginUserFactory = puf;
|
|
cleaner = pct;
|
|
urlProvider = provider;
|
|
persistentCacheFactory = cacheFactory;
|
|
serverPluginFactory = pluginFactory;
|
|
|
|
remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
|
|
this.gerritRuntime = gerritRuntime;
|
|
|
|
long checkFrequency =
|
|
ConfigUtil.getTimeUnit(
|
|
cfg,
|
|
"plugins",
|
|
null,
|
|
"checkFrequency",
|
|
TimeUnit.MINUTES.toMillis(1),
|
|
TimeUnit.MILLISECONDS);
|
|
if (checkFrequency > 0) {
|
|
scanner = new PluginScannerThread(this, checkFrequency);
|
|
} else {
|
|
scanner = null;
|
|
}
|
|
}
|
|
|
|
public boolean isRemoteAdminEnabled() {
|
|
return remoteAdmin;
|
|
}
|
|
|
|
public void checkRemoteAdminEnabled() throws MethodNotAllowedException {
|
|
if (!remoteAdmin) {
|
|
throw new MethodNotAllowedException("remote plugin administration is disabled");
|
|
}
|
|
}
|
|
|
|
public Plugin get(String name) {
|
|
Plugin p = running.get(name);
|
|
if (p != null) {
|
|
return p;
|
|
}
|
|
return disabled.get(name);
|
|
}
|
|
|
|
public Iterable<Plugin> getPlugins(boolean all) {
|
|
if (!all) {
|
|
return running.values();
|
|
}
|
|
List<Plugin> plugins = new ArrayList<>(running.values());
|
|
plugins.addAll(disabled.values());
|
|
return plugins;
|
|
}
|
|
|
|
public String installPluginFromStream(String originalName, InputStream in)
|
|
throws IOException, PluginInstallException {
|
|
checkRemoteInstall();
|
|
|
|
String fileName = originalName;
|
|
Path tmp = PluginUtil.asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
|
|
String name = MoreObjects.firstNonNull(getGerritPluginName(tmp), PluginUtil.nameOf(fileName));
|
|
if (!originalName.equals(name)) {
|
|
logger.atWarning().log(
|
|
"Plugin provides its own name: <%s>, use it instead of the input name: <%s>",
|
|
name, originalName);
|
|
}
|
|
|
|
String fileExtension = getExtension(fileName);
|
|
Path dst = pluginsDir.resolve(name + fileExtension);
|
|
synchronized (this) {
|
|
Plugin active = running.get(name);
|
|
if (active != null) {
|
|
fileName = active.getSrcFile().getFileName().toString();
|
|
logger.atInfo().log("Replacing plugin %s", active.getName());
|
|
Path old = pluginsDir.resolve(".last_" + fileName);
|
|
Files.deleteIfExists(old);
|
|
Files.move(active.getSrcFile(), old);
|
|
}
|
|
|
|
Files.deleteIfExists(pluginsDir.resolve(fileName + ".disabled"));
|
|
Files.move(tmp, dst);
|
|
try {
|
|
Plugin plugin = runPlugin(name, dst, active);
|
|
if (active == null) {
|
|
logger.atInfo().log("Installed plugin %s", plugin.getName());
|
|
}
|
|
} catch (PluginInstallException e) {
|
|
Files.deleteIfExists(dst);
|
|
throw e;
|
|
}
|
|
|
|
cleanInBackground();
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
private synchronized void unloadPlugin(Plugin plugin) {
|
|
persistentCacheFactory.onStop(plugin.getName());
|
|
String name = plugin.getName();
|
|
logger.atInfo().log("Unloading plugin %s, version %s", name, plugin.getVersion());
|
|
plugin.stop(env);
|
|
env.onStopPlugin(plugin);
|
|
running.remove(name);
|
|
disabled.remove(name);
|
|
toCleanup.add(plugin);
|
|
}
|
|
|
|
public void disablePlugins(Set<String> names) {
|
|
if (!isRemoteAdminEnabled()) {
|
|
logger.atWarning().log(
|
|
"Remote plugin administration is disabled, ignoring disablePlugins(%s)", names);
|
|
return;
|
|
}
|
|
|
|
synchronized (this) {
|
|
for (String name : names) {
|
|
Plugin active = running.get(name);
|
|
if (active == null) {
|
|
continue;
|
|
}
|
|
|
|
logger.atInfo().log("Disabling plugin %s", active.getName());
|
|
Path off =
|
|
active.getSrcFile().resolveSibling(active.getSrcFile().getFileName() + ".disabled");
|
|
try {
|
|
Files.move(active.getSrcFile(), off);
|
|
} catch (IOException e) {
|
|
logger.atSevere().withCause(e).log("Failed to disable plugin");
|
|
// In theory we could still unload the plugin even if the rename
|
|
// failed. However, it would be reloaded on the next server startup,
|
|
// which is probably not what the user expects.
|
|
continue;
|
|
}
|
|
|
|
unloadPlugin(active);
|
|
try {
|
|
FileSnapshot snapshot = FileSnapshot.save(off.toFile());
|
|
Plugin offPlugin = loadPlugin(name, off, snapshot);
|
|
disabled.put(name, offPlugin);
|
|
} catch (Throwable e) {
|
|
// This shouldn't happen, as the plugin was loaded earlier.
|
|
logger.atWarning().withCause(e.getCause()).log(
|
|
"Cannot load disabled plugin %s", active.getName());
|
|
}
|
|
}
|
|
cleanInBackground();
|
|
}
|
|
}
|
|
|
|
public void enablePlugins(Set<String> names) throws PluginInstallException {
|
|
if (!isRemoteAdminEnabled()) {
|
|
logger.atWarning().log(
|
|
"Remote plugin administration is disabled, ignoring enablePlugins(%s)", names);
|
|
return;
|
|
}
|
|
|
|
synchronized (this) {
|
|
for (String name : names) {
|
|
Plugin off = disabled.get(name);
|
|
if (off == null) {
|
|
continue;
|
|
}
|
|
|
|
logger.atInfo().log("Enabling plugin %s", name);
|
|
String n = off.getSrcFile().toFile().getName();
|
|
if (n.endsWith(".disabled")) {
|
|
n = n.substring(0, n.lastIndexOf('.'));
|
|
}
|
|
Path on = pluginsDir.resolve(n);
|
|
try {
|
|
Files.move(off.getSrcFile(), on);
|
|
} catch (IOException e) {
|
|
logger.atSevere().withCause(e).log("Failed to move plugin %s into place", name);
|
|
continue;
|
|
}
|
|
disabled.remove(name);
|
|
runPlugin(name, on, null);
|
|
}
|
|
cleanInBackground();
|
|
}
|
|
}
|
|
|
|
private void removeStalePluginFiles() {
|
|
DirectoryStream.Filter<Path> filter =
|
|
entry -> entry.getFileName().toString().startsWith("plugin_");
|
|
try (DirectoryStream<Path> files = Files.newDirectoryStream(tempDir, filter)) {
|
|
for (Path file : files) {
|
|
logger.atInfo().log("Removing stale plugin file: %s", file.toFile().getName());
|
|
try {
|
|
Files.delete(file);
|
|
} catch (IOException e) {
|
|
logger.atSevere().log(
|
|
"Failed to remove stale plugin file %s: %s", file.toFile().getName(), e.getMessage());
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
logger.atWarning().log("Unable to discover stale plugin files: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public synchronized void start() {
|
|
removeStalePluginFiles();
|
|
Path absolutePath = pluginsDir.toAbsolutePath();
|
|
if (!Files.exists(absolutePath)) {
|
|
logger.atInfo().log("%s does not exist; creating", absolutePath);
|
|
try {
|
|
Files.createDirectories(absolutePath);
|
|
} catch (IOException e) {
|
|
logger.atSevere().log("Failed to create %s: %s", absolutePath, e.getMessage());
|
|
}
|
|
}
|
|
logger.atInfo().log("Loading plugins from %s", absolutePath);
|
|
srvInfoImpl.state = ServerInformation.State.STARTUP;
|
|
rescan();
|
|
srvInfoImpl.state = ServerInformation.State.RUNNING;
|
|
if (scanner != null) {
|
|
scanner.start();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void stop() {
|
|
if (scanner != null) {
|
|
scanner.end();
|
|
}
|
|
srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
|
|
synchronized (this) {
|
|
for (Plugin p : running.values()) {
|
|
unloadPlugin(p);
|
|
}
|
|
running.clear();
|
|
disabled.clear();
|
|
broken.clear();
|
|
if (!toCleanup.isEmpty()) {
|
|
System.gc();
|
|
processPendingCleanups();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void reload(List<String> names) throws InvalidPluginException, PluginInstallException {
|
|
synchronized (this) {
|
|
List<Plugin> reload = Lists.newArrayListWithCapacity(names.size());
|
|
List<String> bad = Lists.newArrayListWithExpectedSize(4);
|
|
for (String name : names) {
|
|
Plugin active = running.get(name);
|
|
if (active != null) {
|
|
reload.add(active);
|
|
} else {
|
|
bad.add(name);
|
|
}
|
|
}
|
|
if (!bad.isEmpty()) {
|
|
throw new InvalidPluginException(
|
|
String.format("Plugin(s) \"%s\" not running", Joiner.on("\", \"").join(bad)));
|
|
}
|
|
|
|
for (Plugin active : reload) {
|
|
String name = active.getName();
|
|
try {
|
|
logger.atInfo().log("Reloading plugin %s", name);
|
|
Plugin newPlugin = runPlugin(name, active.getSrcFile(), active);
|
|
logger.atInfo().log(
|
|
"Reloaded plugin %s, version %s", newPlugin.getName(), newPlugin.getVersion());
|
|
} catch (PluginInstallException e) {
|
|
logger.atWarning().withCause(e.getCause()).log("Cannot reload plugin %s", name);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
cleanInBackground();
|
|
}
|
|
}
|
|
|
|
public synchronized void rescan() {
|
|
SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
|
|
if (pluginsFiles.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
syncDisabledPlugins(pluginsFiles);
|
|
|
|
Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
|
|
for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
|
|
String name = entry.getKey();
|
|
Path path = entry.getValue();
|
|
String fileName = path.getFileName().toString();
|
|
if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
|
|
logger.atWarning().log(
|
|
"No Plugin provider was found that handles this file format: %s", fileName);
|
|
continue;
|
|
}
|
|
|
|
FileSnapshot brokenTime = broken.get(name);
|
|
if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
|
|
continue;
|
|
}
|
|
|
|
Plugin active = running.get(name);
|
|
if (active != null && !active.isModified(path)) {
|
|
continue;
|
|
}
|
|
|
|
if (active != null) {
|
|
logger.atInfo().log("Reloading plugin %s", active.getName());
|
|
}
|
|
|
|
try {
|
|
Plugin loadedPlugin = runPlugin(name, path, active);
|
|
if (!loadedPlugin.isDisabled()) {
|
|
logger.atInfo().log(
|
|
"%s plugin %s, version %s",
|
|
active == null ? "Loaded" : "Reloaded",
|
|
loadedPlugin.getName(),
|
|
loadedPlugin.getVersion());
|
|
}
|
|
} catch (PluginInstallException e) {
|
|
logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
|
|
}
|
|
}
|
|
|
|
cleanInBackground();
|
|
}
|
|
|
|
private void addAllEntries(Map<String, Path> from, TreeSet<Entry<String, Path>> to) {
|
|
Iterator<Entry<String, Path>> it = from.entrySet().iterator();
|
|
while (it.hasNext()) {
|
|
Entry<String, Path> entry = it.next();
|
|
to.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue()));
|
|
}
|
|
}
|
|
|
|
private TreeSet<Entry<String, Path>> jarsFirstSortedPluginsSet(Map<String, Path> activePlugins) {
|
|
TreeSet<Entry<String, Path>> sortedPlugins =
|
|
Sets.newTreeSet(
|
|
new Comparator<Entry<String, Path>>() {
|
|
@Override
|
|
public int compare(Entry<String, Path> e1, Entry<String, Path> e2) {
|
|
Path n1 = e1.getValue().getFileName();
|
|
Path n2 = e2.getValue().getFileName();
|
|
return ComparisonChain.start()
|
|
.compareTrueFirst(isJar(n1), isJar(n2))
|
|
.compare(n1, n2)
|
|
.result();
|
|
}
|
|
|
|
private boolean isJar(Path n1) {
|
|
return n1.toString().endsWith(".jar");
|
|
}
|
|
});
|
|
|
|
addAllEntries(activePlugins, sortedPlugins);
|
|
return sortedPlugins;
|
|
}
|
|
|
|
private void syncDisabledPlugins(SetMultimap<String, Path> jars) {
|
|
stopRemovedPlugins(jars);
|
|
dropRemovedDisabledPlugins(jars);
|
|
}
|
|
|
|
private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin)
|
|
throws PluginInstallException {
|
|
FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
|
|
try {
|
|
Plugin newPlugin = loadPlugin(name, plugin, snapshot);
|
|
if (newPlugin.getCleanupHandle() != null) {
|
|
cleanupHandles.put(newPlugin, newPlugin.getCleanupHandle());
|
|
}
|
|
/*
|
|
* Pluggable plugin provider may have assigned a plugin name that could be
|
|
* actually different from the initial one assigned during scan. It is
|
|
* safer then to reassign it.
|
|
*/
|
|
name = newPlugin.getName();
|
|
boolean reload = oldPlugin != null && oldPlugin.canReload() && newPlugin.canReload();
|
|
if (!reload && oldPlugin != null) {
|
|
unloadPlugin(oldPlugin);
|
|
}
|
|
if (!newPlugin.isDisabled()) {
|
|
try {
|
|
newPlugin.start(env);
|
|
} catch (Throwable e) {
|
|
newPlugin.stop(env);
|
|
throw e;
|
|
}
|
|
}
|
|
if (reload) {
|
|
env.onReloadPlugin(oldPlugin, newPlugin);
|
|
unloadPlugin(oldPlugin);
|
|
} else if (!newPlugin.isDisabled()) {
|
|
env.onStartPlugin(newPlugin);
|
|
}
|
|
if (!newPlugin.isDisabled()) {
|
|
running.put(name, newPlugin);
|
|
} else {
|
|
disabled.put(name, newPlugin);
|
|
}
|
|
broken.remove(name);
|
|
return newPlugin;
|
|
} catch (Throwable err) {
|
|
broken.put(name, snapshot);
|
|
throw new PluginInstallException(err);
|
|
}
|
|
}
|
|
|
|
private void stopRemovedPlugins(SetMultimap<String, Path> jars) {
|
|
Set<String> unload = Sets.newHashSet(running.keySet());
|
|
for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
|
|
for (Path path : entry.getValue()) {
|
|
if (!path.getFileName().toString().endsWith(".disabled")) {
|
|
unload.remove(entry.getKey());
|
|
}
|
|
}
|
|
}
|
|
for (String name : unload) {
|
|
unloadPlugin(running.get(name));
|
|
}
|
|
}
|
|
|
|
private void dropRemovedDisabledPlugins(SetMultimap<String, Path> jars) {
|
|
Set<String> unload = Sets.newHashSet(disabled.keySet());
|
|
for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
|
|
for (Path path : entry.getValue()) {
|
|
if (path.getFileName().toString().endsWith(".disabled")) {
|
|
unload.remove(entry.getKey());
|
|
}
|
|
}
|
|
}
|
|
for (String name : unload) {
|
|
disabled.remove(name);
|
|
}
|
|
}
|
|
|
|
synchronized int processPendingCleanups() {
|
|
Iterator<Plugin> iterator = toCleanup.iterator();
|
|
while (iterator.hasNext()) {
|
|
Plugin plugin = iterator.next();
|
|
iterator.remove();
|
|
|
|
CleanupHandle cleanupHandle = cleanupHandles.remove(plugin);
|
|
if (cleanupHandle != null) {
|
|
cleanupHandle.cleanup();
|
|
}
|
|
}
|
|
return toCleanup.size();
|
|
}
|
|
|
|
private void cleanInBackground() {
|
|
int cnt = toCleanup.size();
|
|
if (0 < cnt) {
|
|
cleaner.get().clean(cnt);
|
|
}
|
|
}
|
|
|
|
private String getExtension(String name) {
|
|
int ext = name.lastIndexOf('.');
|
|
return 0 < ext ? name.substring(ext) : "";
|
|
}
|
|
|
|
private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot)
|
|
throws InvalidPluginException {
|
|
String pluginName = srcPlugin.getFileName().toString();
|
|
if (isUiPlugin(pluginName)) {
|
|
return loadJsPlugin(name, srcPlugin, snapshot);
|
|
} else if (serverPluginFactory.handles(srcPlugin)) {
|
|
return loadServerPlugin(srcPlugin, snapshot);
|
|
} else {
|
|
throw new InvalidPluginException(
|
|
String.format("Unsupported plugin type: %s", srcPlugin.getFileName()));
|
|
}
|
|
}
|
|
|
|
private Path getPluginDataDir(String name) {
|
|
return dataDir.resolve(name);
|
|
}
|
|
|
|
private String getPluginCanonicalWebUrl(String name) {
|
|
String canonicalWebUrl = urlProvider.get();
|
|
if (Strings.isNullOrEmpty(canonicalWebUrl)) {
|
|
return "/plugins/" + name;
|
|
}
|
|
|
|
String url =
|
|
String.format(
|
|
"%s/plugins/%s/", CharMatcher.is('/').trimTrailingFrom(canonicalWebUrl), name);
|
|
return url;
|
|
}
|
|
|
|
private Plugin loadJsPlugin(String name, Path srcJar, FileSnapshot snapshot) {
|
|
return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot);
|
|
}
|
|
|
|
private ServerPlugin loadServerPlugin(Path scriptFile, FileSnapshot snapshot)
|
|
throws InvalidPluginException {
|
|
String name = serverPluginFactory.getPluginName(scriptFile);
|
|
return serverPluginFactory.get(
|
|
scriptFile,
|
|
snapshot,
|
|
new PluginDescription(
|
|
pluginUserFactory.create(name),
|
|
getPluginCanonicalWebUrl(name),
|
|
getPluginDataDir(name),
|
|
gerritRuntime));
|
|
}
|
|
|
|
// Only one active plugin per plugin name can exist for each plugin name.
|
|
// Filter out disabled plugins and transform the multimap to a map
|
|
private Map<String, Path> filterDisabled(SetMultimap<String, Path> pluginPaths) {
|
|
Map<String, Path> activePlugins = Maps.newHashMapWithExpectedSize(pluginPaths.keys().size());
|
|
for (String name : pluginPaths.keys()) {
|
|
for (Path pluginPath : pluginPaths.asMap().get(name)) {
|
|
if (!pluginPath.getFileName().toString().endsWith(".disabled")) {
|
|
assert !activePlugins.containsKey(name);
|
|
activePlugins.put(name, pluginPath);
|
|
}
|
|
}
|
|
}
|
|
return activePlugins;
|
|
}
|
|
|
|
// Scan the $site_path/plugins directory and fetch all files and directories.
|
|
// The Key in returned multimap is the plugin name initially assigned from its filename.
|
|
// Values are the files. Plugins can optionally provide their name in MANIFEST file.
|
|
// If multiple plugin files provide the same plugin name, then only
|
|
// the first plugin remains active and all other plugins with the same
|
|
// name are disabled.
|
|
//
|
|
// NOTE: Bear in mind that the plugin name can be reassigned after load by the
|
|
// Server plugin provider.
|
|
public SetMultimap<String, Path> prunePlugins(Path pluginsDir) {
|
|
List<Path> pluginPaths = scanPathsInPluginsDirectory(pluginsDir);
|
|
SetMultimap<String, Path> map;
|
|
map = asMultimap(pluginPaths);
|
|
for (String plugin : map.keySet()) {
|
|
Collection<Path> files = map.asMap().get(plugin);
|
|
if (files.size() == 1) {
|
|
continue;
|
|
}
|
|
// retrieve enabled plugins
|
|
Iterable<Path> enabled = filterDisabledPlugins(files);
|
|
// If we have only one (the winner) plugin, nothing to do
|
|
if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
|
|
continue;
|
|
}
|
|
Path winner = Iterables.getFirst(enabled, null);
|
|
assert winner != null;
|
|
// Disable all loser plugins by renaming their file names to
|
|
// "file.disabled" and replace the disabled files in the multimap.
|
|
Collection<Path> elementsToRemove = new ArrayList<>();
|
|
Collection<Path> elementsToAdd = new ArrayList<>();
|
|
for (Path loser : Iterables.skip(enabled, 1)) {
|
|
logger.atWarning().log(
|
|
"Plugin <%s> was disabled, because"
|
|
+ " another plugin <%s>"
|
|
+ " with the same name <%s> already exists",
|
|
loser, winner, plugin);
|
|
Path disabledPlugin = Paths.get(loser + ".disabled");
|
|
elementsToAdd.add(disabledPlugin);
|
|
elementsToRemove.add(loser);
|
|
try {
|
|
Files.move(loser, disabledPlugin);
|
|
} catch (IOException e) {
|
|
logger.atWarning().withCause(e).log("Failed to fully disable plugin %s", loser);
|
|
}
|
|
}
|
|
Iterables.removeAll(files, elementsToRemove);
|
|
Iterables.addAll(files, elementsToAdd);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
private List<Path> scanPathsInPluginsDirectory(Path pluginsDir) {
|
|
try {
|
|
return PluginUtil.listPlugins(pluginsDir);
|
|
} catch (IOException e) {
|
|
logger.atSevere().withCause(e).log("Cannot list %s", pluginsDir.toAbsolutePath());
|
|
return ImmutableList.of();
|
|
}
|
|
}
|
|
|
|
private Iterable<Path> filterDisabledPlugins(Collection<Path> paths) {
|
|
return Iterables.filter(paths, p -> !p.getFileName().toString().endsWith(".disabled"));
|
|
}
|
|
|
|
public String getGerritPluginName(Path srcPath) {
|
|
String fileName = srcPath.getFileName().toString();
|
|
if (isUiPlugin(fileName)) {
|
|
return fileName.substring(0, fileName.lastIndexOf('.'));
|
|
}
|
|
if (serverPluginFactory.handles(srcPath)) {
|
|
return serverPluginFactory.getPluginName(srcPath);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private SetMultimap<String, Path> asMultimap(List<Path> plugins) {
|
|
SetMultimap<String, Path> map = LinkedHashMultimap.create();
|
|
for (Path srcPath : plugins) {
|
|
map.put(getPluginName(srcPath), srcPath);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
private boolean isUiPlugin(String name) {
|
|
return isPlugin(name, "js") || isPlugin(name, "html");
|
|
}
|
|
|
|
private boolean isPlugin(String fileName, String ext) {
|
|
String fullExt = "." + ext;
|
|
return fileName.endsWith(fullExt) || fileName.endsWith(fullExt + ".disabled");
|
|
}
|
|
|
|
private void checkRemoteInstall() throws PluginInstallException {
|
|
if (!isRemoteAdminEnabled()) {
|
|
throw new PluginInstallException("remote installation is disabled");
|
|
}
|
|
}
|
|
}
|