diff --git a/Jenkinsfile b/Jenkinsfile index ebbe349db8..355e20ebc5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -49,7 +49,7 @@ for(j = 0; j < jdks.size(); j++) { if(isUnix()) { sh mvnCmd - sh 'test `git status --short | tee /dev/stderr | wc --bytes` -eq 0' + sh 'git add . && git diff --exit-code HEAD' } else { bat mvnCmd } diff --git a/core/pom.xml b/core/pom.xml index b6d8645735..3f1d559fa2 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -119,7 +119,7 @@ THE SOFTWARE. org.jenkins-ci trilead-ssh2 - build-217-jenkins-11 + build-217-jenkins-14 org.kohsuke.stapler @@ -646,7 +646,6 @@ THE SOFTWARE. org.codehaus.mojo build-helper-maven-plugin - 1.7 add-source diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java index cd553f3aba..a24976b219 100644 --- a/core/src/main/java/hudson/EnvVars.java +++ b/core/src/main/java/hudson/EnvVars.java @@ -88,7 +88,7 @@ public class EnvVars extends TreeMap { /** * Gets the platform for which these env vars targeted. - * @since TODO + * @since 2.144 * @return The platform. */ public @CheckForNull Platform getPlatform() { @@ -97,7 +97,7 @@ public class EnvVars extends TreeMap { /** * Sets the platform for which these env vars target. - * @since TODO + * @since 2.144 * @param platform the platform to set. */ public void setPlatform(@Nonnull Platform platform) { diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 41e58ec21d..d6f8f794f6 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -1824,6 +1824,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas if (toInstall.isForNewerHudson()) { LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version}); } + if (toInstall.isForNewerJava()) { + LOGGER.log(WARNING, "{0}@{1} was built for a newer Java", new Object[] {toInstall.name, toInstall.version}); + } jobs.add(toInstall.deploy(true)); } else if (pw.isOlderThan(requestedPlugin.getValue())) { // upgrade UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey(), requestedPlugin.getValue()); @@ -1841,6 +1844,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas if (toInstall.isForNewerHudson()) { LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version}); } + if (toInstall.isForNewerJava()) { + LOGGER.log(WARNING, "{0}@{1} was built for a newer Java", new Object[] {toInstall.name, toInstall.version}); + } if (!toInstall.isCompatibleWithInstalledVersion()) { LOGGER.log(WARNING, "{0}@{1} is incompatible with the installed @{2}", new Object[] {toInstall.name, toInstall.version, pw.getVersion()}); } diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 16f0fcc63f..b9cdd610db 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -35,6 +35,7 @@ import hudson.model.UpdateSite; import hudson.util.VersionNumber; import jenkins.YesNoMaybe; import jenkins.model.Jenkins; +import jenkins.util.java.JavaUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.LogFactory; import org.kohsuke.accmod.Restricted; @@ -495,6 +496,20 @@ public class PluginWrapper implements Comparable, ModelObject { return null; } + /** + * Returns the minimum Java version of this plugin, as specified in the plugin metadata. + * Generally coming from the java.level extracted as MANIFEST's metadata with + * this addition on the plugins' parent pom. + * + * @see maven-hpi-plugin#PR-75. + * + * @since TODO + */ + @Exported + public @CheckForNull String getMinimumJavaVersion() { + return manifest.getMainAttributes().getValue("Minimum-Java-Version"); + } + /** * Returns the version number of this plugin */ @@ -745,6 +760,14 @@ public class PluginWrapper implements Comparable, ModelObject { versionDependencyError(Messages.PluginWrapper_obsoleteCore(Jenkins.getVersion().toString(), requiredCoreVersion), Jenkins.getVersion().toString(), requiredCoreVersion); } } + + String minimumJavaVersion = getMinimumJavaVersion(); + if (minimumJavaVersion != null) { + VersionNumber actualVersion = JavaUtils.getCurrentJavaRuntimeVersionNumber(); + if (actualVersion.isOlderThan(new VersionNumber(minimumJavaVersion))) { + versionDependencyError(Messages.PluginWrapper_obsoleteJava(actualVersion.toString(), minimumJavaVersion), actualVersion.toString(), minimumJavaVersion); + } + } } // make sure dependencies exist for (Dependency d : dependencies) { diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 6b90412cdc..4f521ec0ac 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -23,14 +23,13 @@ */ package hudson; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - import hudson.model.TaskListener; import jenkins.util.MemoryReductionUtil; import hudson.util.QuotedStringTokenizer; import hudson.util.VariableResolver; import jenkins.util.SystemProperties; +import jenkins.util.io.PathRemover; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.NullOutputStream; @@ -61,14 +60,9 @@ import java.nio.file.FileSystemException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.InvalidPathException; -import java.nio.file.LinkOption; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFileAttributes; -import java.nio.file.attribute.DosFileAttributes; import java.nio.file.attribute.PosixFilePermissions; import java.security.DigestInputStream; import java.security.MessageDigest; @@ -249,16 +243,7 @@ public class Util { * if the operation fails. */ public static void deleteContentsRecursive(@Nonnull File file) throws IOException { - for( int numberOfAttempts=1 ; ; numberOfAttempts++ ) { - try { - tryOnceDeleteContentsRecursive(file); - break; // success - } catch (IOException ex) { - boolean threadWasInterrupted = pauseBetweenDeletes(numberOfAttempts); - if( numberOfAttempts>= DELETION_MAX || threadWasInterrupted) - throw new IOException(deleteFailExceptionMessage(file, numberOfAttempts, threadWasInterrupted), ex); - } - } + newPathRemover().forceRemoveDirectoryContents(fileToPath(file)); } /** @@ -269,88 +254,7 @@ public class Util { * @throws IOException if it exists but could not be successfully deleted */ public static void deleteFile(@Nonnull File f) throws IOException { - for( int numberOfAttempts=1 ; ; numberOfAttempts++ ) { - try { - tryOnceDeleteFile(f); - break; // success - } catch (IOException ex) { - boolean threadWasInterrupted = pauseBetweenDeletes(numberOfAttempts); - if( numberOfAttempts>= DELETION_MAX || threadWasInterrupted) - throw new IOException(deleteFailExceptionMessage(f, numberOfAttempts, threadWasInterrupted), ex); - } - } - } - - /** - * Deletes this file, working around most problems which might make - * this difficult. - * - * @param f - * What to delete. If a directory, it'll need to be empty. - * @throws IOException if it exists but could not be successfully deleted, - * or if it represents an invalid {@link Path}. - */ - private static void tryOnceDeleteFile(File f) throws IOException { - Path path = fileToPath(f); - try { - Files.deleteIfExists(path); - } catch (IOException e) { - // perhaps this file is read-only? - makeWritable(path); - /* - on Unix both the file and the directory that contains it has to be writable - for a file deletion to be successful. (Confirmed on Solaris 9) - - $ ls -la - total 6 - dr-xr-sr-x 2 hudson hudson 512 Apr 18 14:41 . - dr-xr-sr-x 3 hudson hudson 512 Apr 17 19:36 .. - -r--r--r-- 1 hudson hudson 469 Apr 17 19:36 manager.xml - -rw-r--r-- 1 hudson hudson 0 Apr 18 14:41 x - $ rm x - rm: x not removed: Permission denied - */ - Path parent = path.getParent(); - if (parent != null) { - makeWritable(parent); - } - try { - Files.deleteIfExists(path); - } catch (IOException e2) { - // see https://java.net/projects/hudson/lists/users/archive/2008-05/message/357 - // I suspect other processes putting files in this directory - File[] files = f.listFiles(); - if(files!=null && files.length>0) - throw new IOException("Unable to delete " + f.getPath()+" - files in dir: "+Arrays.asList(files), e2); - throw e2; - } - } - } - - /** - * Makes the file at the given path writable by any means possible. - */ - private static void makeWritable(@Nonnull Path path) throws IOException { - if (!Functions.isWindows()) { - try { - PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); - Set newPermissions = attrs.permissions(); - newPermissions.add(PosixFilePermission.OWNER_WRITE); - Files.setPosixFilePermissions(path, newPermissions); - return; - } catch (NoSuchFileException e) { - return; - } catch (UnsupportedOperationException e) { - // PosixFileAttributes not supported, fall back to old IO. - } - } - - /** - * We intentionally do not check the return code of setWritable, because if it - * is false we prefer to rethrow the exception thrown by Files.deleteIfExists, - * which will have a more useful message than something we make up here. - */ - path.toFile().setWritable(true); + newPathRemover().forceRemoveFile(fileToPath(f)); } /** @@ -362,137 +266,7 @@ public class Util { * if the operation fails. */ public static void deleteRecursive(@Nonnull File dir) throws IOException { - for( int numberOfAttempts=1 ; ; numberOfAttempts++ ) { - try { - tryOnceDeleteRecursive(dir); - break; // success - } catch (IOException ex) { - boolean threadWasInterrupted = pauseBetweenDeletes(numberOfAttempts); - if( numberOfAttempts>= DELETION_MAX || threadWasInterrupted) - throw new IOException(deleteFailExceptionMessage(dir, numberOfAttempts, threadWasInterrupted), ex); - } - } - } - - /** - * Deletes a file or folder, throwing the first exception encountered, but - * having a go at deleting everything. i.e. it does not stop on the - * first exception, but tries (to delete) everything once. - * - * @param dir - * What to delete. If a directory, the contents will be deleted - * too. - * @throws The first exception encountered. - */ - private static void tryOnceDeleteRecursive(File dir) throws IOException { - if(!isSymlink(dir)) - tryOnceDeleteContentsRecursive(dir); - tryOnceDeleteFile(dir); - } - - /** - * Deletes a folder's contents, throwing the first exception encountered, - * but having a go at deleting everything. i.e. it does not stop - * on the first exception, but tries (to delete) everything once. - * - * @param directory - * The directory whose contents will be deleted. - * @throws The first exception encountered. - */ - private static void tryOnceDeleteContentsRecursive(File directory) throws IOException { - File[] directoryContents = directory.listFiles(); - if(directoryContents==null) - return; // the directory didn't exist in the first place - IOException firstCaught = null; - for (File child : directoryContents) { - try { - tryOnceDeleteRecursive(child); - } catch (IOException justCaught) { - if( firstCaught==null) { - firstCaught = justCaught; - } - } - } - if( firstCaught!=null ) - throw firstCaught; - } - - /** - * Pauses between delete attempts, and says if it's ok to try again. - * This does not wait if the wait time is zero or if we have tried - * too many times already. - *

- * See {@link #WAIT_BETWEEN_DELETION_RETRIES} for details of - * the pause duration.
- * See {@link #GC_AFTER_FAILED_DELETE} for when {@link System#gc()} is called. - * - * @return false if it is ok to continue trying to delete things, true if - * we were interrupted (and should stop now). - */ - @SuppressFBWarnings(value = "DM_GC", justification = "Garbage collection happens only when " - + "GC_AFTER_FAILED_DELETE is true. It's an experimental feature in Jenkins.") - private static boolean pauseBetweenDeletes(int numberOfAttemptsSoFar) { - long delayInMs; - if( numberOfAttemptsSoFar>=DELETION_MAX ) return false; - /* If the Jenkins process had the file open earlier, and it has not - * closed it then Windows won't let us delete it until the Java object - * with the open stream is Garbage Collected, which can result in builds - * failing due to "file in use" on Windows despite working perfectly - * well on other OSs. */ - if (GC_AFTER_FAILED_DELETE) { - System.gc(); - } - if (WAIT_BETWEEN_DELETION_RETRIES>=0) { - delayInMs = WAIT_BETWEEN_DELETION_RETRIES; - } else { - delayInMs = -numberOfAttemptsSoFar*WAIT_BETWEEN_DELETION_RETRIES; - } - if (delayInMs<=0) - return Thread.interrupted(); - try { - Thread.sleep(delayInMs); - return false; - } catch (InterruptedException e) { - return true; - } - } - - /** - * Creates a "couldn't delete file" message that explains how hard we tried. - * See {@link #DELETION_MAX}, {@link #WAIT_BETWEEN_DELETION_RETRIES} - * and {@link #GC_AFTER_FAILED_DELETE} for more details. - */ - private static String deleteFailExceptionMessage(File whatWeWereTryingToRemove, int retryCount, boolean wasInterrupted) { - StringBuilder sb = new StringBuilder(); - sb.append("Unable to delete '"); - sb.append(whatWeWereTryingToRemove); - sb.append("'. Tried "); - sb.append(retryCount); - sb.append(" time"); - if( retryCount!=1 ) sb.append('s'); - if( DELETION_MAX>1 ) { - sb.append(" (of a maximum of "); - sb.append(DELETION_MAX); - sb.append(')'); - if( GC_AFTER_FAILED_DELETE ) - sb.append(" garbage-collecting"); - if( WAIT_BETWEEN_DELETION_RETRIES!=0 && GC_AFTER_FAILED_DELETE ) - sb.append(" and"); - if( WAIT_BETWEEN_DELETION_RETRIES!=0 ) { - sb.append(" waiting "); - sb.append(getTimeSpanString(Math.abs(WAIT_BETWEEN_DELETION_RETRIES))); - if( WAIT_BETWEEN_DELETION_RETRIES<0 ) { - sb.append("-"); - sb.append(getTimeSpanString(Math.abs(WAIT_BETWEEN_DELETION_RETRIES)*DELETION_MAX)); - } - } - if( WAIT_BETWEEN_DELETION_RETRIES!=0 || GC_AFTER_FAILED_DELETE) - sb.append(" between attempts"); - } - if( wasInterrupted ) - sb.append(". The delete operation was interrupted before it completed successfully"); - sb.append('.'); - return sb.toString(); + newPathRemover().forceRemoveRecursive(fileToPath(dir)); } /* @@ -511,36 +285,25 @@ public class Util { * limitations under the License. */ /** - * Checks if the given file represents a symlink. + * Checks if the given file represents a symlink. Unlike {@link Files#isSymbolicLink(Path)}, this method also + * considers NTFS junction points as symbolic + * links. */ public static boolean isSymlink(@Nonnull File file) throws IOException { + return isSymlink(fileToPath(file)); + } + + @Restricted(NoExternalUse.class) + public static boolean isSymlink(@Nonnull Path path) { + if (Files.isSymbolicLink(path)) return true; /* - * Windows Directory Junctions are effectively the same as Linux symlinks to directories. - * Unfortunately, the Java 7 NIO2 API function isSymbolicLink does not treat them as such. - * It thinks of them as normal directories. To use the NIO2 API & treat it like a symlink, - * you have to go through BasicFileAttributes and do the following check: - * isSymbolicLink() || isOther() - * The isOther() call will include Windows reparse points, of which a directory junction is. - * It also includes includes devices, but reading the attributes of a device with NIO fails - * or returns false for isOther(). (i.e. named pipes such as \\.\pipe\JenkinsTestPipe return - * false for isOther(), and drives such as \\.\PhysicalDrive0 throw an exception when - * calling readAttributes. + In Windows, a directory junction is not considered a symbolic link despite being nearly the same feature. + To avoid relying directly on Windows-specific filesystem API calls, we can instead determine if a path is + logically a symbolic link by comparing its absolute path to its real path. */ try { - Path path = fileToPath(file); - BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - if (attrs.isSymbolicLink()) { - return true; - } else if (attrs instanceof DosFileAttributes) { - /* Returns true for non-symbolic link reparse points and devices. We could call - * WindowsFileAttributes#isReparsePoint with reflection instead to exclude devices, - * but as mentioned in the above comment this does not appear to be an issue. - */ - return attrs.isOther(); - } else { - return false; - } - } catch (NoSuchFileException e) { + return path.toAbsolutePath().compareTo(path.toRealPath()) != 0; + } catch (IOException ignored) { return false; } } @@ -1203,7 +966,7 @@ public class Util { /** * Convert {@code null} to a default value. * @param defaultValue Default value. It may be immutable or not, depending on the implementation. - * @since TODO + * @since 2.144 */ @Nonnull public static T fixNull(@CheckForNull T s, @Nonnull T defaultValue) { @@ -1812,6 +1575,10 @@ public class Util { @Restricted(value = NoExternalUse.class) static boolean GC_AFTER_FAILED_DELETE = SystemProperties.getBoolean(Util.class.getName() + ".performGCOnFailedDelete"); + private static PathRemover newPathRemover() { + return PathRemover.newRobustRemover(DELETION_MAX - 1, GC_AFTER_FAILED_DELETE, WAIT_BETWEEN_DELETION_RETRIES); + } + /** * If this flag is true, native implementations of {@link FilePath#chmod} * and {@link hudson.util.IOUtils#mode} are used instead of NIO. diff --git a/core/src/main/java/hudson/cli/DisablePluginCommand.java b/core/src/main/java/hudson/cli/DisablePluginCommand.java index 36d4b6bd8c..354d69a561 100644 --- a/core/src/main/java/hudson/cli/DisablePluginCommand.java +++ b/core/src/main/java/hudson/cli/DisablePluginCommand.java @@ -36,7 +36,7 @@ import java.util.List; /** * Disable one or more installed plugins. - * @since TODO + * @since 2.151 */ @Extension public class DisablePluginCommand extends CLICommand { diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index d3b1627740..739229faa8 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -70,6 +70,7 @@ import jenkins.model.DownloadSettings; import jenkins.security.UpdateSiteWarningsConfiguration; import jenkins.util.JSONSignatureValidator; import jenkins.util.SystemProperties; +import jenkins.util.java.JavaUtils; import net.sf.json.JSONArray; import net.sf.json.JSONException; import net.sf.json.JSONObject; @@ -967,6 +968,13 @@ public class UpdateSite { */ @Exported public final String requiredCore; + /** + * Version of Java this plugin requires to run. + * + * @since TODO + */ + @Exported + public final String minimumJavaVersion; /** * Categories for grouping plugins, taken from labels assigned to wiki page. * Can be null. @@ -993,6 +1001,7 @@ public class UpdateSite { this.title = get(o,"title"); this.excerpt = get(o,"excerpt"); this.compatibleSinceVersion = Util.intern(get(o,"compatibleSinceVersion")); + this.minimumJavaVersion = Util.intern(get(o, "minimumJavaVersion")); this.requiredCore = Util.intern(get(o,"requiredCore")); this.categories = o.has("labels") ? internInPlace((String[])o.getJSONArray("labels").toArray(EMPTY_STRING_ARRAY)) : null; JSONArray ja = o.getJSONArray("dependencies"); @@ -1118,6 +1127,21 @@ public class UpdateSite { } } + /** + * Returns true iff the plugin declares a minimum Java version and it's newer than what the Jenkins master is running on. + * @since TODO + */ + public boolean isForNewerJava() { + try { + final VersionNumber currentRuntimeJavaVersion = JavaUtils.getCurrentJavaRuntimeVersionNumber(); + return minimumJavaVersion != null && new VersionNumber(minimumJavaVersion).isNewerThan( + currentRuntimeJavaVersion); + } catch (NumberFormatException nfe) { + logBadMinJavaVersion(); + return false; // treat this as undeclared minimum Java version + } + } + public VersionNumber getNeededDependenciesRequiredCore() { VersionNumber versionNumber = null; try { @@ -1132,6 +1156,36 @@ public class UpdateSite { return versionNumber; } + /** + * Returns the minimum Java version needed to use the plugin and all its dependencies. + * @since TODO + * @return the minimum Java version needed to use the plugin and all its dependencies, or null if unspecified. + */ + @CheckForNull + public VersionNumber getNeededDependenciesMinimumJavaVersion() { + VersionNumber versionNumber = null; + try { + versionNumber = minimumJavaVersion == null ? null : new VersionNumber(minimumJavaVersion); + } catch (NumberFormatException nfe) { + logBadMinJavaVersion(); + } + for (Plugin p: getNeededDependencies()) { + VersionNumber v = p.getNeededDependenciesMinimumJavaVersion(); + if (v == null) { + continue; + } + if (versionNumber == null || v.isNewerThan(versionNumber)) { + versionNumber = v; + } + } + return versionNumber; + } + + private void logBadMinJavaVersion() { + LOGGER.log(Level.WARNING, "minimumJavaVersion was specified for plugin {0} but unparseable (received {1})", + new String[]{this.name, this.minimumJavaVersion}); + } + public boolean isNeededDependenciesForNewerJenkins() { return isNeededDependenciesForNewerJenkins(new PluginManager.MetadataCache()); } @@ -1148,6 +1202,20 @@ public class UpdateSite { }); } + /** + * Returns true iff any of the plugin dependencies require a newer Java than Jenkins is running on. + * + * @since TODO + */ + public boolean isNeededDependenciesForNewerJava() { + for (Plugin p: getNeededDependencies()) { + if (p.isForNewerJava() || p.isNeededDependenciesForNewerJava()) { + return true; + } + } + return false; + } + /** * If at least some of the plugin's needed dependencies are already installed, and the new version of the * needed dependencies plugin have a "compatibleSinceVersion" diff --git a/core/src/main/java/jenkins/install/SetupWizard.java b/core/src/main/java/jenkins/install/SetupWizard.java index a59777079e..44d709030d 100644 --- a/core/src/main/java/jenkins/install/SetupWizard.java +++ b/core/src/main/java/jenkins/install/SetupWizard.java @@ -515,7 +515,7 @@ public class SetupWizard extends PageDecorator { for (UpdateSite site : jenkins.getUpdateCenter().getSiteList()) { UpdateSite.Plugin sitePlug = site.getPlugin(pluginName); if (sitePlug != null - && !sitePlug.isForNewerHudson() + && !sitePlug.isForNewerHudson() && !sitePlug.isForNewerJava() && !sitePlug.isNeededDependenciesForNewerJenkins()) { foundCompatibleVersion = true; break; diff --git a/core/src/main/java/jenkins/model/GlobalConfiguration.java b/core/src/main/java/jenkins/model/GlobalConfiguration.java index 76320768b0..4a261cbed5 100644 --- a/core/src/main/java/jenkins/model/GlobalConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalConfiguration.java @@ -25,7 +25,7 @@ import javax.annotation.Nonnull; * properties defined in your GlobalConfiguration subclass, here are two possibilities: *

  • @{@link javax.inject.Inject} into your other {@link hudson.Extension}s (so this does not work * for classes not annotated with {@link hudson.Extension})
  • - *
  • access it via a call to {@code GlobalConfiguration.all().get(.class)}
+ *
  • access it via a call to {@code ExtensionList.lookupSingleton(.class)}
  • * *

    * While an implementation might store its actual configuration data in various ways, diff --git a/core/src/main/java/jenkins/model/SimplePageDecorator.java b/core/src/main/java/jenkins/model/SimplePageDecorator.java index 89e6fe3a8e..f655ff271b 100644 --- a/core/src/main/java/jenkins/model/SimplePageDecorator.java +++ b/core/src/main/java/jenkins/model/SimplePageDecorator.java @@ -60,7 +60,7 @@ public class SimplePageDecorator extends Descriptor impleme /** * Returns all login page decorators. - * @since TODO + * @since 2.156 */ public static List all() { return Jenkins.get().getDescriptorList(SimplePageDecorator.class); diff --git a/core/src/main/java/jenkins/util/io/CompositeIOException.java b/core/src/main/java/jenkins/util/io/CompositeIOException.java new file mode 100644 index 0000000000..dfd347cbd6 --- /dev/null +++ b/core/src/main/java/jenkins/util/io/CompositeIOException.java @@ -0,0 +1,74 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.util.io; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; + +@Restricted(NoExternalUse.class) +public class CompositeIOException extends IOException { + private final List exceptions; + + public CompositeIOException(String message, @Nonnull List exceptions) { + super(message); + this.exceptions = exceptions; + } + + public CompositeIOException(String message, IOException... exceptions) { + this(message, Arrays.asList(exceptions)); + } + + public List getExceptions() { + return exceptions; + } + + @Override + public void printStackTrace(PrintStream s) { + super.printStackTrace(s); + for (IOException exception : exceptions) { + exception.printStackTrace(s); + } + } + + @Override + public void printStackTrace(PrintWriter s) { + super.printStackTrace(s); + for (IOException exception : exceptions) { + exception.printStackTrace(s); + } + } + + public UncheckedIOException asUncheckedIOException() { + return new UncheckedIOException(this); + } +} diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java new file mode 100644 index 0000000000..763f8f8589 --- /dev/null +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -0,0 +1,285 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.util.io; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Functions; +import hudson.Util; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Restricted(NoExternalUse.class) +public class PathRemover { + + public static PathRemover newSimpleRemover() { + return new PathRemover(ignored -> false); + } + + public static PathRemover newRemoverWithStrategy(@Nonnull RetryStrategy retryStrategy) { + return new PathRemover(retryStrategy); + } + + public static PathRemover newRobustRemover(int maxRetries, boolean gcAfterFailedRemove, long waitBetweenRetries) { + return new PathRemover(new PausingGCRetryStrategy(maxRetries < 1 ? 1 : maxRetries, gcAfterFailedRemove, waitBetweenRetries)); + } + + private final RetryStrategy retryStrategy; + + private PathRemover(@Nonnull RetryStrategy retryStrategy) { + this.retryStrategy = retryStrategy; + } + + public void forceRemoveFile(@Nonnull Path path) throws IOException { + for (int retryAttempts = 0; ; retryAttempts++) { + Optional maybeError = tryRemoveFile(path); + if (!maybeError.isPresent()) return; + if (retryStrategy.shouldRetry(retryAttempts)) continue; + IOException error = maybeError.get(); + throw new IOException(retryStrategy.failureMessage(path, retryAttempts), error); + } + } + + public void forceRemoveDirectoryContents(@Nonnull Path path) throws IOException { + for (int retryAttempt = 0; ; retryAttempt++) { + List errors = tryRemoveDirectoryContents(path); + if (errors.isEmpty()) return; + if (retryStrategy.shouldRetry(retryAttempt)) continue; + throw new CompositeIOException(retryStrategy.failureMessage(path, retryAttempt), errors); + } + } + + public void forceRemoveRecursive(@Nonnull Path path) throws IOException { + for (int retryAttempt = 0; ; retryAttempt++) { + List errors = tryRemoveRecursive(path); + if (errors.isEmpty()) return; + if (retryStrategy.shouldRetry(retryAttempt)) continue; + throw new CompositeIOException(retryStrategy.failureMessage(path, retryAttempt), errors); + } + } + + @Restricted(NoExternalUse.class) + @FunctionalInterface + public interface RetryStrategy { + boolean shouldRetry(int retriesAttempted); + + default String failureMessage(@Nonnull Path fileToRemove, int retryCount) { + StringBuilder sb = new StringBuilder() + .append("Unable to delete '") + .append(fileToRemove) + .append("'. Tried ") + .append(retryCount) + .append(" time"); + if (retryCount != 1) sb.append('s'); + sb.append('.'); + return sb.toString(); + } + } + + private static class PausingGCRetryStrategy implements RetryStrategy { + private final int maxRetries; + private final boolean gcAfterFailedRemove; + private final long waitBetweenRetries; + private final ThreadLocal interrupted = ThreadLocal.withInitial(() -> false); + + private PausingGCRetryStrategy(int maxRetries, boolean gcAfterFailedRemove, long waitBetweenRetries) { + this.maxRetries = maxRetries; + this.gcAfterFailedRemove = gcAfterFailedRemove; + this.waitBetweenRetries = waitBetweenRetries; + } + + @SuppressFBWarnings(value = "DM_GC", justification = "Garbage collection happens only when " + + "GC_AFTER_FAILED_DELETE is true. It's an experimental feature in Jenkins.") + private void gcIfEnabled() { + /* If the Jenkins process had the file open earlier, and it has not + * closed it then Windows won't let us delete it until the Java object + * with the open stream is Garbage Collected, which can result in builds + * failing due to "file in use" on Windows despite working perfectly + * well on other OSs. */ + if (gcAfterFailedRemove) System.gc(); + } + + @Override + public boolean shouldRetry(int retriesAttempted) { + if (retriesAttempted >= maxRetries) return false; + gcIfEnabled(); + long delayMillis = waitBetweenRetries >= 0 ? waitBetweenRetries : -(retriesAttempted + 1) * waitBetweenRetries; + if (delayMillis <= 0) return !Thread.interrupted(); + try { + Thread.sleep(delayMillis); + return true; + } catch (InterruptedException e) { + interrupted.set(true); + return false; + } + } + + @Override + public String failureMessage(@Nonnull Path fileToRemove, int retryCount) { + StringBuilder sb = new StringBuilder(); + sb.append("Unable to delete '"); + sb.append(fileToRemove); + sb.append("'. Tried "); + sb.append(retryCount + 1); + sb.append(" time"); + if (retryCount != 1) sb.append('s'); + if (maxRetries > 0) { + sb.append(" (of a maximum of "); + sb.append(maxRetries + 1); + sb.append(')'); + if (gcAfterFailedRemove) + sb.append(" garbage-collecting"); + if (waitBetweenRetries != 0 && gcAfterFailedRemove) + sb.append(" and"); + if (waitBetweenRetries != 0) { + sb.append(" waiting "); + sb.append(Util.getTimeSpanString(Math.abs(waitBetweenRetries))); + if (waitBetweenRetries < 0) { + sb.append("-"); + sb.append(Util.getTimeSpanString(Math.abs(waitBetweenRetries) * (maxRetries + 1))); + } + } + if (waitBetweenRetries != 0 || gcAfterFailedRemove) + sb.append(" between attempts"); + } + if (interrupted.get()) + sb.append(". The delete operation was interrupted before it completed successfully"); + sb.append('.'); + interrupted.set(false); + return sb.toString(); + } + } + + private static Optional tryRemoveFile(@Nonnull Path path) { + try { + removeOrMakeRemovableThenRemove(path); + return Optional.empty(); + } catch (IOException e) { + return Optional.of(e); + } + } + + private static List tryRemoveRecursive(@Nonnull Path path) { + List accumulatedErrors = Util.isSymlink(path) ? new ArrayList<>() : + tryRemoveDirectoryContents(path); + tryRemoveFile(path).ifPresent(accumulatedErrors::add); + return accumulatedErrors; + } + + private static List tryRemoveDirectoryContents(@Nonnull Path path) { + List accumulatedErrors = new ArrayList<>(); + if (!Files.isDirectory(path)) return accumulatedErrors; + try (DirectoryStream children = Files.newDirectoryStream(path)) { + for (Path child : children) { + accumulatedErrors.addAll(tryRemoveRecursive(child)); + } + } catch (IOException e) { + accumulatedErrors.add(e); + } + return accumulatedErrors; + } + + private static void removeOrMakeRemovableThenRemove(@Nonnull Path path) throws IOException { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + makeRemovable(path); + try { + Files.deleteIfExists(path); + } catch (IOException e2) { + // see https://java.net/projects/hudson/lists/users/archive/2008-05/message/357 + // I suspect other processes putting files in this directory + if (Files.isDirectory(path)) { + List entries; + try (Stream children = Files.list(path)) { + entries = children.map(Path::toString).collect(Collectors.toList()); + } + throw new CompositeIOException("Unable to remove directory " + path + " with directory contents: " + entries, e, e2); + } + throw new CompositeIOException("Unable to remove file " + path, e, e2); + } + } + } + + private static void makeRemovable(@Nonnull Path path) throws IOException { + if (!Files.isWritable(path)) { + makeWritable(path); + } + /* + on Unix both the file and the directory that contains it has to be writable + for a file deletion to be successful. (Confirmed on Solaris 9) + + $ ls -la + total 6 + dr-xr-sr-x 2 hudson hudson 512 Apr 18 14:41 . + dr-xr-sr-x 3 hudson hudson 512 Apr 17 19:36 .. + -r--r--r-- 1 hudson hudson 469 Apr 17 19:36 manager.xml + -rw-r--r-- 1 hudson hudson 0 Apr 18 14:41 x + $ rm x + rm: x not removed: Permission denied + */ + Path parent = path.getParent(); + if (parent != null && !Files.isWritable(parent)) { + makeWritable(parent); + } + } + + private static void makeWritable(@Nonnull Path path) throws IOException { + if (!Functions.isWindows()) { + try { + PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); + Set newPermissions = attrs.permissions(); + newPermissions.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(path, newPermissions); + } catch (NoSuchFileException ignored) { + return; + } catch (UnsupportedOperationException ignored) { + // PosixFileAttributes not supported, fall back to old IO. + } + } + + /* + * We intentionally do not check the return code of setWritable, because if it + * is false we prefer to rethrow the exception thrown by Files.deleteIfExists, + * which will have a more useful message than something we make up here. + */ + path.toFile().setWritable(true); + } + +} diff --git a/core/src/main/java/jenkins/util/java/JavaUtils.java b/core/src/main/java/jenkins/util/java/JavaUtils.java index 909c3e8457..38a28e72c0 100644 --- a/core/src/main/java/jenkins/util/java/JavaUtils.java +++ b/core/src/main/java/jenkins/util/java/JavaUtils.java @@ -23,6 +23,7 @@ */ package jenkins.util.java; +import hudson.util.VersionNumber; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -42,7 +43,7 @@ public class JavaUtils { * @return {@code true} if it is Java 8 or older version */ public static boolean isRunningWithJava8OrBelow() { - String javaVersion = System.getProperty("java.specification.version"); + String javaVersion = getCurrentRuntimeJavaVersion(); return javaVersion.startsWith("1."); } @@ -51,8 +52,23 @@ public class JavaUtils { * @return {@code true} if it is Java 9 or above */ public static boolean isRunningWithPostJava8() { - String javaVersion = System.getProperty("java.specification.version"); + String javaVersion = getCurrentRuntimeJavaVersion(); return !javaVersion.startsWith("1."); } + /** + * Returns the JVM's current version as a {@link VersionNumber} instance. + */ + public static VersionNumber getCurrentJavaRuntimeVersionNumber() { + return new VersionNumber(getCurrentRuntimeJavaVersion()); + } + + /** + * Returns the JVM's current version as a {@link String}. + * @see System#getProperty(String) + */ + public static String getCurrentRuntimeJavaVersion() { + // TODO: leverage Runtime.version() once on Java 9+ + return System.getProperty("java.specification.version"); + } } diff --git a/core/src/main/java/jenkins/util/xstream/SafeURLConverter.java b/core/src/main/java/jenkins/util/xstream/SafeURLConverter.java index 0de0a10074..972fb3251b 100644 --- a/core/src/main/java/jenkins/util/xstream/SafeURLConverter.java +++ b/core/src/main/java/jenkins/util/xstream/SafeURLConverter.java @@ -38,7 +38,7 @@ import java.net.URLStreamHandler; * for {@link URLStreamHandler#equals(URL, URL)} or {@link URLStreamHandler#hashCode(URL)}. * Required to protect against SECURITY-637 * - * @since TODO + * @since 2.121.3 */ @Restricted(NoExternalUse.class) public class SafeURLConverter extends URLConverter { diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index 82525fa854..235b5e3e09 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -79,13 +79,14 @@ ProxyConfiguration.Success=Success Functions.NoExceptionDetails=No Exception details -PluginWrapper.missing={0} v{1} is missing. To fix, install v{1} or later. -PluginWrapper.failed_to_load_plugin={0} v{1} failed to load. -PluginWrapper.failed_to_load_dependency={0} v{1} failed to load. Fix this plugin first. -PluginWrapper.disabledAndObsolete={0} v{1} is disabled and older than required. To fix, install v{2} or later and enable it. +PluginWrapper.missing={0} version {1} is missing. To fix, install version {1} or later. +PluginWrapper.failed_to_load_plugin={0} version {1} failed to load. +PluginWrapper.failed_to_load_dependency={0} version {1} failed to load. Fix this plugin first. +PluginWrapper.disabledAndObsolete={0} version {1} is disabled and older than required. To fix, install version {2} or later and enable it. PluginWrapper.disabled={0} is disabled. To fix, enable it. -PluginWrapper.obsolete={0} v{1} is older than required. To fix, install v{2} or later. -PluginWrapper.obsoleteCore=You must update Jenkins from v{0} to v{1} or later to run this plugin. +PluginWrapper.obsolete={0} version {1} is older than required. To fix, install version {2} or later. +PluginWrapper.obsoleteCore=You must update Jenkins from version {0} to version {1} or later to run this plugin. +PluginWrapper.obsoleteJava=You must update Java from version {0} to version {1} or later to run this plugin. PluginWrapper.PluginWrapperAdministrativeMonitor.DisplayName=Plugins Failed To Load PluginWrapper.Already.Disabled=The plugin ''{0}'' was already disabled PluginWrapper.Plugin.Has.Dependant=The plugin ''{0}'' has, at least, one dependant plugin ({1}) and the disable strategy is {2}, so it cannot be disabled diff --git a/core/src/main/resources/hudson/PluginManager/_table.css b/core/src/main/resources/hudson/PluginManager/_table.css index 86125969bb..9107c2be8b 100644 --- a/core/src/main/resources/hudson/PluginManager/_table.css +++ b/core/src/main/resources/hudson/PluginManager/_table.css @@ -8,3 +8,6 @@ margin: 1em; text-align: right; } +.compatWarning, .securityWarning { + font-weight: bold; +} diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly index 5458466f3b..8052c1754c 100644 --- a/core/src/main/resources/hudson/PluginManager/table.jelly +++ b/core/src/main/resources/hudson/PluginManager/table.jelly @@ -107,12 +107,18 @@ THE SOFTWARE.

    ${%coreWarning(p.requiredCore)}
    + +
    ${%javaWarning(p.minimumJavaVersion)}
    +
    ${%depCompatWarning}
    ${%depCoreWarning(p.getNeededDependenciesRequiredCore().toString())}
    + +
    ${%depJavaWarning(p.getNeededDependenciesMinimumJavaVersion().toString())}
    +
    ${%securityWarning}
      diff --git a/core/src/main/resources/hudson/PluginManager/table.properties b/core/src/main/resources/hudson/PluginManager/table.properties index 63c161534c..0849059887 100644 --- a/core/src/main/resources/hudson/PluginManager/table.properties +++ b/core/src/main/resources/hudson/PluginManager/table.properties @@ -26,6 +26,9 @@ compatWarning=\ coreWarning=\ Warning: This plugin is built for Jenkins {0} or newer. \ Jenkins will refuse to load this plugin if installed. +javaWarning=\ + Warning: This plugin requires Java {0} or newer. \ + Jenkins will refuse to load this plugin if installed. depCompatWarning=\ Warning: This plugin requires dependent plugins be upgraded and at least one of these dependent plugins claims to use a different settings format than the installed version. \ Jobs using that plugin may need to be reconfigured, and/or you may not be able to cleanly revert to the prior version without manually restoring old settings. \ @@ -34,6 +37,10 @@ depCoreWarning=\ Warning: This plugin requires dependent plugins that require Jenkins {0} or newer. \ Jenkins will refuse to load the dependent plugins requiring a newer version of Jenkins, \ and in turn loading this plugin will fail. +depJavaWarning=\ + Warning: this plugin requires other plugins which in turn require Java {0} or newer. \ + Jenkins will refuse to load the dependent plugins requiring a newer version of Jenkins, \ + and in turn loading this plugin will fail. securityWarning=\ Warning: This plugin version may not be safe to use. Please review the following security notices: diff --git a/core/src/main/resources/hudson/model/AllView/noJob_pl.properties b/core/src/main/resources/hudson/model/AllView/noJob_pl.properties index 15c9ac098f..bd7723f8a8 100644 --- a/core/src/main/resources/hudson/model/AllView/noJob_pl.properties +++ b/core/src/main/resources/hudson/model/AllView/noJob_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2017, Sun Microsystems, Inc., Damian Szczepanik +# Copyright (c) 2004-2019, Sun Microsystems, Inc., Damian Szczepanik # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,6 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. Welcome\ to\ Jenkins!=Witamy w Jenkinsie! -newJob=Utw\u00F3rz nowe zadanie, aby rozpocz\u0105\u0107 prac\u0119. +newJob=Utw\u00F3rz nowy projekt, aby rozpocz\u0105\u0107 prac\u0119. login=Zaloguj si\u0119, aby utworzy\u0107 nowe zadanie. signup=Je\u015Bli nie masz jeszcze konta, mo\u017Cesz si\u0119 zarejestrowa\u0107 teraz. diff --git a/core/src/main/resources/hudson/model/Messages_pl.properties b/core/src/main/resources/hudson/model/Messages_pl.properties index 5baa85e5ee..988d71ab09 100644 --- a/core/src/main/resources/hudson/model/Messages_pl.properties +++ b/core/src/main/resources/hudson/model/Messages_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2017, Sun Microsystems, Damian Szczepanik +# Copyright (c) 2004-2019, Sun Microsystems, Damian Szczepanik # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -79,3 +79,4 @@ Node.Mode.EXCLUSIVE=Uruchamiaj te projekty, kt\u00F3re maj\u0105 etykiet\u0119 p Hudson.ViewName=Wszystkie RunParameterDefinition.DisplayName=Parametr uruchomienia# SCM check out aborted Hudson.JobAlreadyExists=Projekt o nazwie \u2018{0}\u2019 ju\u017C istnieje +AbstractProject.CustomWorkspaceEmpty=Nie wprowadzono dedykowanego katalogu projektu. diff --git a/core/src/main/resources/hudson/model/MyView/noJob_pl.properties b/core/src/main/resources/hudson/model/MyView/noJob_pl.properties new file mode 100644 index 0000000000..5a9808d319 --- /dev/null +++ b/core/src/main/resources/hudson/model/MyView/noJob_pl.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2009, Damian Szczepanik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +blurb=Ten widok nie ma jeszcze \u017Cadnych projekt\u00F3w. diff --git a/core/src/main/resources/hudson/model/User/index_pl.properties b/core/src/main/resources/hudson/model/User/index_pl.properties index b349e3a89f..8ceaf119dc 100644 --- a/core/src/main/resources/hudson/model/User/index_pl.properties +++ b/core/src/main/resources/hudson/model/User/index_pl.properties @@ -1,3 +1,23 @@ -# This file is under the MIT License by authors - -Jenkins\ User\ Id=Identyfikator u\u017Cytkownika +# The MIT License +# +# Copyright (c) 2013-2019, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +Groups=Grupy +Jenkins\ User\ ID=Identyfikator u\u017Cytkownika Jenkins diff --git a/core/src/main/resources/hudson/model/View/newJobButtonBar_pl.properties b/core/src/main/resources/hudson/model/View/newJobButtonBar_pl.properties new file mode 100644 index 0000000000..c9c7b7b218 --- /dev/null +++ b/core/src/main/resources/hudson/model/View/newJobButtonBar_pl.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2019, Damian Szczepanik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +OK=OK diff --git a/core/src/main/resources/hudson/tasks/Messages_pl.properties b/core/src/main/resources/hudson/tasks/Messages_pl.properties index 649fb965a1..b0c36ea446 100644 --- a/core/src/main/resources/hudson/tasks/Messages_pl.properties +++ b/core/src/main/resources/hudson/tasks/Messages_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2016-2017, Damian Szczepanik +# Copyright (c) 2016-2019, Damian Szczepanik # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -28,3 +28,4 @@ Ant.GlobalConfigNeeded=By\u0107 mo\u017Ce musisz skonfigurowa\u0107, gdzie jest Ant.NotADirectory={0} nie jest katalogiem Ant.NotAntDirectory={0} nie wygl\u0105da, aby by\u0142 katalogiem Ant'a Ant.ProjectConfigNeeded=By\u0107 mo\u017Ce musisz skonfigurowa\u0107 projekt, aby wybra\u0107 jedn\u0105 z instalacji Ant'a? +Shell.DisplayName=Uruchom pow\u0142ok\u0119 diff --git a/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly b/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly index 75157ee49a..2ed44ba863 100644 --- a/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly +++ b/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly @@ -25,12 +25,14 @@ THE SOFTWARE. - + + + ${h.initPageVariables(context)} @@ -60,4 +62,4 @@ THE SOFTWARE. - \ No newline at end of file + diff --git a/core/src/main/resources/hudson/util/HudsonIsLoading/index_pl.properties b/core/src/main/resources/hudson/util/HudsonIsLoading/index_pl.properties index f97f2f8647..e5fbb2fb5e 100644 --- a/core/src/main/resources/hudson/util/HudsonIsLoading/index_pl.properties +++ b/core/src/main/resources/hudson/util/HudsonIsLoading/index_pl.properties @@ -1,4 +1,25 @@ -# This file is under the MIT License by authors +# The MIT License +# +# Copyright (c) 2013-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. Please\ wait\ while\ Jenkins\ is\ getting\ ready\ to\ work=Poczekaj prosz\u0119, a\u017C Jenkins b\u0119dzie gotowy do pracy Your\ browser\ will\ reload\ automatically\ when\ Jenkins\ is\ ready.=Twoja przegl\u0105darka automatycznie prze\u0142aduje stron\u0119, kiedy Jenkins b\u0119dzie gotowy. +Starting\ Jenkins=Uruchamianie Jenkinsa diff --git a/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly b/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly index aafd2cfc60..65f30b5d94 100644 --- a/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly +++ b/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly @@ -25,12 +25,14 @@ THE SOFTWARE. - + + + ${h.initPageVariables(context)} diff --git a/core/src/main/resources/jenkins/diagnostics/RootUrlNotSetMonitor/message_pl.properties b/core/src/main/resources/jenkins/diagnostics/RootUrlNotSetMonitor/message_pl.properties new file mode 100644 index 0000000000..5e6faf0ee8 --- /dev/null +++ b/core/src/main/resources/jenkins/diagnostics/RootUrlNotSetMonitor/message_pl.properties @@ -0,0 +1,29 @@ +# The MIT License +# +# Copyright (c) 2019, Damian Szczepanik, Sun Microsystems, Inc., and a number of other of contributors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Dismiss=Odrzu\u0107 +urlIsNull=Bazowy adres URL Jenkinsa jest pusty, ale jest niezb\u0119dny dla poprawno\u015Bci dzia\u0142ania wielu funkcjonalno\u015Bci Jenkinsa, takich jak powiadomienia email, \ +statusy zmian oraz zmienne \u015Brodowiskowe, takie jak BUILD_URL. +urlIsInvalid=Bazowy adres Jenkinsa wydaje si\u0119 by\u0107 niepoprawny. Jest niezb\u0119dny dla poprawno\u015Bci dzia\u0142ania wielu funkcjonalno\u015Bci Jenkinsa, takich jak powiadomienia email, \ +statusy zmian oraz zmienne \u015Brodowiskowe, takie jak BUILD_URL +actionToTake=Wprowad\u017A odpowiednie dane na stronie {0}. +actionUrlContent=Konfiguracja Jenkinsa diff --git a/core/src/main/resources/jenkins/install/SetupWizard/setupWizardConfigureInstance_pl.properties b/core/src/main/resources/jenkins/install/SetupWizard/setupWizardConfigureInstance_pl.properties new file mode 100644 index 0000000000..a12767f701 --- /dev/null +++ b/core/src/main/resources/jenkins/install/SetupWizard/setupWizardConfigureInstance_pl.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2018, Damian Szczepanik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Jenkins\ URL=URL Jenkinsa +Instance\ Configuration=Konfiguracja instancji \ No newline at end of file diff --git a/core/src/main/resources/jenkins/install/pluginSetupWizard.properties b/core/src/main/resources/jenkins/install/pluginSetupWizard.properties index 1a682d15d6..6dc6e00099 100644 --- a/core/src/main/resources/jenkins/install/pluginSetupWizard.properties +++ b/core/src/main/resources/jenkins/install/pluginSetupWizard.properties @@ -1,3 +1,25 @@ +# The MIT License +# +# Copyright (c) 2016-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# installWizard_welcomePanel_title=Getting Started installWizard_welcomePanel_banner=Customize Jenkins installWizard_welcomePanel_message=Plugins extend Jenkins with additional features to support many different needs. @@ -13,6 +35,7 @@ For information about installing Jenkins without an internet connection, see the Offline Jenkins Installation Documentation.

      \ You may choose to continue by configuring a proxy or skipping plugin installation. \

      +installWizard_error_title=Error installWizard_error_header=An error occurred installWizard_error_message=An error occurred during installation: installWizard_error_connection=Unable to connect to Jenkins diff --git a/core/src/main/resources/jenkins/install/pluginSetupWizard_pl.properties b/core/src/main/resources/jenkins/install/pluginSetupWizard_pl.properties index e94da4e1c8..193e53ff13 100644 --- a/core/src/main/resources/jenkins/install/pluginSetupWizard_pl.properties +++ b/core/src/main/resources/jenkins/install/pluginSetupWizard_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2017, Damian Szczepanik +# Copyright (c) 2017-2019, Damian Szczepanik # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -46,6 +46,7 @@ installWizard_installCustom_pluginListDesc=Zauwa\u017C, \u017Ce poni\u017Cej nie installWizard_goBack=Wr\u00F3\u0107 installWizard_goInstall=Instaluj installWizard_error_connection=Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z Jenkinsem +installWizard_error_title=B\u0142\u0105d installWizard_error_header=Wyst\u0105pi\u0142 b\u0142\u0105d installWizard_installingConsole_dependencyIndicatorNote=** - zale\u017Cno\u015Bci wymagane installWizard_retry=Pon\u00F3w @@ -60,6 +61,9 @@ Aby uzyska\u0107 wi\u0119cej informacji o instalowaniu Jenkinsa bez dost\u0119pu Offline Jenkins Installation Documentation.

      \ Mo\u017Cesz kontynuowa\u0107 konfiguruj\u0105c proxy lub pomin\u0105\u0107 instalacje wtyczek.\

      +installWizard_configureInstance_title=Konfiguracja instancji +installWizard_saveConfigureInstance=Zapisz i zako\u0144cz +installWizard_skipConfigureInstance=Nie teraz installWizard_configureProxy_label=Konfiguruj proxy installWizard_skipPluginInstallations=Pomi\u0144 instalacje wtyczek installWizard_configureProxy_save=Zapisz i kontynuuj @@ -76,3 +80,7 @@ installWizard_installComplete_installComplete_restartRequiredNotSupportedMessage installWizard_gettingStarted_title=Zaczynamy installWizard_saveSecurity=Zapisz i kontynuuj installWizard_installIncomplete_dependenciesLabel=Zale\u017Cno\u015Bci +installWizard_configureInstanceSkippedMessage=
      \ +Pomini\u0119to konfiguracje bazowego adresu URL Jenkinsa.

      \ +Aby go skonfigurowa\u0107, przejd\u017A do strony "Zarz\u0105dzaj Jenkinsem".\ +
      diff --git a/core/src/main/resources/jenkins/model/DefaultSimplePageDecorator/simple-header_pl.properties b/core/src/main/resources/jenkins/model/DefaultSimplePageDecorator/simple-header_pl.properties new file mode 100644 index 0000000000..9d0f82046a --- /dev/null +++ b/core/src/main/resources/jenkins/model/DefaultSimplePageDecorator/simple-header_pl.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2019, Damian Szczepanik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Welcome\ to\ Jenkins!=Witamy w Jenkinsie! diff --git a/core/src/main/resources/jenkins/model/Jenkins/login_pl.properties b/core/src/main/resources/jenkins/model/Jenkins/login_pl.properties index f353e83663..aeb61a1eb7 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/login_pl.properties +++ b/core/src/main/resources/jenkins/model/Jenkins/login_pl.properties @@ -1,8 +1,28 @@ -# This file is under the MIT License by authors +# The MIT License +# +# Copyright (c) 2013-2019, Sun Microsystems, Inc., Kohsuke Kawaguchi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. Password=Has\u0142o -Remember\ me\ on\ this\ computer=Zapami\u0119taj mnie na tym komputerze -User=Login -login=Zaloguj -signUp=Za\u0142\u00F3\u017C konto je\u015Bli nie jeste\u015B jeszcze uczestnikiem. - +signUp=Zaloguj si\u0119 poni\u017Cej lub za\u0142\u00F3\u017C konto. +signIn=Zaloguj +Keep\ me\ signed\ in=Zapami\u0119taj mnie +Username=U\u017Cytkownik +Invalid\ username\ or\ password=Niepoprawny u\u017Cytkownik lub has\u0142o diff --git a/core/src/main/resources/jenkins/model/Jenkins/newView_pl.properties b/core/src/main/resources/jenkins/model/Jenkins/newView_pl.properties index db2578a753..02bf92fc5e 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/newView_pl.properties +++ b/core/src/main/resources/jenkins/model/Jenkins/newView_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2010, Sun Microsystems, Inc. +# Copyright (c) 2004-2019, Damian Szczepanik, Sun Microsystems, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -21,3 +21,5 @@ # THE SOFTWARE. View\ name=Nazwa widoku +New\ View=Nowy widok +Copy\ Existing\ View=Kopiuj istniej\u0105cy widok diff --git a/core/src/main/resources/jenkins/model/Messages.properties b/core/src/main/resources/jenkins/model/Messages.properties index f7d11a855f..402781b4da 100644 --- a/core/src/main/resources/jenkins/model/Messages.properties +++ b/core/src/main/resources/jenkins/model/Messages.properties @@ -67,7 +67,7 @@ PatternProjectNamingStrategy.NamePatternInvalidSyntax=regular expression''s synt ParameterizedJobMixIn.build_with_parameters=Build with Parameters ParameterizedJobMixIn.build_now=Build Now BlockedBecauseOfBuildInProgress.shortDescription=Build #{0} is already in progress{1} -BlockedBecauseOfBuildInProgress.ETA=\ (ETA:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (ETA: {0}) BuildDiscarderProperty.displayName=Discard old builds DownloadSettings.Warning.DisplayName=Browser-based metadata download diff --git a/core/src/main/resources/jenkins/model/Messages_da.properties b/core/src/main/resources/jenkins/model/Messages_da.properties index ff3e3356ac..dedadb72f3 100644 --- a/core/src/main/resources/jenkins/model/Messages_da.properties +++ b/core/src/main/resources/jenkins/model/Messages_da.properties @@ -50,7 +50,7 @@ Mailer.Address.Not.Configured=Adresse ikke konfigureret Mailer.Localhost.Error=Venligst inds\u00e6t et gyldigt v\u00e6rtsnavn, istedet for localhost ParameterizedJobMixIn.build_now=Byg nu BlockedBecauseOfBuildInProgress.shortDescription=Byg #{0} er allerede i gang {1} -BlockedBecauseOfBuildInProgress.ETA=(ETA:{0}) +BlockedBecauseOfBuildInProgress.ETA=(ETA: {0}) BuildDiscarderProperty.displayName=Fjern Gamle Byg CLI.disable-job.shortDescription=Sl\u00e5r et job fra CLI.enable-job.shortDescription=Sl\u00e5r et job til diff --git a/core/src/main/resources/jenkins/model/Messages_de.properties b/core/src/main/resources/jenkins/model/Messages_de.properties index 9def12e647..687729c835 100644 --- a/core/src/main/resources/jenkins/model/Messages_de.properties +++ b/core/src/main/resources/jenkins/model/Messages_de.properties @@ -57,7 +57,7 @@ PatternProjectNamingStrategy.NamePatternInvalidSyntax=Der Regul\u00E4re Ausdruck ParameterizedJobMixIn.build_with_parameters=Bauen mit Parametern ParameterizedJobMixIn.build_now=Jetzt bauen BlockedBecauseOfBuildInProgress.shortDescription=Build #{0} ist bereits in Arbeit{1} -BlockedBecauseOfBuildInProgress.ETA=\ (ETA:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (ETA: {0}) BuildDiscarderProperty.displayName=Alte Builds verwerfen IdStrategy.CaseSensitive.DisplayName=Gro\u00DF- und Kleinschreibung beachtend DownloadSettings.Warning.DisplayName=Browser-basierter Download von Metadaten diff --git a/core/src/main/resources/jenkins/model/Messages_es.properties b/core/src/main/resources/jenkins/model/Messages_es.properties index 9df1bffcb5..34aa5c6979 100644 --- a/core/src/main/resources/jenkins/model/Messages_es.properties +++ b/core/src/main/resources/jenkins/model/Messages_es.properties @@ -60,7 +60,7 @@ Mailer.Address.Not.Configured=Direcci Mailer.Localhost.Error=Escriba un nombre de servidor correcto en lugar de "localhost" ParameterizedJobMixIn.build_now=Construir ahora BlockedBecauseOfBuildInProgress.shortDescription=La ejecuci\u00f3n #{0} ya est\u00e1 en progreso {1} -BlockedBecauseOfBuildInProgress.ETA= (ETA:{0}) +BlockedBecauseOfBuildInProgress.ETA= (ETA: {0}) BuildDiscarderProperty.displayName=Desechar ejecuciones antiguas CLI.disable-job.shortDescription=Desactivar una tarea CLI.enable-job.shortDescription=Activar una tarea diff --git a/core/src/main/resources/jenkins/model/Messages_it.properties b/core/src/main/resources/jenkins/model/Messages_it.properties index 4604275fe3..867bcfdd07 100644 --- a/core/src/main/resources/jenkins/model/Messages_it.properties +++ b/core/src/main/resources/jenkins/model/Messages_it.properties @@ -1,6 +1,6 @@ ParameterizedJobMixIn.build_now=Esegui compilazione BlockedBecauseOfBuildInProgress.shortDescription=La compilazione {0} è già in corso{1} -BlockedBecauseOfBuildInProgress.ETA=\ (Tempo stimato:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (Tempo stimato: {0}) BuildDiscarderProperty.displayName=Elimina compilazioni precedenti CLI.disable-job.shortDescription=Disabilita un processo CLI.enable-job.shortDescription=Abilita un processo diff --git a/core/src/main/resources/jenkins/model/Messages_ja.properties b/core/src/main/resources/jenkins/model/Messages_ja.properties index 46da45fc18..7e2c0395aa 100644 --- a/core/src/main/resources/jenkins/model/Messages_ja.properties +++ b/core/src/main/resources/jenkins/model/Messages_ja.properties @@ -61,7 +61,7 @@ Mailer.Localhost.Error=localhost\u306e\u4ee3\u308f\u308a\u306b\u59a5\u5f53\u306a ParameterizedJobMixIn.build_with_parameters=\u30d1\u30e9\u30e1\u30fc\u30bf\u4ed8\u304d\u30d3\u30eb\u30c9 ParameterizedJobMixIn.build_now=\u30d3\u30eb\u30c9\u5b9f\u884c BlockedBecauseOfBuildInProgress.shortDescription=\u30d3\u30eb\u30c9 #{0} \u306f\u65e2\u306b\u5b9f\u884c\u4e2d\u3067\u3059\u3002{1} -BlockedBecauseOfBuildInProgress.ETA=\ (\u4e88\u5b9a\u6642\u9593:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (\u4e88\u5b9a\u6642\u9593: {0}) BuildDiscarderProperty.displayName=\u53e4\u3044\u30d3\u30eb\u30c9\u306e\u7834\u68c4 CLI.disable-job.shortDescription=\u30b8\u30e7\u30d6\u3092\u7121\u52b9\u5316\u3057\u307e\u3059\u3002 CLI.enable-job.shortDescription=\u30b8\u30e7\u30d6\u3092\u6709\u52b9\u5316\u3057\u307e\u3059\u3002 diff --git a/core/src/main/resources/jenkins/model/Messages_lt.properties b/core/src/main/resources/jenkins/model/Messages_lt.properties index 7385705308..2e3598af6c 100644 --- a/core/src/main/resources/jenkins/model/Messages_lt.properties +++ b/core/src/main/resources/jenkins/model/Messages_lt.properties @@ -1,4 +1,4 @@ -BlockedBecauseOfBuildInProgress.ETA= \ (ETA:{0}) +BlockedBecauseOfBuildInProgress.ETA= \ (ETA: {0}) BlockedBecauseOfBuildInProgress.shortDescription=Darbas #{0} jau vykdomas{1} BuildDiscarderProperty.displayName=Pa\u0161alinti senus darbus diff --git a/core/src/main/resources/jenkins/model/Messages_nl.properties b/core/src/main/resources/jenkins/model/Messages_nl.properties index 3462e69fc7..87d523ed1c 100644 --- a/core/src/main/resources/jenkins/model/Messages_nl.properties +++ b/core/src/main/resources/jenkins/model/Messages_nl.properties @@ -32,5 +32,5 @@ Hudson.UnsafeChar=''{0}'' is een onveilig character Hudson.ViewName=Alle ParameterizedJobMixIn.build_now=Start nu een bouwpoging BlockedBecauseOfBuildInProgress.shortDescription=Bouwpoging #{0} is reeds actief {1} -BlockedBecauseOfBuildInProgress.ETA= (einde voorzien op:{0}) +BlockedBecauseOfBuildInProgress.ETA= (einde voorzien op: {0}) BuildDiscarderProperty.displayName=Oude builds automatisch verwijderen diff --git a/core/src/main/resources/jenkins/model/Messages_pl.properties b/core/src/main/resources/jenkins/model/Messages_pl.properties index 5105a854b6..6e4595c8b6 100644 --- a/core/src/main/resources/jenkins/model/Messages_pl.properties +++ b/core/src/main/resources/jenkins/model/Messages_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2016, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors +# Copyright (c) 2004-2019, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,3 +26,4 @@ ParameterizedJobMixIn.build_now=Uruchom Mailer.Address.Not.Configured=adres jeszcze nie jest skonfigurowany # Please set a valid host name, instead of localhost Mailer.Localhost.Error=Ustaw prawid\u0142ow\u0105 nazw\u0119 hosta, inn\u0105 ni\u017C localhost +NewViewLink.NewView=Nowy widok diff --git a/core/src/main/resources/jenkins/model/Messages_pt_BR.properties b/core/src/main/resources/jenkins/model/Messages_pt_BR.properties index 9b2eea4915..2a36f86d84 100644 --- a/core/src/main/resources/jenkins/model/Messages_pt_BR.properties +++ b/core/src/main/resources/jenkins/model/Messages_pt_BR.properties @@ -68,7 +68,7 @@ DefaultProjectNamingStrategy.DisplayName=Padr\u00e3o ParameterizedJobMixIn.build_with_parameters=Construir com par\u00e2metros ParameterizedJobMixIn.build_now=Construir agora BlockedBecauseOfBuildInProgress.shortDescription=A builds #{0} j\u00e1 est\u00e1 em progresso{1} -BlockedBecauseOfBuildInProgress.ETA=\ (ETA:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (ETA: {0}) BuildDiscarderProperty.displayName=Descartar builds antigos CLI.disable-job.shortDescription=Desabilitar uma job CLI.enable-job.shortDescription=Habilita um job diff --git a/core/src/main/resources/jenkins/model/Messages_ru.properties b/core/src/main/resources/jenkins/model/Messages_ru.properties index 998fc23651..5fe1b31482 100644 --- a/core/src/main/resources/jenkins/model/Messages_ru.properties +++ b/core/src/main/resources/jenkins/model/Messages_ru.properties @@ -32,7 +32,7 @@ Hudson.UnsafeChar=''{0}'' \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441 Hudson.ViewName=\u0412\u0441\u0435 ParameterizedJobMixIn.build_now=\u0421\u043E\u0431\u0440\u0430\u0442\u044C \u0441\u0435\u0439\u0447\u0430\u0441 BlockedBecauseOfBuildInProgress.shortDescription=\u0421\u0431\u043E\u0440\u043A\u0430 #{0} \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0435 {1} -BlockedBecauseOfBuildInProgress.ETA=\ (\u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (\u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C: {0}) BuildDiscarderProperty.displayName=\u0423\u0434\u0430\u043B\u044F\u0442\u044C \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u0441\u0431\u043E\u0440\u043A\u0438 Hudson.NotUsesUTF8ToDecodeURL=\u0412\u0430\u0448 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 \u043D\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442 UTF-8 \u0434\u043B\u044F \u0434\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F URL-\u0430\u0434\u0440\u0435\u0441\u043E\u0432. \u0415\u0441\u043B\u0438 \u0432\u044B \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0435 \u043D\u0435-ASCII \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0432 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043C\u0435\u043D\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438 \u0442.\u0434., \ \u044D\u0442\u043E \u043C\u043E\u0436\u0435\u0442 \u0432\u044B\u0437\u0432\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B.\ diff --git a/core/src/main/resources/jenkins/model/Messages_sr.properties b/core/src/main/resources/jenkins/model/Messages_sr.properties index be52dc9aca..894f3d284a 100644 --- a/core/src/main/resources/jenkins/model/Messages_sr.properties +++ b/core/src/main/resources/jenkins/model/Messages_sr.properties @@ -36,7 +36,7 @@ PatternProjectNamingStrategy.NamePatternInvalidSyntax=\u041d\u0435\u043f\u0440\u ParameterizedJobMixIn.build_with_parameters=\u0418\u0437\u0433\u0440\u0430\u0434\u0438 \u0441\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u043c\u0430 ParameterizedJobMixIn.build_now=\u0418\u0437\u0433\u0440\u0430\u0434\u0438 \u0441\u0430\u0434\u0430 BlockedBecauseOfBuildInProgress.shortDescription=\u0418\u0437\u0433\u0440\u0430\u0434\u045a\u0430 #{0} \u0458\u0435 \u0432\u0435\u045b \u0443 \u0442\u043e\u043a\u0443 {1} -BlockedBecauseOfBuildInProgress.ETA=\ (\u043e\u0441\u0442\u0430\u043b\u043e:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (\u043e\u0441\u0442\u0430\u043b\u043e: {0}) BuildDiscarderProperty.displayName=\u0418\u0437\u0431\u0440\u0438\u0448\u0438 \u0441\u0442\u0430\u0440\u0430 \u0438\u0437\u0433\u0440\u0430\u0434\u045a\u0430 Din= vil= diff --git a/core/src/main/resources/jenkins/model/Messages_tr.properties b/core/src/main/resources/jenkins/model/Messages_tr.properties index 78702dbdda..340e62e534 100644 --- a/core/src/main/resources/jenkins/model/Messages_tr.properties +++ b/core/src/main/resources/jenkins/model/Messages_tr.properties @@ -32,5 +32,5 @@ Hudson.UnsafeChar=''{0}'' g\u00fcvenli olmayan bir karakter Hudson.ViewName=Hepsi ParameterizedJobMixIn.build_now=\u015eimdi Yap\u0131land\u0131r BlockedBecauseOfBuildInProgress.shortDescription=Yap\u0131land\u0131rma #{0} zaten i\u015flemde {1} -BlockedBecauseOfBuildInProgress.ETA=\ (ETA:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (ETA: {0}) BuildDiscarderProperty.displayName=Eski Yap\u0131land\u0131rmalardan Kurtul diff --git a/core/src/main/resources/jenkins/model/Messages_zh_TW.properties b/core/src/main/resources/jenkins/model/Messages_zh_TW.properties index 09a7308cf1..a2a752a23d 100644 --- a/core/src/main/resources/jenkins/model/Messages_zh_TW.properties +++ b/core/src/main/resources/jenkins/model/Messages_zh_TW.properties @@ -61,7 +61,7 @@ PatternProjectNamingStrategy.NamePatternRequired=\u4e00\u5b9a\u8981\u8f38\u5165\ PatternProjectNamingStrategy.NamePatternInvalidSyntax=\u6b63\u898f\u8868\u793a\u5f0f\u8a9e\u6cd5\u932f\u8aa4\u3002 ParameterizedJobMixIn.build_now=\u99ac\u4e0a\u5efa\u7f6e BlockedBecauseOfBuildInProgress.shortDescription=\u5efa\u7f6e #{0} \u57f7\u884c\u4e2d{1} -BlockedBecauseOfBuildInProgress.ETA=\ (\u9810\u4f30\u6642\u9593:{0}) +BlockedBecauseOfBuildInProgress.ETA=\ (\u9810\u4f30\u6642\u9593: {0}) BuildDiscarderProperty.displayName=\u5FFD\u7565\u820ABuilds CLI.disable-job.shortDescription=\u505c\u7528\u4f5c\u696d\u3002 CLI.enable-job.shortDescription=\u555f\u7528\u4f5c\u696d\u3002 diff --git a/core/src/main/resources/lib/hudson/iconSize_pl.properties b/core/src/main/resources/lib/hudson/iconSize_pl.properties index 5917729e15..81752a2f10 100644 --- a/core/src/main/resources/lib/hudson/iconSize_pl.properties +++ b/core/src/main/resources/lib/hudson/iconSize_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2010, Sun Microsystems, Inc. +# Copyright (c) 2004-2019, Sun Microsystems, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -21,3 +21,6 @@ # THE SOFTWARE. Icon=Ikona +M=Ma\u0142a +S=\u015Arednia +L=Du\u017Ca diff --git a/core/src/main/resources/lib/hudson/newFromList/form_pl.properties b/core/src/main/resources/lib/hudson/newFromList/form_pl.properties index df4463a6e0..6200b5f601 100644 --- a/core/src/main/resources/lib/hudson/newFromList/form_pl.properties +++ b/core/src/main/resources/lib/hudson/newFromList/form_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2010, Sun Microsystems, Inc. +# Copyright (c) 2004-2019, Damian Szczepanik, Sun Microsystems, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -21,3 +21,4 @@ # THE SOFTWARE. Copy\ from=Kopiuj z +OK=OK diff --git a/core/src/main/resources/lib/hudson/project/config-customWorkspace_pl.properties b/core/src/main/resources/lib/hudson/project/config-customWorkspace_pl.properties index 79803b201f..1a2fbd22f0 100644 --- a/core/src/main/resources/lib/hudson/project/config-customWorkspace_pl.properties +++ b/core/src/main/resources/lib/hudson/project/config-customWorkspace_pl.properties @@ -1,3 +1,23 @@ -# This file is under the MIT License by authors - +# The MIT License +# +# Copyright (c) 2013-2019, Sun Microsystems, Inc., Kohsuke Kawaguchi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. Directory=Katalog +Use\ custom\ workspace=Ustaw dedykowany katalog projektu diff --git a/core/src/test/java/hudson/PluginWrapperTest.java b/core/src/test/java/hudson/PluginWrapperTest.java index e15c803dca..bc93164ff9 100644 --- a/core/src/test/java/hudson/PluginWrapperTest.java +++ b/core/src/test/java/hudson/PluginWrapperTest.java @@ -53,7 +53,7 @@ public class PluginWrapperTest { pw.resolvePluginDependencies(); fail(); } catch (IOException ex) { - assertContains(ex, "fake v42 failed to load", "update Jenkins from v2.0 to v3.0"); + assertContains(ex, "fake version 42 failed to load", "update Jenkins from version 2.0 to version 3.0"); } } @@ -64,7 +64,7 @@ public class PluginWrapperTest { pw.resolvePluginDependencies(); fail(); } catch (IOException ex) { - assertContains(ex, "dependee v42 failed to load", "dependency v42 is missing. To fix, install v42 or later"); + assertContains(ex, "dependee version 42 failed to load", "dependency version 42 is missing. To fix, install version 42 or later"); } } @@ -76,7 +76,7 @@ public class PluginWrapperTest { pw.resolvePluginDependencies(); fail(); } catch (IOException ex) { - assertContains(ex, "dependee v42 failed to load", "dependency v3 is older than required. To fix, install v5 or later"); + assertContains(ex, "dependee version 42 failed to load", "dependency version 3 is older than required. To fix, install version 5 or later"); } } @@ -88,7 +88,7 @@ public class PluginWrapperTest { pw.resolvePluginDependencies(); fail(); } catch (IOException ex) { - assertContains(ex, "dependee v42 failed to load", "dependency v5 failed to load. Fix this plugin first"); + assertContains(ex, "dependee version 42 failed to load", "dependency version 5 failed to load. Fix this plugin first"); } } diff --git a/core/src/test/java/hudson/RemoveWindowsDirectoryJunctionTest.java b/core/src/test/java/hudson/RemoveWindowsDirectoryJunctionTest.java index cd631503c3..a48c9d833c 100644 --- a/core/src/test/java/hudson/RemoveWindowsDirectoryJunctionTest.java +++ b/core/src/test/java/hudson/RemoveWindowsDirectoryJunctionTest.java @@ -13,8 +13,11 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.For; import org.jvnet.hudson.test.Issue; +@For(Util.class) +// https://superuser.com/q/343074 public class RemoveWindowsDirectoryJunctionTest { @Rule public TemporaryFolder tmp = new TemporaryFolder(); diff --git a/core/src/test/java/hudson/UtilTest.java b/core/src/test/java/hudson/UtilTest.java index 1002ac70a3..36586408e5 100644 --- a/core/src/test/java/hudson/UtilTest.java +++ b/core/src/test/java/hudson/UtilTest.java @@ -27,38 +27,25 @@ package hudson; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.Locale; import java.util.Properties; -import java.util.concurrent.Callable; -import java.util.concurrent.atomic.AtomicReference; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.FileSystemException; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; -import static org.hamcrest.CoreMatchers.allOf; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.*; -import org.apache.commons.io.FileUtils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.Assume; -import org.junit.Ignore; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.jvnet.hudson.test.Issue; import hudson.util.StreamTaskListener; @@ -66,8 +53,6 @@ import hudson.util.StreamTaskListener; import org.junit.Rule; import org.junit.rules.TemporaryFolder; -import com.google.common.collect.Lists; - /** * @author Kohsuke Kawaguchi */ @@ -75,6 +60,8 @@ public class UtilTest { @Rule public TemporaryFolder tmp = new TemporaryFolder(); + @Rule public ExpectedException expectedException = ExpectedException.none(); + @Test public void testReplaceMacro() { Map m = new HashMap(); @@ -192,7 +179,7 @@ public class UtilTest { @Test public void testSymlink() throws Exception { - Assume.assumeTrue(!Functions.isWindows()); + Assume.assumeFalse(Functions.isWindows()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); StreamTaskListener l = new StreamTaskListener(baos); @@ -240,7 +227,7 @@ public class UtilTest { @Test public void testIsSymlink() throws IOException, InterruptedException { - Assume.assumeTrue(!Functions.isWindows()); + Assume.assumeFalse(Functions.isWindows()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); StreamTaskListener l = new StreamTaskListener(baos); @@ -281,284 +268,6 @@ public class UtilTest { assertTrue(Util.isSymlink(new File(d, "junction"))); } - @Test - public void testDeleteFile() throws Exception { - File f = tmp.newFile(); - // Test: File is deleted - mkfiles(f); - Util.deleteFile(f); - assertFalse("f exists after calling Util.deleteFile", f.exists()); - } - - @Test - public void testDeleteFile_onWindows() throws Exception { - Assume.assumeTrue(Functions.isWindows()); - final int defaultDeletionMax = Util.DELETION_MAX; - try { - File f = tmp.newFile(); - // Test: If we cannot delete a file, we throw explaining why - mkfiles(f); - lockFileForDeletion(f); - Util.DELETION_MAX = 1; - try { - Util.deleteFile(f); - fail("should not have been deletable"); - } catch (IOException x) { - assertThat(calcExceptionHierarchy(x), hasItem(FileSystemException.class)); - assertThat(x.getMessage(), containsString(f.getPath())); - } - } finally { - Util.DELETION_MAX = defaultDeletionMax; - unlockFilesForDeletion(); - } - } - - @Test - public void testDeleteFileReadOnly() throws Exception { - // Removing the calls to Util#makeWritable in Util#tryOnceDeleteFile should cause this test to fail. - Path file = tmp.newFolder().toPath().resolve("file.tmp"); - Files.createDirectories(file.getParent()); - Files.createFile(file); - // Using old IO so the test can run on Windows. - file.getParent().toFile().setWritable(false); - file.toFile().setWritable(false); - Util.deleteFile(file.toFile()); - assertFalse(Files.exists(file)); - } - - @Test - public void testDeleteFileDoesNotExist() throws Exception { - Path file = tmp.newFolder().toPath().resolve("file.tmp"); - assertFalse(Files.exists(file)); - // Should not throw an exception. - Util.deleteFile(file.toFile()); - } - - @Test - public void testDeleteContentsRecursive() throws Exception { - final File dir = tmp.newFolder(); - final File d1 = new File(dir, "d1"); - final File d2 = new File(dir, "d2"); - final File f1 = new File(dir, "f1"); - final File d1f1 = new File(d1, "d1f1"); - final File d2f2 = new File(d2, "d1f2"); - // Test: Files and directories are deleted - mkdirs(dir, d1, d2); - mkfiles(f1, d1f1, d2f2); - Util.deleteContentsRecursive(dir); - assertTrue("dir exists", dir.exists()); - assertFalse("d1 exists", d1.exists()); - assertFalse("d2 exists", d2.exists()); - assertFalse("f1 exists", f1.exists()); - } - - @Ignore("TODO often fails in CI") - @Test - public void testDeleteContentsRecursive_onWindows() throws Exception { - Assume.assumeTrue(Functions.isWindows()); - final File dir = tmp.newFolder(); - final File d1 = new File(dir, "d1"); - final File d2 = new File(dir, "d2"); - final File f1 = new File(dir, "f1"); - final File d1f1 = new File(d1, "d1f1"); - final File d2f2 = new File(d2, "d1f2"); - final int defaultDeletionMax = Util.DELETION_MAX; - final int defaultDeletionWait = Util.WAIT_BETWEEN_DELETION_RETRIES; - final boolean defaultDeletionGC = Util.GC_AFTER_FAILED_DELETE; - try { - // Test: If we cannot delete a file, we throw - // but still deletes everything it can - // even if we are not retrying deletes. - mkdirs(dir, d1, d2); - mkfiles(f1, d1f1, d2f2); - lockFileForDeletion(d1f1); - Util.GC_AFTER_FAILED_DELETE = false; - Util.DELETION_MAX = 2; - Util.WAIT_BETWEEN_DELETION_RETRIES = 0; - try { - Util.deleteContentsRecursive(dir); - fail("Expected IOException"); - } catch (IOException x) { - assertFalse("d2 should not exist", d2.exists()); - assertFalse("f1 should not exist", f1.exists()); - assertFalse("d1f2 should not exist", d2f2.exists()); - assertThat(x.getMessage(), containsString(dir.getPath())); - assertThat(x.getMessage(), allOf(not(containsString("interrupted")), containsString("Tried 2 times (of a maximum of 2)."), not(containsString("garbage-collecting")), not(containsString("wait")))); - } - } finally { - Util.DELETION_MAX = defaultDeletionMax; - Util.WAIT_BETWEEN_DELETION_RETRIES = defaultDeletionWait; - Util.GC_AFTER_FAILED_DELETE = defaultDeletionGC; - unlockFilesForDeletion(); - } - } - - @Test - public void testDeleteRecursive() throws Exception { - final File dir = tmp.newFolder(); - final File d1 = new File(dir, "d1"); - final File d2 = new File(dir, "d2"); - final File f1 = new File(dir, "f1"); - final File d1f1 = new File(d1, "d1f1"); - final File d2f2 = new File(d2, "d1f2"); - // Test: Files and directories are deleted - mkdirs(dir, d1, d2); - mkfiles(f1, d1f1, d2f2); - Util.deleteRecursive(dir); - assertFalse("dir exists", dir.exists()); - } - - @Ignore("JENKINS-55016") - @Test - public void testDeleteRecursive_onWindows() throws Exception { - Assume.assumeTrue(Functions.isWindows()); - final File dir = tmp.newFolder(); - final File d1 = new File(dir, "d1"); - final File d2 = new File(dir, "d2"); - final File f1 = new File(dir, "f1"); - final File d1f1 = new File(d1, "d1f1"); - final File d2f2 = new File(d2, "d1f2"); - final int defaultDeletionMax = Util.DELETION_MAX; - final int defaultDeletionWait = Util.WAIT_BETWEEN_DELETION_RETRIES; - final boolean defaultDeletionGC = Util.GC_AFTER_FAILED_DELETE; - try { - // Test: If we cannot delete a file, we throw - // but still deletes everything it can - // even if we are not retrying deletes. - // (And when we are not retrying deletes, - // we do not do the "between retries" delay) - mkdirs(dir, d1, d2); - mkfiles(f1, d1f1, d2f2); - lockFileForDeletion(d1f1); - Util.GC_AFTER_FAILED_DELETE = false; - Util.DELETION_MAX = 1; - Util.WAIT_BETWEEN_DELETION_RETRIES = 10000; // long enough to notice - long timeWhenDeletionStarted = System.currentTimeMillis(); - try { - Util.deleteRecursive(dir); - fail("Expected IOException"); - } catch (IOException x) { - long timeWhenDeletionEnded = System.currentTimeMillis(); - assertTrue("dir exists", dir.exists()); - assertTrue("d1 exists", d1.exists()); - assertTrue("d1f1 exists", d1f1.exists()); - assertFalse("d2 should not exist", d2.exists()); - assertFalse("f1 should not exist", f1.exists()); - assertFalse("d1f2 should not exist", d2f2.exists()); - assertThat(x.getMessage(), containsString(dir.getPath())); - assertThat(x.getMessage(), allOf(not(containsString("interrupted")), not(containsString("maximum of")), not(containsString("garbage-collecting")))); - long actualTimeSpentDeleting = timeWhenDeletionEnded - timeWhenDeletionStarted; - assertTrue("did not wait - took " + actualTimeSpentDeleting + "ms", actualTimeSpentDeleting<1000L); - } - unlockFileForDeletion(d1f1); - // Deletes get retried if they fail 1st time around, - // allowing the operation to succeed on subsequent attempts. - // Note: This is what bug JENKINS-15331 is all about. - mkdirs(dir, d1, d2); - mkfiles(f1, d1f1, d2f2); - lockFileForDeletion(d2f2); - Util.DELETION_MAX=4; - Util.WAIT_BETWEEN_DELETION_RETRIES = 100; - Thread unlockAfterDelay = new Thread("unlockFileAfterDelay") { - public void run() { - try { - Thread.sleep(Util.WAIT_BETWEEN_DELETION_RETRIES); - unlockFileForDeletion(d2f2); - } catch( Exception x ) { /* ignored */ } - } - }; - unlockAfterDelay.start(); - Util.deleteRecursive(dir); - assertFalse("dir should have been deleted", dir.exists()); - unlockAfterDelay.join(); - // An interrupt aborts the delete and makes it fail, even - // if we had been told to retry a lot. - mkdirs(dir, d1, d2); - mkfiles(f1, d1f1, d2f2); - lockFileForDeletion(d1f1); - Util.DELETION_MAX=10; - Util.WAIT_BETWEEN_DELETION_RETRIES = -1000; - Util.GC_AFTER_FAILED_DELETE = true; - final AtomicReference thrown = new AtomicReference(); - Thread deleteToBeInterrupted = new Thread("deleteToBeInterrupted") { - public void run() { - try { Util.deleteRecursive(dir); } - catch( Throwable x ) { thrown.set(x); } - } - }; - deleteToBeInterrupted.start(); - deleteToBeInterrupted.interrupt(); - deleteToBeInterrupted.join(500); - assertFalse("deletion stopped", deleteToBeInterrupted.isAlive()); - assertTrue("d1f1 still exists", d1f1.exists()); - unlockFileForDeletion(d1f1); - Throwable deletionInterruptedEx = thrown.get(); - assertThat(deletionInterruptedEx, instanceOf(IOException.class)); - assertThat(deletionInterruptedEx.getMessage(), allOf(containsString("interrupted"), containsString("maximum of " + Util.DELETION_MAX), containsString("garbage-collecting"))); - } finally { - Util.DELETION_MAX = defaultDeletionMax; - Util.WAIT_BETWEEN_DELETION_RETRIES = defaultDeletionWait; - Util.GC_AFTER_FAILED_DELETE = defaultDeletionGC; - unlockFilesForDeletion(); - } - } - - /** Creates multiple directories. */ - private static void mkdirs(File... dirs) { - for( File d : dirs ) { - d.mkdir(); - assertTrue(d.getPath(), d.isDirectory()); - } - } - - /** Creates multiple files, each containing their filename as text content. */ - private static void mkfiles(File... files) throws IOException { - for( File f : files ) - FileUtils.write(f, f.getName()); - } - - /** Means of unlocking all the files we have locked, indexed by {@link File}. */ - private final Map> unlockFileCallables = new HashMap>(); - - /** Prevents a file from being deleted, so we can stress the deletion code's retries. */ - private void lockFileForDeletion(File f) throws IOException, InterruptedException { - assert !unlockFileCallables.containsKey(f) : f + " is already locked." ; - // Limitation: Only works on Windows. On unix we can delete anything we can create. - // On unix, can't use "chmod a-w" on the dir as the code-under-test undoes that. - // On unix, can't use "chattr +i" because that needs root. - // On unix, can't use "chattr +u" because ext fs ignores it. - // On Windows, can't use FileChannel.lock() because that doesn't block deletion - // On Windows, we can't delete files that are open for reading, so we use that. - // NOTE: This is a hack in any case as there is no guarantee that all Windows filesystems - // will enforce blocking deletion on open files... just that the ones we normally - // test with seem to block. - assert Functions.isWindows(); - final InputStream s = new FileInputStream(f); // intentional use of FileInputStream - unlockFileCallables.put(f, new Callable() { - public Void call() throws IOException { s.close(); return null; }; - }); - } - - /** Undoes a call to {@link #lockFileForDeletion(File)}. */ - private void unlockFileForDeletion(File f) throws Exception { - unlockFileCallables.remove(f).call(); - } - - /** Undoes all calls to {@link #lockFileForDeletion(File)}. */ - private void unlockFilesForDeletion() throws Exception { - while( !unlockFileCallables.isEmpty() ) { - unlockFileForDeletion(unlockFileCallables.keySet().iterator().next()); - } - } - - /** Returns all classes in the exception hierarchy. */ - private static Iterable> calcExceptionHierarchy(Throwable t) { - final List> result = Lists.newArrayList(); - for( ; t!=null ; t = t.getCause()) - result.add(t.getClass()); - return result; - } - @Test public void testHtmlEscape() { assertEquals("
      ", Util.escape("\n")); @@ -764,12 +473,8 @@ public class UtilTest { assertEquals("Non-permission bits should be ignored", PosixFilePermissions.fromString("r-xr-----"), Util.modeToPermissions(0100540)); - try { - Util.modeToPermissions(01777); - fail("Did not detect invalid mode"); - } catch (IOException e) { - assertThat(e.getMessage(), startsWith("Invalid mode")); - } + expectedException.expectMessage(startsWith("Invalid mode")); + Util.modeToPermissions(01777); } @Test diff --git a/core/src/test/java/hudson/scheduler/CronTabTest.java b/core/src/test/java/hudson/scheduler/CronTabTest.java index 66894c3546..65115091a7 100644 --- a/core/src/test/java/hudson/scheduler/CronTabTest.java +++ b/core/src/test/java/hudson/scheduler/CronTabTest.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Locale; +import java.util.TimeZone; import static org.junit.Assert.*; import org.junit.Test; @@ -199,7 +200,7 @@ public class CronTabTest { } /** - * Humans can't easily see difference in two {@link Calendar}s, do help the diagnosis by using {@link DateFormat}. + * Humans can't easily see difference in two {@link Calendar}s, do help the diagnosis by using {@link DateFormat}. */ private void compare(Calendar expected, Calendar actual) { DateFormat f = DateFormat.getDateTimeInstance(); @@ -307,7 +308,9 @@ public class CronTabTest { CronTabList tabs = CronTabList.create("TZ=Australia/Sydney\nH * * * *\nH * * * *", Hash.from("seed")); List times = new ArrayList(); for (int i = 0; i < 60; i++) { - if (tabs.check(new GregorianCalendar(2013, 3, 3, 11, i, 0))) { + GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.set(2013, Calendar.APRIL, 3, 11, i, 0); + if (tabs.check(calendar)) { times.add(i); } } diff --git a/core/src/test/java/jenkins/util/io/FileLockerRule.java b/core/src/test/java/jenkins/util/io/FileLockerRule.java new file mode 100644 index 0000000000..df0dab652e --- /dev/null +++ b/core/src/test/java/jenkins/util/io/FileLockerRule.java @@ -0,0 +1,80 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.util.io; + +import hudson.Functions; +import org.junit.rules.ExternalResource; + +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Helper class for tracking which files are locked. + * Only useful in Windows environments as POSIX seems to allow locked files to be deleted. + */ +public class FileLockerRule extends ExternalResource { + private final Map locks = new HashMap<>(); + + @Override + protected void after() { + List exceptions = new ArrayList<>(); + Iterator it = locks.values().iterator(); + while (it.hasNext()) { + try (Closeable ignored = it.next()) { + it.remove(); + } catch (IOException e) { + exceptions.add(e); + } + } + if (!exceptions.isEmpty()) + throw new CompositeIOException("Could not unlock all files", exceptions).asUncheckedIOException(); + } + + public synchronized void acquireLock(@Nonnull File file) throws IOException { + assertTrue(Functions.isWindows()); + assertThat(file + " is already locked.", locks, not(hasKey(file))); + Closeable lock = new FileInputStream(file); + locks.put(file, lock); + } + + public synchronized void releaseLock(@Nonnull File file) throws Exception { + assertTrue(Functions.isWindows()); + assertThat(file + " is not locked.", locks, hasKey(file)); + locks.remove(file).close(); + } +} diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java new file mode 100644 index 0000000000..55c7970424 --- /dev/null +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -0,0 +1,328 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.util.io; + +import hudson.Functions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.Timeout; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.spi.FileSystemProvider; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +public class PathRemoverTest { + + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + @Rule public ExpectedException expectedException = ExpectedException.none(); + @Rule public Timeout timeout = new Timeout(10, TimeUnit.SECONDS); + @Rule public FileLockerRule locker = new FileLockerRule(); + + @Test + public void testForceRemoveFile() throws IOException { + File file = tmp.newFile(); + touchWithFileName(file); + + PathRemover remover = PathRemover.newSimpleRemover(); + remover.forceRemoveFile(file.toPath()); + + assertFalse("Unable to delete file: " + file, file.exists()); + } + + @Test + public void testForceRemoveFile_LockedFile() throws Exception { + String filename = "/var/lock/jenkins.lock"; + File file = mock(File.class); + Path path = mock(Path.class); + FileSystem fs = mock(FileSystem.class); + FileSystemProvider fsProvider = mock(FileSystemProvider.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + + given(file.getPath()).willReturn(filename); + given(file.toPath()).willReturn(path); + given(path.toString()).willReturn(filename); + given(path.toFile()).willReturn(file); + given(path.getFileSystem()).willReturn(fs); + given(fs.provider()).willReturn(fsProvider); + given(fsProvider.deleteIfExists(path)).willThrow(new FileSystemException(filename)); + given(fsProvider.readAttributes(path, BasicFileAttributes.class)).willReturn(attributes); + given(attributes.isDirectory()).willReturn(false); + + PathRemover remover = PathRemover.newSimpleRemover(); + try { + remover.forceRemoveFile(file.toPath()); + fail("Should not have been deleted: " + file); + } catch (IOException e) { + assertThat(calcExceptionHierarchy(e), hasItem(FileSystemException.class)); + assertThat(e.getMessage(), containsString(filename)); + } + + } + + private static List> calcExceptionHierarchy(Throwable t) { + List> hierarchy = new ArrayList<>(); + for (; t != null; t = t.getCause()) { + hierarchy.add(t.getClass()); + // with a composite exception, we might be hiding some other causes + if (t instanceof CompositeIOException) { + CompositeIOException e = (CompositeIOException) t; + e.getExceptions().forEach(ex -> hierarchy.addAll(calcExceptionHierarchy(ex))); + } + } + return hierarchy; + } + + @Test + public void testForceRemoveFile_ReadOnly() throws IOException { + File dir = tmp.newFolder(); + File file = new File(dir, "file.tmp"); + touchWithFileName(file); + assertTrue("Unable to make file read-only: " + file, file.setWritable(false)); + + PathRemover remover = PathRemover.newSimpleRemover(); + remover.forceRemoveFile(file.toPath()); + + assertFalse("Unable to delete file: " + file, file.exists()); + } + + @Test + public void testForceRemoveFile_DoesNotExist() throws IOException { + File dir = tmp.newFolder(); + File file = new File(dir, "invalid.file"); + assertFalse(file.exists()); + + PathRemover remover = PathRemover.newSimpleRemover(); + remover.forceRemoveFile(file.toPath()); + + assertFalse("Unable to delete file: " + file, file.exists()); + } + + @Test + public void testForceRemoveDirectoryContents() throws IOException { + File dir = tmp.newFolder(); + File d1 = new File(dir, "d1"); + File d2 = new File(dir, "d2"); + File f1 = new File(dir, "f1"); + File d1f1 = new File(d1, "d1f1"); + File d2f2 = new File(d2, "d1f2"); + mkdirs(d1, d2); + touchWithFileName(f1, d1f1, d2f2); + + PathRemover remover = PathRemover.newSimpleRemover(); + remover.forceRemoveDirectoryContents(dir.toPath()); + + assertTrue(dir.exists()); + assertFalse(d1.exists()); + assertFalse(d2.exists()); + assertFalse(f1.exists()); + } + + @Test + public void testForceRemoveDirectoryContents_LockedFile() throws Exception { + assumeTrue(Functions.isWindows()); + File dir = tmp.newFolder(); + File d1 = new File(dir, "d1"); + File d2 = new File(dir, "d2"); + File f1 = new File(dir, "f1"); + File d1f1 = new File(d1, "d1f1"); + File d2f2 = new File(d2, "d1f2"); + mkdirs(d1, d2); + touchWithFileName(f1, d1f1, d2f2); + locker.acquireLock(d1f1); + PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> retriesAttempted < 1); + expectedException.expectMessage(allOf( + containsString(dir.getPath()), + containsString("Tried 1 time.") + )); + remover.forceRemoveDirectoryContents(dir.toPath()); + assertFalse(d2.exists()); + assertFalse(f1.exists()); + assertFalse(d2f2.exists()); + } + + @Test + public void testForceRemoveRecursive() throws IOException { + File dir = tmp.newFolder(); + File d1 = new File(dir, "d1"); + File d2 = new File(dir, "d2"); + File f1 = new File(dir, "f1"); + File d1f1 = new File(d1, "d1f1"); + File d2f2 = new File(d2, "d1f2"); + mkdirs(d1, d2); + touchWithFileName(f1, d1f1, d2f2); + + PathRemover remover = PathRemover.newSimpleRemover(); + remover.forceRemoveRecursive(dir.toPath()); + + assertFalse(dir.exists()); + } + + @Test + public void testForceRemoveRecursive_DeletesAsMuchAsPossibleWithLockedFiles() throws Exception { + assumeTrue(Functions.isWindows()); + File dir = tmp.newFolder(); + File d1 = new File(dir, "d1"); + File d2 = new File(dir, "d2"); + File f1 = new File(dir, "f1"); + File d1f1 = new File(d1, "d1f1"); + File d2f2 = new File(d2, "d1f2"); + mkdirs(d1, d2); + touchWithFileName(f1, d1f1, d2f2); + + locker.acquireLock(d1f1); + PathRemover remover = PathRemover.newSimpleRemover(); + expectedException.expectMessage(containsString(dir.getPath())); + remover.forceRemoveRecursive(dir.toPath()); + assertTrue(dir.exists()); + assertTrue(d1.exists()); + assertTrue(d1f1.exists()); + assertFalse(d2.exists()); + assertFalse(d2f2.exists()); + assertFalse(f1.exists()); + } + + @Test + public void testForceRemoveRecursive_RetryOnFailure() throws Exception { + assumeTrue(Functions.isWindows()); + File dir = tmp.newFolder(); + File d1 = new File(dir, "d1"); + File d2 = new File(dir, "d2"); + File f1 = new File(dir, "f1"); + File d1f1 = new File(d1, "d1f1"); + File d2f2 = new File(d2, "d1f2"); + mkdirs(d1, d2); + touchWithFileName(f1, d1f1, d2f2); + locker.acquireLock(d2f2); + CountDownLatch unlockLatch = new CountDownLatch(1); + CountDownLatch deleteLatch = new CountDownLatch(1); + AtomicBoolean lockedFileExists = new AtomicBoolean(); + Thread thread = new Thread(() -> { + try { + unlockLatch.await(); + locker.releaseLock(d2f2); + deleteLatch.countDown(); + } catch (Exception ignored) { + } + }); + thread.start(); + PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { + if (retriesAttempted == 0) { + lockedFileExists.set(d2f2.exists()); + unlockLatch.countDown(); + try { + deleteLatch.await(); + return true; + } catch (InterruptedException e) { + return false; + } + } + return false; + }); + remover.forceRemoveRecursive(dir.toPath()); + thread.join(); + assertTrue(lockedFileExists.get()); + assertFalse(dir.exists()); + } + + @Test + public void testForceRemoveRecursive_FailsWhenInterrupted() throws Exception { + assumeTrue(Functions.isWindows()); + File dir = tmp.newFolder(); + File d1 = new File(dir, "d1"); + File d2 = new File(dir, "d2"); + File f1 = new File(dir, "f1"); + File d1f1 = new File(d1, "d1f1"); + File d2f2 = new File(d2, "d1f2"); + mkdirs(d1, d2); + touchWithFileName(f1, d1f1, d2f2); + locker.acquireLock(d1f1); + AtomicReference interrupted = new AtomicReference<>(); + AtomicReference removed = new AtomicReference<>(); + PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { + try { + TimeUnit.SECONDS.sleep(retriesAttempted + 1); + return true; + } catch (InterruptedException e) { + interrupted.set(e); + return false; + } + }); + Thread thread = new Thread(() -> { + try { + remover.forceRemoveRecursive(dir.toPath()); + } catch (IOException e) { + removed.set(e); + } + }); + thread.start(); + TimeUnit.MILLISECONDS.sleep(100); + thread.interrupt(); + thread.join(); + assertFalse(thread.isAlive()); + assertTrue(d1f1.exists()); + IOException ioException = removed.get(); + assertNotNull(ioException); + assertThat(ioException.getMessage(), containsString(dir.getPath())); + assertNotNull(interrupted.get()); + } + + private static void mkdirs(File... dirs) { + for (File dir : dirs) { + assertTrue("Could not mkdir " + dir, dir.mkdir()); + assertTrue(dir.isDirectory()); + } + } + + private static void touchWithFileName(File... files) throws IOException { + for (File file : files) { + try (FileWriter writer = new FileWriter(file)) { + writer.append(file.getName()).append(System.lineSeparator()); + } + assertTrue(file.isFile()); + } + } + +} \ No newline at end of file diff --git a/essentials.yml b/essentials.yml index 63d7908fec..66b3542247 100644 --- a/essentials.yml +++ b/essentials.yml @@ -1,7 +1,7 @@ --- ath: useLocalSnapshots: false - athRevision: "809fb7629c311ebddc6cada91fc865a950155bb0" - athImage: "jenkins/ath:acceptance-test-harness-1.63" + athRevision: "8c3126005c0d5394ed225a5c9f3eab99ab385afe" + athImage: "jenkins/ath@sha256:284c2fdfaf1a51e95783126367c0a88c07af83ff9234063d12ba3001959628e8" categories: - org.jenkinsci.test.acceptance.junit.SmokeTest diff --git a/pom.xml b/pom.xml index 6345a44a1d..7a1616abd4 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci jenkins - 1.50 + 1.51 @@ -76,7 +76,7 @@ THE SOFTWARE. - 2.157 + 2.159 -SNAPSHOT