From 3fa7427c1764fb8531157da16557757b67a1a49e Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Mon, 11 Sep 2017 02:42:34 +0200 Subject: [PATCH 01/74] [JENKINS-20679] Add support for plugin Java requirement metadata --- core/src/main/java/hudson/PluginWrapper.java | 23 ++++++++ .../main/java/hudson/model/UpdateSite.java | 54 +++++++++++++++++++ .../main/resources/hudson/Messages.properties | 13 ++--- .../resources/hudson/PluginManager/_table.css | 3 ++ .../hudson/PluginManager/table.jelly | 6 +++ .../hudson/PluginManager/table.properties | 7 +++ 6 files changed, 100 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 363d588f0a..75322ab32a 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -457,6 +457,20 @@ public class PluginWrapper implements Comparable, ModelObject { return null; } + /** + * Returns the required Java version of this plugin. + * @return the required Java version of this plugin. + * + * @since TODO + */ + @Exported + public @CheckForNull String getRequiredJavaVersion() { + String v = manifest.getMainAttributes().getValue("Minimum-Java-Version"); + if (v!= null) return v; + + return null; + } + /** * Returns the version number of this plugin */ @@ -600,6 +614,15 @@ public class PluginWrapper implements Comparable, ModelObject { versionDependencyError(Messages.PluginWrapper_obsoleteCore(Jenkins.getVersion().toString(), requiredCoreVersion), Jenkins.getVersion().toString(), requiredCoreVersion); } } + + String requiredJavaVersion = getRequiredJavaVersion(); + if (requiredJavaVersion != null) { + // TODO replace with calls to Runtime.version() once we're on Java 9 + VersionNumber actualVersion = new VersionNumber(System.getProperty("java.version")); + if (actualVersion.isOlderThan(new VersionNumber(requiredJavaVersion))) { + dependencyErrors.add(Messages.PluginWrapper_obsoleteJava(actualVersion.toString(), requiredJavaVersion)); + } + } } // make sure dependencies exist for (Dependency d : dependencies) { diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 0a27890a12..b3ff312768 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -948,6 +948,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. @@ -974,6 +981,8 @@ public class UpdateSite { this.title = get(o,"title"); this.excerpt = get(o,"excerpt"); this.compatibleSinceVersion = get(o,"compatibleSinceVersion"); + this.minimumJavaVersion = get(o,"minimumJavaVersion"); + this.requiredCore = get(o,"requiredCore"); this.categories = o.has("labels") ? (String[])o.getJSONArray("labels").toArray(new String[0]) : null; for(Object jo : o.getJSONArray("dependencies")) { @@ -1098,6 +1107,19 @@ public class UpdateSite { } } + /** + * @since TODO + * @return + */ + public boolean isForNewerJava() { + try { + return minimumJavaVersion != null && new VersionNumber(minimumJavaVersion).isNewerThan( + new VersionNumber(System.getProperty("java.version"))); + } catch (NumberFormatException nfe) { + return false; // plugin doesn't declare a minimum Java version + } + } + public VersionNumber getNeededDependenciesRequiredCore() { VersionNumber versionNumber = null; try { @@ -1112,6 +1134,29 @@ public class UpdateSite { return versionNumber; } + /** + * @since TODO + * @return + */ + public VersionNumber getNeededDependenciesRequiredJava() { + VersionNumber versionNumber = null; + try { + versionNumber = minimumJavaVersion == null ? null : new VersionNumber(minimumJavaVersion); + } catch (NumberFormatException nfe) { + // unable to parse version + } + for (Plugin p: getNeededDependencies()) { + VersionNumber v = p.getNeededDependenciesRequiredJava(); + if (v == null) { + continue; + } + if (versionNumber == null || v.isNewerThan(versionNumber)) { + versionNumber = v; + } + } + return versionNumber; + } + public boolean isNeededDependenciesForNewerJenkins() { return isNeededDependenciesForNewerJenkins(new PluginManager.MetadataCache()); } @@ -1128,6 +1173,15 @@ public class UpdateSite { }); } + 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/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index 80ecb8db83..1666f15cc5 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -75,13 +75,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 TcpSlaveAgentListener.PingAgentProtocol.displayName=Ping protocol diff --git a/core/src/main/resources/hudson/PluginManager/_table.css b/core/src/main/resources/hudson/PluginManager/_table.css index 86125969bb..90b80cfd19 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; +} \ No newline at end of file diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly index 26dafbc2fc..1480b90742 100644 --- a/core/src/main/resources/hudson/PluginManager/table.jelly +++ b/core/src/main/resources/hudson/PluginManager/table.jelly @@ -106,12 +106,18 @@ THE SOFTWARE.
${%coreWarning(p.requiredCore)}
+ +
${%javaWarning(p.minimumJavaVersion)}
+
${%depCompatWarning}
${%depCoreWarning(p.getNeededDependenciesRequiredCore().toString())}
+ +
${%depJavaWarning(p.getNeededDependenciesRequiredJava().toString())}
+
${%securityWarning}
    diff --git a/core/src/main/resources/hudson/PluginManager/table.properties b/core/src/main/resources/hudson/PluginManager/table.properties index 63c161534c..7d2edec8a6 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 dependent plugins that 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: From e65ceca2f8a6bf09b5028d2e1967405fdb038e23 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Mon, 11 Sep 2017 03:57:32 +0200 Subject: [PATCH 02/74] Rename 'minimumJavaVersion' to 'requiredJava' --- core/src/main/java/hudson/model/UpdateSite.java | 8 ++++---- core/src/main/resources/hudson/PluginManager/table.jelly | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index b3ff312768..34a8c04b2d 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -954,7 +954,7 @@ public class UpdateSite { * @since TODO */ @Exported - public final String minimumJavaVersion; + public final String requiredJava; /** * Categories for grouping plugins, taken from labels assigned to wiki page. * Can be null. @@ -981,7 +981,7 @@ public class UpdateSite { this.title = get(o,"title"); this.excerpt = get(o,"excerpt"); this.compatibleSinceVersion = get(o,"compatibleSinceVersion"); - this.minimumJavaVersion = get(o,"minimumJavaVersion"); + this.requiredJava = get(o,"requiredJava"); this.requiredCore = get(o,"requiredCore"); this.categories = o.has("labels") ? (String[])o.getJSONArray("labels").toArray(new String[0]) : null; @@ -1113,7 +1113,7 @@ public class UpdateSite { */ public boolean isForNewerJava() { try { - return minimumJavaVersion != null && new VersionNumber(minimumJavaVersion).isNewerThan( + return requiredJava != null && new VersionNumber(requiredJava).isNewerThan( new VersionNumber(System.getProperty("java.version"))); } catch (NumberFormatException nfe) { return false; // plugin doesn't declare a minimum Java version @@ -1141,7 +1141,7 @@ public class UpdateSite { public VersionNumber getNeededDependenciesRequiredJava() { VersionNumber versionNumber = null; try { - versionNumber = minimumJavaVersion == null ? null : new VersionNumber(minimumJavaVersion); + versionNumber = requiredJava == null ? null : new VersionNumber(requiredJava); } catch (NumberFormatException nfe) { // unable to parse version } diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly index 1480b90742..4667cee418 100644 --- a/core/src/main/resources/hudson/PluginManager/table.jelly +++ b/core/src/main/resources/hudson/PluginManager/table.jelly @@ -107,7 +107,7 @@ THE SOFTWARE.
    ${%coreWarning(p.requiredCore)}
    -
    ${%javaWarning(p.minimumJavaVersion)}
    +
    ${%javaWarning(p.requiredJava)}
    ${%depCompatWarning}
    From d1c972a055aff07b5cd5476d647d35405375e8f5 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Mon, 11 Sep 2017 20:25:42 +0200 Subject: [PATCH 03/74] Adapt tests to changed localizable strings --- core/src/test/java/hudson/PluginWrapperTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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"); } } From 725fb3026bf288709b07a6dc555eb2cadf1b07cd Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Mon, 11 Sep 2017 20:30:08 +0200 Subject: [PATCH 04/74] Update Javadocs and comments --- core/src/main/java/hudson/PluginManager.java | 2 ++ core/src/main/java/hudson/model/UpdateSite.java | 17 +++++++++++++++-- .../resources/hudson/PluginManager/_table.css | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index be83e07456..fdee127447 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -1719,6 +1719,7 @@ 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}); } + // TODO check Java dependency jobs.add(toInstall.deploy(true)); } else if (pw.isOlderThan(requestedPlugin.getValue())) { // upgrade UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey()); @@ -1736,6 +1737,7 @@ 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}); } + // TODO check Java dependency 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/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 34a8c04b2d..0812aa7c44 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -1108,8 +1108,11 @@ 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. + * + * @return true iff the plugin declares a minimum Java version and it's newer than what the Jenkins master is running on. + * * @since TODO - * @return */ public boolean isForNewerJava() { try { @@ -1135,8 +1138,11 @@ public class UpdateSite { } /** + * Returns the minimum Java version needed to use the plugin and all its dependencies. + * + * @return the minimum Java version needed to use the plugin and all its dependencies. + * * @since TODO - * @return */ public VersionNumber getNeededDependenciesRequiredJava() { VersionNumber versionNumber = null; @@ -1173,6 +1179,13 @@ public class UpdateSite { }); } + /** + * Returns true iff this plugin or any of its dependencies require a newer Java than Jenkins is running on. + * + * @return true iff this plugin or any of its dependencies require a newer Java than Jenkins is running on. + * + * @since TODO + */ public boolean isNeededDependenciesForNewerJava() { for (Plugin p: getNeededDependencies()) { if (p.isForNewerJava() || p.isNeededDependenciesForNewerJava()) { diff --git a/core/src/main/resources/hudson/PluginManager/_table.css b/core/src/main/resources/hudson/PluginManager/_table.css index 90b80cfd19..9107c2be8b 100644 --- a/core/src/main/resources/hudson/PluginManager/_table.css +++ b/core/src/main/resources/hudson/PluginManager/_table.css @@ -10,4 +10,4 @@ } .compatWarning, .securityWarning { font-weight: bold; -} \ No newline at end of file +} From f55864fe4973f310059c62437c3b9b0b30bb9d91 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Mon, 11 Sep 2017 20:35:48 +0200 Subject: [PATCH 05/74] Add Java version check to setup wizard and installNecessaryPlugins I'm not sure what the log message is supposed to be for, but other checks also log, so apply the same behavior for Java dependency. --- core/src/main/java/hudson/PluginManager.java | 8 ++++++-- core/src/main/java/jenkins/install/SetupWizard.java | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index fdee127447..dcf02ccd27 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -1719,7 +1719,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}); } - // TODO check Java dependency + 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()); @@ -1737,7 +1739,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}); } - // TODO check Java dependency + 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/jenkins/install/SetupWizard.java b/core/src/main/java/jenkins/install/SetupWizard.java index 3ae5a0ea24..30214ea4bd 100644 --- a/core/src/main/java/jenkins/install/SetupWizard.java +++ b/core/src/main/java/jenkins/install/SetupWizard.java @@ -500,7 +500,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; From 5cdbf39d9c5faa406ea4d0bf6ed2b51c17b93fd8 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Thu, 20 Sep 2018 13:44:08 -0700 Subject: [PATCH 06/74] Use java.specification.version --- core/src/main/java/hudson/PluginWrapper.java | 9 +++------ core/src/main/java/hudson/model/UpdateSite.java | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 75322ab32a..ef73feea6f 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -465,10 +465,7 @@ public class PluginWrapper implements Comparable, ModelObject { */ @Exported public @CheckForNull String getRequiredJavaVersion() { - String v = manifest.getMainAttributes().getValue("Minimum-Java-Version"); - if (v!= null) return v; - - return null; + return manifest.getMainAttributes().getValue("Minimum-Java-Version"); } /** @@ -617,8 +614,8 @@ public class PluginWrapper implements Comparable, ModelObject { String requiredJavaVersion = getRequiredJavaVersion(); if (requiredJavaVersion != null) { - // TODO replace with calls to Runtime.version() once we're on Java 9 - VersionNumber actualVersion = new VersionNumber(System.getProperty("java.version")); + // TODO replace with calls to Runtime.version() once we're on Java 9+ + VersionNumber actualVersion = new VersionNumber(System.getProperty("java.specification.version")); if (actualVersion.isOlderThan(new VersionNumber(requiredJavaVersion))) { dependencyErrors.add(Messages.PluginWrapper_obsoleteJava(actualVersion.toString(), requiredJavaVersion)); } diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 0812aa7c44..b1950c8131 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -1117,7 +1117,7 @@ public class UpdateSite { public boolean isForNewerJava() { try { return requiredJava != null && new VersionNumber(requiredJava).isNewerThan( - new VersionNumber(System.getProperty("java.version"))); + new VersionNumber(System.getProperty("java.specification.version"))); } catch (NumberFormatException nfe) { return false; // plugin doesn't declare a minimum Java version } From e364a84ee553a9aa0288ef2c2a1effd113503ed6 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Thu, 20 Sep 2018 13:46:54 -0700 Subject: [PATCH 07/74] Adapt to newer core internals --- core/src/main/java/hudson/PluginWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index ef73feea6f..9e5ee42f32 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -617,7 +617,7 @@ public class PluginWrapper implements Comparable, ModelObject { // TODO replace with calls to Runtime.version() once we're on Java 9+ VersionNumber actualVersion = new VersionNumber(System.getProperty("java.specification.version")); if (actualVersion.isOlderThan(new VersionNumber(requiredJavaVersion))) { - dependencyErrors.add(Messages.PluginWrapper_obsoleteJava(actualVersion.toString(), requiredJavaVersion)); + versionDependencyError(Messages.PluginWrapper_obsoleteJava(actualVersion.toString(), requiredJavaVersion), actualVersion.toString(), requiredJavaVersion); } } } From c58164a25d21de098d8dab626bdfeed368298e40 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Wed, 12 Dec 2018 14:29:26 -0600 Subject: [PATCH 08/74] Use ExpectedException in test Signed-off-by: Matt Sicker --- core/src/test/java/hudson/UtilTest.java | 64 ++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/core/src/test/java/hudson/UtilTest.java b/core/src/test/java/hudson/UtilTest.java index 98ecae9595..19bffde721 100644 --- a/core/src/test/java/hudson/UtilTest.java +++ b/core/src/test/java/hudson/UtilTest.java @@ -59,6 +59,7 @@ 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; @@ -75,6 +76,8 @@ public class UtilTest { @Rule public TemporaryFolder tmp = new TemporaryFolder(); + @Rule public ExpectedException expectedException = ExpectedException.none(); + @Test public void testReplaceMacro() { Map m = new HashMap(); @@ -374,16 +377,17 @@ public class UtilTest { 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")))); - } + expectedException.expectMessage(allOf( + containsString(dir.getPath()), + not(containsString("interrupted")), + containsString("Tried 2 times (of a maximum of 2)."), + not(containsString("garbage-collecting")), + not(containsString("wait")) + )); + Util.deleteContentsRecursive(dir); + assertFalse("d2 should not exist", d2.exists()); + assertFalse("f1 should not exist", f1.exists()); + assertFalse("d1f2 should not exist", d2f2.exists()); } finally { Util.DELETION_MAX = defaultDeletionMax; Util.WAIT_BETWEEN_DELETION_RETRIES = defaultDeletionWait; @@ -433,22 +437,22 @@ public class UtilTest { 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); - } + expectedException.expectMessage(allOf( + containsString(dir.getPath()), + not(containsString("interrupted")), + not(containsString("maximum of")), + not(containsString("garbage-collecting")) + )); + Util.deleteRecursive(dir); + 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()); + 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. @@ -763,12 +767,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 From aa0d3467009da86f165e66e11fdfe8ab5b59ca81 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Wed, 12 Dec 2018 14:31:34 -0600 Subject: [PATCH 09/74] Simplify assume expressions Signed-off-by: Matt Sicker --- core/src/test/java/hudson/UtilTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/hudson/UtilTest.java b/core/src/test/java/hudson/UtilTest.java index 19bffde721..051e9b7472 100644 --- a/core/src/test/java/hudson/UtilTest.java +++ b/core/src/test/java/hudson/UtilTest.java @@ -195,7 +195,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); @@ -243,7 +243,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); From 3ee6a384849ed65c7fd1a0c7befee8491b6c72b2 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Thu, 13 Dec 2018 17:16:58 -0600 Subject: [PATCH 10/74] Add PathRemover refactoring of Util Signed-off-by: Matt Sicker --- .../jenkins/util/io/CompositeIOException.java | 59 ++++ .../java/jenkins/util/io/PathRemover.java | 179 +++++++++++ .../test/java/jenkins/util/io/FileLocker.java | 66 ++++ .../java/jenkins/util/io/PathRemoverTest.java | 284 ++++++++++++++++++ 4 files changed, 588 insertions(+) create mode 100644 core/src/main/java/jenkins/util/io/CompositeIOException.java create mode 100644 core/src/main/java/jenkins/util/io/PathRemover.java create mode 100644 core/src/test/java/jenkins/util/io/FileLocker.java create mode 100644 core/src/test/java/jenkins/util/io/PathRemoverTest.java 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..0c7a9fbbc1 --- /dev/null +++ b/core/src/main/java/jenkins/util/io/CompositeIOException.java @@ -0,0 +1,59 @@ +/* + * 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 java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.List; + +public class CompositeIOException extends IOException { + private final List exceptions; + + public CompositeIOException(String message, List exceptions) { + super(message); + this.exceptions = 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); + } + } +} 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..dd237d6745 --- /dev/null +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -0,0 +1,179 @@ +/* + * 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.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.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@Restricted(NoExternalUse.class) +public class PathRemover { + + private final RetryStrategy retryStrategy; + + public PathRemover() { + this(ignored -> false); + } + + public 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 Optional tryRemoveFile(@Nonnull Path path) { + try { + makeRemovable(path); + Files.deleteIfExists(path); + return Optional.empty(); + } catch (IOException e) { + return Optional.of(e); + } + } + + private static List tryRemoveRecursive(@Nonnull Path path) { + List accumulatedErrors = Files.isSymbolicLink(path) ? new ArrayList<>() : + tryRemoveDirectoryContents(path); + tryRemoveFile(path).ifPresent(accumulatedErrors::add); + return accumulatedErrors; + } + + private static List tryRemoveDirectoryContents(@Nonnull Path path) { + if (!Files.isDirectory(path)) return Collections.emptyList(); + List accumulatedErrors = new ArrayList<>(); + 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 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/test/java/jenkins/util/io/FileLocker.java b/core/src/test/java/jenkins/util/io/FileLocker.java new file mode 100644 index 0000000000..29b7fc7d27 --- /dev/null +++ b/core/src/test/java/jenkins/util/io/FileLocker.java @@ -0,0 +1,66 @@ +/* + * 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 java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +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 FileLocker implements AutoCloseable { + private final Map locks = new HashMap<>(); + + public synchronized AutoCloseable acquireLock(File file) throws IOException { + assertTrue(Functions.isWindows()); + assertThat(file + " is already locked.", locks, not(hasKey(file))); + AutoCloseable lock = new FileInputStream(file); + locks.put(file, lock); + return lock; + } + + public synchronized void releaseLock(File file) throws Exception { + assertThat(file + " is not locked.", locks, hasKey(file)); + locks.get(file).close(); + } + + @Override + public synchronized void close() throws Exception { + while (!locks.isEmpty()) { + releaseLock(locks.keySet().iterator().next()); + } + } +} 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..492633b907 --- /dev/null +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -0,0 +1,284 @@ +/* + * 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 java.io.File; +import java.io.FileWriter; +import java.io.IOException; +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.junit.Assert.*; +import static org.junit.Assume.assumeTrue; + +public class PathRemoverTest { + + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testForceRemoveFile() throws IOException { + File file = tmp.newFile(); + touchWithFileName(file); + + PathRemover remover = new PathRemover(); + remover.forceRemoveFile(file.toPath()); + + assertFalse(file.exists()); + } + + @Test + public void testForceRemoveFile_LockedFile() throws Exception { + assumeTrue(Functions.isWindows()); + try (FileLocker locker = new FileLocker()) { + File file = tmp.newFile(); + touchWithFileName(file); + locker.acquireLock(file); + + PathRemover remover = new PathRemover(); + expectedException.expectMessage(containsString(file.getPath())); + + remover.forceRemoveFile(file.toPath()); + } + } + + @Test + public void testForceRemoveFile_ReadOnly() throws IOException { + File dir = tmp.newFolder(); + File file = new File(dir, "file.tmp"); + touchWithFileName(file); + assertTrue(file.setWritable(false)); + assertTrue(dir.setWritable(false)); + + PathRemover remover = new PathRemover(); + remover.forceRemoveFile(file.toPath()); + + assertFalse(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 = new PathRemover(); + remover.forceRemoveFile(file.toPath()); + + assertFalse(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 = new PathRemover(); + 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); + try (FileLocker locker = new FileLocker()) { + locker.acquireLock(d1f1); + PathRemover remover = new PathRemover(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 = new PathRemover(); + remover.forceRemoveRecursive(dir.toPath()); + + assertFalse(dir.exists()); + } + + @Test + public void testForceRemoveRecursive_LockedFiles() 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); + + try (FileLocker locker = new FileLocker()) { + // Test: If we cannot delete a file, we throw + // but still deletes everything it can + // even if we are not retrying deletes. + try (AutoCloseable ignored = locker.acquireLock(d1f1)) { + PathRemover remover = new PathRemover(); + 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()); + } + + // 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); + touchWithFileName(f1, d1f1, d2f2); + locker.acquireLock(d2f2); + Object readyToUnlockSignal = new Object(); + Object readyToDeleteSignal = new Object(); + AtomicBoolean lockedFileExists = new AtomicBoolean(); + new Thread(() -> { + try { + readyToUnlockSignal.wait(); + locker.releaseLock(d2f2); + readyToDeleteSignal.notifyAll(); + } catch (Exception ignored) { + } + }).start(); + PathRemover remover = new PathRemover(retriesAttempted -> { + if (retriesAttempted == 0) { + lockedFileExists.set(d2f2.exists()); + readyToUnlockSignal.notifyAll(); + try { + readyToDeleteSignal.wait(); + return true; + } catch (InterruptedException e) { + return false; + } + } + return false; + }); + remover.forceRemoveRecursive(dir.toPath()); + assertTrue(lockedFileExists.get()); + assertFalse(dir.exists()); + } + + // An interrupt aborts the delete and makes it fail, even + // if we had been told to retry a lot. + { + mkdirs(dir, d1, d2); + touchWithFileName(f1, d1f1, d2f2); + locker.acquireLock(d1f1); + AtomicReference interrupted = new AtomicReference<>(); + AtomicReference removed = new AtomicReference<>(); + PathRemover remover = new PathRemover(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 From 2e8197a2c712fad704b60e345abef5881fe215c8 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 14 Dec 2018 10:33:18 -0600 Subject: [PATCH 11/74] Complete refactoring of Util.delete methods This removes duplicated tests from UtilTest that were added in PathRemoverTest. This also ports the sleep/gc strategy for use by default to preserve backward compatibility. Signed-off-by: Matt Sicker --- core/src/main/java/hudson/Util.java | 234 +------------- .../java/jenkins/util/io/PathRemover.java | 94 +++++- core/src/test/java/hudson/UtilTest.java | 294 ------------------ .../java/jenkins/util/io/PathRemoverTest.java | 20 +- 4 files changed, 107 insertions(+), 535 deletions(-) diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 8007200a62..1f266746aa 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; @@ -67,7 +66,6 @@ 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; @@ -247,16 +245,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)); } /** @@ -267,88 +256,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)); } /** @@ -360,137 +268,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)); } /* @@ -1797,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/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index dd237d6745..a4f7075ce8 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -24,7 +24,9 @@ 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; @@ -45,13 +47,21 @@ import java.util.Set; @Restricted(NoExternalUse.class) public class PathRemover { - private final RetryStrategy retryStrategy; - - public PathRemover() { - this(ignored -> false); + public static PathRemover newSimpleRemover() { + return new PathRemover(ignored -> false); } - public PathRemover(@Nonnull RetryStrategy retryStrategy) { + 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; } @@ -100,6 +110,80 @@ public class PathRemover { 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 * 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); + 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 { diff --git a/core/src/test/java/hudson/UtilTest.java b/core/src/test/java/hudson/UtilTest.java index 051e9b7472..36586408e5 100644 --- a/core/src/test/java/hudson/UtilTest.java +++ b/core/src/test/java/hudson/UtilTest.java @@ -27,37 +27,23 @@ 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; @@ -67,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 */ @@ -284,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()); - } - - @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; - expectedException.expectMessage(allOf( - containsString(dir.getPath()), - not(containsString("interrupted")), - containsString("Tried 2 times (of a maximum of 2)."), - not(containsString("garbage-collecting")), - not(containsString("wait")) - )); - Util.deleteContentsRecursive(dir); - assertFalse("d2 should not exist", d2.exists()); - assertFalse("f1 should not exist", f1.exists()); - assertFalse("d1f2 should not exist", d2f2.exists()); - } 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(); - expectedException.expectMessage(allOf( - containsString(dir.getPath()), - not(containsString("interrupted")), - not(containsString("maximum of")), - not(containsString("garbage-collecting")) - )); - Util.deleteRecursive(dir); - 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()); - 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")); diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 492633b907..614475a25f 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -52,7 +52,7 @@ public class PathRemoverTest { File file = tmp.newFile(); touchWithFileName(file); - PathRemover remover = new PathRemover(); + PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveFile(file.toPath()); assertFalse(file.exists()); @@ -66,7 +66,7 @@ public class PathRemoverTest { touchWithFileName(file); locker.acquireLock(file); - PathRemover remover = new PathRemover(); + PathRemover remover = PathRemover.newSimpleRemover(); expectedException.expectMessage(containsString(file.getPath())); remover.forceRemoveFile(file.toPath()); @@ -81,7 +81,7 @@ public class PathRemoverTest { assertTrue(file.setWritable(false)); assertTrue(dir.setWritable(false)); - PathRemover remover = new PathRemover(); + PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveFile(file.toPath()); assertFalse(file.exists()); @@ -93,7 +93,7 @@ public class PathRemoverTest { File file = new File(dir, "invalid.file"); assertFalse(file.exists()); - PathRemover remover = new PathRemover(); + PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveFile(file.toPath()); assertFalse(file.exists()); @@ -110,7 +110,7 @@ public class PathRemoverTest { mkdirs(d1, d2); touchWithFileName(f1, d1f1, d2f2); - PathRemover remover = new PathRemover(); + PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveDirectoryContents(dir.toPath()); assertTrue(dir.exists()); @@ -132,7 +132,7 @@ public class PathRemoverTest { touchWithFileName(f1, d1f1, d2f2); try (FileLocker locker = new FileLocker()) { locker.acquireLock(d1f1); - PathRemover remover = new PathRemover(retriesAttempted -> retriesAttempted < 1); + PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> retriesAttempted < 1); expectedException.expectMessage(allOf( containsString(dir.getPath()), containsString("Tried 1 time.") @@ -155,7 +155,7 @@ public class PathRemoverTest { mkdirs(d1, d2); touchWithFileName(f1, d1f1, d2f2); - PathRemover remover = new PathRemover(); + PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveRecursive(dir.toPath()); assertFalse(dir.exists()); @@ -178,7 +178,7 @@ public class PathRemoverTest { // but still deletes everything it can // even if we are not retrying deletes. try (AutoCloseable ignored = locker.acquireLock(d1f1)) { - PathRemover remover = new PathRemover(); + PathRemover remover = PathRemover.newSimpleRemover(); expectedException.expectMessage(containsString(dir.getPath())); remover.forceRemoveRecursive(dir.toPath()); assertTrue(dir.exists()); @@ -207,7 +207,7 @@ public class PathRemoverTest { } catch (Exception ignored) { } }).start(); - PathRemover remover = new PathRemover(retriesAttempted -> { + PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { if (retriesAttempted == 0) { lockedFileExists.set(d2f2.exists()); readyToUnlockSignal.notifyAll(); @@ -233,7 +233,7 @@ public class PathRemoverTest { locker.acquireLock(d1f1); AtomicReference interrupted = new AtomicReference<>(); AtomicReference removed = new AtomicReference<>(); - PathRemover remover = new PathRemover(retriesAttempted -> { + PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { try { TimeUnit.SECONDS.sleep(retriesAttempted + 1); return true; From e67d24c2efa0479cd363c48df6fc15eba37d1dac Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 14 Dec 2018 11:21:04 -0600 Subject: [PATCH 12/74] Fix off by one math logic Signed-off-by: Matt Sicker --- core/src/main/java/jenkins/util/io/PathRemover.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index a4f7075ce8..2902529b61 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -138,7 +138,7 @@ public class PathRemover { public boolean shouldRetry(int retriesAttempted) { if (retriesAttempted >= maxRetries) return false; gcIfEnabled(); - long delayMillis = waitBetweenRetries >= 0 ? waitBetweenRetries : -retriesAttempted * waitBetweenRetries; + long delayMillis = waitBetweenRetries >= 0 ? waitBetweenRetries : -(retriesAttempted + 1) * waitBetweenRetries; if (delayMillis <= 0) return !Thread.interrupted(); try { Thread.sleep(delayMillis); From 97f539d003cc579fc57de29011bebae1320747b6 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 14 Dec 2018 14:05:44 -0600 Subject: [PATCH 13/74] Fix off by one error in error message Signed-off-by: Matt Sicker --- core/src/main/java/jenkins/util/io/PathRemover.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index 2902529b61..5a4ed7636f 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -155,7 +155,7 @@ public class PathRemover { sb.append("Unable to delete '"); sb.append(fileToRemove); sb.append("'. Tried "); - sb.append(retryCount); + sb.append(retryCount + 1); sb.append(" time"); if (retryCount != 1) sb.append('s'); if (maxRetries > 0) { From e892a0bb6e221f4da7be152c559eb26713b08f66 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 14 Dec 2018 14:24:47 -0600 Subject: [PATCH 14/74] Add convenience constructor Signed-off-by: Matt Sicker --- .../main/java/jenkins/util/io/CompositeIOException.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/jenkins/util/io/CompositeIOException.java b/core/src/main/java/jenkins/util/io/CompositeIOException.java index 0c7a9fbbc1..61d72c5f17 100644 --- a/core/src/main/java/jenkins/util/io/CompositeIOException.java +++ b/core/src/main/java/jenkins/util/io/CompositeIOException.java @@ -24,19 +24,25 @@ package jenkins.util.io; +import javax.annotation.Nonnull; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; +import java.util.Arrays; import java.util.List; public class CompositeIOException extends IOException { private final List exceptions; - public CompositeIOException(String message, 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; } From c0201861308cbdd4707c0336471deef822a78874 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 14 Dec 2018 14:25:44 -0600 Subject: [PATCH 15/74] Revert behavior of making writable Instead of attempting to make every file or directory writable before deleting, this reverts behavior back to trying to delete before setting writable file options and retrying. Signed-off-by: Matt Sicker --- .../java/jenkins/util/io/PathRemover.java | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index 5a4ed7636f..19c004c5f0 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -43,6 +43,8 @@ import java.util.Collections; 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 { @@ -187,8 +189,7 @@ public class PathRemover { private static Optional tryRemoveFile(@Nonnull Path path) { try { - makeRemovable(path); - Files.deleteIfExists(path); + removeOrMakeRemovableThenMove(path); return Optional.empty(); } catch (IOException e) { return Optional.of(e); @@ -215,6 +216,27 @@ public class PathRemover { return accumulatedErrors; } + private static void removeOrMakeRemovableThenMove(@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); + } + } + } + } + private static void makeRemovable(@Nonnull Path path) throws IOException { if (!Files.isWritable(path)) { makeWritable(path); From b42d0fcb7d7124d25170c8f2adf7e8e0a9c7a4c3 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 14 Dec 2018 14:30:15 -0600 Subject: [PATCH 16/74] Fix typo in method name Signed-off-by: Matt Sicker --- core/src/main/java/jenkins/util/io/PathRemover.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index 19c004c5f0..ff45760984 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -189,7 +189,7 @@ public class PathRemover { private static Optional tryRemoveFile(@Nonnull Path path) { try { - removeOrMakeRemovableThenMove(path); + removeOrMakeRemovableThenRemove(path); return Optional.empty(); } catch (IOException e) { return Optional.of(e); @@ -216,7 +216,7 @@ public class PathRemover { return accumulatedErrors; } - private static void removeOrMakeRemovableThenMove(@Nonnull Path path) throws IOException { + private static void removeOrMakeRemovableThenRemove(@Nonnull Path path) throws IOException { try { Files.deleteIfExists(path); } catch (IOException e) { From 685fb49c4fb059ecbc4047f4b55697a1844a5600 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Mon, 17 Dec 2018 09:53:11 -0600 Subject: [PATCH 17/74] Ensure test thread is killed eventually Signed-off-by: Matt Sicker --- core/src/test/java/jenkins/util/io/PathRemoverTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 614475a25f..95c64443fa 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -199,14 +199,15 @@ public class PathRemoverTest { Object readyToUnlockSignal = new Object(); Object readyToDeleteSignal = new Object(); AtomicBoolean lockedFileExists = new AtomicBoolean(); - new Thread(() -> { + Thread thread = new Thread(() -> { try { readyToUnlockSignal.wait(); locker.releaseLock(d2f2); readyToDeleteSignal.notifyAll(); } catch (Exception ignored) { } - }).start(); + }); + thread.start(); PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { if (retriesAttempted == 0) { lockedFileExists.set(d2f2.exists()); @@ -221,6 +222,7 @@ public class PathRemoverTest { return false; }); remover.forceRemoveRecursive(dir.toPath()); + thread.join(); assertTrue(lockedFileExists.get()); assertFalse(dir.exists()); } From c05829c7806eb187fe48a3fac64f4df61d63d9a5 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Mon, 17 Dec 2018 11:57:04 -0600 Subject: [PATCH 18/74] Add explicit test timeout Signed-off-by: Matt Sicker --- core/src/test/java/jenkins/util/io/PathRemoverTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 95c64443fa..1e106ac0f3 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -29,6 +29,7 @@ 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; @@ -46,6 +47,7 @@ public class PathRemoverTest { @Rule public TemporaryFolder tmp = new TemporaryFolder(); @Rule public ExpectedException expectedException = ExpectedException.none(); + @Rule public Timeout timeout = new Timeout(10, TimeUnit.SECONDS); @Test public void testForceRemoveFile() throws IOException { From 592af986dce7dcd027010ea16ca4a24a62513916 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Mon, 17 Dec 2018 15:17:48 -0600 Subject: [PATCH 19/74] Clarify what this test is for Signed-off-by: Matt Sicker --- .../test/java/hudson/RemoveWindowsDirectoryJunctionTest.java | 3 +++ 1 file changed, 3 insertions(+) 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(); From 1b23a9e119e21f4330fa05c671ca3c48404bc7af Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Mon, 17 Dec 2018 15:18:17 -0600 Subject: [PATCH 20/74] Fix symlink check logic to support junctions again Signed-off-by: Matt Sicker --- core/src/main/java/hudson/Util.java | 21 +++++++------------ .../java/jenkins/util/io/PathRemover.java | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 1f266746aa..ea43cdb265 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -290,6 +290,11 @@ public class Util { * Checks if the given file represents a symlink. */ public static boolean isSymlink(@Nonnull File file) throws IOException { + return isSymlink(fileToPath(file)); + } + + @Restricted(NoExternalUse.class) + public static boolean isSymlink(@Nonnull Path path) { /* * 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. @@ -303,21 +308,11 @@ public class Util { * calling readAttributes. */ 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; - } + return path.compareTo(path.toRealPath()) != 0; } catch (NoSuchFileException e) { return false; + } catch (IOException e) { + throw new UncheckedIOException(e); } } diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index ff45760984..bc73bec8af 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -197,7 +197,7 @@ public class PathRemover { } private static List tryRemoveRecursive(@Nonnull Path path) { - List accumulatedErrors = Files.isSymbolicLink(path) ? new ArrayList<>() : + List accumulatedErrors = Util.isSymlink(path) ? new ArrayList<>() : tryRemoveDirectoryContents(path); tryRemoveFile(path).ifPresent(accumulatedErrors::add); return accumulatedErrors; From cc809df4c24a28ab976b1ef1cfe56655d54d91ee Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Mon, 17 Dec 2018 15:47:18 -0600 Subject: [PATCH 21/74] Use Files.isSymbolicLink on non-Windows platforms Signed-off-by: Matt Sicker --- core/src/main/java/hudson/Util.java | 43 +++++++++++++++-------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index ea43cdb265..0b1d143739 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -60,13 +60,10 @@ 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.DosFileAttributes; import java.nio.file.attribute.PosixFilePermissions; import java.security.DigestInputStream; import java.security.MessageDigest; @@ -295,24 +292,28 @@ public class Util { @Restricted(NoExternalUse.class) public static boolean isSymlink(@Nonnull Path path) { - /* - * 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. - */ - try { - return path.compareTo(path.toRealPath()) != 0; - } catch (NoSuchFileException e) { - return false; - } catch (IOException e) { - throw new UncheckedIOException(e); + if (Functions.isWindows()) { + /* + * 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. + */ + try { + return path.compareTo(path.toRealPath()) != 0; + } catch (NoSuchFileException ignored) { + return false; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } else { + return Files.isSymbolicLink(path); } } From 41fbaeeb8e97684984d576d3c3c2a3f295456e21 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Mon, 17 Dec 2018 16:06:54 -0600 Subject: [PATCH 22/74] Break up testForceRemoveRecursive_LockedFiles into logical tests Signed-off-by: Matt Sicker --- .../test/java/jenkins/util/io/FileLocker.java | 3 +- .../java/jenkins/util/io/PathRemoverTest.java | 177 +++++++++--------- 2 files changed, 94 insertions(+), 86 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/FileLocker.java b/core/src/test/java/jenkins/util/io/FileLocker.java index 29b7fc7d27..9d2a9b2b56 100644 --- a/core/src/test/java/jenkins/util/io/FileLocker.java +++ b/core/src/test/java/jenkins/util/io/FileLocker.java @@ -44,12 +44,11 @@ import static org.junit.Assert.assertTrue; public class FileLocker implements AutoCloseable { private final Map locks = new HashMap<>(); - public synchronized AutoCloseable acquireLock(File file) throws IOException { + public synchronized void acquireLock(File file) throws IOException { assertTrue(Functions.isWindows()); assertThat(file + " is already locked.", locks, not(hasKey(file))); AutoCloseable lock = new FileInputStream(file); locks.put(file, lock); - return lock; } public synchronized void releaseLock(File file) throws Exception { diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 1e106ac0f3..a66700bfac 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -164,7 +164,7 @@ public class PathRemoverTest { } @Test - public void testForceRemoveRecursive_LockedFiles() throws Exception { + public void testForceRemoveRecursive_DeletesAsMuchAsPossibleWithLockedFiles() throws Exception { assumeTrue(Functions.isWindows()); File dir = tmp.newFolder(); File d1 = new File(dir, "d1"); @@ -176,97 +176,106 @@ public class PathRemoverTest { touchWithFileName(f1, d1f1, d2f2); try (FileLocker locker = new FileLocker()) { - // Test: If we cannot delete a file, we throw - // but still deletes everything it can - // even if we are not retrying deletes. - try (AutoCloseable ignored = 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()); - } + 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()); + } + } - // 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); - touchWithFileName(f1, d1f1, d2f2); - locker.acquireLock(d2f2); - Object readyToUnlockSignal = new Object(); - Object readyToDeleteSignal = new Object(); - AtomicBoolean lockedFileExists = new AtomicBoolean(); - Thread thread = new Thread(() -> { + @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); + try (FileLocker locker = new FileLocker()) { + locker.acquireLock(d2f2); + Object readyToUnlockSignal = new Object(); + Object readyToDeleteSignal = new Object(); + AtomicBoolean lockedFileExists = new AtomicBoolean(); + Thread thread = new Thread(() -> { + try { + readyToUnlockSignal.wait(); + locker.releaseLock(d2f2); + readyToDeleteSignal.notifyAll(); + } catch (Exception ignored) { + } + }); + thread.start(); + PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { + if (retriesAttempted == 0) { + lockedFileExists.set(d2f2.exists()); + readyToUnlockSignal.notifyAll(); try { - readyToUnlockSignal.wait(); - locker.releaseLock(d2f2); - readyToDeleteSignal.notifyAll(); - } catch (Exception ignored) { - } - }); - thread.start(); - PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { - if (retriesAttempted == 0) { - lockedFileExists.set(d2f2.exists()); - readyToUnlockSignal.notifyAll(); - try { - readyToDeleteSignal.wait(); - return true; - } catch (InterruptedException e) { - return false; - } - } - return false; - }); - remover.forceRemoveRecursive(dir.toPath()); - thread.join(); - assertTrue(lockedFileExists.get()); - assertFalse(dir.exists()); - } - - // An interrupt aborts the delete and makes it fail, even - // if we had been told to retry a lot. - { - mkdirs(dir, 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); + readyToDeleteSignal.wait(); 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()); - } + } + 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); + try (FileLocker locker = new FileLocker()) { + 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) { From f71bf079932fd5f3c5cac1c505c46542809d42f9 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Tue, 18 Dec 2018 10:46:09 -0600 Subject: [PATCH 23/74] Refactor FileLocker into JUnit rule Also fixes a bug where locked files are not cleaned up in the cleanup. Signed-off-by: Matt Sicker --- .../jenkins/util/io/CompositeIOException.java | 9 + .../{FileLocker.java => FileLockerRule.java} | 41 ++-- .../java/jenkins/util/io/PathRemoverTest.java | 177 +++++++++--------- 3 files changed, 121 insertions(+), 106 deletions(-) rename core/src/test/java/jenkins/util/io/{FileLocker.java => FileLockerRule.java} (63%) diff --git a/core/src/main/java/jenkins/util/io/CompositeIOException.java b/core/src/main/java/jenkins/util/io/CompositeIOException.java index 61d72c5f17..dfd347cbd6 100644 --- a/core/src/main/java/jenkins/util/io/CompositeIOException.java +++ b/core/src/main/java/jenkins/util/io/CompositeIOException.java @@ -24,13 +24,18 @@ 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; @@ -62,4 +67,8 @@ public class CompositeIOException extends IOException { exception.printStackTrace(s); } } + + public UncheckedIOException asUncheckedIOException() { + return new UncheckedIOException(this); + } } diff --git a/core/src/test/java/jenkins/util/io/FileLocker.java b/core/src/test/java/jenkins/util/io/FileLockerRule.java similarity index 63% rename from core/src/test/java/jenkins/util/io/FileLocker.java rename to core/src/test/java/jenkins/util/io/FileLockerRule.java index 9d2a9b2b56..df0dab652e 100644 --- a/core/src/test/java/jenkins/util/io/FileLocker.java +++ b/core/src/test/java/jenkins/util/io/FileLockerRule.java @@ -25,11 +25,17 @@ 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; @@ -41,25 +47,34 @@ 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 FileLocker implements AutoCloseable { - private final Map locks = new HashMap<>(); +public class FileLockerRule extends ExternalResource { + private final Map locks = new HashMap<>(); - public synchronized void acquireLock(File file) throws IOException { + @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))); - AutoCloseable lock = new FileInputStream(file); + Closeable lock = new FileInputStream(file); locks.put(file, lock); } - public synchronized void releaseLock(File file) throws Exception { + public synchronized void releaseLock(@Nonnull File file) throws Exception { + assertTrue(Functions.isWindows()); assertThat(file + " is not locked.", locks, hasKey(file)); - locks.get(file).close(); - } - - @Override - public synchronized void close() throws Exception { - while (!locks.isEmpty()) { - releaseLock(locks.keySet().iterator().next()); - } + 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 index a66700bfac..40c1f30582 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -48,6 +48,7 @@ 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 { @@ -63,16 +64,14 @@ public class PathRemoverTest { @Test public void testForceRemoveFile_LockedFile() throws Exception { assumeTrue(Functions.isWindows()); - try (FileLocker locker = new FileLocker()) { - File file = tmp.newFile(); - touchWithFileName(file); - locker.acquireLock(file); + File file = tmp.newFile(); + touchWithFileName(file); + locker.acquireLock(file); - PathRemover remover = PathRemover.newSimpleRemover(); - expectedException.expectMessage(containsString(file.getPath())); + PathRemover remover = PathRemover.newSimpleRemover(); + expectedException.expectMessage(containsString(file.getPath())); - remover.forceRemoveFile(file.toPath()); - } + remover.forceRemoveFile(file.toPath()); } @Test @@ -132,18 +131,16 @@ public class PathRemoverTest { File d2f2 = new File(d2, "d1f2"); mkdirs(d1, d2); touchWithFileName(f1, d1f1, d2f2); - try (FileLocker locker = new FileLocker()) { - 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()); - } + 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 @@ -175,18 +172,16 @@ public class PathRemoverTest { mkdirs(d1, d2); touchWithFileName(f1, d1f1, d2f2); - try (FileLocker locker = new FileLocker()) { - 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()); - } + 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 @@ -200,38 +195,36 @@ public class PathRemoverTest { File d2f2 = new File(d2, "d1f2"); mkdirs(d1, d2); touchWithFileName(f1, d1f1, d2f2); - try (FileLocker locker = new FileLocker()) { - locker.acquireLock(d2f2); - Object readyToUnlockSignal = new Object(); - Object readyToDeleteSignal = new Object(); - AtomicBoolean lockedFileExists = new AtomicBoolean(); - Thread thread = new Thread(() -> { + locker.acquireLock(d2f2); + Object readyToUnlockSignal = new Object(); + Object readyToDeleteSignal = new Object(); + AtomicBoolean lockedFileExists = new AtomicBoolean(); + Thread thread = new Thread(() -> { + try { + readyToUnlockSignal.wait(); + locker.releaseLock(d2f2); + readyToDeleteSignal.notifyAll(); + } catch (Exception ignored) { + } + }); + thread.start(); + PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { + if (retriesAttempted == 0) { + lockedFileExists.set(d2f2.exists()); + readyToUnlockSignal.notifyAll(); try { - readyToUnlockSignal.wait(); - locker.releaseLock(d2f2); - readyToDeleteSignal.notifyAll(); - } catch (Exception ignored) { + readyToDeleteSignal.wait(); + return true; + } catch (InterruptedException e) { + return false; } - }); - thread.start(); - PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { - if (retriesAttempted == 0) { - lockedFileExists.set(d2f2.exists()); - readyToUnlockSignal.notifyAll(); - try { - readyToDeleteSignal.wait(); - return true; - } catch (InterruptedException e) { - return false; - } - } - return false; - }); - remover.forceRemoveRecursive(dir.toPath()); - thread.join(); - assertTrue(lockedFileExists.get()); - assertFalse(dir.exists()); - } + } + return false; + }); + remover.forceRemoveRecursive(dir.toPath()); + thread.join(); + assertTrue(lockedFileExists.get()); + assertFalse(dir.exists()); } @Test @@ -245,37 +238,35 @@ public class PathRemoverTest { File d2f2 = new File(d2, "d1f2"); mkdirs(d1, d2); touchWithFileName(f1, d1f1, d2f2); - try (FileLocker locker = new FileLocker()) { - 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()); - } + 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) { From 55b8911c5db17485e82b2a4f1a58820ade765924 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Tue, 18 Dec 2018 10:57:59 -0600 Subject: [PATCH 24/74] Use CountDownLatch instead of Object Signed-off-by: Matt Sicker --- .../test/java/jenkins/util/io/PathRemoverTest.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 40c1f30582..37d4d0679b 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -34,6 +34,7 @@ import org.junit.rules.Timeout; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -196,14 +197,14 @@ public class PathRemoverTest { mkdirs(d1, d2); touchWithFileName(f1, d1f1, d2f2); locker.acquireLock(d2f2); - Object readyToUnlockSignal = new Object(); - Object readyToDeleteSignal = new Object(); + CountDownLatch unlockLatch = new CountDownLatch(1); + CountDownLatch deleteLatch = new CountDownLatch(1); AtomicBoolean lockedFileExists = new AtomicBoolean(); Thread thread = new Thread(() -> { try { - readyToUnlockSignal.wait(); + unlockLatch.await(); locker.releaseLock(d2f2); - readyToDeleteSignal.notifyAll(); + deleteLatch.countDown(); } catch (Exception ignored) { } }); @@ -211,9 +212,9 @@ public class PathRemoverTest { PathRemover remover = PathRemover.newRemoverWithStrategy(retriesAttempted -> { if (retriesAttempted == 0) { lockedFileExists.set(d2f2.exists()); - readyToUnlockSignal.notifyAll(); + unlockLatch.countDown(); try { - readyToDeleteSignal.wait(); + deleteLatch.await(); return true; } catch (InterruptedException e) { return false; From d5c93400410db87dfb8f8c22537a075993160bfe Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Tue, 18 Dec 2018 11:13:25 -0600 Subject: [PATCH 25/74] Improve assertions Signed-off-by: Matt Sicker --- .../java/jenkins/util/io/PathRemoverTest.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 37d4d0679b..2ea2ebf5ee 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -34,6 +34,7 @@ import org.junit.rules.Timeout; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -59,7 +60,7 @@ public class PathRemoverTest { PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveFile(file.toPath()); - assertFalse(file.exists()); + assertFalse("Unable to delete file: " + file, file.exists()); } @Test @@ -70,7 +71,7 @@ public class PathRemoverTest { locker.acquireLock(file); PathRemover remover = PathRemover.newSimpleRemover(); - expectedException.expectMessage(containsString(file.getPath())); + expectedException.expect(IOException.class); remover.forceRemoveFile(file.toPath()); } @@ -80,13 +81,15 @@ public class PathRemoverTest { File dir = tmp.newFolder(); File file = new File(dir, "file.tmp"); touchWithFileName(file); - assertTrue(file.setWritable(false)); - assertTrue(dir.setWritable(false)); + file.setWritable(false); + dir.setWritable(false); + assertFalse("Unable to make file read-only: " + file, Files.isWritable(file.toPath())); + assertFalse("Unable to make directory read-only: " + dir, Files.isWritable(dir.toPath())); PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveFile(file.toPath()); - assertFalse(file.exists()); + assertFalse("Unable to delete file: " + file, file.exists()); } @Test @@ -98,7 +101,7 @@ public class PathRemoverTest { PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveFile(file.toPath()); - assertFalse(file.exists()); + assertFalse("Unable to delete file: " + file, file.exists()); } @Test From dfb6d1d41c3a8284806baa86e162e919bbfbc903 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Tue, 18 Dec 2018 11:35:07 -0600 Subject: [PATCH 26/74] Improve test robustness Signed-off-by: Matt Sicker --- .../java/jenkins/util/io/PathRemoverTest.java | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 2ea2ebf5ee..4d176b49a1 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -34,7 +34,14 @@ import org.junit.rules.Timeout; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.UncheckedIOException; 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.Arrays; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -78,18 +85,35 @@ public class PathRemoverTest { @Test public void testForceRemoveFile_ReadOnly() throws IOException { - File dir = tmp.newFolder(); - File file = new File(dir, "file.tmp"); - touchWithFileName(file); - file.setWritable(false); - dir.setWritable(false); - assertFalse("Unable to make file read-only: " + file, Files.isWritable(file.toPath())); - assertFalse("Unable to make directory read-only: " + dir, Files.isWritable(dir.toPath())); + Path dir = Files.createTempDirectory("remover"); + Path file = dir.resolve("file.tmp"); + touchWithFileName(file.toFile()); + makeReadOnly(file); + makeReadOnly(dir); + assertFalse("Unable to make file read-only: " + file, Files.isWritable(file)); + assertFalse("Unable to make directory read-only: " + dir, Files.isWritable(dir)); PathRemover remover = PathRemover.newSimpleRemover(); - remover.forceRemoveFile(file.toPath()); + remover.forceRemoveFile(file); - assertFalse("Unable to delete file: " + file, file.exists()); + assertTrue("Unable to delete file: " + file, Files.notExists(file)); + } + + private static void makeReadOnly(Path path) { + try { + PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); + Set permissions = attrs.permissions(); + permissions.removeAll(Arrays.asList(PosixFilePermission.OWNER_WRITE, PosixFilePermission.GROUP_WRITE, PosixFilePermission.OTHERS_WRITE)); + Files.setPosixFilePermissions(path, permissions); + } catch (NoSuchFileException ignored) { + return; + } catch (UnsupportedOperationException ignored) { + // retry with setWritable + } catch (IOException e) { + throw new UncheckedIOException(e); + } + // fallback method for filesystems that don't support POSIX attributes + path.toFile().setWritable(false); } @Test From 323b106ffeb902030af1b39d0c5f5c089c9096dc Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Tue, 18 Dec 2018 11:40:42 -0600 Subject: [PATCH 27/74] Revert "Improve test robustness" This reverts commit dfb6d1d4 Signed-off-by: Matt Sicker --- .../java/jenkins/util/io/PathRemoverTest.java | 42 ++++--------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 4d176b49a1..2ea2ebf5ee 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -34,14 +34,7 @@ import org.junit.rules.Timeout; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.io.UncheckedIOException; 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.Arrays; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -85,35 +78,18 @@ public class PathRemoverTest { @Test public void testForceRemoveFile_ReadOnly() throws IOException { - Path dir = Files.createTempDirectory("remover"); - Path file = dir.resolve("file.tmp"); - touchWithFileName(file.toFile()); - makeReadOnly(file); - makeReadOnly(dir); - assertFalse("Unable to make file read-only: " + file, Files.isWritable(file)); - assertFalse("Unable to make directory read-only: " + dir, Files.isWritable(dir)); + File dir = tmp.newFolder(); + File file = new File(dir, "file.tmp"); + touchWithFileName(file); + file.setWritable(false); + dir.setWritable(false); + assertFalse("Unable to make file read-only: " + file, Files.isWritable(file.toPath())); + assertFalse("Unable to make directory read-only: " + dir, Files.isWritable(dir.toPath())); PathRemover remover = PathRemover.newSimpleRemover(); - remover.forceRemoveFile(file); + remover.forceRemoveFile(file.toPath()); - assertTrue("Unable to delete file: " + file, Files.notExists(file)); - } - - private static void makeReadOnly(Path path) { - try { - PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); - Set permissions = attrs.permissions(); - permissions.removeAll(Arrays.asList(PosixFilePermission.OWNER_WRITE, PosixFilePermission.GROUP_WRITE, PosixFilePermission.OTHERS_WRITE)); - Files.setPosixFilePermissions(path, permissions); - } catch (NoSuchFileException ignored) { - return; - } catch (UnsupportedOperationException ignored) { - // retry with setWritable - } catch (IOException e) { - throw new UncheckedIOException(e); - } - // fallback method for filesystems that don't support POSIX attributes - path.toFile().setWritable(false); + assertFalse("Unable to delete file: " + file, file.exists()); } @Test From 24da0712ccd0637e6a270b2e8babf3e4a2174729 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Tue, 18 Dec 2018 11:43:47 -0600 Subject: [PATCH 28/74] Simplify test Signed-off-by: Matt Sicker --- core/src/test/java/jenkins/util/io/PathRemoverTest.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 2ea2ebf5ee..75c9f8ff9b 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -34,7 +34,6 @@ import org.junit.rules.Timeout; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.nio.file.Files; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -81,10 +80,7 @@ public class PathRemoverTest { File dir = tmp.newFolder(); File file = new File(dir, "file.tmp"); touchWithFileName(file); - file.setWritable(false); - dir.setWritable(false); - assertFalse("Unable to make file read-only: " + file, Files.isWritable(file.toPath())); - assertFalse("Unable to make directory read-only: " + dir, Files.isWritable(dir.toPath())); + assertTrue("Unable to make file read-only: " + file, file.setWritable(false)); PathRemover remover = PathRemover.newSimpleRemover(); remover.forceRemoveFile(file.toPath()); From e4937bbb7869e6c33bef1eea02bb5a07a9f03fc4 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Tue, 18 Dec 2018 14:33:16 -0600 Subject: [PATCH 29/74] Revert test logic back to original Signed-off-by: Matt Sicker --- .../java/jenkins/util/io/PathRemoverTest.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 75c9f8ff9b..22c70a1137 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -34,6 +34,9 @@ import org.junit.rules.Timeout; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.nio.file.FileSystemException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -41,6 +44,7 @@ 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; @@ -70,9 +74,22 @@ public class PathRemoverTest { locker.acquireLock(file); PathRemover remover = PathRemover.newSimpleRemover(); - expectedException.expect(IOException.class); + 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(file.getPath())); + } - remover.forceRemoveFile(file.toPath()); + } + + private static List> calcExceptionHierarchy(Throwable t) { + List> hierarchy = new ArrayList<>(); + for (; t != null; t = t.getCause()) { + hierarchy.add(t.getClass()); + } + return hierarchy; } @Test From 729e10b49ae930651371c17b73f813d30405a897 Mon Sep 17 00:00:00 2001 From: Ramon Leon Date: Wed, 19 Dec 2018 09:32:23 +0100 Subject: [PATCH 30/74] Pin ath revision to latest --- essentials.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/essentials.yml b/essentials.yml index 63d7908fec..03219b402b 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: "4e295015ce6b4d5c7a4617bb99dc2968e2436634" + athImage: "jenkins/ath@sha256:284c2fdfaf1a51e95783126367c0a88c07af83ff9234063d12ba3001959628e8" categories: - org.jenkinsci.test.acceptance.junit.SmokeTest From ae89a29bb39472c70a1a5adaf6182acbfe23d82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Thu, 20 Dec 2018 11:49:38 +0100 Subject: [PATCH 31/74] Bump ATH revision once more --- essentials.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/essentials.yml b/essentials.yml index 03219b402b..66b3542247 100644 --- a/essentials.yml +++ b/essentials.yml @@ -1,7 +1,7 @@ --- ath: useLocalSnapshots: false - athRevision: "4e295015ce6b4d5c7a4617bb99dc2968e2436634" + athRevision: "8c3126005c0d5394ed225a5c9f3eab99ab385afe" athImage: "jenkins/ath@sha256:284c2fdfaf1a51e95783126367c0a88c07af83ff9234063d12ba3001959628e8" categories: - org.jenkinsci.test.acceptance.junit.SmokeTest From 89fcbb623d66e118069d8e3bcf955848020ae0da Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Thu, 6 Dec 2018 13:56:57 -0800 Subject: [PATCH 32/74] JENKINS-55062 - set resURL and rootURL for restarting template to load from the right places --- .../main/resources/hudson/util/HudsonIsLoading/index.jelly | 6 ++++-- .../resources/hudson/util/HudsonIsRestarting/index.jelly | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly b/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly index 75157ee49a..b3d162ac2b 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/HudsonIsRestarting/index.jelly b/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly index aafd2cfc60..100a50235f 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)} From 285c979643c4f9ce71190072d236c8c8a0dc50c6 Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Thu, 13 Dec 2018 15:13:24 -0800 Subject: [PATCH 33/74] [JENKINS-55062] Add tests to confirm res and root urls are now set --- .../java/hudson/util/HudsonIsLoading.java | 32 +++++++++++++++++++ .../java/hudson/util/HudsonIsRestarting.java | 32 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 test/src/test/java/hudson/util/HudsonIsLoading.java create mode 100644 test/src/test/java/hudson/util/HudsonIsRestarting.java diff --git a/test/src/test/java/hudson/util/HudsonIsLoading.java b/test/src/test/java/hudson/util/HudsonIsLoading.java new file mode 100644 index 0000000000..10c11adfbf --- /dev/null +++ b/test/src/test/java/hudson/util/HudsonIsLoading.java @@ -0,0 +1,32 @@ +package hudson.util; + +import com.gargoylesoftware.htmlunit.Page; +import org.hamcrest.CoreMatchers; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class HudsonIsLoading { + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + @Issue("JENKINS-55062") + public void withPrefix() throws Exception { + j.jenkins.servletContext.setAttribute("app", new HudsonIsLoading()); + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); // this is a failure page already + wc.getOptions().setJavaScriptEnabled(false); + + Page p = wc.goTo("", "text/html"); + assertTrue( p.isHtmlPage() ); + String body = p.getWebResponse().getContentAsString(); + assertThat(body, CoreMatchers.containsString("data-resurl=\"")); + assertThat(body, CoreMatchers.containsString("data-rooturl=\"")); + assertThat(body, CoreMatchers.containsString("resURL=\"")); + } +} diff --git a/test/src/test/java/hudson/util/HudsonIsRestarting.java b/test/src/test/java/hudson/util/HudsonIsRestarting.java new file mode 100644 index 0000000000..3b0d8f76b2 --- /dev/null +++ b/test/src/test/java/hudson/util/HudsonIsRestarting.java @@ -0,0 +1,32 @@ +package hudson.util; + +import com.gargoylesoftware.htmlunit.Page; +import org.hamcrest.CoreMatchers; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.junit.Assert.*; + +public class HudsonIsRestarting { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + @Issue("JENKINS-55062") + public void withPrefix() throws Exception { + j.jenkins.servletContext.setAttribute("app", new HudsonIsRestarting()); + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); // this is a failure page already + wc.getOptions().setJavaScriptEnabled(false); + + Page p = wc.goTo("", "text/html"); + assertTrue( p.isHtmlPage() ); + String body = p.getWebResponse().getContentAsString(); + assertThat(body, CoreMatchers.containsString("data-resurl=\"")); + assertThat(body, CoreMatchers.containsString("data-rooturl=\"")); + assertThat(body, CoreMatchers.containsString("resURL=\"")); + } +} From 9e77fde2bebf6452db67b7acc25f3f2d154d351a Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Fri, 14 Dec 2018 14:48:11 -0800 Subject: [PATCH 34/74] remove incorrect comment that was copy and pasted --- core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly | 2 +- .../main/resources/hudson/util/HudsonIsRestarting/index.jelly | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly b/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly index b3d162ac2b..2ed44ba863 100644 --- a/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly +++ b/core/src/main/resources/hudson/util/HudsonIsLoading/index.jelly @@ -25,7 +25,7 @@ THE SOFTWARE. - + diff --git a/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly b/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly index 100a50235f..65f30b5d94 100644 --- a/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly +++ b/core/src/main/resources/hudson/util/HudsonIsRestarting/index.jelly @@ -25,7 +25,7 @@ THE SOFTWARE. - + From baba9532588aa5fffbba5535116ceee061b58aa7 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Thu, 20 Dec 2018 15:52:00 -0600 Subject: [PATCH 35/74] Add missing exception Without this, no exception would be thrown when a file cannot be deleted, but an exception would be thrown when a directory cannot be deleted. Signed-off-by: Matt Sicker --- core/src/main/java/jenkins/util/io/PathRemover.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index bc73bec8af..e3f1c56594 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -233,6 +233,7 @@ public class PathRemover { } throw new CompositeIOException("Unable to remove directory " + path + " with directory contents: " + entries, e, e2); } + throw new CompositeIOException("Unable to remove file " + path, e, e2); } } } From 15be8b0c1e90fb0e672b21e0f0e1b140e0d3722d Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Thu, 20 Dec 2018 15:54:16 -0600 Subject: [PATCH 36/74] Make test platform independent via Mockito Signed-off-by: Matt Sicker --- .../java/jenkins/util/io/PathRemoverTest.java | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/core/src/test/java/jenkins/util/io/PathRemoverTest.java b/core/src/test/java/jenkins/util/io/PathRemoverTest.java index 22c70a1137..55c7970424 100644 --- a/core/src/test/java/jenkins/util/io/PathRemoverTest.java +++ b/core/src/test/java/jenkins/util/io/PathRemoverTest.java @@ -34,7 +34,11 @@ 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; @@ -47,6 +51,8 @@ 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 { @@ -68,10 +74,22 @@ public class PathRemoverTest { @Test public void testForceRemoveFile_LockedFile() throws Exception { - assumeTrue(Functions.isWindows()); - File file = tmp.newFile(); - touchWithFileName(file); - locker.acquireLock(file); + 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 { @@ -79,7 +97,7 @@ public class PathRemoverTest { fail("Should not have been deleted: " + file); } catch (IOException e) { assertThat(calcExceptionHierarchy(e), hasItem(FileSystemException.class)); - assertThat(e.getMessage(), containsString(file.getPath())); + assertThat(e.getMessage(), containsString(filename)); } } @@ -88,6 +106,11 @@ public class PathRemoverTest { 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; } From 960dfcc0c6f89a08840254bb9d06d8037763ec5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Fri, 21 Dec 2018 08:35:56 +0100 Subject: [PATCH 37/74] rebuild please From 1e82509bcc696ac6d308268a6aefc2597fe0cafe Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 21 Dec 2018 09:15:24 -0600 Subject: [PATCH 38/74] Use mutable list in mutable context Signed-off-by: Matt Sicker --- core/src/main/java/jenkins/util/io/PathRemover.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java index e3f1c56594..763f8f8589 100644 --- a/core/src/main/java/jenkins/util/io/PathRemover.java +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -39,7 +39,6 @@ import java.nio.file.Path; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; @@ -204,8 +203,8 @@ public class PathRemover { } private static List tryRemoveDirectoryContents(@Nonnull Path path) { - if (!Files.isDirectory(path)) return Collections.emptyList(); List accumulatedErrors = new ArrayList<>(); + if (!Files.isDirectory(path)) return accumulatedErrors; try (DirectoryStream children = Files.newDirectoryStream(path)) { for (Path child : children) { accumulatedErrors.addAll(tryRemoveRecursive(child)); From c453b9228f9653eca3b23c0ad216c5d00b05bf7c Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 21 Dec 2018 09:18:35 -0600 Subject: [PATCH 39/74] Update Util.isSymlink commentary Signed-off-by: Matt Sicker --- core/src/main/java/hudson/Util.java | 37 ++++++++++------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 0b1d143739..a48db3106e 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -60,7 +60,6 @@ import java.nio.file.FileSystemException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.InvalidPathException; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermission; @@ -284,7 +283,9 @@ 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)); @@ -292,28 +293,16 @@ public class Util { @Restricted(NoExternalUse.class) public static boolean isSymlink(@Nonnull Path path) { - if (Functions.isWindows()) { - /* - * 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. - */ - try { - return path.compareTo(path.toRealPath()) != 0; - } catch (NoSuchFileException ignored) { - return false; - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } else { - return Files.isSymbolicLink(path); + if (Files.isSymbolicLink(path)) return true; + /* + 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 { + return path.toAbsolutePath().compareTo(path.toRealPath()) != 0; + } catch (IOException ignored) { + return false; } } From f254f5d876521ad1068912e161ea0a49cc9e59d9 Mon Sep 17 00:00:00 2001 From: Abhyudaya Sharma Date: Fri, 21 Dec 2018 21:57:21 +0530 Subject: [PATCH 40/74] Fix build on Windows 10 --- core/src/test/java/hudson/scheduler/CronTabTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/hudson/scheduler/CronTabTest.java b/core/src/test/java/hudson/scheduler/CronTabTest.java index 66894c3546..545f457a9b 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("Australia/Sydney")); + calendar.set(2013, Calendar.APRIL, 3, 11, i, 0); + if (tabs.check(calendar)) { times.add(i); } } From a60f37ec134405dd8508a03fc09c3463f60c07cd Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Fri, 21 Dec 2018 15:00:13 -0500 Subject: [PATCH 41/74] Add space after ETA: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit n.b. The French Localizer already did this Old: (pending—Build #3,585 is already in progress (ETA:N/A)) New: (pending—Build #3,585 is already in progress (ETA: N/A)) --- core/src/main/resources/jenkins/model/Messages.properties | 2 +- core/src/main/resources/jenkins/model/Messages_da.properties | 2 +- core/src/main/resources/jenkins/model/Messages_de.properties | 2 +- core/src/main/resources/jenkins/model/Messages_es.properties | 2 +- core/src/main/resources/jenkins/model/Messages_it.properties | 2 +- core/src/main/resources/jenkins/model/Messages_ja.properties | 2 +- core/src/main/resources/jenkins/model/Messages_lt.properties | 2 +- core/src/main/resources/jenkins/model/Messages_nl.properties | 2 +- core/src/main/resources/jenkins/model/Messages_pt_BR.properties | 2 +- core/src/main/resources/jenkins/model/Messages_ru.properties | 2 +- core/src/main/resources/jenkins/model/Messages_sr.properties | 2 +- core/src/main/resources/jenkins/model/Messages_tr.properties | 2 +- core/src/main/resources/jenkins/model/Messages_zh_TW.properties | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) 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_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 From 756ce57ac77d7a7a2524a2b47b470d912a294976 Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Fri, 28 Dec 2018 11:14:40 +0530 Subject: [PATCH 42/74] Use UTC instead of "Australia/Sydney" Co-Authored-By: AbhyudayaSharma <11471599+AbhyudayaSharma@users.noreply.github.com> --- core/src/test/java/hudson/scheduler/CronTabTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/java/hudson/scheduler/CronTabTest.java b/core/src/test/java/hudson/scheduler/CronTabTest.java index 545f457a9b..65115091a7 100644 --- a/core/src/test/java/hudson/scheduler/CronTabTest.java +++ b/core/src/test/java/hudson/scheduler/CronTabTest.java @@ -308,7 +308,7 @@ 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++) { - GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("Australia/Sydney")); + GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); calendar.set(2013, Calendar.APRIL, 3, 11, i, 0); if (tabs.check(calendar)) { times.add(i); From 1ab11ea121749d41416ac4d0a27ace9f479c886e Mon Sep 17 00:00:00 2001 From: Ivan Fernandez Calvo Date: Fri, 28 Dec 2018 14:47:09 +0100 Subject: [PATCH 43/74] [JENKINS-47458][JENKINS-55133] update trilead-ssh2 version --- core/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/pom.xml b/core/pom.xml index b6d8645735..f8fb4b82df 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 From 1f41576449abeae08053f726effb01bb7fe48aff Mon Sep 17 00:00:00 2001 From: Damian Szczepanik Date: Fri, 21 Dec 2018 00:10:09 +0100 Subject: [PATCH 44/74] Adds Polish translations - Provides translations mostly for SetupWizard - Makes title of error panel translatable ### Proposed changelog entries ### Submitter checklist - [ ] JIRA issue is well described - [ ] Changelog entry appropriate for the audience affected by the change (users or developer, depending on the change). - [ ] Appropriate autotests or explanation to why this change has no tests - [ ] For dependency updates: links to external changelogs and, if possible, full diffs ### Desired reviewers @batmat --- .../util/HudsonIsLoading/index_pl.properties | 23 +++++++++++++++++- ...setupWizardConfigureInstance_pl.properties | 24 +++++++++++++++++++ .../install/pluginSetupWizard.properties | 23 ++++++++++++++++++ .../install/pluginSetupWizard_pl.properties | 6 ++++- .../jenkins/model/Jenkins/login_pl.properties | 23 +++++++++++++++++- war/src/main/js/templates/errorPanel.hbs | 2 +- 6 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 core/src/main/resources/jenkins/install/SetupWizard/setupWizardConfigureInstance_pl.properties 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/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..bbcdb392d2 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-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 @@ -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 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..a9039c6c40 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/login_pl.properties +++ b/core/src/main/resources/jenkins/model/Jenkins/login_pl.properties @@ -1,4 +1,24 @@ -# 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. Password=Has\u0142o Remember\ me\ on\ this\ computer=Zapami\u0119taj mnie na tym komputerze @@ -6,3 +26,4 @@ User=Login login=Zaloguj signUp=Za\u0142\u00F3\u017C konto je\u015Bli nie jeste\u015B jeszcze uczestnikiem. +signIn=Rejestracja diff --git a/war/src/main/js/templates/errorPanel.hbs b/war/src/main/js/templates/errorPanel.hbs index 42cb0a69f1..78c496db22 100644 --- a/war/src/main/js/templates/errorPanel.hbs +++ b/war/src/main/js/templates/errorPanel.hbs @@ -1,5 +1,5 @@
* *

* While an implementation might store its actual configuration data in various ways, From b8a619aa888a2366695fbc20de215b600d3b3ba0 Mon Sep 17 00:00:00 2001 From: Damian Szczepanik Date: Sat, 5 Jan 2019 23:11:27 +0100 Subject: [PATCH 51/74] Adds Polish translations --- .../hudson/model/AllView/noJob_pl.properties | 4 +-- .../hudson/model/Messages_pl.properties | 3 +- .../hudson/model/MyView/noJob_pl.properties | 23 +++++++++++++++ .../hudson/model/User/index_pl.properties | 26 +++++++++++++++-- .../model/View/newJobButtonBar_pl.properties | 23 +++++++++++++++ .../hudson/tasks/Messages_pl.properties | 3 +- .../message_pl.properties | 29 +++++++++++++++++++ .../install/pluginSetupWizard_pl.properties | 6 +++- .../simple-header_pl.properties | 23 +++++++++++++++ .../jenkins/model/Jenkins/login_pl.properties | 13 ++++----- .../model/Jenkins/newView_pl.properties | 4 ++- .../jenkins/model/Messages_pl.properties | 3 +- .../lib/hudson/iconSize_pl.properties | 5 +++- .../lib/hudson/newFromList/form_pl.properties | 3 +- .../config-customWorkspace_pl.properties | 24 +++++++++++++-- 15 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 core/src/main/resources/hudson/model/MyView/noJob_pl.properties create mode 100644 core/src/main/resources/hudson/model/View/newJobButtonBar_pl.properties create mode 100644 core/src/main/resources/jenkins/diagnostics/RootUrlNotSetMonitor/message_pl.properties create mode 100644 core/src/main/resources/jenkins/model/DefaultSimplePageDecorator/simple-header_pl.properties 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/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/pluginSetupWizard_pl.properties b/core/src/main/resources/jenkins/install/pluginSetupWizard_pl.properties index bbcdb392d2..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-2018, 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 @@ -80,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 a9039c6c40..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,6 +1,6 @@ # The MIT License # -# Copyright (c) 2013-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi +# 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 @@ -21,9 +21,8 @@ # 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. - -signIn=Rejestracja +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_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/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 From e82436fe31abd5892d1c6d1d3e5bd442031c831b Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Sun, 6 Jan 2019 14:39:39 -0800 Subject: [PATCH 52/74] [maven-release-plugin] prepare release jenkins-2.157 --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test-jdk8/pom.xml | 2 +- test-pom/pom.xml | 2 +- test/pom.xml | 2 +- war/pom.xml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 0a5aa6a5c3..4a0e095304 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main jenkins-parent - ${revision}${changelist} + 2.157 cli diff --git a/core/pom.xml b/core/pom.xml index 3f1d559fa2..1a7b989240 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-parent - ${revision}${changelist} + 2.157 jenkins-core diff --git a/pom.xml b/pom.xml index 155ff0e9e4..69dd99ea68 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-parent - ${revision}${changelist} + 2.157 pom Jenkins main module @@ -60,7 +60,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - ${scmTag} + jenkins-2.157 diff --git a/test-jdk8/pom.xml b/test-jdk8/pom.xml index 70c4870837..6ea2fb9a35 100644 --- a/test-jdk8/pom.xml +++ b/test-jdk8/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-test-parent - ${revision}${changelist} + 2.157 ../test-pom diff --git a/test-pom/pom.xml b/test-pom/pom.xml index 2cc12fad52..dbe4ce1d90 100644 --- a/test-pom/pom.xml +++ b/test-pom/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-parent - ${revision}${changelist} + 2.157 jenkins-test-parent diff --git a/test/pom.xml b/test/pom.xml index 4c5abecc6f..39330102dc 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-test-parent - ${revision}${changelist} + 2.157 ../test-pom diff --git a/war/pom.xml b/war/pom.xml index 6ed9553904..20d6ed3f06 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-parent - ${revision}${changelist} + 2.157 jenkins-war From 78bad9afb80f9a59783724851f49636dfdb40e22 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Sun, 6 Jan 2019 14:39:58 -0800 Subject: [PATCH 53/74] [maven-release-plugin] prepare for next development iteration --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 6 +++--- test-jdk8/pom.xml | 2 +- test-pom/pom.xml | 2 +- test/pom.xml | 2 +- war/pom.xml | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 4a0e095304..0a5aa6a5c3 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main jenkins-parent - 2.157 + ${revision}${changelist} cli diff --git a/core/pom.xml b/core/pom.xml index 1a7b989240..3f1d559fa2 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-parent - 2.157 + ${revision}${changelist} jenkins-core diff --git a/pom.xml b/pom.xml index 69dd99ea68..fdc5425bc4 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ THE SOFTWARE. org.jenkins-ci.main jenkins-parent - 2.157 + ${revision}${changelist} pom Jenkins main module @@ -60,7 +60,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - jenkins-2.157 + ${scmTag} @@ -76,7 +76,7 @@ THE SOFTWARE. - 2.157 + 2.158 -SNAPSHOT