Convert plugin loading code to use Path

All of the internal scanning/loading code now uses Path, as do the
plugin and data directories in SitePaths. For backwards compatibility,
we still inject the @PluginData directory as a File. We do break
backwards compatibility of the ServerPluginProvider extension point,
under the theory that there should be very few such extensions and
they can handle the pain.

Tested:
-Installed and reloaded cookbook plugin multiple times.
-Expanded dynamic *.ssh plugin loading in the cookbook, with
 monkey-testing.

Change-Id: I5212d248d9288b88d08731c9854b42196999aa73
This commit is contained in:
Dave Borowitz
2015-02-24 10:17:04 -08:00
parent 05b1ae22d5
commit 9e158756e5
17 changed files with 350 additions and 293 deletions

View File

@@ -14,17 +14,17 @@
package com.google.gerrit.server.plugins;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarFile;
class CleanupHandle {
private final File tmpFile;
private final Path tmp;
private final JarFile jarFile;
CleanupHandle(File tmpFile,
JarFile jarFile) {
this.tmpFile = tmpFile;
CleanupHandle(Path tmp, JarFile jarFile) {
this.tmp = tmp;
this.jarFile = jarFile;
}
@@ -34,12 +34,13 @@ class CleanupHandle {
} catch (IOException err) {
PluginLoader.log.error("Cannot close " + jarFile.getName(), err);
}
if (!tmpFile.delete() && tmpFile.exists()) {
PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath()
+ ", retrying to delete it on termination of the virtual machine");
tmpFile.deleteOnExit();
} else {
PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
try {
Files.deleteIfExists(tmp);
PluginLoader.log.info("Cleaned plugin " + tmp.getFileName());
} catch (IOException e) {
PluginLoader.log.warn("Cannot delete " + tmp.toAbsolutePath()
+ ", retrying to delete it on termination of the virtual machine", e);
tmp.toFile().deleteOnExit();
}
}
}

View File

