();
diff --git a/core/src/main/java/hudson/CopyOnWrite.java b/core/src/main/java/hudson/CopyOnWrite.java
index efe21357c4405e122651a284c0cb4c30157fce62..891cbf04bb9ffef6d6f657d05eb4708d94fa0f5e 100644
--- a/core/src/main/java/hudson/CopyOnWrite.java
+++ b/core/src/main/java/hudson/CopyOnWrite.java
@@ -43,7 +43,7 @@ import java.lang.annotation.Target;
*
*
* The field marked with this annotation usually needs to be marked as
- * volatile.
+ * {@code volatile}.
*
* @author Kohsuke Kawaguchi
*/
diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java
index 1849ecbf66aebaf6ca6a4df5ce197b5b825c8b36..cd553f3aba01ea325c58912b990d88aab9cb5e66 100644
--- a/core/src/main/java/hudson/EnvVars.java
+++ b/core/src/main/java/hudson/EnvVars.java
@@ -44,6 +44,7 @@ import java.util.TreeSet;
import java.util.UUID;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
+import javax.annotation.CheckForNull;
/**
* Environment variables.
@@ -54,7 +55,7 @@ import javax.annotation.Nonnull;
* but case insensitive way (that is, cmd.exe can get both FOO and foo as environment variables
* when it's launched, and the "set" command will display it accordingly, but "echo %foo%" results in
* echoing the value of "FOO", not "foo" — this is presumably caused by the behavior of the underlying
- * Win32 API GetEnvironmentVariable acting in case insensitive way.) Windows users are also
+ * Win32 API {@code GetEnvironmentVariable} acting in case insensitive way.) Windows users are also
* used to write environment variable case-insensitively (like %Path% vs %PATH%), and you can see many
* documents on the web that claims Windows environment variables are case insensitive.
*
@@ -65,10 +66,10 @@ import javax.annotation.Nonnull;
*
* In Jenkins, often we need to build up "environment variable overrides"
* on master, then to execute the process on agents. This causes a problem
- * when working with variables like PATH. So to make this work,
- * we introduce a special convention PATH+FOO — all entries
- * that starts with PATH+ are merged and prepended to the inherited
- * PATH variable, on the process where a new process is executed.
+ * when working with variables like {@code PATH}. So to make this work,
+ * we introduce a special convention {@code PATH+FOO} — all entries
+ * that starts with {@code PATH+} are merged and prepended to the inherited
+ * {@code PATH} variable, on the process where a new process is executed.
*
* @author Kohsuke Kawaguchi
*/
@@ -84,7 +85,24 @@ public class EnvVars extends TreeMap {
* So this property remembers that information.
*/
private Platform platform;
+
+ /**
+ * Gets the platform for which these env vars targeted.
+ * @since TODO
+ * @return The platform.
+ */
+ public @CheckForNull Platform getPlatform() {
+ return platform;
+ }
+ /**
+ * Sets the platform for which these env vars target.
+ * @since TODO
+ * @param platform the platform to set.
+ */
+ public void setPlatform(@Nonnull Platform platform) {
+ this.platform = platform;
+ }
public EnvVars() {
super(CaseInsensitiveComparator.INSTANCE);
}
@@ -107,7 +125,7 @@ public class EnvVars extends TreeMap {
}
/**
- * Builds an environment variables from an array of the form "key","value","key","value"...
+ * Builds an environment variables from an array of the form {@code "key","value","key","value"...}
*/
public EnvVars(String... keyValuePairs) {
this();
@@ -121,7 +139,7 @@ public class EnvVars extends TreeMap {
* Overrides the current entry by the given entry.
*
*
- * Handles PATH+XYZ notation.
+ * Handles {@code PATH+XYZ} notation.
*/
public void override(String key, String value) {
if(value==null || value.length()==0) {
@@ -425,7 +443,7 @@ public class EnvVars extends TreeMap {
*
*
* If you access this field from agents, then this is the environment
- * variable of the agent agent.
+ * variable of the agent.
*/
public static final Map masterEnvVars = initMaster();
diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java
index 6208f6cec8322a0145f2fc4436519000d07e0ce6..1ff0638c10d6b578927ea90757d5acb8bdbeff6b 100644
--- a/core/src/main/java/hudson/ExtensionFinder.java
+++ b/core/src/main/java/hudson/ExtensionFinder.java
@@ -23,6 +23,7 @@
*/
package hudson;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -35,11 +36,13 @@ import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.Scopes;
+import com.google.inject.matcher.Matchers;
import com.google.inject.name.Names;
-import com.google.common.collect.ImmutableList;
+import com.google.inject.spi.ProvisionListener;
import hudson.init.InitMilestone;
import hudson.model.Descriptor;
import hudson.model.Hudson;
+import hudson.util.ReflectionUtils;
import jenkins.ExtensionComponentSet;
import jenkins.ExtensionFilter;
import jenkins.ExtensionRefreshException;
@@ -49,20 +52,25 @@ import net.java.sezpoz.Index;
import net.java.sezpoz.IndexItem;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.springframework.util.ClassUtils;
+import javax.annotation.PostConstruct;
import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-import java.util.logging.Logger;
+import java.util.Set;
import java.util.logging.Level;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.lang.reflect.AnnotatedElement;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
+import java.util.logging.Logger;
/**
* Discovers the implementations of an extension point.
@@ -254,12 +262,9 @@ public abstract class ExtensionFinder implements ExtensionPoint {
private Map,GuiceExtensionAnnotation>> extensionAnnotations = Maps.newHashMap();
public GuiceFinder() {
- for (ExtensionComponent ec : moduleFinder.find(GuiceExtensionAnnotation.class, Hudson.getInstance())) {
- GuiceExtensionAnnotation gea = ec.getInstance();
- extensionAnnotations.put(gea.annotationType,gea);
- }
+ refreshExtensionAnnotations();
- sezpozIndex = loadSezpozIndices(Jenkins.getInstance().getPluginManager().uberClassLoader);
+ SezpozModule extensions = new SezpozModule(loadSezpozIndices(Jenkins.getInstance().getPluginManager().uberClassLoader));
List modules = new ArrayList<>();
modules.add(new AbstractModule() {
@@ -270,7 +275,7 @@ public abstract class ExtensionFinder implements ExtensionPoint {
bind(PluginManager.class).toInstance(j.getPluginManager());
}
});
- modules.add(new SezpozModule(sezpozIndex));
+ modules.add(extensions);
for (ExtensionComponent ec : moduleFinder.find(Module.class, Hudson.getInstance())) {
modules.add(ec.getInstance());
@@ -278,6 +283,7 @@ public abstract class ExtensionFinder implements ExtensionPoint {
try {
container = Guice.createInjector(modules);
+ sezpozIndex = extensions.getLoadedIndex();
} catch (Throwable e) {
LOGGER.log(Level.SEVERE, "Failed to create Guice container from all the plugins",e);
// failing to load all bindings are disastrous, so recover by creating minimum that works
@@ -293,6 +299,13 @@ public abstract class ExtensionFinder implements ExtensionPoint {
});
}
+ private void refreshExtensionAnnotations() {
+ for (ExtensionComponent ec : moduleFinder.find(GuiceExtensionAnnotation.class, Hudson.getInstance())) {
+ GuiceExtensionAnnotation gea = ec.getInstance();
+ extensionAnnotations.put(gea.annotationType,gea);
+ }
+ }
+
private ImmutableList> loadSezpozIndices(ClassLoader classLoader) {
List> indices = Lists.newArrayList();
for (GuiceExtensionAnnotation> gea : extensionAnnotations.values()) {
@@ -315,17 +328,17 @@ public abstract class ExtensionFinder implements ExtensionPoint {
*/
@Override
public synchronized ExtensionComponentSet refresh() throws ExtensionRefreshException {
+ refreshExtensionAnnotations();
// figure out newly discovered sezpoz components
List> delta = Lists.newArrayList();
for (Class extends Annotation> annotationType : extensionAnnotations.keySet()) {
delta.addAll(Sezpoz.listDelta(annotationType,sezpozIndex));
}
- List> l = Lists.newArrayList(sezpozIndex);
- l.addAll(delta);
- sezpozIndex = l;
+
+ SezpozModule deltaExtensions = new SezpozModule(delta);
List modules = new ArrayList<>();
- modules.add(new SezpozModule(delta));
+ modules.add(deltaExtensions);
for (ExtensionComponent ec : moduleFinder.refresh().find(Module.class)) {
modules.add(ec.getInstance());
}
@@ -333,6 +346,9 @@ public abstract class ExtensionFinder implements ExtensionPoint {
try {
final Injector child = container.createChildInjector(modules);
container = child;
+ List> l = Lists.newArrayList(sezpozIndex);
+ l.addAll(deltaExtensions.getLoadedIndex());
+ sezpozIndex = l;
return new ExtensionComponentSet() {
@Override
@@ -446,11 +462,13 @@ public abstract class ExtensionFinder implements ExtensionPoint {
* Instead of using SezPoz to instantiate, we'll instantiate them by using Guice,
* so that we can take advantage of dependency injection.
*/
- private class SezpozModule extends AbstractModule {
+ private class SezpozModule extends AbstractModule implements ProvisionListener {
private final List> index;
+ private final List> loadedIndex;
public SezpozModule(List> index) {
this.index = index;
+ this.loadedIndex = new ArrayList<>();
}
/**
@@ -493,6 +511,9 @@ public abstract class ExtensionFinder implements ExtensionPoint {
@SuppressWarnings({"unchecked", "ChainOfInstanceofChecks"})
@Override
protected void configure() {
+
+ bindListener(Matchers.any(), this);
+
for (final IndexItem,Object> item : index) {
boolean optional = isOptional(item.annotation());
try {
@@ -527,6 +548,7 @@ public abstract class ExtensionFinder implements ExtensionPoint {
}
}).in(scope);
}
+ loadedIndex.add(item);
} catch (Exception|LinkageError e) {
// sometimes the instantiation fails in an indirect classloading failure,
// which results in a LinkageError
@@ -535,9 +557,68 @@ public abstract class ExtensionFinder implements ExtensionPoint {
}
}
}
+
+ public List> getLoadedIndex() {
+ return Collections.unmodifiableList(loadedIndex);
+ }
+
+ @Override
+ public void onProvision(ProvisionInvocation provision) {
+ final T instance = provision.provision();
+ if (instance == null) return;
+ List methods = new LinkedList<>();
+ Class c = instance.getClass();
+
+ // find PostConstruct methods in class hierarchy, the one from parent class being first in list
+ // so that we invoke them before derived class one. This isn't specified in JSR-250 but implemented
+ // this way in Spring and what most developers would expect to happen.
+
+ final Set interfaces = ClassUtils.getAllInterfacesAsSet(instance);
+
+ while (c != Object.class) {
+ Arrays.stream(c.getDeclaredMethods())
+ .map(m -> getMethodAndInterfaceDeclarations(m, interfaces))
+ .flatMap(Collection::stream)
+ .filter(m -> m.getAnnotation(PostConstruct.class) != null)
+ .findFirst()
+ .ifPresent(method -> methods.add(0, method));
+ c = c.getSuperclass();
+ }
+
+ for (Method postConstruct : methods) {
+ try {
+ postConstruct.setAccessible(true);
+ postConstruct.invoke(instance);
+ } catch (final Exception e) {
+ throw new RuntimeException(String.format("@PostConstruct %s", postConstruct), e);
+ }
+ }
+ }
}
}
+ /**
+ * Returns initial {@link Method} as well as all matching ones found in interfaces.
+ * This allows to introspect metadata for a method which is both declared in parent class and in implemented
+ * interface(s). interfaces
typically is obtained by {@link ClassUtils#getAllInterfacesAsSet}
+ */
+ Collection getMethodAndInterfaceDeclarations(Method method, Collection interfaces) {
+ final List methods = new ArrayList<>();
+ methods.add(method);
+
+ // we search for matching method by iteration and comparison vs getMethod to avoid repeated NoSuchMethodException
+ // being thrown, while interface typically only define a few set of methods to check.
+ interfaces.stream()
+ .map(Class::getMethods)
+ .flatMap(Arrays::stream)
+ .filter(m -> m.getName().equals(method.getName()) && Arrays.equals(m.getParameterTypes(), method.getParameterTypes()))
+ .findFirst()
+ .ifPresent(methods::add);
+
+ return methods;
+ }
+
+
/**
* The bootstrap implementation that looks for the {@link Extension} marker.
*
diff --git a/core/src/main/java/hudson/ExtensionList.java b/core/src/main/java/hudson/ExtensionList.java
index 9a4c77d2f5f5db24c736c6f58433737505f9a4c6..566e3de404911db2fbc418bab933a4bdd8261c79 100644
--- a/core/src/main/java/hudson/ExtensionList.java
+++ b/core/src/main/java/hudson/ExtensionList.java
@@ -145,15 +145,29 @@ public class ExtensionList extends AbstractList implements OnMaster {
* Looks for the extension instance of the given type (subclasses excluded),
* or return null.
*/
- public @CheckForNull U get(Class type) {
+ public @CheckForNull U get(@Nonnull Class type) {
for (T ext : this)
if(ext.getClass()==type)
return type.cast(ext);
return null;
}
+ /**
+ * Looks for the extension instance of the given type (subclasses excluded),
+ * or throws an IllegalStateException.
+ *
+ * Meant to simplify call inside @Extension annotated class to retrieve their own instance.
+ */
+ public @Nonnull U getInstance(@Nonnull Class type) throws IllegalStateException {
+ for (T ext : this)
+ if(ext.getClass()==type)
+ return type.cast(ext);
+
+ throw new IllegalStateException("The class " + type.getName() + " was not found, potentially not yet loaded");
+ }
+
@Override
- public Iterator iterator() {
+ public @Nonnull Iterator iterator() {
// we need to intercept mutation, so for now don't allow Iterator.remove
return new AdaptedIterator,T>(Iterators.readOnly(ensureLoaded().iterator())) {
protected T adapt(ExtensionComponent item) {
diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java
index b7b5b2d79d3a14090cf421c583d7265afbf2b360..9452064e8105e124c0e62328f3310c03765b4ff7 100644
--- a/core/src/main/java/hudson/FilePath.java
+++ b/core/src/main/java/hudson/FilePath.java
@@ -58,6 +58,9 @@ import hudson.util.IOUtils;
import hudson.util.NamingThreadFactory;
import hudson.util.io.Archiver;
import hudson.util.io.ArchiverFactory;
+
+import static java.util.logging.Level.FINE;
+
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
@@ -88,7 +91,6 @@ import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Enumeration;
@@ -139,6 +141,7 @@ import static hudson.Util.fixEmpty;
import static hudson.Util.isSymlink;
import java.util.Collections;
+import org.apache.tools.ant.BuildException;
/**
* {@link File} like object with remoting support.
@@ -225,9 +228,14 @@ public final class FilePath implements Serializable {
* This is used to determine whether we are running on the master or the agent.
*/
private transient VirtualChannel channel;
-
- // since the platform of the agent might be different, can't use java.io.File
- private final String remote;
+
+ /**
+ * Represent the path to the file in the master or the agent
+ * Since the platform of the agent might be different, can't use java.io.File
+ *
+ * The field could not be final since it's modified in {@link #readResolve()}
+ */
+ private /*final*/ String remote;
/**
* If this {@link FilePath} is deserialized to handle file access request from a remote computer,
@@ -275,6 +283,11 @@ public final class FilePath implements Serializable {
this.remote = normalize(resolvePathIfRelative(base, rel));
}
+ private Object readResolve() {
+ this.remote = normalize(this.remote);
+ return this;
+ }
+
private String resolvePathIfRelative(@Nonnull FilePath base, @Nonnull String rel) {
if(isAbsolute(rel)) return rel;
if(base.isUnix()) {
@@ -302,7 +315,8 @@ public final class FilePath implements Serializable {
* {@link File#getParent()} etc cannot handle ".." and "." in the path component very well,
* so remove them.
*/
- private static String normalize(@Nonnull String path) {
+ @Restricted(NoExternalUse.class)
+ public static String normalize(@Nonnull String path) {
StringBuilder buf = new StringBuilder();
// Check for prefix designating absolute path
Matcher m = ABSOLUTE_PREFIX_PATTERN.matcher(path);
@@ -466,7 +480,18 @@ public final class FilePath implements Serializable {
*/
public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException {
final OutputStream out = (channel!=null)?new RemoteOutputStream(os):os;
- return act(new SecureFileCallable() {
+ return act(new Archive(factory, out, scanner));
+ }
+ private class Archive extends SecureFileCallable {
+ private final ArchiverFactory factory;
+ private final OutputStream out;
+ private final DirScanner scanner;
+ Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner) {
+ this.factory = factory;
+ this.out = out;
+ this.scanner = scanner;
+ }
+ @Override
public Integer invoke(File f, VirtualChannel channel) throws IOException {
Archiver a = factory.create(out);
try {
@@ -478,7 +503,6 @@ public final class FilePath implements Serializable {
}
private static final long serialVersionUID = 1L;
- });
}
public int archive(final ArchiverFactory factory, OutputStream os, final FileFilter filter) throws IOException, InterruptedException {
@@ -501,27 +525,32 @@ public final class FilePath implements Serializable {
// TODO: post release, re-unite two branches by introducing FileStreamCallable that resolves InputStream
if (this.channel!=target.channel) {// local -> remote or remote->local
final RemoteInputStream in = new RemoteInputStream(read(), Flag.GREEDY);
- target.act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
- unzip(dir, in);
- return null;
- }
-
- private static final long serialVersionUID = 1L;
- });
+ target.act(new UnzipRemote(in));
} else {// local -> local or remote->remote
- target.act(new SecureFileCallable() {
-
- public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
- assert !FilePath.this.isRemote(); // this.channel==target.channel above
- unzip(dir, reading(new File(FilePath.this.getRemote()))); // shortcut to local file
- return null;
- }
-
- private static final long serialVersionUID = 1L;
- });
+ target.act(new UnzipLocal());
}
}
+ private class UnzipRemote extends SecureFileCallable {
+ private final RemoteInputStream in;
+ UnzipRemote(RemoteInputStream in) {
+ this.in = in;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
+ unzip(dir, in);
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
+ }
+ private class UnzipLocal extends SecureFileCallable {
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
+ assert !FilePath.this.isRemote(); // this.channel==target.channel above
+ unzip(dir, reading(new File(FilePath.this.getRemote()))); // shortcut to local file
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
+ }
/**
* When this {@link FilePath} represents a tar file, extracts that tar file.
@@ -537,24 +566,37 @@ public final class FilePath implements Serializable {
// TODO: post release, re-unite two branches by introducing FileStreamCallable that resolves InputStream
if (this.channel!=target.channel) {// local -> remote or remote->local
final RemoteInputStream in = new RemoteInputStream(read(), Flag.GREEDY);
- target.act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
- readFromTar(FilePath.this.getName(),dir,compression.extract(in));
- return null;
- }
-
- private static final long serialVersionUID = 1L;
- });
+ target.act(new UntarRemote(compression, in));
} else {// local -> local or remote->remote
- target.act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
- readFromTar(FilePath.this.getName(),dir,compression.extract(FilePath.this.read()));
- return null;
- }
- private static final long serialVersionUID = 1L;
- });
+ target.act(new UntarLocal(compression));
}
}
+ private class UntarRemote extends SecureFileCallable {
+ private final TarCompression compression;
+ private final RemoteInputStream in;
+ UntarRemote(TarCompression compression, RemoteInputStream in) {
+ this.compression = compression;
+ this.in = in;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
+ readFromTar(FilePath.this.getName(), dir, compression.extract(in));
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
+ }
+ private class UntarLocal extends SecureFileCallable {
+ private final TarCompression compression;
+ UntarLocal(TarCompression compression) {
+ this.compression = compression;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
+ readFromTar(FilePath.this.getName(), dir, compression.extract(FilePath.this.read()));
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
+ }
/**
* Reads the given InputStream as a zip file and extracts it into this directory.
@@ -566,13 +608,19 @@ public final class FilePath implements Serializable {
*/
public void unzipFrom(InputStream _in) throws IOException, InterruptedException {
final InputStream in = new RemoteInputStream(_in, Flag.GREEDY);
- act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException {
- unzip(dir, in);
- return null;
- }
- private static final long serialVersionUID = 1L;
- });
+ act(new UnzipFrom(in));
+ }
+ private class UnzipFrom extends SecureFileCallable {
+ private final InputStream in;
+ UnzipFrom(InputStream in) {
+ this.in = in;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException {
+ unzip(dir, in);
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
}
private void unzip(File dir, InputStream in) throws IOException {
@@ -597,6 +645,10 @@ public final class FilePath implements Serializable {
while (entries.hasMoreElements()) {
ZipEntry e = entries.nextElement();
File f = new File(dir, e.getName());
+ if (!f.toPath().normalize().startsWith(dir.toPath())) {
+ throw new IOException(
+ "Zip " + zipFile.getPath() + " contains illegal file name that breaks out of the target directory: " + e.getName());
+ }
if (e.isDirectory()) {
mkdirs(f);
} else {
@@ -627,12 +679,13 @@ public final class FilePath implements Serializable {
* Absolutizes this {@link FilePath} and returns the new one.
*/
public FilePath absolutize() throws IOException, InterruptedException {
- return new FilePath(channel,act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public String invoke(File f, VirtualChannel channel) throws IOException {
- return f.getAbsolutePath();
- }
- }));
+ return new FilePath(channel, act(new Absolutize()));
+ }
+ private static class Absolutize extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ public String invoke(File f, VirtualChannel channel) throws IOException {
+ return f.getAbsolutePath();
+ }
}
/**
@@ -645,14 +698,22 @@ public final class FilePath implements Serializable {
* @since 1.456
*/
public void symlinkTo(final String target, final TaskListener listener) throws IOException, InterruptedException {
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
- symlinking(f);
- Util.createSymlink(f.getParentFile(),target,f.getName(),listener);
- return null;
- }
- });
+ act(new SymlinkTo(target, listener));
+ }
+ private class SymlinkTo extends SecureFileCallable {
+ private final String target;
+ private final TaskListener listener;
+ SymlinkTo(String target, TaskListener listener) {
+ this.target = target;
+ this.listener = listener;
+ }
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
+ symlinking(f);
+ Util.createSymlink(f.getParentFile(), target, f.getName(), listener);
+ return null;
+ }
}
/**
@@ -663,12 +724,14 @@ public final class FilePath implements Serializable {
* @since 1.456
*/
public String readLink() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
- return Util.resolveSymlink(reading(f));
- }
- });
+ return act(new ReadLink());
+ }
+ private class ReadLink extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
+ return Util.resolveSymlink(reading(f));
+ }
}
@Override
@@ -732,17 +795,25 @@ public final class FilePath implements Serializable {
public void untarFrom(InputStream _in, final TarCompression compression) throws IOException, InterruptedException {
try {
final InputStream in = new RemoteInputStream(_in, Flag.GREEDY);
- act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException {
- readFromTar("input stream",dir, compression.extract(in));
- return null;
- }
- private static final long serialVersionUID = 1L;
- });
+ act(new UntarFrom(compression, in));
} finally {
_in.close();
}
}
+ private class UntarFrom extends SecureFileCallable {
+ private final TarCompression compression;
+ private final InputStream in;
+ UntarFrom(TarCompression compression, InputStream in) {
+ this.compression = compression;
+ this.in = in;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException {
+ readFromTar("input stream",dir, compression.extract(in));
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
+ }
/**
* Given a tgz/zip file, extracts it to the given target directory, if necessary.
@@ -892,9 +963,10 @@ public final class FilePath implements Serializable {
}
/**
- * Reads the URL on the current VM, and writes all the data to this {@link FilePath}
- * (this is different from resolving URL remotely.)
- *
+ * Reads the URL on the current VM, and streams the data to this file using the Remoting channel.
+ * This is different from resolving URL remotely.
+ * If you instead wished to open an HTTP(S) URL on the remote side,
+ * prefer {@code RobustHTTPClient.copyFromRemotely}.
* @since 1.293
*/
public void copyFrom(URL url) throws IOException, InterruptedException {
@@ -998,11 +1070,6 @@ public final class FilePath implements Serializable {
return channel.call(wrapper);
} catch (TunneledInterruptedException e) {
throw (InterruptedException)new InterruptedException(e.getMessage()).initCause(e);
- } catch (AbortException e) {
- throw e; // pass through so that the caller can catch it as AbortException
- } catch (IOException e) {
- // wrap it into a new IOException so that we get the caller's stack trace as well.
- throw new IOException("remote file operation failed: " + remote + " at " + channel + ": " + e, e);
}
} else {
// the file is on the local machine.
@@ -1105,23 +1172,28 @@ public final class FilePath implements Serializable {
* @since 1.522
*/
public Callable asCallableWith(final FileCallable task) {
- return new Callable() {
- @Override
- public V call() throws IOException {
- try {
- return act(task);
- } catch (InterruptedException e) {
- throw (IOException)new InterruptedIOException().initCause(e);
- }
+ return new CallableWith<>(task);
+ }
+ private class CallableWith implements Callable {
+ private final FileCallable task;
+ CallableWith(FileCallable task) {
+ this.task = task;
+ }
+ @Override
+ public V call() throws IOException {
+ try {
+ return act(task);
+ } catch (InterruptedException e) {
+ throw (IOException)new InterruptedIOException().initCause(e);
}
+ }
- @Override
- public void checkRoles(RoleChecker checker) throws SecurityException {
- task.checkRoles(checker);
- }
+ @Override
+ public void checkRoles(RoleChecker checker) throws SecurityException {
+ task.checkRoles(checker);
+ }
- private static final long serialVersionUID = 1L;
- };
+ private static final long serialVersionUID = 1L;
}
/**
@@ -1129,12 +1201,14 @@ public final class FilePath implements Serializable {
* on which this file is available.
*/
public URI toURI() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public URI invoke(File f, VirtualChannel channel) {
- return f.toURI();
- }
- });
+ return act(new ToURI());
+ }
+ private static class ToURI extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public URI invoke(File f, VirtualChannel channel) {
+ return f.toURI();
+ }
}
/**
@@ -1167,45 +1241,52 @@ public final class FilePath implements Serializable {
* Creates this directory.
*/
public void mkdirs() throws IOException, InterruptedException {
- if(!act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
- if(mkdirs(f) || f.exists())
- return true; // OK
+ if (!act(new Mkdirs())) {
+ throw new IOException("Failed to mkdirs: " + remote);
+ }
+ }
+ private class Mkdirs extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
+ if(mkdirs(f) || f.exists())
+ return true; // OK
- // following Ant task to avoid possible race condition.
- Thread.sleep(10);
+ // following Ant task to avoid possible race condition.
+ Thread.sleep(10);
- return mkdirs(f) || f.exists();
- }
- }))
- throw new IOException("Failed to mkdirs: "+remote);
+ return mkdirs(f) || f.exists();
+ }
}
/**
* Deletes this directory, including all its contents recursively.
*/
public void deleteRecursive() throws IOException, InterruptedException {
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- deleteRecursive(deleting(f));
- return null;
- }
- });
+ act(new DeleteRecursive());
+ }
+ private class DeleteRecursive extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ deleteRecursive(deleting(f));
+ return null;
+ }
}
/**
* Deletes all the contents of this directory, but not the directory itself
*/
public void deleteContents() throws IOException, InterruptedException {
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- deleteContentsRecursive(f);
- return null;
- }
- });
+ act(new DeleteContents());
+ }
+ private class DeleteContents extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ deleteContentsRecursive(f);
+ return null;
+ }
}
private void deleteRecursive(File dir) throws IOException {
@@ -1316,17 +1397,25 @@ public final class FilePath implements Serializable {
*/
public FilePath createTempFile(final String prefix, final String suffix) throws IOException, InterruptedException {
try {
- return new FilePath(this,act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public String invoke(File dir, VirtualChannel channel) throws IOException {
- File f = writing(File.createTempFile(prefix, suffix, dir));
- return f.getName();
- }
- }));
+ return new FilePath(this, act(new CreateTempFile(prefix, suffix)));
} catch (IOException e) {
throw new IOException("Failed to create a temp file on "+remote,e);
}
}
+ private class CreateTempFile extends SecureFileCallable {
+ private final String prefix;
+ private final String suffix;
+ CreateTempFile(String prefix, String suffix) {
+ this.prefix = prefix;
+ this.suffix = suffix;
+ }
+ private static final long serialVersionUID = 1L;
+ @Override
+ public String invoke(File dir, VirtualChannel channel) throws IOException {
+ File f = writing(File.createTempFile(prefix, suffix, dir));
+ return f.getName();
+ }
+ }
/**
* Creates a temporary file in this directory and set the contents to the
@@ -1372,30 +1461,42 @@ public final class FilePath implements Serializable {
*/
public FilePath createTextTempFile(final String prefix, final String suffix, final String contents, final boolean inThisDirectory) throws IOException, InterruptedException {
try {
- return new FilePath(channel,act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public String invoke(File dir, VirtualChannel channel) throws IOException {
- if(!inThisDirectory)
- dir = new File(System.getProperty("java.io.tmpdir"));
- else
- mkdirs(dir);
+ return new FilePath(channel, act(new CreateTextTempFile(inThisDirectory, prefix, suffix, contents)));
+ } catch (IOException e) {
+ throw new IOException("Failed to create a temp file on "+remote,e);
+ }
+ }
+ private final class CreateTextTempFile extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ private final boolean inThisDirectory;
+ private final String prefix;
+ private final String suffix;
+ private final String contents;
+ CreateTextTempFile(boolean inThisDirectory, String prefix, String suffix, String contents) {
+ this.inThisDirectory = inThisDirectory;
+ this.prefix = prefix;
+ this.suffix = suffix;
+ this.contents = contents;
+ }
+ @Override
+ public String invoke(File dir, VirtualChannel channel) throws IOException {
+ if(!inThisDirectory)
+ dir = new File(System.getProperty("java.io.tmpdir"));
+ else
+ mkdirs(dir);
- File f;
- try {
- f = creating(File.createTempFile(prefix, suffix, dir));
- } catch (IOException e) {
- throw new IOException("Failed to create a temporary directory in "+dir,e);
- }
+ File f;
+ try {
+ f = creating(File.createTempFile(prefix, suffix, dir));
+ } catch (IOException e) {
+ throw new IOException("Failed to create a temporary directory in "+dir,e);
+ }
- try (Writer w = new FileWriter(writing(f))) {
- w.write(contents);
- }
+ try (Writer w = new FileWriter(writing(f))) {
+ w.write(contents);
+ }
- return f.getAbsolutePath();
- }
- }));
- } catch (IOException e) {
- throw new IOException("Failed to create a temp file on "+remote,e);
+ return f.getAbsolutePath();
}
}
@@ -1422,29 +1523,35 @@ public final class FilePath implements Serializable {
s = new String[]{prefix, suffix};
}
String name = StringUtils.join(s, ".");
- return new FilePath(this,act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public String invoke(File dir, VirtualChannel channel) throws IOException {
+ return new FilePath(this, act(new CreateTempDir(name)));
+ } catch (IOException e) {
+ throw new IOException("Failed to create a temp directory on "+remote,e);
+ }
+ }
+ private class CreateTempDir extends SecureFileCallable {
+ private final String name;
+ CreateTempDir(String name) {
+ this.name = name;
+ }
+ private static final long serialVersionUID = 1L;
+ @Override
+ public String invoke(File dir, VirtualChannel channel) throws IOException {
- Path tempPath;
- final boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
+ Path tempPath;
+ final boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
- if (isPosix) {
- tempPath = Files.createTempDirectory(Util.fileToPath(dir), name,
- PosixFilePermissions.asFileAttribute(EnumSet.allOf(PosixFilePermission.class)));
- } else {
- tempPath = Files.createTempDirectory(Util.fileToPath(dir), name, new FileAttribute>[] {});
- }
+ if (isPosix) {
+ tempPath = Files.createTempDirectory(Util.fileToPath(dir), name,
+ PosixFilePermissions.asFileAttribute(EnumSet.allOf(PosixFilePermission.class)));
+ } else {
+ tempPath = Files.createTempDirectory(Util.fileToPath(dir), name, new FileAttribute>[] {});
+ }
- if (tempPath.toFile() == null) {
- throw new IOException("Failed to obtain file from path " + dir + " on " + remote);
- }
- return tempPath.toFile().getName();
+ if (tempPath.toFile() == null) {
+ throw new IOException("Failed to obtain file from path " + dir + " on " + remote);
}
- }));
- } catch (IOException e) {
- throw new IOException("Failed to create a temp directory on "+remote,e);
- }
+ return tempPath.toFile().getName();
+ }
}
/**
@@ -1453,26 +1560,30 @@ public final class FilePath implements Serializable {
* @return true, for a modicum of compatibility
*/
public boolean delete() throws IOException, InterruptedException {
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- Util.deleteFile(deleting(f));
- return null;
- }
- });
+ act(new Delete());
return true;
}
+ private class Delete extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ Util.deleteFile(deleting(f));
+ return null;
+ }
+ }
/**
* Checks if the file exists.
*/
public boolean exists() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Boolean invoke(File f, VirtualChannel channel) throws IOException {
- return stating(f).exists();
- }
- });
+ return act(new Exists());
+ }
+ private class Exists extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Boolean invoke(File f, VirtualChannel channel) throws IOException {
+ return stating(f).exists();
+ }
}
/**
@@ -1483,12 +1594,14 @@ public final class FilePath implements Serializable {
* @see #touch(long)
*/
public long lastModified() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Long invoke(File f, VirtualChannel channel) throws IOException {
- return stating(f).lastModified();
- }
- });
+ return act(new LastModified());
+ }
+ private class LastModified extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Long invoke(File f, VirtualChannel channel) throws IOException {
+ return stating(f).lastModified();
+ }
}
/**
@@ -1497,8 +1610,15 @@ public final class FilePath implements Serializable {
* @since 1.299
*/
public void touch(final long timestamp) throws IOException, InterruptedException {
- act(new SecureFileCallable() {
+ act(new Touch(timestamp));
+ }
+ private class Touch extends SecureFileCallable {
+ private final long timestamp;
+ Touch(long timestamp) {
+ this.timestamp = timestamp;
+ }
private static final long serialVersionUID = -5094638816500738429L;
+ @Override
public Void invoke(File f, VirtualChannel channel) throws IOException {
if(!f.exists()) {
try {
@@ -1511,12 +1631,22 @@ public final class FilePath implements Serializable {
throw new IOException("Failed to set the timestamp of "+f+" to "+timestamp);
return null;
}
- });
}
private void setLastModifiedIfPossible(final long timestamp) throws IOException, InterruptedException {
- String message = act(new SecureFileCallable() {
+ String message = act(new SetLastModified(timestamp));
+
+ if (message!=null) {
+ LOGGER.warning(message);
+ }
+ }
+ private class SetLastModified extends SecureFileCallable {
+ private final long timestamp;
+ SetLastModified(long timestamp) {
+ this.timestamp = timestamp;
+ }
private static final long serialVersionUID = -828220335793641630L;
+ @Override
public String invoke(File f, VirtualChannel channel) throws IOException {
if(!writing(f).setLastModified(timestamp)) {
if (Functions.isWindows()) {
@@ -1529,23 +1659,20 @@ public final class FilePath implements Serializable {
}
return null;
}
- });
-
- if (message!=null) {
- LOGGER.warning(message);
- }
}
/**
* Checks if the file is a directory.
*/
public boolean isDirectory() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Boolean invoke(File f, VirtualChannel channel) throws IOException {
- return stating(f).isDirectory();
- }
- });
+ return act(new IsDirectory());
+ }
+ private final class IsDirectory extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Boolean invoke(File f, VirtualChannel channel) throws IOException {
+ return stating(f).isDirectory();
+ }
}
/**
@@ -1554,12 +1681,14 @@ public final class FilePath implements Serializable {
* @since 1.129
*/
public long length() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Long invoke(File f, VirtualChannel channel) throws IOException {
- return stating(f).length();
- }
- });
+ return act(new Length());
+ }
+ private class Length extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Long invoke(File f, VirtualChannel channel) throws IOException {
+ return stating(f).length();
+ }
}
/**
@@ -1567,12 +1696,14 @@ public final class FilePath implements Serializable {
* @since 1.542
*/
public long getFreeDiskSpace() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- @Override public Long invoke(File f, VirtualChannel channel) throws IOException {
- return f.getFreeSpace();
- }
- });
+ return act(new GetFreeDiskSpace());
+ }
+ private static class GetFreeDiskSpace extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Long invoke(File f, VirtualChannel channel) throws IOException {
+ return f.getFreeSpace();
+ }
}
/**
@@ -1580,12 +1711,14 @@ public final class FilePath implements Serializable {
* @since 1.542
*/
public long getTotalDiskSpace() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- @Override public Long invoke(File f, VirtualChannel channel) throws IOException {
- return f.getTotalSpace();
- }
- });
+ return act(new GetTotalDiskSpace());
+ }
+ private static class GetTotalDiskSpace extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Long invoke(File f, VirtualChannel channel) throws IOException {
+ return f.getTotalSpace();
+ }
}
/**
@@ -1593,12 +1726,14 @@ public final class FilePath implements Serializable {
* @since 1.542
*/
public long getUsableDiskSpace() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- @Override public Long invoke(File f, VirtualChannel channel) throws IOException {
- return f.getUsableSpace();
- }
- });
+ return act(new GetUsableDiskSpace());
+ }
+ private static class GetUsableDiskSpace extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Long invoke(File f, VirtualChannel channel) throws IOException {
+ return f.getUsableSpace();
+ }
}
/**
@@ -1623,14 +1758,20 @@ public final class FilePath implements Serializable {
*/
public void chmod(final int mask) throws IOException, InterruptedException {
if(!isUnix() || mask==-1) return;
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- _chmod(writing(f), mask);
+ act(new Chmod(mask));
+ }
+ private class Chmod extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ private final int mask;
+ Chmod(int mask) {
+ this.mask = mask;
+ }
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ _chmod(writing(f), mask);
- return null;
- }
- });
+ return null;
+ }
}
/**
@@ -1660,12 +1801,14 @@ public final class FilePath implements Serializable {
*/
public int mode() throws IOException, InterruptedException, PosixException {
if(!isUnix()) return -1;
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Integer invoke(File f, VirtualChannel channel) throws IOException {
- return IOUtils.mode(stating(f));
- }
- });
+ return act(new Mode());
+ }
+ private class Mode extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Integer invoke(File f, VirtualChannel channel) throws IOException {
+ return IOUtils.mode(stating(f));
+ }
}
/**
@@ -1710,8 +1853,15 @@ public final class FilePath implements Serializable {
if (filter != null && !(filter instanceof Serializable)) {
throw new IllegalArgumentException("Non-serializable filter of " + filter.getClass());
}
- return act(new SecureFileCallable>() {
+ return act(new ListFilter(filter), (filter != null ? filter : this).getClass().getClassLoader());
+ }
+ private class ListFilter extends SecureFileCallable> {
+ private final FileFilter filter;
+ ListFilter(FileFilter filter) {
+ this.filter = filter;
+ }
private static final long serialVersionUID = 1L;
+ @Override
public List invoke(File f, VirtualChannel channel) throws IOException {
File[] children = reading(f).listFiles(filter);
if (children == null) {
@@ -1724,7 +1874,6 @@ public final class FilePath implements Serializable {
return r;
}
- }, (filter!=null?filter:this).getClass().getClassLoader());
}
/**
@@ -1768,8 +1917,19 @@ public final class FilePath implements Serializable {
*/
@Nonnull
public FilePath[] list(final String includes, final String excludes, final boolean defaultExcludes) throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
+ return act(new ListGlob(includes, excludes, defaultExcludes));
+ }
+ private class ListGlob extends SecureFileCallable {
+ private final String includes;
+ private final String excludes;
+ private final boolean defaultExcludes;
+ ListGlob(String includes, String excludes, boolean defaultExcludes) {
+ this.includes = includes;
+ this.excludes = excludes;
+ this.defaultExcludes = defaultExcludes;
+ }
private static final long serialVersionUID = 1L;
+ @Override
public FilePath[] invoke(File f, VirtualChannel channel) throws IOException {
String[] files = glob(reading(f), includes, excludes, defaultExcludes);
@@ -1779,7 +1939,6 @@ public final class FilePath implements Serializable {
return r;
}
- });
}
/**
@@ -1794,7 +1953,12 @@ public final class FilePath implements Serializable {
throw new IOException("Expecting Ant GLOB pattern, but saw '"+includes+"'. See http://ant.apache.org/manual/Types/fileset.html for syntax");
FileSet fs = Util.createFileSet(dir,includes,excludes);
fs.setDefaultexcludes(defaultExcludes);
- DirectoryScanner ds = fs.getDirectoryScanner(new Project());
+ DirectoryScanner ds;
+ try {
+ ds = fs.getDirectoryScanner(new Project());
+ } catch (BuildException x) {
+ throw new IOException(x.getMessage());
+ }
String[] files = ds.getIncludedFiles();
return files;
}
@@ -1812,25 +1976,29 @@ public final class FilePath implements Serializable {
}
final Pipe p = Pipe.createRemoteToLocal();
- actAsync(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
-
- @Override
- public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
- try (InputStream fis = Files.newInputStream(reading(f).toPath());
- OutputStream out = p.getOut()) {
- org.apache.commons.io.IOUtils.copy(fis, out);
- } catch (InvalidPathException e) {
- p.error(new IOException(e));
- } catch (Exception x) {
- p.error(x);
- }
- return null;
- }
- });
+ actAsync(new Read(p));
return p.getIn();
}
+ private class Read extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ private final Pipe p;
+ Read(Pipe p) {
+ this.p = p;
+ }
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
+ try (InputStream fis = Files.newInputStream(reading(f).toPath());
+ OutputStream out = p.getOut()) {
+ org.apache.commons.io.IOUtils.copy(fis, out);
+ } catch (InvalidPathException e) {
+ p.error(new IOException(e));
+ } catch (Exception x) {
+ p.error(x);
+ }
+ return null;
+ }
+ }
/**
* Reads this file from the specific offset.
@@ -1929,8 +2097,11 @@ public final class FilePath implements Serializable {
}
}
- return act(new SecureFileCallable() {
+ return act(new WritePipe());
+ }
+ private class WritePipe extends SecureFileCallable {
private static final long serialVersionUID = 1L;
+ @Override
public OutputStream invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
f = f.getAbsoluteFile();
mkdirs(f.getParentFile());
@@ -1941,7 +2112,6 @@ public final class FilePath implements Serializable {
throw new IOException(e);
}
}
- });
}
/**
@@ -1952,19 +2122,27 @@ public final class FilePath implements Serializable {
* @since 1.105
*/
public void write(final String content, final String encoding) throws IOException, InterruptedException {
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- mkdirs(f.getParentFile());
- try (OutputStream fos = Files.newOutputStream(writing(f).toPath());
- Writer w = encoding != null ? new OutputStreamWriter(fos, encoding) : new OutputStreamWriter(fos)) {
- w.write(content);
- } catch (InvalidPathException e) {
- throw new IOException(e);
- }
- return null;
+ act(new Write(encoding, content));
+ }
+ private class Write extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ private final String encoding;
+ private final String content;
+ Write(String encoding, String content) {
+ this.encoding = encoding;
+ this.content = content;
+ }
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ mkdirs(f.getParentFile());
+ try (OutputStream fos = Files.newOutputStream(writing(f).toPath());
+ Writer w = encoding != null ? new OutputStreamWriter(fos, encoding) : new OutputStreamWriter(fos)) {
+ w.write(content);
+ } catch (InvalidPathException e) {
+ throw new IOException(e);
}
- });
+ return null;
+ }
}
/**
@@ -1972,12 +2150,14 @@ public final class FilePath implements Serializable {
* @see Util#getDigestOf(File)
*/
public String digest() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public String invoke(File f, VirtualChannel channel) throws IOException {
- return Util.getDigestOf(reading(f));
- }
- });
+ return act(new Digest());
+ }
+ private class Digest extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public String invoke(File f, VirtualChannel channel) throws IOException {
+ return Util.getDigestOf(reading(f));
+ }
}
/**
@@ -1988,13 +2168,19 @@ public final class FilePath implements Serializable {
if(this.channel != target.channel) {
throw new IOException("renameTo target must be on the same host");
}
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- Files.move(fileToPath(reading(f)), fileToPath(creating(new File(target.remote))), LinkOption.NOFOLLOW_LINKS);
- return null;
- }
- });
+ act(new RenameTo(target));
+ }
+ private class RenameTo extends SecureFileCallable {
+ private final FilePath target;
+ RenameTo(FilePath target) {
+ this.target = target;
+ }
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ Files.move(fileToPath(reading(f)), fileToPath(creating(new File(target.remote))), LinkOption.NOFOLLOW_LINKS);
+ return null;
+ }
}
/**
@@ -2006,8 +2192,15 @@ public final class FilePath implements Serializable {
if(this.channel != target.channel) {
throw new IOException("pullUpTo target must be on the same host");
}
- act(new SecureFileCallable() {
+ act(new MoveAllChildrenTo(target));
+ }
+ private class MoveAllChildrenTo extends SecureFileCallable {
+ private final FilePath target;
+ MoveAllChildrenTo(FilePath target) {
+ this.target = target;
+ }
private static final long serialVersionUID = 1L;
+ @Override
public Void invoke(File f, VirtualChannel channel) throws IOException {
// JENKINS-16846: if f.getName() is the same as one of the files/directories in f,
// then the rename op will fail
@@ -2016,7 +2209,7 @@ public final class FilePath implements Serializable {
throw new IOException("Failed to rename "+f+" to "+tmp);
File t = new File(target.getRemote());
-
+
for(File child : reading(tmp).listFiles()) {
File target = new File(t, child.getName());
if(!stating(child).renameTo(creating(target)))
@@ -2025,7 +2218,6 @@ public final class FilePath implements Serializable {
deleting(tmp).delete();
return null;
}
- });
}
/**
@@ -2048,16 +2240,7 @@ public final class FilePath implements Serializable {
public void copyToWithPermission(FilePath target) throws IOException, InterruptedException {
// Use NIO copy with StandardCopyOption.COPY_ATTRIBUTES when copying on the same machine.
if (this.channel == target.channel) {
- act(new SecureFileCallable() {
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- File targetFile = new File(target.remote);
- File targetDir = targetFile.getParentFile();
- filterNonNull().mkdirs(targetDir);
- Files.createDirectories(fileToPath(targetDir));
- Files.copy(fileToPath(reading(f)), fileToPath(writing(targetFile)), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
- return null;
- }
- });
+ act(new CopyToWithPermission(target));
return;
}
@@ -2066,6 +2249,21 @@ public final class FilePath implements Serializable {
target.chmod(mode());
target.setLastModifiedIfPossible(lastModified());
}
+ private class CopyToWithPermission extends SecureFileCallable {
+ private final FilePath target;
+ CopyToWithPermission(FilePath target) {
+ this.target = target;
+ }
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ File targetFile = new File(target.remote);
+ File targetDir = targetFile.getParentFile();
+ filterNonNull().mkdirs(targetDir);
+ Files.createDirectories(fileToPath(targetDir));
+ Files.copy(fileToPath(reading(f)), fileToPath(writing(targetFile)), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
+ return null;
+ }
+ }
/**
* Sends the contents of this file into the given {@link OutputStream}.
@@ -2073,24 +2271,30 @@ public final class FilePath implements Serializable {
public void copyTo(OutputStream os) throws IOException, InterruptedException {
final OutputStream out = new RemoteOutputStream(os);
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 4088559042349254141L;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- try (InputStream fis = Files.newInputStream(reading(f).toPath())) {
- org.apache.commons.io.IOUtils.copy(fis, out);
- return null;
- } catch (InvalidPathException e) {
- throw new IOException(e);
- } finally {
- out.close();
- }
- }
- });
+ act(new CopyTo(out));
// make sure the writes fully got delivered to 'os' before we return.
// this is needed because I/O operation is asynchronous
syncIO();
}
+ private class CopyTo extends SecureFileCallable {
+ private static final long serialVersionUID = 4088559042349254141L;
+ private final OutputStream out;
+ CopyTo(OutputStream out) {
+ this.out = out;
+ }
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ try (InputStream fis = Files.newInputStream(reading(f).toPath())) {
+ org.apache.commons.io.IOUtils.copy(fis, out);
+ return null;
+ } catch (InvalidPathException e) {
+ throw new IOException(e);
+ } finally {
+ out.close();
+ }
+ }
+ }
/**
* With fix to JENKINS-11251 (remoting 2.15), this is no longer necessary.
@@ -2105,7 +2309,7 @@ public final class FilePath implements Serializable {
// legacy agent.jar. Handle this gracefully
try {
LOGGER.log(Level.WARNING,"Looks like an old agent.jar. Please update "+ Which.jarFile(Channel.class)+" to the new version",e);
- } catch (IOException _) {
+ } catch (IOException ignored) {
// really ignore this time
}
}
@@ -2188,64 +2392,14 @@ public final class FilePath implements Serializable {
public int copyRecursiveTo(final DirScanner scanner, final FilePath target, final String description) throws IOException, InterruptedException {
if(this.channel==target.channel) {
// local to local copy.
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Integer invoke(File base, VirtualChannel channel) throws IOException {
- if(!base.exists()) return 0;
- assert target.channel==null;
- final File dest = new File(target.remote);
- final AtomicInteger count = new AtomicInteger();
- scanner.scan(base, reading(new FileVisitor() {
- @Override
- public void visit(File f, String relativePath) throws IOException {
- if (f.isFile()) {
- File target = new File(dest, relativePath);
- mkdirsE(target.getParentFile());
- Util.copyFile(f, writing(target));
- count.incrementAndGet();
- }
- }
-
- @Override
- public boolean understandsSymlink() {
- return true;
- }
-
- @Override
- public void visitSymlink(File link, String target, String relativePath) throws IOException {
- try {
- mkdirsE(new File(dest, relativePath).getParentFile());
- writing(new File(dest, target));
- Util.createSymlink(dest, target, relativePath, TaskListener.NULL);
- } catch (InterruptedException x) {
- throw new IOException(x);
- }
- count.incrementAndGet();
- }
- }));
- return count.get();
- }
- });
+ return act(new CopyRecursiveLocal(target, scanner));
} else
if(this.channel==null) {
// local -> remote copy
final Pipe pipe = Pipe.createLocalToRemote();
- Future future = target.actAsync(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- try (InputStream in = pipe.getIn()) {
- readFromTar(remote + '/' + description, f,TarCompression.GZIP.extract(in));
- return null;
- }
- }
- });
- Future future2 = actAsync(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- @Override public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
- return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut()));
- }
- });
+ Future future = target.actAsync(new ReadToTar(pipe, description));
+ Future future2 = actAsync(new WriteToTar(scanner, pipe));
try {
// JENKINS-9540 in case the reading side failed, report that error first
future.get();
@@ -2262,14 +2416,7 @@ public final class FilePath implements Serializable {
// remote -> local copy
final Pipe pipe = Pipe.createRemoteToLocal();
- Future future = actAsync(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Integer invoke(File f, VirtualChannel channel) throws IOException {
- try (OutputStream out = pipe.getOut()) {
- return writeToTar(f, scanner, TarCompression.GZIP.compress(out));
- }
- }
- });
+ Future future = actAsync(new CopyRecursiveRemoteToLocal(pipe, scanner));
try {
readFromTar(remote + '/' + description,new File(target.remote),TarCompression.GZIP.extract(pipe.getIn()));
} catch (IOException e) {// BuildException or IOException
@@ -2280,8 +2427,8 @@ public final class FilePath implements Serializable {
// report both errors
e.addSuppressed(x);
throw e;
- } catch (TimeoutException _) {
- // remote is hanging
+ } catch (TimeoutException ignored) {
+ // remote is hanging, just throw the original exception
throw e;
}
}
@@ -2297,7 +2444,117 @@ public final class FilePath implements Serializable {
}
}
}
-
+ private class CopyRecursiveLocal extends SecureFileCallable {
+ private final FilePath target;
+ private final DirScanner scanner;
+ CopyRecursiveLocal(FilePath target, DirScanner scanner) {
+ this.target = target;
+ this.scanner = scanner;
+ }
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Integer invoke(File base, VirtualChannel channel) throws IOException {
+ if (!base.exists()) {
+ return 0;
+ }
+ assert target.channel == null;
+ final File dest = new File(target.remote);
+ final AtomicInteger count = new AtomicInteger();
+ scanner.scan(base, reading(new FileVisitor() {
+ private boolean exceptionEncountered;
+ private boolean logMessageShown;
+ @Override
+ public void visit(File f, String relativePath) throws IOException {
+ if (f.isFile()) {
+ File target = new File(dest, relativePath);
+ mkdirsE(target.getParentFile());
+ Path targetPath = fileToPath(writing(target));
+ exceptionEncountered = exceptionEncountered || !tryCopyWithAttributes(f, targetPath);
+ if (exceptionEncountered) {
+ Files.copy(fileToPath(f), targetPath, StandardCopyOption.REPLACE_EXISTING);
+ if (!logMessageShown) {
+ LOGGER.log(Level.INFO,
+ "JENKINS-52325: Jenkins failed to retain attributes when copying to {0}, so proceeding without attributes.",
+ dest.getAbsolutePath());
+ logMessageShown = true;
+ }
+ }
+ count.incrementAndGet();
+ }
+ }
+ private boolean tryCopyWithAttributes(File f, Path targetPath) {
+ try {
+ Files.copy(fileToPath(f), targetPath,
+ StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
+ LOGGER.log(Level.FINE, "Unable to copy: {0}", e.getMessage());
+ return false;
+ }
+ return true;
+ }
+ @Override
+ public boolean understandsSymlink() {
+ return true;
+ }
+ @Override
+ public void visitSymlink(File link, String target, String relativePath) throws IOException {
+ try {
+ mkdirsE(new File(dest, relativePath).getParentFile());
+ writing(new File(dest, target));
+ Util.createSymlink(dest, target, relativePath, TaskListener.NULL);
+ } catch (InterruptedException x) {
+ throw new IOException(x);
+ }
+ count.incrementAndGet();
+ }
+ }));
+ return count.get();
+ }
+ }
+ private class ReadToTar extends SecureFileCallable {
+ private final Pipe pipe;
+ private final String description;
+ ReadToTar(Pipe pipe, String description) {
+ this.pipe = pipe;
+ this.description = description;
+ }
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ try (InputStream in = pipe.getIn()) {
+ readFromTar(remote + '/' + description, f, TarCompression.GZIP.extract(in));
+ return null;
+ }
+ }
+ }
+ private class WriteToTar extends SecureFileCallable {
+ private final DirScanner scanner;
+ private final Pipe pipe;
+ WriteToTar(DirScanner scanner, Pipe pipe) {
+ this.scanner = scanner;
+ this.pipe = pipe;
+ }
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
+ return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut()));
+ }
+ }
+ private class CopyRecursiveRemoteToLocal extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ private final Pipe pipe;
+ private final DirScanner scanner;
+ CopyRecursiveRemoteToLocal(Pipe pipe, DirScanner scanner) {
+ this.pipe = pipe;
+ this.scanner = scanner;
+ }
+ @Override
+ public Integer invoke(File f, VirtualChannel channel) throws IOException {
+ try (OutputStream out = pipe.getOut()) {
+ return writeToTar(f, scanner, TarCompression.GZIP.compress(out));
+ }
+ }
+ }
/**
* Writes files in 'this' directory to a tar stream.
@@ -2347,6 +2604,10 @@ public final class FilePath implements Serializable {
TarArchiveEntry te;
while ((te = t.getNextTarEntry()) != null) {
File f = new File(baseDir, te.getName());
+ if (!f.toPath().normalize().startsWith(baseDir.toPath())) {
+ throw new IOException(
+ "Tar " + name + " contains illegal file name that breaks out of the target directory: " + te.getName());
+ }
if (te.isDirectory()) {
mkdirs(f);
} else {
@@ -2438,9 +2699,20 @@ public final class FilePath implements Serializable {
* @throws InterruptedException not only in case of a channel failure, but also if too many operations were performed without finding any matches
* @since 1.484
*/
- public String validateAntFileMask(final String fileMasks, final int bound, final boolean caseSensitive) throws IOException, InterruptedException {
- return act(new MasterToSlaveFileCallable() {
+ public @CheckForNull String validateAntFileMask(final String fileMasks, final int bound, final boolean caseSensitive) throws IOException, InterruptedException {
+ return act(new ValidateAntFileMask(fileMasks, caseSensitive, bound));
+ }
+ private class ValidateAntFileMask extends MasterToSlaveFileCallable {
+ private final String fileMasks;
+ private final boolean caseSensitive;
+ private final int bound;
+ ValidateAntFileMask(String fileMasks, boolean caseSensitive, int bound) {
+ this.fileMasks = fileMasks;
+ this.caseSensitive = caseSensitive;
+ this.bound = bound;
+ }
private static final long serialVersionUID = 1;
+ @Override
public String invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
if(fileMasks.startsWith("~"))
return Messages.FilePath_TildaDoesntWork();
@@ -2586,7 +2858,6 @@ public final class FilePath implements Serializable {
if(idx2==-1) return idx1;
return Math.min(idx1,idx2);
}
- });
}
private static final UrlFactory DEFAULT_URL_FACTORY = new UrlFactory();
@@ -2828,6 +3099,11 @@ public final class FilePath implements Serializable {
return classLoader;
}
+ @Override
+ public String toString() {
+ return callable.toString();
+ }
+
private static final long serialVersionUID = 1L;
}
@@ -2852,11 +3128,13 @@ public final class FilePath implements Serializable {
* (User's home directory in the Unix sense) of the given channel.
*/
public static FilePath getHomeDirectory(VirtualChannel ch) throws InterruptedException, IOException {
- return ch.call(new MasterToSlaveCallable() {
- public FilePath call() throws IOException {
- return new FilePath(new File(System.getProperty("user.home")));
- }
- });
+ return ch.call(new GetHomeDirectory());
+ }
+ private static class GetHomeDirectory extends MasterToSlaveCallable {
+ @Override
+ public FilePath call() throws IOException {
+ return new FilePath(new File(System.getProperty("user.home")));
+ }
}
/**
@@ -3002,5 +3280,60 @@ public final class FilePath implements Serializable {
return IOUtils.mkdirs(dir);
}
+ /**
+ * Check if the relative child is really a descendant after symlink resolution if any.
+ *
+ * TODO un-restrict it in a weekly after the patch
+ */
+ @Restricted(NoExternalUse.class)
+ public boolean isDescendant(@Nonnull String potentialChildRelativePath) throws IOException, InterruptedException {
+ return act(new IsDescendant(potentialChildRelativePath));
+ }
+
+ private class IsDescendant extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ private String potentialChildRelativePath;
+
+ private IsDescendant(@Nonnull String potentialChildRelativePath){
+ this.potentialChildRelativePath = potentialChildRelativePath;
+ }
+
+ @Override
+ public Boolean invoke(@Nonnull File parentFile, @Nonnull VirtualChannel channel) throws IOException, InterruptedException {
+ if (new File(potentialChildRelativePath).isAbsolute()) {
+ throw new IllegalArgumentException("Only a relative path is supported, the given path is absolute: " + potentialChildRelativePath);
+ }
+
+ Path parent = parentFile.getAbsoluteFile().toPath().normalize();
+
+ String remainingPath = potentialChildRelativePath;
+ File currentFile = parentFile;
+ while (!remainingPath.isEmpty()) {
+ File directChild = this.getDirectChild(currentFile, remainingPath);
+ File childUsingFullPath = new File(currentFile, remainingPath);
+ remainingPath = childUsingFullPath.getAbsolutePath().substring(directChild.getAbsolutePath().length());
+
+ File childFileSymbolic = Util.resolveSymlinkToFile(directChild);
+ if (childFileSymbolic == null) {
+ currentFile = directChild;
+ } else {
+ currentFile = childFileSymbolic;
+ }
+ }
+
+ //TODO could be refactored using Util#isDescendant(File, File) from 2.80+
+ Path child = currentFile.getAbsoluteFile().toPath().normalize();
+ return child.startsWith(parent);
+ }
+
+ private @CheckForNull File getDirectChild(File parentFile, String childPath){
+ File current = new File(parentFile, childPath);
+ while (current != null && !parentFile.equals(current.getParentFile())) {
+ current = current.getParentFile();
+ }
+ return current;
+ }
+ }
+
private static final SoloFilePathFilter UNRESTRICTED = SoloFilePathFilter.wrap(FilePathFilter.UNRESTRICTED);
}
diff --git a/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java b/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java
index 1e02e11f7f4dfdbedf2f00b853535725c1d43f86..9e162d666feb466d4487911fb4b2b2cdbd81a99a 100644
--- a/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java
+++ b/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java
@@ -50,12 +50,12 @@ public abstract class FileSystemProvisionerDescriptor extends Descriptortrue.
+ * perform the necessary deletion operation, and return {@code true}.
*
*
* If the workspace isn't the one created by this {@link FileSystemProvisioner}, or if the
* workspace can be simply deleted by {@link FilePath#deleteRecursive()}, then simply
- * return false to give other {@link FileSystemProvisionerDescriptor}s a chance to
+ * return {@code false} to give other {@link FileSystemProvisionerDescriptor}s a chance to
* discard them.
*
* @param ws
diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java
index 8462b889946e8facd489ca242f940d754aa63afe..5e2ab71644b933ba7424afd14268e11a2e339b9b 100644
--- a/core/src/main/java/hudson/Functions.java
+++ b/core/src/main/java/hudson/Functions.java
@@ -47,6 +47,7 @@ import hudson.model.JobPropertyDescriptor;
import hudson.model.ModelObject;
import hudson.model.Node;
import hudson.model.PageDecorator;
+import jenkins.model.SimplePageDecorator;
import hudson.model.PaneStatusProperties;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterDefinition.ParameterDescriptor;
@@ -133,8 +134,6 @@ import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithChildren;
import jenkins.model.ModelObjectWithContextMenu;
-import org.acegisecurity.Authentication;
-import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
@@ -397,11 +396,11 @@ public class Functions {
* is chosen, this part remains intact.
*
*
- * The 524 is the path from {@link Job} to {@link Run}.
+ * The {@code 524} is the path from {@link Job} to {@link Run}.
*
*
- * The bbb portion is the path after that till the last
- * {@link Run} subtype. The ccc portion is the part
+ * The {@code bbb} portion is the path after that till the last
+ * {@link Run} subtype. The {@code ccc} portion is the part
* after that.
*/
public static final class RunUrl {
@@ -619,7 +618,7 @@ public class Functions {
private static final SimpleFormatter formatter = new SimpleFormatter();
/**
- * Used by layout.jelly to control the auto refresh behavior.
+ * Used by {@code layout.jelly} to control the auto refresh behavior.
*
* @param noAutoRefresh
* On certain pages, like a page with forms, will have annoying interference
@@ -773,7 +772,7 @@ public class Functions {
}
/**
- * This version is so that the 'checkPermission' on layout.jelly
+ * This version is so that the 'checkPermission' on {@code layout.jelly}
* degrades gracefully if "it" is not an {@link AccessControlled} object.
* Otherwise it will perform no check and that problem is hard to notice.
*/
@@ -1379,6 +1378,7 @@ public class Functions {
}
public static String jsStringEscape(String s) {
+ if (s == null) return null;
StringBuilder buf = new StringBuilder();
for( int i=0; i
- * This is primarily used in slave-agent.jnlp.jelly to specify the destination
+ * This is primarily used in {@code slave-agent.jnlp.jelly} to specify the destination
* that the agents talk to.
*/
public String getServerName() {
@@ -1746,7 +1746,7 @@ public class Functions {
/**
* If the given href link is matching the current page, return true.
*
- * Used in task.jelly to decide if the page should be highlighted.
+ * Used in {@code task.jelly} to decide if the page should be highlighted.
*/
public boolean hyperlinkMatchesCurrentPage(String href) throws UnsupportedEncodingException {
String url = Stapler.getCurrentRequest().getRequestURL().toString();
@@ -1771,7 +1771,14 @@ public class Functions {
if(Jenkins.getInstanceOrNull()==null) return Collections.emptyList();
return PageDecorator.all();
}
-
+ /**
+ * Gets only one {@link SimplePageDecorator}.
+ * @since 2.128
+ */
+ public static SimplePageDecorator getSimplePageDecorator() {
+ return SimplePageDecorator.first();
+ }
+
public static List> getCloudDescriptors() {
return Cloud.all();
}
@@ -1820,7 +1827,7 @@ public class Functions {
* from {@link ConsoleAnnotatorFactory}s and {@link ConsoleAnnotationDescriptor}s.
*/
public static String generateConsoleAnnotationScriptAndStylesheet() {
- String cp = Stapler.getCurrentRequest().getContextPath();
+ String cp = Stapler.getCurrentRequest().getContextPath() + Jenkins.RESOURCE_PATH;
StringBuilder buf = new StringBuilder();
for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) {
String path = cp + "/extensionList/" + ConsoleAnnotatorFactory.class.getName() + "/" + f.getClass().getName();
diff --git a/core/src/main/java/hudson/Indenter.java b/core/src/main/java/hudson/Indenter.java
index c1f6971b6540a6b66c36f34e0378b605835bd290..aa4348b57a9bbc54b2a5b89fe272e4ed663562c5 100644
--- a/core/src/main/java/hudson/Indenter.java
+++ b/core/src/main/java/hudson/Indenter.java
@@ -26,7 +26,7 @@ package hudson;
import hudson.model.Job;
/**
- * Used by projectView.jelly to indent modules.
+ * Used by {@code projectView.jelly} to indent modules.
*
* @author Kohsuke Kawaguchi
*/
diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java
index 432a5afe249c7bee7e68c11d2a9ec0be490cb8b8..f0f18b4f81df12bb56440e68872bf0871fca1a55 100644
--- a/core/src/main/java/hudson/Launcher.java
+++ b/core/src/main/java/hudson/Launcher.java
@@ -26,6 +26,7 @@ package hudson;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Proc.LocalProc;
import hudson.model.Computer;
+import jenkins.util.MemoryReductionUtil;
import hudson.util.QuotedStringTokenizer;
import jenkins.model.Jenkins;
import hudson.model.TaskListener;
@@ -166,6 +167,8 @@ public abstract class Launcher {
@CheckForNull
protected OutputStream stdout = NULL_OUTPUT_STREAM, stderr;
@CheckForNull
+ private TaskListener stdoutListener;
+ @CheckForNull
protected InputStream stdin = NULL_INPUT_STREAM;
@CheckForNull
protected String[] envs = null;
@@ -280,22 +283,25 @@ public abstract class Launcher {
* Sets STDOUT destination.
*
* @param out Output stream.
- * Use {@code null} to send STDOUT to /dev/null.
+ * Use {@code null} to send STDOUT to {@code /dev/null}.
* @return {@code this}
*/
public ProcStarter stdout(@CheckForNull OutputStream out) {
this.stdout = out;
+ stdoutListener = null;
return this;
}
/**
* Sends the stdout to the given {@link TaskListener}.
*
- * @param out Task listener
+ * @param out Task listener (must be safely remotable)
* @return {@code this}
*/
public ProcStarter stdout(@Nonnull TaskListener out) {
- return stdout(out.getLogger());
+ stdout = out.getLogger();
+ stdoutListener = out;
+ return this;
}
/**
@@ -329,7 +335,7 @@ public abstract class Launcher {
/**
* Controls where the stdin of the process comes from.
- * By default, /dev/null.
+ * By default, {@code /dev/null}.
*
* @return {@code this}
*/
@@ -391,7 +397,7 @@ public abstract class Launcher {
*/
@Nonnull
public String[] envs() {
- return envs != null ? envs.clone() : new String[0];
+ return envs != null ? envs.clone() : MemoryReductionUtil.EMPTY_STRING_ARRAY;
}
/**
@@ -490,6 +496,7 @@ public abstract class Launcher {
@Nonnull
public ProcStarter copy() {
ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs).quiet(quiet);
+ rhs.stdoutListener = stdoutListener;
rhs.reverseStdin = this.reverseStdin;
rhs.reverseStderr = this.reverseStderr;
rhs.reverseStdout = this.reverseStdout;
@@ -1041,7 +1048,7 @@ public abstract class Launcher {
}
public Proc launch(ProcStarter ps) throws IOException {
- final OutputStream out = ps.stdout == null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stdout));
+ final OutputStream out = ps.stdout == null || ps.stdoutListener != null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stdout));
final OutputStream err = ps.stderr==null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stderr));
final InputStream in = (ps.stdin==null || ps.stdin==NULL_INPUT_STREAM) ? null : new RemoteInputStream(ps.stdin,false);
@@ -1049,7 +1056,7 @@ public abstract class Launcher {
final String workDir = psPwd==null ? null : psPwd.getRemote();
try {
- return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener)));
+ return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener)));
} catch (InterruptedException e) {
throw (IOException)new InterruptedIOException().initCause(e);
}
@@ -1265,6 +1272,7 @@ public abstract class Launcher {
private final @CheckForNull OutputStream err;
private final @CheckForNull String workDir;
private final @Nonnull TaskListener listener;
+ private final @CheckForNull TaskListener stdoutListener;
private final boolean reverseStdin, reverseStdout, reverseStderr;
private final boolean quiet;
@@ -1272,7 +1280,7 @@ public abstract class Launcher {
@CheckForNull InputStream in, boolean reverseStdin,
@CheckForNull OutputStream out, boolean reverseStdout,
@CheckForNull OutputStream err, boolean reverseStderr,
- boolean quiet, @CheckForNull String workDir, @Nonnull TaskListener listener) {
+ boolean quiet, @CheckForNull String workDir, @Nonnull TaskListener listener, @CheckForNull TaskListener stdoutListener) {
this.cmd = new ArrayList<>(cmd);
this.masks = masks;
this.env = env;
@@ -1281,6 +1289,7 @@ public abstract class Launcher {
this.err = err;
this.workDir = workDir;
this.listener = listener;
+ this.stdoutListener = stdoutListener;
this.reverseStdin = reverseStdin;
this.reverseStdout = reverseStdout;
this.reverseStderr = reverseStderr;
@@ -1290,7 +1299,12 @@ public abstract class Launcher {
public RemoteProcess call() throws IOException {
final Channel channel = getOpenChannelOrFail();
Launcher.ProcStarter ps = new LocalLauncher(listener).launch();
- ps.cmds(cmd).masks(masks).envs(env).stdin(in).stdout(out).stderr(err).quiet(quiet);
+ ps.cmds(cmd).masks(masks).envs(env).stdin(in).stderr(err).quiet(quiet);
+ if (stdoutListener != null) {
+ ps.stdout(stdoutListener.getLogger());
+ } else {
+ ps.stdout(out);
+ }
if(workDir!=null) ps.pwd(workDir);
if (reverseStdin) ps.writeStdin();
if (reverseStdout) ps.readStdout();
diff --git a/core/src/main/java/hudson/Main.java b/core/src/main/java/hudson/Main.java
index 896ae4dd53398525db56af00f19aeec7c40dde05..11cb516cee31be90a3f118b308f27179083f9d5a 100644
--- a/core/src/main/java/hudson/Main.java
+++ b/core/src/main/java/hudson/Main.java
@@ -144,7 +144,7 @@ public class Main {
int ret;
try (OutputStream os = Files.newOutputStream(tmpFile.toPath());
Writer w = new OutputStreamWriter(os,"UTF-8")) {
- w.write("");
+ w.write("");
w.write("");
w.flush();
diff --git a/core/src/main/java/hudson/Plugin.java b/core/src/main/java/hudson/Plugin.java
index 9539737878719eb636c01d89acb8a9b8fcdc2314..2f065102becc4c8a3a247a9869a6ebb8a0150844 100644
--- a/core/src/main/java/hudson/Plugin.java
+++ b/core/src/main/java/hudson/Plugin.java
@@ -35,6 +35,7 @@ import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.File;
@@ -42,10 +43,10 @@ import net.sf.json.JSONObject;
import com.thoughtworks.xstream.XStream;
import hudson.init.Initializer;
import hudson.init.Terminator;
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Locale;
+import java.util.logging.Logger;
import jenkins.model.GlobalConfiguration;
-import org.kohsuke.stapler.HttpResponses;
/**
* Base class of Hudson plugin.
@@ -61,26 +62,28 @@ import org.kohsuke.stapler.HttpResponses;
* to plugin functionality.
*
*
- * A plugin is bound to URL space of Hudson as ${rootURL}/plugin/foo/,
+ * A plugin is bound to URL space of Hudson as {@code ${rootURL}/plugin/foo/},
* where "foo" is taken from your plugin name "foo.jpi". All your web resources
* in src/main/webapp are visible from this URL, and you can also define Jelly
* views against your Plugin class, and those are visible in this URL, too.
*
*
- * {@link Plugin} can have an optional config.jelly page. If present,
+ * {@link Plugin} can have an optional {@code config.jelly} page. If present,
* it will become a part of the system configuration page (http://server/hudson/configure).
* This is convenient for exposing/maintaining configuration that doesn't
* fit any {@link Descriptor}s.
*
*
* Up until Hudson 1.150 or something, subclasses of {@link Plugin} required
- * @plugin javadoc annotation, but that is no longer a requirement.
+ * {@code @plugin} javadoc annotation, but that is no longer a requirement.
*
* @author Kohsuke Kawaguchi
* @since 1.42
*/
public abstract class Plugin implements Saveable {
+ private static final Logger LOGGER = Logger.getLogger(Plugin.class.getName());
+
/**
* You do not need to create custom subtypes:
*
@@ -191,11 +194,11 @@ public abstract class Plugin implements Saveable {
* Handles the submission for the system configuration.
*
*
- * If this class defines config.jelly view, be sure to
+ * If this class defines {@code config.jelly} view, be sure to
* override this method and persists the submitted values accordingly.
*
*
- * The following is a sample config.jelly that you can start yours with:
+ * The following is a sample {@code config.jelly} that you can start yours with:
*
{@code
*
*
@@ -219,18 +222,22 @@ public abstract class Plugin implements Saveable {
}
/**
- * This method serves static resources in the plugin under hudson/plugin/SHORTNAME.
+ * This method serves static resources in the plugin under {@code hudson/plugin/SHORTNAME}.
*/
public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
String path = req.getRestOfPath();
- if (path.startsWith("/META-INF/") || path.startsWith("/WEB-INF/")) {
- throw HttpResponses.notFound();
+ String pathUC = path.toUpperCase(Locale.ENGLISH);
+ if (path.isEmpty() || path.contains("..") || path.startsWith(".") || path.contains("%")
+ || pathUC.contains("META-INF") || pathUC.contains("WEB-INF")
+ // ClassicPluginStrategy#explode produce that file to know if a new explosion is required or not
+ || pathUC.equals("/.TIMESTAMP2")
+ ) {
+ LOGGER.warning("rejecting possibly malicious " + req.getRequestURIWithQueryString());
+ rsp.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
}
- if(path.length()==0)
- path = "/";
-
// Stapler routes requests like the "/static/.../foo/bar/zot" to be treated like "/foo/bar/zot"
// and this is used to serve long expiration header, by using Jenkins.VERSION_HASH as "..."
// to create unique URLs. Recognize that and set a long expiration header.
@@ -240,11 +247,7 @@ public abstract class Plugin implements Saveable {
long expires = staticLink ? TimeUnit.DAYS.toMillis(365) : -1;
// use serveLocalizedFile to support automatic locale selection
- try {
- rsp.serveLocalizedFile(req, wrapper.baseResourceURL.toURI().resolve(new URI(null, '.' + path, null)).toURL(), expires);
- } catch (URISyntaxException x) {
- throw new IOException(x);
- }
+ rsp.serveLocalizedFile(req, new URL(wrapper.baseResourceURL, '.' + path), expires);
}
//
diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java
index a7971ca33c7701b1b11a8866227fe21f2566d72e..8e2c5fd400e7dc1b3b38f249dd26a4c08101414c 100644
--- a/core/src/main/java/hudson/PluginManager.java
+++ b/core/src/main/java/hudson/PluginManager.java
@@ -25,8 +25,6 @@ package hudson;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import hudson.security.ACLContext;
-import jenkins.util.SystemProperties;
import hudson.PluginWrapper.Dependency;
import hudson.init.InitMilestone;
import hudson.init.InitStrategy;
@@ -36,22 +34,26 @@ import hudson.model.AbstractModelObject;
import hudson.model.AdministrativeMonitor;
import hudson.model.Api;
import hudson.model.Descriptor;
+import hudson.model.DownloadService;
import hudson.model.Failure;
import hudson.model.ItemGroupMixIn;
import hudson.model.UpdateCenter;
-import hudson.model.UpdateSite;
import hudson.model.UpdateCenter.DownloadJob;
import hudson.model.UpdateCenter.InstallationJob;
+import hudson.model.UpdateSite;
import hudson.security.ACL;
+import hudson.security.ACLContext;
import hudson.security.Permission;
import hudson.security.PermissionScope;
import hudson.util.CyclicGraphDetector;
import hudson.util.CyclicGraphDetector.CycleDetectedException;
+import hudson.util.FormValidation;
import hudson.util.PersistedList;
import hudson.util.Service;
import hudson.util.VersionNumber;
import hudson.util.XStream2;
import jenkins.ClassLoaderReflectionToolkit;
+import jenkins.ExtensionRefreshException;
import jenkins.InitReactorRunner;
import jenkins.MissingDependencyException;
import jenkins.RestartRequiredException;
@@ -59,12 +61,12 @@ import jenkins.YesNoMaybe;
import jenkins.install.InstallState;
import jenkins.install.InstallUtil;
import jenkins.model.Jenkins;
+import jenkins.security.CustomClassFilter;
+import jenkins.util.SystemProperties;
import jenkins.util.io.OnMaster;
import jenkins.util.xml.RestrictiveEntityResolver;
-
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
-
import org.acegisecurity.Authentication;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
@@ -82,17 +84,24 @@ import org.jvnet.hudson.reactor.Reactor;
import org.jvnet.hudson.reactor.ReactorException;
import org.jvnet.hudson.reactor.TaskBuilder;
import org.jvnet.hudson.reactor.TaskGraphBuilder;
+import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerOverridable;
+import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
@@ -100,6 +109,7 @@ import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
+import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FilenameFilter;
@@ -107,10 +117,12 @@ import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
+import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
+import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -121,6 +133,7 @@ import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
@@ -128,31 +141,15 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
+import java.util.function.Supplier;
+import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
-import org.xml.sax.Attributes;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
import static hudson.init.InitMilestone.*;
-import hudson.model.DownloadService;
-import hudson.util.FormValidation;
-import java.io.ByteArrayInputStream;
-import java.net.JarURLConnection;
-import java.net.URLConnection;
-import java.util.ServiceLoader;
-import java.util.jar.JarEntry;
-
-import static java.util.logging.Level.FINE;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.SEVERE;
-import static java.util.logging.Level.WARNING;
-import jenkins.security.CustomClassFilter;
-import org.kohsuke.accmod.Restricted;
-import org.kohsuke.accmod.restrictions.NoExternalUse;
+import static java.util.logging.Level.*;
/**
* Manages {@link PluginWrapper}s.
@@ -176,7 +173,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
* @author Kohsuke Kawaguchi
*/
@ExportedBean
-public abstract class PluginManager extends AbstractModelObject implements OnMaster, StaplerOverridable {
+public abstract class PluginManager extends AbstractModelObject implements OnMaster, StaplerOverridable, StaplerProxy {
/** Custom plugin manager system property or context param. */
public static final String CUSTOM_PLUGIN_MANAGER = PluginManager.class.getName() + ".className";
@@ -920,6 +917,11 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
// Redo who depends on who.
resolveDependantPlugins();
+ try {
+ Jenkins.get().refreshExtensions();
+ } catch (ExtensionRefreshException e) {
+ throw new IOException("Failed to refresh extensions after installing " + sn + " plugin", e);
+ }
LOGGER.info("Plugin " + p.getShortName()+":"+p.getVersion() + " dynamically installed");
}
}
@@ -927,8 +929,15 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
@Restricted(NoExternalUse.class)
public synchronized void resolveDependantPlugins() {
for (PluginWrapper plugin : plugins) {
+ // Set of optional dependants plugins of plugin
+ Set optionalDependants = new HashSet<>();
Set dependants = new HashSet<>();
for (PluginWrapper possibleDependant : plugins) {
+ // No need to check if plugin is dependant of itself
+ if(possibleDependant.getShortName().equals(plugin.getShortName())) {
+ continue;
+ }
+
// The plugin could have just been deleted. If so, it doesn't
// count as a dependant.
if (possibleDependant.isDeleted()) {
@@ -938,10 +947,20 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
for (Dependency dependency : dependencies) {
if (dependency.shortName.equals(plugin.getShortName())) {
dependants.add(possibleDependant.getShortName());
+
+ // If, in addition, the dependency is optional, add to the optionalDependants list
+ if (dependency.optional) {
+ optionalDependants.add(possibleDependant.getShortName());
+ }
+
+ // already know possibleDependant depends on plugin, no need to continue with the rest of
+ // dependencies. We continue with the next possibleDependant
+ break;
}
}
}
plugin.setDependants(dependants);
+ plugin.setOptionalDependants(optionalDependants);
}
}
@@ -1208,7 +1227,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
/**
* Discover all the service provider implementations of the given class,
- * via META-INF/services.
+ * via {@code META-INF/services}.
* @deprecated Use {@link ServiceLoader} instead, or (more commonly) {@link ExtensionList}.
*/
@Deprecated
@@ -1485,7 +1504,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
}
updateCenter.persistInstallStatus();
if(!failures) {
- try (ACLContext _ = ACL.as(currentAuth)) {
+ try (ACLContext acl = ACL.as(currentAuth)) {
InstallUtil.proceedToNextStateFrom(InstallState.INITIAL_PLUGINS_INSTALLING);
}
}
@@ -1816,6 +1835,44 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
return requestedPlugins;
}
+ @Restricted(DoNotUse.class) // table.jelly
+ public MetadataCache createCache() {
+ return new MetadataCache();
+ }
+
+ /**
+ * Disable a list of plugins using a strategy for their dependants plugins.
+ * @param strategy the strategy regarding how the dependant plugins are processed
+ * @param plugins the list of plugins
+ * @return the list of results for every plugin and their dependant plugins.
+ * @throws IOException see {@link PluginWrapper#disable()}
+ */
+ public @NonNull List disablePlugins(@NonNull PluginWrapper.PluginDisableStrategy strategy, @NonNull List plugins) throws IOException {
+ // Where we store the results of each plugin disablement
+ List results = new ArrayList<>(plugins.size());
+
+ // Disable all plugins passed
+ for (String pluginName : plugins) {
+ PluginWrapper plugin = this.getPlugin(pluginName);
+
+ if (plugin == null) {
+ results.add(new PluginWrapper.PluginDisableResult(pluginName, PluginWrapper.PluginDisableStatus.NO_SUCH_PLUGIN, Messages.PluginWrapper_NoSuchPlugin(pluginName)));
+ } else {
+ results.add(plugin.disable(strategy));
+ }
+ }
+
+ return results;
+ }
+
+ @Restricted(NoExternalUse.class) // table.jelly
+ public static final class MetadataCache {
+ private final Map data = new HashMap<>();
+ public T of(String key, Class type, Supplier func) {
+ return type.cast(data.computeIfAbsent(key, _ignored -> func.get()));
+ }
+ }
+
/**
* {@link ClassLoader} that can see all plugins.
*/
@@ -2063,4 +2120,19 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
}
}
+
+ @Override
+ @Restricted(NoExternalUse.class)
+ public Object getTarget() {
+ if (!SKIP_PERMISSION_CHECK) {
+ Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
+ }
+ return this;
+ }
+
+ /**
+ * Escape hatch for StaplerProxy-based access control
+ */
+ @Restricted(NoExternalUse.class)
+ public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(PluginManager.class.getName() + ".skipPermissionCheck");
}
diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java
index 75e358a81193d1b45212fe0f06b7af16a93e1111..16f0fcc63f48fa2e0eee5cd1f40adc792fccf7b7 100644
--- a/core/src/main/java/hudson/PluginWrapper.java
+++ b/core/src/main/java/hudson/PluginWrapper.java
@@ -25,18 +25,21 @@
package hudson;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import hudson.PluginManager.PluginInstanceStore;
import hudson.model.AdministrativeMonitor;
import hudson.model.Api;
import hudson.model.ModelObject;
-import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
-import jenkins.YesNoMaybe;
-import jenkins.model.Jenkins;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite;
import hudson.util.VersionNumber;
-import org.jvnet.localizer.ResourceBundleHolder;
+import jenkins.YesNoMaybe;
+import jenkins.model.Jenkins;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.LogFactory;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.DoNotUse;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.StaplerRequest;
@@ -45,16 +48,15 @@ import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.logging.LogFactory;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.Closeable;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -65,12 +67,20 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.function.Predicate;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import static hudson.PluginWrapper.PluginDisableStatus.ALREADY_DISABLED;
+import static hudson.PluginWrapper.PluginDisableStatus.DISABLED;
+import static hudson.PluginWrapper.PluginDisableStatus.ERROR_DISABLING;
+import static hudson.PluginWrapper.PluginDisableStatus.NOT_DISABLED_DEPENDANTS;
+import static hudson.PluginWrapper.PluginDisableStatus.NO_SUCH_PLUGIN;
import static java.util.logging.Level.WARNING;
import static org.apache.commons.io.FilenameUtils.getBaseName;
@@ -79,7 +89,7 @@ import static org.apache.commons.io.FilenameUtils.getBaseName;
* for Jenkins to control {@link Plugin}.
*
*
- * A plug-in is packaged into a jar file whose extension is ".jpi" (or ".hpi" for backward compatibility),
+ * A plug-in is packaged into a jar file whose extension is {@code ".jpi"} (or {@code ".hpi"} for backward compatibility),
* A plugin needs to have a special manifest entry to identify what it is.
*
*
@@ -124,7 +134,7 @@ public class PluginWrapper implements Comparable, ModelObject {
/**
* Base URL for loading static resources from this plugin.
* Null if disabled. The static resources are mapped under
- * CONTEXTPATH/plugin/SHORTNAME/.
+ * {@code CONTEXTPATH/plugin/SHORTNAME/}.
*/
public final URL baseResourceURL;
@@ -149,7 +159,7 @@ public class PluginWrapper implements Comparable, ModelObject {
/**
* True if this plugin is activated for this session.
- * The snapshot of disableFile.exists() as of the start up.
+ * The snapshot of {@code disableFile.exists()} as of the start up.
*/
private final boolean active;
@@ -159,10 +169,34 @@ public class PluginWrapper implements Comparable, ModelObject {
private final List optionalDependencies;
public List getDependencyErrors() {
- return Collections.unmodifiableList(dependencyErrors);
+ return Collections.unmodifiableList(new ArrayList<>(dependencyErrors.keySet()));
}
- private final transient List dependencyErrors = new ArrayList<>();
+ @Restricted(NoExternalUse.class) // Jelly use
+ public List getOriginalDependencyErrors() {
+ Predicate> p = Map.Entry::getValue;
+ return dependencyErrors.entrySet().stream().filter(p.negate()).map(Map.Entry::getKey).collect(Collectors.toList());
+ }
+
+ @Restricted(NoExternalUse.class) // Jelly use
+ public boolean hasOriginalDependencyErrors() {
+ return !getOriginalDependencyErrors().isEmpty();
+ }
+
+ @Restricted(NoExternalUse.class) // Jelly use
+ public List getDerivedDependencyErrors() {
+ return dependencyErrors.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey).collect(Collectors.toList());
+ }
+
+ @Restricted(NoExternalUse.class) // Jelly use
+ public boolean hasDerivedDependencyErrors() {
+ return !getDerivedDependencyErrors().isEmpty();
+ }
+
+ /**
+ * A String error message, and a boolean indicating whether it's an original error (false) or downstream from an original one (true)
+ */
+ private final transient Map dependencyErrors = new HashMap<>(0);
/**
* Is this plugin bundled in jenkins.war?
@@ -174,6 +208,11 @@ public class PluginWrapper implements Comparable, ModelObject {
*/
private Set dependants = Collections.emptySet();
+ /**
+ * List of plugins that depend optionally on this plugin.
+ */
+ private Set optionalDependants = Collections.emptySet();
+
/**
* The core can depend on a plugin if it is bundled. Sometimes it's the only thing that
* depends on the plugin e.g. UI support library bundle plugin.
@@ -188,6 +227,14 @@ public class PluginWrapper implements Comparable, ModelObject {
this.dependants = dependants;
}
+ /**
+ * Set the list of components that depend optionally on this plugin.
+ * @param optionalDependants The list of components that depend optionally on this plugin.
+ */
+ public void setOptionalDependants(@Nonnull Set optionalDependants) {
+ this.optionalDependants = optionalDependants;
+ }
+
/**
* Get the list of components that depend on this plugin.
* @return The list of components that depend on this plugin.
@@ -200,6 +247,13 @@ public class PluginWrapper implements Comparable, ModelObject {
}
}
+ /**
+ * @return The list of components that depend optionally on this plugin.
+ */
+ public @Nonnull Set getOptionalDependants() {
+ return optionalDependants;
+ }
+
/**
* Does this plugin have anything that depends on it.
* @return {@code true} if something (Jenkins core, or another plugin) depends on this
@@ -208,7 +262,17 @@ public class PluginWrapper implements Comparable, ModelObject {
public boolean hasDependants() {
return (isBundled || !dependants.isEmpty());
}
-
+
+ /**
+ * Does this plugin have anything that depends optionally on it.
+ * @return {@code true} if something (Jenkins core, or another plugin) depends optionally on this
+ * plugin, otherwise {@code false}.
+ */
+ public boolean hasOptionalDependants() {
+ return !optionalDependants.isEmpty();
+ }
+
+
/**
* Does this plugin depend on any other plugins.
* @return {@code true} if this plugin depends on other plugins, otherwise {@code false}.
@@ -230,8 +294,8 @@ public class PluginWrapper implements Comparable, ModelObject {
int idx = s.indexOf(':');
if(idx==-1)
throw new IllegalArgumentException("Illegal dependency specifier "+s);
- this.shortName = s.substring(0,idx);
- String version = s.substring(idx+1);
+ this.shortName = Util.intern(s.substring(0,idx));
+ String version = Util.intern(s.substring(idx+1));
boolean isOptional = false;
String[] osgiProperties = version.split("[;]");
@@ -274,7 +338,7 @@ public class PluginWrapper implements Comparable, ModelObject {
List dependencies, List optionalDependencies) {
this.parent = parent;
this.manifest = manifest;
- this.shortName = computeShortName(manifest, archive.getName());
+ this.shortName = Util.intern(computeShortName(manifest, archive.getName()));
this.baseResourceURL = baseResourceURL;
this.classLoader = classLoader;
this.disableFile = disableFile;
@@ -493,9 +557,19 @@ public class PluginWrapper implements Comparable, ModelObject {
}
/**
- * Disables this plugin next time Jenkins runs.
+ * Disables this plugin next time Jenkins runs. As it doesn't check anything, it's recommended to use the method
+ * {@link #disable(PluginDisableStrategy)}
*/
+ @Deprecated //see https://issues.jenkins-ci.org/browse/JENKINS-27177
public void disable() throws IOException {
+ disableWithoutCheck();
+ }
+
+ /**
+ * Disable a plugin wihout checking any dependency. Only add the disable file.
+ * @throws IOException
+ */
+ private void disableWithoutCheck() throws IOException {
// creates an empty file
try (OutputStream os = Files.newOutputStream(disableFile.toPath())) {
os.close();
@@ -504,6 +578,103 @@ public class PluginWrapper implements Comparable, ModelObject {
}
}
+ /**
+ * Disable this plugin using a strategy.
+ * @param strategy strategy to use
+ * @return an object representing the result of the disablement of this plugin and its dependants plugins.
+ */
+ public @Nonnull PluginDisableResult disable(@Nonnull PluginDisableStrategy strategy) {
+ PluginDisableResult result = new PluginDisableResult(shortName);
+
+ if (!this.isEnabled()) {
+ result.setMessage(Messages.PluginWrapper_Already_Disabled(shortName));
+ result.setStatus(ALREADY_DISABLED);
+ return result;
+ }
+
+ // Act as a flag indicating if this plugin, finally, can be disabled. If there is a not-disabled-dependant
+ // plugin, this one couldn't be disabled.
+ String aDependantNotDisabled = null;
+
+ // List of dependants plugins to 'check'. 'Check' means disable for mandatory or all strategies, or review if
+ // this dependant-mandatory plugin is enabled in order to return an error for the NONE strategy.
+ Set dependantsToCheck = dependantsToCheck(strategy);
+
+ // Review all the dependants and add to the plugin result what happened with its dependants
+ for (String dependant : dependantsToCheck) {
+ PluginWrapper dependantPlugin = parent.getPlugin(dependant);
+
+ // The dependant plugin doesn't exist, add an error to the report
+ if (dependantPlugin == null) {
+ PluginDisableResult dependantStatus = new PluginDisableResult(dependant, NO_SUCH_PLUGIN, Messages.PluginWrapper_NoSuchPlugin(dependant));
+ result.addDependantDisableStatus(dependantStatus);
+
+ // If the strategy is none and there is some enabled dependant plugin, the plugin cannot be disabled. If
+ // this dependant plugin is not enabled, continue searching for one enabled.
+ } else if (strategy.equals(PluginDisableStrategy.NONE)) {
+ if (dependantPlugin.isEnabled()) {
+ aDependantNotDisabled = dependant;
+ break; // in this case, we don't need to continue with the rest of its dependants
+ }
+
+ // If the strategy is not none and this dependant plugin is not enabled, add it as already disabled
+ } else if (!dependantPlugin.isEnabled()) {
+ PluginDisableResult dependantStatus = new PluginDisableResult(dependant, ALREADY_DISABLED, Messages.PluginWrapper_Already_Disabled(dependant));
+ result.addDependantDisableStatus(dependantStatus);
+
+ // If the strategy is not none and this dependant plugin is enabled, disable it
+ } else {
+ // As there is no cycles in the plugin dependencies, the recursion shouldn't be infinite. The
+ // strategy used is the same for its dependants plugins
+ PluginDisableResult dependantResult = dependantPlugin.disable(strategy);
+ PluginDisableStatus dependantStatus = dependantResult.status;
+
+ // If something wrong happened, flag this dependant plugin to set the plugin later as not-disabled due
+ // to its dependants plugins.
+ if (ERROR_DISABLING.equals(dependantStatus) || NOT_DISABLED_DEPENDANTS.equals(dependantStatus)) {
+ aDependantNotDisabled = dependant;
+ break; // we found a dependant plugin enabled, stop looking for dependant plugins to disable.
+ }
+ result.addDependantDisableStatus(dependantResult);
+ }
+ }
+
+ // If there is no enabled-dependant plugin, disable this plugin and add it to the result
+ if (aDependantNotDisabled == null) {
+ try {
+ this.disableWithoutCheck();
+ result.setMessage(Messages.PluginWrapper_Plugin_Disabled(shortName));
+ result.setStatus(DISABLED);
+ } catch (IOException io) {
+ result.setMessage(Messages.PluginWrapper_Error_Disabling(shortName, io.toString()));
+ result.setStatus(ERROR_DISABLING);
+ }
+ // if there is yet some not disabled dependant plugin (only possible with none strategy), this plugin cannot
+ // be disabled.
+ } else {
+ result.setMessage(Messages.PluginWrapper_Plugin_Has_Dependant(shortName, aDependantNotDisabled, strategy));
+ result.setStatus(NOT_DISABLED_DEPENDANTS);
+ }
+
+ return result;
+ }
+
+ private Set dependantsToCheck(PluginDisableStrategy strategy) {
+ Set dependantsToCheck;
+ switch (strategy) {
+ case ALL:
+ // getDependants returns all the dependant plugins, mandatory or optional.
+ dependantsToCheck = this.getDependants();
+ break;
+ default:
+ // It includes MANDATORY, NONE:
+ // with NONE, the process only fail if mandatory dependant plugins exists
+ // As of getDependants has all the dependants, we get the difference between them and only the optionals
+ dependantsToCheck = Sets.difference(this.getDependants(), this.getOptionalDependants());
+ }
+ return dependantsToCheck;
+ }
+
/**
* Returns true if this plugin is enabled for this session.
*/
@@ -571,7 +742,7 @@ public class PluginWrapper implements Comparable, ModelObject {
} else {
VersionNumber actualVersion = Jenkins.getVersion();
if (actualVersion.isOlderThan(new VersionNumber(requiredCoreVersion))) {
- dependencyErrors.add(Messages.PluginWrapper_obsoleteCore(Jenkins.getVersion().toString(), requiredCoreVersion));
+ versionDependencyError(Messages.PluginWrapper_obsoleteCore(Jenkins.getVersion().toString(), requiredCoreVersion), Jenkins.getVersion().toString(), requiredCoreVersion);
}
}
}
@@ -581,21 +752,21 @@ public class PluginWrapper implements Comparable, ModelObject {
if (dependency == null) {
PluginWrapper failedDependency = NOTICE.getPlugin(d.shortName);
if (failedDependency != null) {
- dependencyErrors.add(Messages.PluginWrapper_failed_to_load_dependency(failedDependency.getLongName(), failedDependency.getVersion()));
+ dependencyErrors.put(Messages.PluginWrapper_failed_to_load_dependency(failedDependency.getLongName(), failedDependency.getVersion()), true);
break;
} else {
- dependencyErrors.add(Messages.PluginWrapper_missing(d.shortName, d.version));
+ dependencyErrors.put(Messages.PluginWrapper_missing(d.shortName, d.version), false);
}
} else {
if (dependency.isActive()) {
if (isDependencyObsolete(d, dependency)) {
- dependencyErrors.add(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version));
+ versionDependencyError(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version);
}
} else {
if (isDependencyObsolete(d, dependency)) {
- dependencyErrors.add(Messages.PluginWrapper_disabledAndObsolete(dependency.getLongName(), dependency.getVersion(), d.version));
+ versionDependencyError(Messages.PluginWrapper_disabledAndObsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version);
} else {
- dependencyErrors.add(Messages.PluginWrapper_disabled(dependency.getLongName()));
+ dependencyErrors.put(Messages.PluginWrapper_disabled(dependency.getLongName()), false);
}
}
@@ -606,7 +777,7 @@ public class PluginWrapper implements Comparable, ModelObject {
PluginWrapper dependency = parent.getPlugin(d.shortName);
if (dependency != null && dependency.isActive()) {
if (isDependencyObsolete(d, dependency)) {
- dependencyErrors.add(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version));
+ versionDependencyError(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version);
} else {
dependencies.add(d);
}
@@ -616,7 +787,7 @@ public class PluginWrapper implements Comparable, ModelObject {
NOTICE.addPlugin(this);
StringBuilder messageBuilder = new StringBuilder();
messageBuilder.append(Messages.PluginWrapper_failed_to_load_plugin(getLongName(), getVersion())).append(System.lineSeparator());
- for (Iterator iterator = dependencyErrors.iterator(); iterator.hasNext(); ) {
+ for (Iterator iterator = dependencyErrors.keySet().iterator(); iterator.hasNext(); ) {
String dependencyError = iterator.next();
messageBuilder.append(" - ").append(dependencyError);
if (iterator.hasNext()) {
@@ -631,6 +802,26 @@ public class PluginWrapper implements Comparable, ModelObject {
return ENABLE_PLUGIN_DEPENDENCIES_VERSION_CHECK && dependency.getVersionNumber().isOlderThan(new VersionNumber(d.version));
}
+ /**
+ * Called when there appears to be a core or plugin version which is too old for a stated dependency.
+ * Normally records an error in {@link #dependencyErrors}.
+ * But if one or both versions {@link #isSnapshot}, just issue a warning (JENKINS-52665).
+ */
+ private void versionDependencyError(String message, String actual, String minimum) {
+ if (isSnapshot(actual) || isSnapshot(minimum)) {
+ LOGGER.log(WARNING, "Suppressing dependency error in {0} v{1}: {2}", new Object[] {getLongName(), getVersion(), message});
+ } else {
+ dependencyErrors.put(message, false);
+ }
+ }
+
+ /**
+ * Similar to {@code org.apache.maven.artifact.ArtifactUtils.isSnapshot}.
+ */
+ static boolean isSnapshot(@Nonnull String version) {
+ return version.contains("-SNAPSHOT") || version.matches(".+-[0-9]{8}.[0-9]{6}-[0-9]+");
+ }
+
/**
* If the plugin has {@link #getUpdateInfo() an update},
* returns the {@link hudson.model.UpdateSite.Plugin} object.
@@ -752,6 +943,11 @@ public class PluginWrapper implements Comparable, ModelObject {
return !plugins.isEmpty();
}
+ @Restricted(DoNotUse.class) // Jelly
+ public boolean hasAnyDerivedDependencyErrors() {
+ return plugins.values().stream().anyMatch(PluginWrapper::hasDerivedDependencyErrors);
+ }
+
@Override
public String getDisplayName() {
return Messages.PluginWrapper_PluginWrapperAdministrativeMonitor_DisplayName();
@@ -780,6 +976,93 @@ public class PluginWrapper implements Comparable, ModelObject {
}
}
+ /**
+ * The result of the disablement of a plugin and its dependants plugins.
+ */
+ public static class PluginDisableResult {
+ private String plugin;
+ private PluginDisableStatus status;
+ private String message;
+ private Set dependantsDisableStatus = new HashSet<>();
+
+ public PluginDisableResult(String plugin) {
+ this.plugin = plugin;
+ }
+
+ public PluginDisableResult(String plugin, PluginDisableStatus status, String message) {
+ this.plugin = plugin;
+ this.status = status;
+ this.message = message;
+ }
+
+ public String getPlugin() {
+ return plugin;
+ }
+
+ public PluginDisableStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PluginDisableResult that = (PluginDisableResult) o;
+ return Objects.equals(plugin, that.plugin);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(plugin);
+ }
+
+ public void setStatus(PluginDisableStatus status) {
+ this.status = status;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public Set getDependantsDisableStatus() {
+ return dependantsDisableStatus;
+ }
+
+ public void addDependantDisableStatus(PluginDisableResult dependantDisableStatus) {
+ dependantsDisableStatus.add(dependantDisableStatus);
+ }
+
+ }
+
+ /**
+ * An enum to hold the status of a disabling action against a plugin. To do it more reader-friendly.
+ */
+ public enum PluginDisableStatus {
+ NO_SUCH_PLUGIN,
+ DISABLED,
+ ALREADY_DISABLED,
+ NOT_DISABLED_DEPENDANTS,
+ ERROR_DISABLING
+ }
+
+ /**
+ * The strategies defined for disabling a plugin.
+ */
+ public enum PluginDisableStrategy {
+ NONE,
+ MANDATORY,
+ ALL;
+
+ @Override
+ public String toString() {
+ return this.name().toLowerCase();
+ }
+ }
+
//
//
// Action methods
diff --git a/core/src/main/java/hudson/ProxyConfiguration.java b/core/src/main/java/hudson/ProxyConfiguration.java
index 3baea805d42d341ae5b37cc6e43469dde3e39070..3f2cfe6f0af3f1260ae21c3d9a4b6d027f0104ee 100644
--- a/core/src/main/java/hudson/ProxyConfiguration.java
+++ b/core/src/main/java/hudson/ProxyConfiguration.java
@@ -110,6 +110,10 @@ public final class ProxyConfiguration extends AbstractDescribableImplproperties.get('key').
+ * Replaces the occurrence of '$key' by {@code properties.get('key')}.
*
*
* Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.)
@@ -142,7 +150,7 @@ public class Util {
}
/**
- * Replaces the occurrence of '$key' by resolver.get('key').
+ * Replaces the occurrence of '$key' by {@code resolver.get('key')}.
*
*
* Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.)
@@ -179,28 +187,54 @@ public class Util {
}
/**
- * Loads the contents of a file into a string.
+ * Reads the entire contents of the text file at logfile
into a
+ * string using the {@link Charset#defaultCharset() default charset} for
+ * decoding. If no such file exists, an empty string is returned.
+ * @param logfile The text file to read in its entirety.
+ * @return The entire text content of logfile
.
+ * @throws IOException If an error occurs while reading the file.
+ * @deprecated call {@link #loadFile(java.io.File, java.nio.charset.Charset)}
+ * instead to specify the charset to use for decoding (preferably
+ * {@link java.nio.charset.StandardCharsets#UTF_8}).
*/
@Nonnull
+ @Deprecated
public static String loadFile(@Nonnull File logfile) throws IOException {
return loadFile(logfile, Charset.defaultCharset());
}
+ /**
+ * Reads the entire contents of the text file at logfile
into a
+ * string using charset
for decoding. If no such file exists,
+ * an empty string is returned.
+ * @param logfile The text file to read in its entirety.
+ * @param charset The charset to use for decoding the bytes in logfile
.
+ * @return The entire text content of logfile
.
+ * @throws IOException If an error occurs while reading the file.
+ */
@Nonnull
public static String loadFile(@Nonnull File logfile, @Nonnull Charset charset) throws IOException {
- if(!logfile.exists())
+ // Note: Until charset handling is resolved (e.g. by implementing
+ // https://issues.jenkins-ci.org/browse/JENKINS-48923 ), this method
+ // must be able to handle character encoding errors. As reported at
+ // https://issues.jenkins-ci.org/browse/JENKINS-49112 Run.getLog() calls
+ // loadFile() to fully read the generated log file. This file might
+ // contain unmappable and/or malformed byte sequences. We need to make
+ // sure that in such cases, no CharacterCodingException is thrown.
+ //
+ // One approach that cannot be used is to call Files.newBufferedReader()
+ // because there is a difference in how an InputStreamReader constructed
+ // from a Charset and the reader returned by Files.newBufferedReader()
+ // handle malformed and unmappable byte sequences for the specified
+ // encoding; the latter is more picky and will throw an exception.
+ // See: https://issues.jenkins-ci.org/browse/JENKINS-49060?focusedCommentId=325989&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-325989
+ try {
+ return FileUtils.readFileToString(logfile, charset);
+ } catch (FileNotFoundException e) {
return "";
-
- StringBuilder str = new StringBuilder((int)logfile.length());
-
- try (BufferedReader r = Files.newBufferedReader(fileToPath(logfile), charset)) {
- char[] buf = new char[1024];
- int len;
- while ((len = r.read(buf, 0, buf.length)) > 0)
- str.append(buf, 0, len);
+ } catch (Exception e) {
+ throw new IOException("Failed to fully read " + logfile, e);
}
-
- return str.toString();
}
/**
@@ -298,7 +332,7 @@ public class Util {
if (!Functions.isWindows()) {
try {
PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class);
- Set newPermissions = ((PosixFileAttributes)attrs).permissions();
+ Set newPermissions = attrs.permissions();
newPermissions.add(PosixFilePermission.OWNER_WRITE);
Files.setPosixFilePermissions(path, newPermissions);
return;
@@ -491,7 +525,7 @@ public class Util {
* calling readAttributes.
*/
try {
- Path path = file.toPath();
+ Path path = fileToPath(file);
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
if (attrs.isSymbolicLink()) {
return true;
@@ -504,8 +538,6 @@ public class Util {
} else {
return false;
}
- } catch (InvalidPathException e) {
- throw new IOException(e);
} catch (NoSuchFileException e) {
return false;
}
@@ -545,25 +577,35 @@ public class Util {
* @see InvalidPathException
*/
public static boolean isDescendant(File forParent, File potentialChild) throws IOException {
- try {
- Path child = potentialChild.getAbsoluteFile().toPath().normalize();
- Path parent = forParent.getAbsoluteFile().toPath().normalize();
- return child.startsWith(parent);
- } catch (InvalidPathException e) {
- throw new IOException(e);
- }
+ Path child = fileToPath(potentialChild.getAbsoluteFile()).normalize();
+ Path parent = fileToPath(forParent.getAbsoluteFile()).normalize();
+ return child.startsWith(parent);
}
/**
* Creates a new temporary directory.
*/
public static File createTempDir() throws IOException {
- File tmp = File.createTempFile("jenkins", "tmp");
- if(!tmp.delete())
- throw new IOException("Failed to delete "+tmp);
- if(!tmp.mkdirs())
- throw new IOException("Failed to create a new directory "+tmp);
- return tmp;
+ // The previously used approach of creating a temporary file, deleting
+ // it, and making a new directory having the same name in its place is
+ // potentially problematic:
+ // https://stackoverflow.com/questions/617414/how-to-create-a-temporary-directory-folder-in-java
+ // We can use the Java 7 Files.createTempDirectory() API, but note that
+ // by default, the permissions of the created directory are 0700&(~umask)
+ // whereas the old approach created a temporary directory with permissions
+ // 0777&(~umask).
+ // To avoid permissions problems like https://issues.jenkins-ci.org/browse/JENKINS-48407
+ // we can pass POSIX file permissions as an attribute (see, for example,
+ // https://github.com/jenkinsci/jenkins/pull/3161 )
+ final Path tempPath;
+ final String tempDirNamePrefix = "jenkins";
+ if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
+ tempPath = Files.createTempDirectory(tempDirNamePrefix,
+ PosixFilePermissions.asFileAttribute(EnumSet.allOf(PosixFilePermission.class)));
+ } else {
+ tempPath = Files.createTempDirectory(tempDirNamePrefix);
+ }
+ return tempPath.toFile();
}
private static final Pattern errorCodeParser = Pattern.compile(".*CreateProcess.*error=([0-9]+).*");
@@ -598,7 +640,7 @@ public class Util {
try {
ResourceBundle rb = ResourceBundle.getBundle("/hudson/win32errors");
return rb.getString("error"+m.group(1));
- } catch (Exception _) {
+ } catch (Exception ignored) {
// silently recover from resource related failures
}
}
@@ -643,10 +685,7 @@ public class Util {
*/
@Deprecated
public static void copyStream(@Nonnull InputStream in,@Nonnull OutputStream out) throws IOException {
- byte[] buf = new byte[8192];
- int len;
- while((len=in.read(buf))>=0)
- out.write(buf,0,len);
+ IOUtils.copy(in, out);
}
/**
@@ -654,10 +693,7 @@ public class Util {
*/
@Deprecated
public static void copyStream(@Nonnull Reader in, @Nonnull Writer out) throws IOException {
- char[] buf = new char[8192];
- int len;
- while((len=in.read(buf))>0)
- out.write(buf,0,len);
+ IOUtils.copy(in, out);
}
/**
@@ -666,7 +702,7 @@ public class Util {
@Deprecated
public static void copyStreamAndClose(@Nonnull InputStream in, @Nonnull OutputStream out) throws IOException {
try (InputStream _in = in; OutputStream _out = out) { // make sure both are closed, and use Throwable.addSuppressed
- copyStream(in,out);
+ IOUtils.copy(_in, _out);
}
}
@@ -676,7 +712,7 @@ public class Util {
@Deprecated
public static void copyStreamAndClose(@Nonnull Reader in, @Nonnull Writer out) throws IOException {
try (Reader _in = in; Writer _out = out) {
- copyStream(in,out);
+ IOUtils.copy(_in, _out);
}
}
@@ -766,15 +802,15 @@ public class Util {
public static String getDigestOf(@Nonnull InputStream source) throws IOException {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
-
- byte[] buffer = new byte[1024];
- try (DigestInputStream in = new DigestInputStream(source, md5)) {
- while (in.read(buffer) >= 0)
- ; // simply discard the input
- }
+ DigestInputStream in = new DigestInputStream(source, md5);
+ // Note: IOUtils.copy() buffers the input internally, so there is no
+ // need to use a BufferedInputStream.
+ IOUtils.copy(in, NullOutputStream.NULL_OUTPUT_STREAM);
return toHexString(md5.digest());
} catch (NoSuchAlgorithmException e) {
throw new IOException("MD5 not installed",e); // impossible
+ } finally {
+ source.close();
}
/* JENKINS-18178: confuses Maven 2 runner
try {
@@ -788,7 +824,7 @@ public class Util {
@Nonnull
public static String getDigestOf(@Nonnull String text) {
try {
- return getDigestOf(new ByteArrayInputStream(text.getBytes("UTF-8")));
+ return getDigestOf(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
} catch (IOException e) {
throw new Error(e);
}
@@ -803,11 +839,8 @@ public class Util {
*/
@Nonnull
public static String getDigestOf(@Nonnull File file) throws IOException {
- try (InputStream is = Files.newInputStream(file.toPath())) {
- return getDigestOf(new BufferedInputStream(is));
- } catch (InvalidPathException e) {
- throw new IOException(e);
- }
+ // Note: getDigestOf() closes the input stream.
+ return getDigestOf(Files.newInputStream(fileToPath(file)));
}
/**
@@ -820,14 +853,12 @@ public class Util {
// turn secretKey into 256 bit hash
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.reset();
- digest.update(s.getBytes("UTF-8"));
+ digest.update(s.getBytes(StandardCharsets.UTF_8));
// Due to the stupid US export restriction JDK only ships 128bit version.
return new SecretKeySpec(digest.digest(),0,128/8, "AES");
} catch (NoSuchAlgorithmException e) {
throw new Error(e);
- } catch (UnsupportedEncodingException e) {
- throw new Error(e);
}
}
@@ -849,6 +880,8 @@ public class Util {
@Nonnull
public static byte[] fromHexString(@Nonnull String data) {
+ if (data.length() % 2 != 0)
+ throw new IllegalArgumentException("data must have an even number of hexadecimal digits");
byte[] r = new byte[data.length() / 2];
for (int i = 0; i < data.length(); i += 2)
r[i / 2] = (byte) Integer.parseInt(data.substring(i, i + 2), 16);
@@ -979,7 +1012,7 @@ public class Util {
StringBuilder out = new StringBuilder(s.length());
ByteArrayOutputStream buf = new ByteArrayOutputStream();
- OutputStreamWriter w = new OutputStreamWriter(buf,"UTF-8");
+ OutputStreamWriter w = new OutputStreamWriter(buf, StandardCharsets.UTF_8);
for (int i = 0; i < s.length(); i++) {
int c = s.charAt(i);
@@ -1042,7 +1075,7 @@ public class Util {
if (!escaped) {
out = new StringBuilder(i + (m - i) * 3);
out.append(s.substring(0, i));
- enc = Charset.forName("UTF-8").newEncoder();
+ enc = StandardCharsets.UTF_8.newEncoder();
buf = CharBuffer.allocate(1);
escaped = true;
}
@@ -1079,8 +1112,8 @@ public class Util {
/**
* Escapes HTML unsafe characters like <, & to the respective character entities.
*/
- @Nonnull
- public static String escape(@Nonnull String text) {
+ @Nullable
+ public static String escape(@CheckForNull String text) {
if (text==null) return null;
StringBuilder buf = new StringBuilder(text.length()+64);
for( int i=0; itouch utility which merely
+ * updates the file's access and/or modification time.
*/
public static void touch(@Nonnull File file) throws IOException {
- try {
- Files.newOutputStream(file.toPath()).close();
- } catch (InvalidPathException e) {
- throw new IOException(e);
- }
+ Files.newOutputStream(fileToPath(file)).close();
}
/**
@@ -1163,8 +1195,17 @@ public class Util {
*/
@Nonnull
public static String fixNull(@CheckForNull String s) {
- if(s==null) return "";
- else return s;
+ return fixNull(s, "");
+ }
+
+ /**
+ * Convert {@code null} to a default value.
+ * @param defaultValue Default value. It may be immutable or not, depending on the implementation.
+ * @since TODO
+ */
+ @Nonnull
+ public static T fixNull(@CheckForNull T s, @Nonnull T defaultValue) {
+ return s != null ? s : defaultValue;
}
/**
@@ -1187,24 +1228,60 @@ public class Util {
return fixEmpty(s.trim());
}
+ /**
+ *
+ * @param l list to check.
+ * @param
+ * Type of the list.
+ * @return
+ * {@code l} if l is not {@code null}.
+ * An empty immutable list if l is {@code null}.
+ */
@Nonnull
public static List fixNull(@CheckForNull List l) {
- return l!=null ? l : Collections.emptyList();
+ return fixNull(l, Collections.emptyList());
}
+ /**
+ *
+ * @param l set to check.
+ * @param
+ * Type of the set.
+ * @return
+ * {@code l} if l is not {@code null}.
+ * An empty immutable set if l is {@code null}.
+ */
@Nonnull
public static Set fixNull(@CheckForNull Set l) {
- return l!=null ? l : Collections.emptySet();
+ return fixNull(l, Collections.emptySet());
}
+ /**
+ *
+ * @param l collection to check.
+ * @param
+ * Type of the collection.
+ * @return
+ * {@code l} if l is not {@code null}.
+ * An empty immutable set if l is {@code null}.
+ */
@Nonnull
public static Collection fixNull(@CheckForNull Collection l) {
- return l!=null ? l : Collections.