diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 5b764b83ec..e24f50bd4b 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -48,6 +48,7 @@ import hudson.remoting.RemoteOutputStream; import hudson.remoting.VirtualChannel; import hudson.remoting.Which; import hudson.security.AccessControlled; +import hudson.slaves.WorkspaceList; import hudson.util.DaemonThreadFactory; import hudson.util.DirScanner; import hudson.util.ExceptionCatchingThreadFactory; @@ -1257,6 +1258,44 @@ public final class FilePath implements SerializableOnlyOverRemoting { return mkdirs(f) || f.exists(); } } + + /** + * Deletes all suffixes recursively. + * @throws IOException if it exists but could not be successfully deleted + * @since TODO + */ + public void deleteSuffixesRecursive() throws IOException, InterruptedException { + act(new DeleteSuffixesRecursive()); + } + + /** + * Deletes all suffixed directories that are separated by {@link WorkspaceList#COMBINATOR}, including all its contents recursively. + */ + private class DeleteSuffixesRecursive extends SecureFileCallable { + private static final long serialVersionUID = 1L; + + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + for (File file : listParentFiles(f)) { + if (file.getName().startsWith(f.getName() + WorkspaceList.COMBINATOR)) { + Util.deleteRecursive(file.toPath(), path -> deleting(path.toFile())); + } + } + + return null; + } + } + + private static File[] listParentFiles(File f) { + File parentFile = f.getParentFile(); + if (parentFile != null) { + File[] files = parentFile.listFiles(); + if (files != null) { + return files; + } + } + return new File[0]; + } /** * Deletes this directory, including all its contents recursively. diff --git a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java index 185be158c0..524f6bd331 100644 --- a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java +++ b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java @@ -89,11 +89,8 @@ public class WorkspaceCleanupThread extends AsyncPeriodicWork { if (check) { listener.getLogger().println("Deleting " + ws + " on " + node.getDisplayName()); try { + ws.deleteSuffixesRecursive(); ws.deleteRecursive(); - FilePath tempDir = WorkspaceList.tempDir(ws); - if (tempDir != null) { - tempDir.deleteRecursive(); - } } catch (IOException | InterruptedException x) { Functions.printStackTrace(x, listener.error("Failed to delete " + ws + " on " + node.getDisplayName())); } diff --git a/core/src/main/java/hudson/slaves/WorkspaceList.java b/core/src/main/java/hudson/slaves/WorkspaceList.java index 8afd10b414..6a4ab03da4 100644 --- a/core/src/main/java/hudson/slaves/WorkspaceList.java +++ b/core/src/main/java/hudson/slaves/WorkspaceList.java @@ -297,7 +297,7 @@ public final class WorkspaceList { * acknowledging that these will be readable by builds of other jobs done on the same node. *

Each plugin using this directory is responsible for specifying sufficiently unique subdirectory/file names. * {@link FilePath#createTempFile} may be used for this purpose if desired. - *

The resulting directory may not exist; you may call {@link FilePath#mkdirs} on it if you need it to. + *

The resulting directory may not exist; you may call {@link FilePath#mkdirs()} on it if you need it to. * It may be deleted alongside the workspace itself during cleanup actions. * @param ws a directory such as a build workspace * @return a sibling directory, for example {@code …/something@tmp} for {@code …/something}, or {@code null} if {@link FilePath#getParent} is null @@ -312,6 +312,7 @@ public final class WorkspaceList { /** * The token that combines the project name and unique number to create unique workspace directory. + * @since TODO */ - private static final String COMBINATOR = SystemProperties.getString(WorkspaceList.class.getName(),"@"); + public static final String COMBINATOR = SystemProperties.getString(WorkspaceList.class.getName(),"@"); } diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index 73d0b6fcc7..26b93b8048 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -28,6 +28,7 @@ import hudson.model.TaskListener; import hudson.os.PosixAPI; import hudson.os.WindowsUtil; import hudson.remoting.VirtualChannel; +import hudson.slaves.WorkspaceList; import hudson.util.NullStream; import hudson.util.StreamTaskListener; import org.apache.commons.io.FileUtils; @@ -810,6 +811,20 @@ public class FilePathTest { assertFalse("could not delete target", Files.exists(toDelete)); } + @Test + @Issue("JENKINS-44909") + public void deleteSuffixesRecursive() throws Exception { + File deleteSuffixesRecursiveFolder = temp.newFolder("deleteSuffixesRecursive"); + FilePath filePath = new FilePath(deleteSuffixesRecursiveFolder); + FilePath suffix = filePath.withSuffix(WorkspaceList.COMBINATOR + "suffixed"); + FilePath textTempFile = suffix.createTextTempFile("tmp", null, "dummy", true); + + assertThat(textTempFile.exists(), is(true)); + + filePath.deleteSuffixesRecursive(); + assertThat(textTempFile.exists(), is(false)); + } + @Test public void deleteRecursiveOnWindows() throws Exception { assumeTrue("Uses Windows-specific features", Functions.isWindows()); Path targetDir = temp.newFolder("targetDir").toPath();