@@ -24,8 +24,6 @@ import org.eclipse.jgit.internal.storage.file.FileSnapshot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
@@ -33,6 +31,7 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@@ -45,52 +44,50 @@ public class JarPluginProvider implements ServerPluginProvider {
static final String JAR_EXTENSION = ".jar";
static final Logger log = LoggerFactory.getLogger(JarPluginProvider.class);
private final File tmpDir;
private final Path tmpDir;
@Inject
JarPluginProvider(SitePaths sitePaths) {
// TODO(dborowitz): Convert to NIO.
tmpDir = sitePaths.tmp_dir.toFile();
tmpDir = sitePaths.tmp_dir;
}
@Override
public boolean handles(File srcFile) {
String fileName = srcFile.getName();
public boolean handles(Path srcPath) {
String fileName = srcPath.getFileName().toString();
return fileName.endsWith(JAR_EXTENSION)
|| fileName.endsWith(JAR_EXTENSION + ".disabled");
}
@Override
public String getPluginName(File srcFile) {
public String getPluginName(Path srcPath) {
try {
return MoreObjects.firstNonNull(getJarPluginName(srcFile),
PluginLoader.nameOf(srcFile));
return MoreObjects.firstNonNull(getJarPluginName(srcPath),
PluginLoader.nameOf(srcPath));
} catch (IOException e) {
throw new IllegalArgumentException("Invalid plugin file " + srcFile
throw new IllegalArgumentException("Invalid plugin file " + srcPath
+ ": cannot get plugin name", e);
}
}
public static String getJarPluginName(File srcFile) throws IOException {
try (JarFile jarFile = new JarFile(srcFile)) {
public static String getJarPluginName(Path srcPath) throws IOException {
try (JarFile jarFile = new JarFile(srcPath.toFile())) {
return jarFile.getManifest().getMainAttributes()
.getValue("Gerrit-PluginName");
}
}
@Override
public ServerPlugin get(File srcFile, FileSnapshot snapshot,
public ServerPlugin get(Path srcPath, FileSnapshot snapshot,
PluginDescription description) throws InvalidPluginException {
try {
String name = getPluginName(srcFile);
String extension = getExtension(srcFile);
try (FileInputStream in = new FileInputStream(srcFile)) {
File tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
return loadJarPlugin(name, srcFile.toPath(), snapshot, tmp,
description);
String name = getPluginName(srcPath);
String extension = getExtension(srcPath);
try (InputStream in = Files.newInputStream(srcPath)) {
Path tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
return loadJarPlugin(name, srcPath, snapshot, tmp, description);
}
} catch (IOException e) {
throw new InvalidPluginException("Cannot load Jar plugin " + srcFile, e);
throw new InvalidPluginException("Cannot load Jar plugin " + srcPath, e);
}
}
@@ -99,8 +96,8 @@ public class JarPluginProvider implements ServerPluginProvider {
return "gerrit";
}
private static String getExtension(File file) {
return getExtension(file.getName());
private static String getExtension(Path path) {
return getExtension(path.getFileName().toString());
}
private static String getExtension(String name) {
@@ -113,19 +110,18 @@ public class JarPluginProvider implements ServerPluginProvider {
return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
}
public static File storeInTemp(String pluginName, InputStream in,
public static Path storeInTemp(String pluginName, InputStream in,
SitePaths sitePaths) throws IOException {
if (!Files.exists(sitePaths.tmp_dir)) {
Files.createDirectories(sitePaths.tmp_dir);
}
return asTemp(in, tempNameFor(pluginName), ".jar",
sitePaths.tmp_dir.toFile());
return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
}
private ServerPlugin loadJarPlugin(String name, Path srcJar,
FileSnapshot snapshot, File tmp, PluginDescription description)
FileSnapshot snapshot, Path tmp, PluginDescription description)
throws IOException, InvalidPluginException, MalformedURLException {
JarFile jarFile = new JarFile(tmp);
JarFile jarFile = new JarFile(tmp.toFile());
boolean keep = false;
try {
Manifest manifest = jarFile.getManifest();
@@ -134,14 +130,13 @@ public class JarPluginProvider implements ServerPluginProvider {
List<URL> urls = new ArrayList<>(2);
String overlay = System.getProperty("gerrit.plugin-classes");
if (overlay != null) {
File classes = new File(new File(new File(overlay), name), "main");
if (classes.isDirectory()) {
log.info(String.format("plugin %s: including %s", name,
classes.getPath()));
urls.add(classes.toURI().toURL());
Path classes = Paths.get(overlay).resolve(name).resolve("main");
if (Files.isDirectory(classes)) {
log.info(String.format("plugin %s: including %s", name, classes));
urls.add(classes.toUri().toURL());
}
}
urls.add(tmp.toURI().toURL());
urls.add(tmp.toUri().toURL());
ClassLoader pluginLoader =
new URLClassLoader(urls.toArray(new URL[urls.size()]),
@@ -149,7 +144,7 @@ public class JarPluginProvider implements ServerPluginProvider {
JarScanner jarScanner = createJarScanner(srcJar);
ServerPlugin plugin = new ServerPlugin(name, description.canonicalUrl,
description.user, srcJar.toFile(), snapshot, jarScanner,
description.user, srcJar, snapshot, jarScanner,
description.dataDir, pluginLoader);
plugin.setCleanupHandle(new CleanupHandle(tmp, jarFile));
keep = true;

View File

@@ -18,14 +18,14 @@ import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import java.io.File;
import java.nio.file.Path;
class MultipleProvidersForPluginException extends IllegalArgumentException {
private static final long serialVersionUID = 1L;
MultipleProvidersForPluginException(File pluginSrcFile,
MultipleProvidersForPluginException(Path pluginSrcPath,
Iterable<ServerPluginProvider> providersHandlers) {
super(pluginSrcFile.getAbsolutePath()
super(pluginSrcPath.toAbsolutePath()
+ " is claimed to be handled by more than one plugin provider: "
+ providersListToString(providersHandlers));
}

View File

@@ -14,6 +14,8 @@
package com.google.gerrit.server.plugins;
import static com.google.gerrit.common.FileUtil.lastModified;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Nullable;
@@ -25,7 +27,6 @@ import com.google.inject.Injector;
import org.eclipse.jgit.internal.storage.file.FileSnapshot;
import java.io.File;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
@@ -81,18 +82,17 @@ public abstract class Plugin {
private List<ReloadableRegistrationHandle<?>> reloadableHandles;
public Plugin(String name,
Path srcFile,
Path srcPath,
PluginUser pluginUser,
FileSnapshot snapshot,
ApiType apiType) {
this.name = name;
// TODO(dborowitz): Rename to srcPath or something.
this.srcFile = srcFile;
this.srcFile = srcPath;
this.apiType = apiType;
this.snapshot = snapshot;
this.pluginUser = pluginUser;
this.cacheKey = new Plugin.CacheKey(name);
this.disabled = srcFile.getFileName().toString().endsWith(".disabled");
this.disabled = srcPath.getFileName().toString().endsWith(".disabled");
}
public CleanupHandle getCleanupHandle() {
@@ -170,7 +170,7 @@ public abstract class Plugin {
abstract boolean canReload();
boolean isModified(File jar) {
return snapshot.lastModified() != jar.lastModified();
boolean isModified(Path jar) {
return snapshot.lastModified() != lastModified(jar);
}
}

View File

@@ -11,14 +11,15 @@
// 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.Optional;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.nio.file.NoSuchFileException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
@@ -57,7 +58,7 @@ public interface PluginContentScanner {
@Override
public InputStream getInputStream(PluginEntry entry) throws IOException {
throw new FileNotFoundException("Empty plugin");
throw new NoSuchFileException("Empty plugin");
}
@Override

View File

@@ -0,0 +1,38 @@
// Copyright (C) 2015 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.extensions.annotations.PluginData;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.File;
import java.nio.file.Path;
@Singleton
class PluginDataAsFileProvider implements Provider<File> {
private final Provider<Path> pathProvider;
@Inject
PluginDataAsFileProvider(@PluginData Provider<Path> pathProvider) {
this.pathProvider = pathProvider;
}
@Override
public File get() {
return pathProvider.get().toFile();
}
}

View File

@@ -18,6 +18,8 @@ import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
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;
@@ -25,6 +27,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -45,15 +48,15 @@ import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@@ -72,13 +75,13 @@ public class PluginLoader implements LifecycleListener {
static final String PLUGIN_TMP_PREFIX = "plugin_";
static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
public String getPluginName(File srcFile) {
return MoreObjects.firstNonNull(getGerritPluginName(srcFile),
nameOf(srcFile));
public String getPluginName(Path srcPath) {
return MoreObjects.firstNonNull(getGerritPluginName(srcPath),
nameOf(srcPath));
}
private final File pluginsDir;
private final File dataDir;
private final Path pluginsDir;
private final Path dataDir;
private final PluginGuiceEnvironment env;
private final ServerInformationImpl srvInfoImpl;
private final PluginUser.Factory pluginUserFactory;
@@ -159,7 +162,7 @@ public class PluginLoader implements LifecycleListener {
checkRemoteInstall();
String fileName = originalName;
File tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
Path tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
String name = MoreObjects.firstNonNull(getGerritPluginName(tmp),
nameOf(fileName));
if (!originalName.equals(name)) {
@@ -169,26 +172,26 @@ public class PluginLoader implements LifecycleListener {
}
String fileExtension = getExtension(fileName);
File dst = new File(pluginsDir, name + fileExtension);
Path dst = pluginsDir.resolve(name + fileExtension);
synchronized (this) {
Plugin active = running.get(name);
if (active != null) {
fileName = active.getSrcFile().getFileName().toString();
log.info(String.format("Replacing plugin %s", active.getName()));
File old = new File(pluginsDir, ".last_" + fileName);
old.delete();
active.getSrcFile().toFile().renameTo(old);
Path old = pluginsDir.resolve(".last_" + fileName);
Files.deleteIfExists(old);
Files.move(active.getSrcFile(), old);
}
new File(pluginsDir, fileName + ".disabled").delete();
tmp.renameTo(dst);
Files.deleteIfExists(pluginsDir.resolve(fileName + ".disabled"));
Files.move(tmp, dst);
try {
Plugin plugin = runPlugin(name, dst, active);
if (active == null) {
log.info(String.format("Installed plugin %s", plugin.getName()));
}
} catch (PluginInstallException e) {
dst.delete();
Files.deleteIfExists(dst);
throw e;
}
@@ -196,21 +199,17 @@ public class PluginLoader implements LifecycleListener {
}
}
static File asTemp(InputStream in, String prefix, String suffix, File dir)
static Path asTemp(InputStream in, String prefix, String suffix, Path dir)
throws IOException {
File tmp = File.createTempFile(prefix, suffix, dir);
Path tmp = Files.createTempFile(dir, prefix, suffix);
boolean keep = false;
try (FileOutputStream out = new FileOutputStream(tmp)) {
byte[] data = new byte[8192];
int n;
while ((n = in.read(data)) > 0) {
out.write(data, 0, n);
}
try (OutputStream out = Files.newOutputStream(tmp)) {
ByteStreams.copy(in, out);
keep = true;
return tmp;
} finally {
if (!keep) {
tmp.delete();
Files.delete(tmp);
}
}
}
@@ -241,12 +240,21 @@ public class PluginLoader implements LifecycleListener {
}
log.info(String.format("Disabling plugin %s", active.getName()));
File off = new File(active.getSrcFile() + ".disabled");
active.getSrcFile().toFile().renameTo(off);
Path off = active.getSrcFile().resolveSibling(
active.getSrcFile().getFileName() + ".disabled");
try {
Files.move(active.getSrcFile(), off);
} catch (IOException e) {
log.error("Failed to disable plugin", e);
// 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);
FileSnapshot snapshot = FileSnapshot.save(off.toFile());
Plugin offPlugin = loadPlugin(name, off, snapshot);
disabled.put(name, offPlugin);
} catch (Throwable e) {
@@ -279,9 +287,13 @@ public class PluginLoader implements LifecycleListener {
if (n.endsWith(".disabled")) {
n = n.substring(0, n.lastIndexOf('.'));
}
File on = new File(pluginsDir, n);
off.getSrcFile().toFile().renameTo(on);
Path on = pluginsDir.resolve(n);
try {
Files.move(off.getSrcFile(), on);
} catch (IOException e) {
log.error("Failed to move plugin " + name + " into place", e);
continue;
}
disabled.remove(name);
runPlugin(name, on, null);
}
@@ -291,7 +303,7 @@ public class PluginLoader implements LifecycleListener {
@Override
public synchronized void start() {
log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
log.info("Loading plugins from " + pluginsDir.toAbsolutePath());
srvInfoImpl.state = ServerInformation.State.STARTUP;
rescan();
srvInfoImpl.state = ServerInformation.State.RUNNING;
@@ -343,7 +355,7 @@ public class PluginLoader implements LifecycleListener {
String name = active.getName();
try {
log.info(String.format("Reloading plugin %s", name));
runPlugin(name, active.getSrcFile().toFile(), active);
runPlugin(name, active.getSrcFile(), active);
} catch (PluginInstallException e) {
log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
throw e;
@@ -355,30 +367,30 @@ public class PluginLoader implements LifecycleListener {
}
public synchronized void rescan() {
Multimap<String, File> pluginsFiles = prunePlugins(pluginsDir);
Multimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
if (pluginsFiles.isEmpty()) {
return;
}
syncDisabledPlugins(pluginsFiles);
Map<String, File> activePlugins = filterDisabled(pluginsFiles);
for (Map.Entry<String, File> entry : jarsFirstSortedPluginsSet(activePlugins)) {
Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
String name = entry.getKey();
File file = entry.getValue();
String fileName = file.getName();
if (!isJsPlugin(fileName) && !serverPluginFactory.handles(file)) {
Path path = entry.getValue();
String fileName = path.getFileName().toString();
if (!isJsPlugin(fileName) && !serverPluginFactory.handles(path)) {
log.warn("No Plugin provider was found that handles this file format: {}", fileName);
continue;
}
FileSnapshot brokenTime = broken.get(name);
if (brokenTime != null && !brokenTime.isModified(file)) {
if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
continue;
}
Plugin active = running.get(name);
if (active != null && !active.isModified(file)) {
if (active != null && !active.isModified(path)) {
continue;
}
@@ -388,7 +400,7 @@ public class PluginLoader implements LifecycleListener {
}
try {
Plugin loadedPlugin = runPlugin(name, file, active);
Plugin loadedPlugin = runPlugin(name, path, active);
if (active == null && !loadedPlugin.isDisabled()) {
log.info(String.format("Loaded plugin %s, version %s",
loadedPlugin.getName(), loadedPlugin.getVersion()));
@@ -401,31 +413,28 @@ public class PluginLoader implements LifecycleListener {
cleanInBackground();
}
private void addAllEntries(Map<String, File> from,
TreeSet<Entry<String, File>> to) {
Iterator<Entry<String, File>> it = from.entrySet().iterator();
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,File> entry = it.next();
Entry<String,Path> entry = it.next();
to.add(new AbstractMap.SimpleImmutableEntry<>(
entry.getKey(), entry.getValue()));
}
}
private TreeSet<Entry<String, File>> jarsFirstSortedPluginsSet(
Map<String, File> activePlugins) {
TreeSet<Entry<String, File>> sortedPlugins =
Sets.newTreeSet(new Comparator<Entry<String, File>>() {
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, File> entry1,
Entry<String, File> entry2) {
String file1 = entry1.getValue().getName();
String file2 = entry2.getValue().getName();
int cmp = file1.compareTo(file2);
if (file1.endsWith(".jar")) {
return (file2.endsWith(".jar") ? cmp : -1);
} else {
return (file2.endsWith(".jar") ? +1 : cmp);
}
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(n1.endsWith(".jar"), n2.endsWith(".jar"))
.compare(n1, n2)
.result();
}
});
@@ -433,14 +442,14 @@ public class PluginLoader implements LifecycleListener {
return sortedPlugins;
}
private void syncDisabledPlugins(Multimap<String, File> jars) {
private void syncDisabledPlugins(Multimap<String, Path> jars) {
stopRemovedPlugins(jars);
dropRemovedDisabledPlugins(jars);
}
private Plugin runPlugin(String name, File plugin, Plugin oldPlugin)
private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin)
throws PluginInstallException {
FileSnapshot snapshot = FileSnapshot.save(plugin);
FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
try {
Plugin newPlugin = loadPlugin(name, plugin, snapshot);
if (newPlugin.getCleanupHandle() != null) {
@@ -480,11 +489,11 @@ public class PluginLoader implements LifecycleListener {
}
}
private void stopRemovedPlugins(Multimap<String, File> jars) {
private void stopRemovedPlugins(Multimap<String, Path> jars) {
Set<String> unload = Sets.newHashSet(running.keySet());
for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) {
for (File file : entry.getValue()) {
if (!file.getName().endsWith(".disabled")) {
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());
}
}
@@ -494,11 +503,11 @@ public class PluginLoader implements LifecycleListener {
}
}
private void dropRemovedDisabledPlugins(Multimap<String, File> jars) {
private void dropRemovedDisabledPlugins(Multimap<String, Path> jars) {
Set<String> unload = Sets.newHashSet(disabled.keySet());
for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) {
for (File file : entry.getValue()) {
if (file.getName().endsWith(".disabled")) {
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());
}
}
@@ -529,8 +538,8 @@ public class PluginLoader implements LifecycleListener {
}
}
public static String nameOf(File plugin) {
return nameOf(plugin.getName());
public static String nameOf(Path plugin) {
return nameOf(plugin.getFileName().toString());
}
private static String nameOf(String name) {
@@ -546,21 +555,21 @@ public class PluginLoader implements LifecycleListener {
return 0 < ext ? name.substring(ext) : "";
}
private Plugin loadPlugin(String name, File srcPlugin, FileSnapshot snapshot)
private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot)
throws InvalidPluginException {
String pluginName = srcPlugin.getName();
String pluginName = srcPlugin.getFileName().toString();
if (isJsPlugin(pluginName)) {
return loadJsPlugin(name, srcPlugin.toPath(), snapshot);
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.getName()));
"Unsupported plugin type: %s", srcPlugin.getFileName()));
}
}
private File getPluginDataDir(String name) {
return new File(dataDir, name);
private Path getPluginDataDir(String name) {
return dataDir.resolve(name);
}
private String getPluginCanonicalWebUrl(String name) {
@@ -574,7 +583,7 @@ public class PluginLoader implements LifecycleListener {
return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot);
}
private ServerPlugin loadServerPlugin(File scriptFile,
private ServerPlugin loadServerPlugin(Path scriptFile,
FileSnapshot snapshot) throws InvalidPluginException {
String name = serverPluginFactory.getPluginName(scriptFile);
return serverPluginFactory.get(scriptFile, snapshot, new PluginDescription(
@@ -598,15 +607,15 @@ public class PluginLoader implements LifecycleListener {
// 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 static Map<String, File> filterDisabled(
Multimap<String, File> pluginFiles) {
Map<String, File> activePlugins = Maps.newHashMapWithExpectedSize(
pluginFiles.keys().size());
for (String name : pluginFiles.keys()) {
for (File pluginFile : pluginFiles.asMap().get(name)) {
if (!pluginFile.getName().endsWith(".disabled")) {
private static Map<String, Path> filterDisabled(
Multimap<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, pluginFile);
activePlugins.put(name, pluginPath);
}
}
}
@@ -622,37 +631,40 @@ public class PluginLoader implements LifecycleListener {
//
// NOTE: Bear in mind that the plugin name can be reassigned after load by the
// Server plugin provider.
public Multimap<String, File> prunePlugins(File pluginsDir) {
List<File> pluginFiles = scanFilesInPluginsDirectory(pluginsDir);
Multimap<String, File> map;
map = asMultimap(pluginFiles);
public Multimap<String, Path> prunePlugins(Path pluginsDir) {
List<Path> pluginPaths = scanPathsInPluginsDirectory(pluginsDir);
Multimap<String, Path> map;
map = asMultimap(pluginPaths);
for (String plugin : map.keySet()) {
Collection<File> files = map.asMap().get(plugin);
Collection<Path> files = map.asMap().get(plugin);
if (files.size() == 1) {
continue;
}
// retrieve enabled plugins
Iterable<File> enabled = filterDisabledPlugins(
files);
Iterable<Path> enabled = filterDisabledPlugins(files);
// If we have only one (the winner) plugin, nothing to do
if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
continue;
}
File winner = Iterables.getFirst(enabled, null);
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<File> elementsToRemove = Lists.newArrayList();
Collection<File> elementsToAdd = Lists.newArrayList();
for (File loser : Iterables.skip(enabled, 1)) {
Collection<Path> elementsToRemove = Lists.newArrayList();
Collection<Path> elementsToAdd = Lists.newArrayList();
for (Path loser : Iterables.skip(enabled, 1)) {
log.warn(String.format("Plugin <%s> was disabled, because"
+ " another plugin <%s>"
+ " with the same name <%s> already exists",
loser, winner, plugin));
File disabledPlugin = new File(loser + ".disabled");
Path disabledPlugin = Paths.get(loser + ".disabled");
elementsToAdd.add(disabledPlugin);
elementsToRemove.add(loser);
loser.renameTo(disabledPlugin);
try {
Files.move(loser, disabledPlugin);
} catch (IOException e) {
log.warn("Failed to fully disable plugin " + loser, e);
}
}
Iterables.removeAll(files, elementsToRemove);
Iterables.addAll(files, elementsToAdd);
@@ -660,50 +672,52 @@ public class PluginLoader implements LifecycleListener {
return map;
}
private List<File> scanFilesInPluginsDirectory(File pluginsDir) {
if (pluginsDir == null || !pluginsDir.exists()) {
private List<Path> scanPathsInPluginsDirectory(Path pluginsDir) {
if (pluginsDir == null || !Files.exists(pluginsDir)) {
return Collections.emptyList();
}
File[] matches = pluginsDir.listFiles(new FileFilter() {
DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
@Override
public boolean accept(File pathname) {
String n = pathname.getName();
public boolean accept(Path entry) throws IOException {
String n = entry.getFileName().toString();
return !n.startsWith(".last_")
&& !n.startsWith(".next_");
}
});
if (matches == null) {
log.error("Cannot list " + pluginsDir.getAbsolutePath());
return Collections.emptyList();
};
try (DirectoryStream<Path> files
= Files.newDirectoryStream(pluginsDir, filter)) {
return ImmutableList.copyOf(files);
} catch (IOException e) {
log.error("Cannot list " + pluginsDir.toAbsolutePath(), e);
return ImmutableList.of();
}
return Arrays.asList(matches);
}
private static Iterable<File> filterDisabledPlugins(
Collection<File> files) {
return Iterables.filter(files, new Predicate<File>() {
private static Iterable<Path> filterDisabledPlugins(
Collection<Path> paths) {
return Iterables.filter(paths, new Predicate<Path>() {
@Override
public boolean apply(File file) {
return !file.getName().endsWith(".disabled");
public boolean apply(Path p) {
return !p.getFileName().toString().endsWith(".disabled");
}
});
}
public String getGerritPluginName(File srcFile) {
String fileName = srcFile.getName();
public String getGerritPluginName(Path srcPath) {
String fileName = srcPath.getFileName().toString();
if (isJsPlugin(fileName)) {
return fileName.substring(0, fileName.length() - 3);
}
if (serverPluginFactory.handles(srcFile)) {
return serverPluginFactory.getPluginName(srcFile);
if (serverPluginFactory.handles(srcPath)) {
return serverPluginFactory.getPluginName(srcPath);
}
return null;
}
private Multimap<String, File> asMultimap(List<File> plugins) {
Multimap<String, File> map = LinkedHashMultimap.create();
for (File srcFile : plugins) {
map.put(getPluginName(srcFile), srcFile);
private Multimap<String, Path> asMultimap(List<Path> plugins) {
Multimap<String, Path> map = LinkedHashMultimap.create();
for (Path srcPath : plugins) {
map.put(getPluginName(srcPath), srcPath);
}
return map;
}

View File

@@ -36,6 +36,8 @@ import org.eclipse.jgit.internal.storage.file.FileSnapshot;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
@@ -59,7 +61,7 @@ public class ServerPlugin extends Plugin {
private final Manifest manifest;
private final PluginContentScanner scanner;
private final File dataDir;
private final Path dataDir;
private final String pluginCanonicalWebUrl;
private final ClassLoader classLoader;
private Class<? extends Module> sysModule;
@@ -75,12 +77,12 @@ public class ServerPlugin extends Plugin {
public ServerPlugin(String name,
String pluginCanonicalWebUrl,
PluginUser pluginUser,
File srcJar,
Path srcJar,
FileSnapshot snapshot,
PluginContentScanner scanner,
File dataDir,
Path dataDir,
ClassLoader classLoader) throws InvalidPluginException {
super(name, srcJar.toPath(), pluginUser, snapshot,
super(name, srcJar, pluginUser, snapshot,
Plugin.getApiType(getPluginManifest(scanner)));
this.pluginCanonicalWebUrl = pluginCanonicalWebUrl;
this.scanner = scanner;
@@ -128,8 +130,8 @@ public class ServerPlugin extends Plugin {
return (Class<? extends Module>) clazz;
}
File getSrcJar() {
return getSrcFile().toFile();
Path getSrcJar() {
return getSrcFile();
}
private static Manifest getPluginManifest(PluginContentScanner scanner)
@@ -245,20 +247,22 @@ public class ServerPlugin extends Plugin {
.annotatedWith(PluginCanonicalWebUrl.class)
.toInstance(pluginCanonicalWebUrl);
bind(File.class)
bind(Path.class)
.annotatedWith(PluginData.class)
.toProvider(new Provider<File>() {
.toProvider(new Provider<Path>() {
private volatile boolean ready;
@Override
public File get() {
public Path get() {
if (!ready) {
synchronized (dataDir) {
if (!ready) {
if (!dataDir.exists() && !dataDir.mkdirs()) {
try {
Files.createDirectories(dataDir);
} catch (IOException e) {
throw new ProvisionException(String.format(
"Cannot create %s for plugin %s",
dataDir.getAbsolutePath(), getName()));
dataDir.toAbsolutePath(), getName()), e);
}
ready = true;
}
@@ -267,6 +271,8 @@ public class ServerPlugin extends Plugin {
return dataDir;
}
});
bind(File.class).annotatedWith(PluginData.class)
.toProvider(PluginDataAsFileProvider.class);
}
});
return Guice.createInjector(modules);

View File

@@ -19,7 +19,7 @@ import com.google.gerrit.server.PluginUser;
import org.eclipse.jgit.internal.storage.file.FileSnapshot;
import java.io.File;
import java.nio.file.Path;
/**
* Provider of one Server plugin from one external file
@@ -31,7 +31,6 @@ import java.io.File;
* group them into a directory tree and then load the directory
* root as a single plugin.
*/
// TODO(dborowitz): Convert to NIO; ensure clients can migrate.
@ExtensionPoint
public interface ServerPluginProvider {
@@ -41,7 +40,7 @@ public interface ServerPluginProvider {
public class PluginDescription {
public final PluginUser user;
public final String canonicalUrl;
public final File dataDir;
public final Path dataDir;
/**
* Creates a new PluginDescription for ServerPluginProvider.
@@ -50,7 +49,7 @@ public interface ServerPluginProvider {
* @param canonicalUrl plugin root Web URL
* @param dataDir directory for plugin data
*/
public PluginDescription(PluginUser user, String canonicalUrl, File dataDir) {
public PluginDescription(PluginUser user, String canonicalUrl, Path dataDir) {
this.user = user;
this.canonicalUrl = canonicalUrl;
this.dataDir = dataDir;
@@ -60,39 +59,39 @@ public interface ServerPluginProvider {
/**
* Declares the availability to manage an external file or directory
*
* @param srcFile the external file or directory
* @param srcPath the external file or directory
* @return true if file or directory can be loaded into a Server Plugin
*/
boolean handles(File srcFile);
boolean handles(Path srcPath);
/**
* Returns the plugin name of an external file or directory
*
* Should be called only if {@link #handles(File) handles(srcFile)}
* Should be called only if {@link #handles(Path) handles(srcFile)}
* returns true and thus srcFile is a supported plugin format.
* An IllegalArgumentException is thrown otherwise as srcFile
* is not a valid file format for extracting its plugin name.
*
* @param srcFile external file or directory
* @param srcPath external file or directory
* @return plugin name
*/
String getPluginName(File srcFile);
String getPluginName(Path srcPath);
/**
* Loads an external file or directory into a Server plugin.
*
* Should be called only if {@link #handles(File) handles(srcFile)}
* Should be called only if {@link #handles(Path) handles(srcFile)}
* returns true and thus srcFile is a supported plugin format.
* An IllegalArgumentException is thrown otherwise as srcFile
* is not a valid file format for extracting its plugin name.
*
* @param srcFile external file or directory
* @param srcPath external file or directory
* @param snapshot snapshot of the external file
* @param pluginDescriptor descriptor of the ServerPlugin to load
* @throws InvalidPluginException if plugin is supposed to be handled
* but cannot be loaded for any other reason
*/
ServerPlugin get(File srcFile, FileSnapshot snapshot,
ServerPlugin get(Path srcPath, FileSnapshot snapshot,
PluginDescription pluginDescriptor) throws InvalidPluginException;
/**

View File

@@ -22,7 +22,7 @@ import org.eclipse.jgit.internal.storage.file.FileSnapshot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@@ -38,27 +38,26 @@ class UniversalServerPluginProvider implements ServerPluginProvider {
}
@Override
public ServerPlugin get(File srcFile, FileSnapshot snapshot,
public ServerPlugin get(Path srcPath, FileSnapshot snapshot,
PluginDescription pluginDescription) throws InvalidPluginException {
return providerOf(srcFile).get(srcFile, snapshot, pluginDescription);
return providerOf(srcPath).get(srcPath, snapshot, pluginDescription);
}
@Override
public String getPluginName(File srcFile) {
return providerOf(srcFile).getPluginName(srcFile);
public String getPluginName(Path srcPath) {
return providerOf(srcPath).getPluginName(srcPath);
}
@Override
public boolean handles(File srcFile) {
List<ServerPluginProvider> providers =
providersForHandlingPlugin(srcFile);
public boolean handles(Path srcPath) {
List<ServerPluginProvider> providers = providersForHandlingPlugin(srcPath);
switch (providers.size()) {
case 1:
return true;
case 0:
return false;
default:
throw new MultipleProvidersForPluginException(srcFile, providers);
throw new MultipleProvidersForPluginException(srcPath, providers);
}
}
@@ -67,27 +66,27 @@ class UniversalServerPluginProvider implements ServerPluginProvider {
return "gerrit";
}
private ServerPluginProvider providerOf(File srcFile) {
private ServerPluginProvider providerOf(Path srcPath) {
List<ServerPluginProvider> providers =
providersForHandlingPlugin(srcFile);
providersForHandlingPlugin(srcPath);
switch (providers.size()) {
case 1:
return providers.get(0);
case 0:
throw new IllegalArgumentException(
"No ServerPluginProvider found/loaded to handle plugin file "
+ srcFile.getAbsolutePath());
+ srcPath.toAbsolutePath());
default:
throw new MultipleProvidersForPluginException(srcFile, providers);
throw new MultipleProvidersForPluginException(srcPath, providers);
}
}
private List<ServerPluginProvider> providersForHandlingPlugin(
final File srcFile) {
final Path srcPath) {
List<ServerPluginProvider> providers = new ArrayList<>();
for (ServerPluginProvider serverPluginProvider : serverPluginProviders) {
boolean handles = serverPluginProvider.handles(srcFile);
log.debug("File {} handled by {} ? => {}", srcFile,
boolean handles = serverPluginProvider.handles(srcPath);
log.debug("File {} handled by {} ? => {}", srcPath,
serverPluginProvider.getProviderPluginName(), handles);
if (handles) {
providers.add(serverPluginProvider);