mirror of
https://github.com/openjdk/jdk.git
synced 2026-02-27 18:50:07 +00:00
8374222: jpackage will exit with error if it fails to clean the temp directory
Reviewed-by: almatvee
This commit is contained in:
parent
0ab8a85e87
commit
9d4fbbe36d
@ -195,11 +195,11 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
|
||||
public void createBundle(BundlingOperationDescriptor op, Options cmdline) {
|
||||
final var bundler = getBundlerSupplier(op).get().orElseThrow();
|
||||
Optional<Path> permanentWorkDirectory = Optional.empty();
|
||||
try (var tempDir = new TempDirectory(cmdline)) {
|
||||
try (var tempDir = new TempDirectory(cmdline, Globals.instance().objectFactory())) {
|
||||
if (!tempDir.deleteOnClose()) {
|
||||
permanentWorkDirectory = Optional.of(tempDir.path());
|
||||
}
|
||||
bundler.accept(tempDir.options());
|
||||
bundler.accept(tempDir.map(cmdline));
|
||||
} catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
} finally {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -26,29 +26,46 @@ package jdk.jpackage.internal;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import jdk.jpackage.internal.cli.Options;
|
||||
import jdk.jpackage.internal.cli.StandardOption;
|
||||
import jdk.jpackage.internal.util.FileUtils;
|
||||
import jdk.jpackage.internal.util.Slot;
|
||||
|
||||
final class TempDirectory implements Closeable {
|
||||
|
||||
TempDirectory(Options options) throws IOException {
|
||||
final var tempDir = StandardOption.TEMP_ROOT.findIn(options);
|
||||
if (tempDir.isPresent()) {
|
||||
this.path = tempDir.orElseThrow();
|
||||
this.options = options;
|
||||
} else {
|
||||
this.path = Files.createTempDirectory("jdk.jpackage");
|
||||
this.options = options.copyWithDefaultValue(StandardOption.TEMP_ROOT, path);
|
||||
}
|
||||
|
||||
deleteOnClose = tempDir.isEmpty();
|
||||
TempDirectory(Options options, RetryExecutorFactory retryExecutorFactory) throws IOException {
|
||||
this(StandardOption.TEMP_ROOT.findIn(options), retryExecutorFactory);
|
||||
}
|
||||
|
||||
Options options() {
|
||||
return options;
|
||||
TempDirectory(Optional<Path> tempDir, RetryExecutorFactory retryExecutorFactory) throws IOException {
|
||||
this(tempDir.isEmpty() ? Files.createTempDirectory("jdk.jpackage") : tempDir.get(),
|
||||
tempDir.isEmpty(),
|
||||
retryExecutorFactory);
|
||||
}
|
||||
|
||||
TempDirectory(Path tempDir, boolean deleteOnClose, RetryExecutorFactory retryExecutorFactory) throws IOException {
|
||||
this.path = Objects.requireNonNull(tempDir);
|
||||
this.deleteOnClose = deleteOnClose;
|
||||
this.retryExecutorFactory = Objects.requireNonNull(retryExecutorFactory);
|
||||
}
|
||||
|
||||
Options map(Options options) {
|
||||
if (deleteOnClose) {
|
||||
return options.copyWithDefaultValue(StandardOption.TEMP_ROOT, path);
|
||||
} else {
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
Path path() {
|
||||
@ -62,11 +79,107 @@ final class TempDirectory implements Closeable {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (deleteOnClose) {
|
||||
FileUtils.deleteRecursive(path);
|
||||
retryExecutorFactory.<Void, IOException>retryExecutor(IOException.class)
|
||||
.setMaxAttemptsCount(5)
|
||||
.setAttemptTimeout(2, TimeUnit.SECONDS)
|
||||
.setExecutable(context -> {
|
||||
try {
|
||||
FileUtils.deleteRecursive(path);
|
||||
} catch (IOException ex) {
|
||||
if (!context.isLastAttempt()) {
|
||||
throw ex;
|
||||
} else {
|
||||
// Collect the list of leftover files. Collect at most the first 100 files.
|
||||
var remainingFiles = DirectoryListing.listFilesAndEmptyDirectories(
|
||||
path, MAX_REPORTED_UNDELETED_FILE_COUNT).paths();
|
||||
|
||||
if (remainingFiles.equals(List.of(path))) {
|
||||
Log.info(I18N.format("warning.tempdir.cleanup-failed", path));
|
||||
} else {
|
||||
remainingFiles.forEach(file -> {
|
||||
Log.info(I18N.format("warning.tempdir.cleanup-file-failed", file));
|
||||
});
|
||||
}
|
||||
|
||||
Log.verbose(ex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}).execute();
|
||||
}
|
||||
}
|
||||
|
||||
record DirectoryListing(List<Path> paths, boolean complete) {
|
||||
DirectoryListing {
|
||||
Objects.requireNonNull(paths);
|
||||
}
|
||||
|
||||
static DirectoryListing listFilesAndEmptyDirectories(Path path, int limit) {
|
||||
Objects.requireNonNull(path);
|
||||
if (limit < 0) {
|
||||
throw new IllegalArgumentException();
|
||||
} else if (limit == 0) {
|
||||
return new DirectoryListing(List.of(), !Files.exists(path));
|
||||
}
|
||||
|
||||
var paths = new ArrayList<Path>();
|
||||
var stopped = Slot.<Boolean>createEmpty();
|
||||
|
||||
stopped.set(false);
|
||||
|
||||
try {
|
||||
Files.walkFileTree(path, new FileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
|
||||
try (var walk = Files.walk(dir)) {
|
||||
if (walk.skip(1).findAny().isEmpty()) {
|
||||
// This is an empty directory, add it to the list.
|
||||
return addPath(dir, FileVisitResult.SKIP_SUBTREE);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Log.verbose(ex);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
return addPath(file, FileVisitResult.CONTINUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) {
|
||||
return addPath(file, FileVisitResult.CONTINUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
private FileVisitResult addPath(Path v, FileVisitResult result) {
|
||||
if (paths.size() < limit) {
|
||||
paths.add(v);
|
||||
return result;
|
||||
} else {
|
||||
stopped.set(true);
|
||||
}
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
});
|
||||
} catch (IOException ex) {
|
||||
Log.verbose(ex);
|
||||
}
|
||||
|
||||
return new DirectoryListing(Collections.unmodifiableList(paths), !stopped.get());
|
||||
}
|
||||
}
|
||||
|
||||
private final Path path;
|
||||
private final Options options;
|
||||
private final boolean deleteOnClose;
|
||||
private final RetryExecutorFactory retryExecutorFactory;
|
||||
|
||||
private final static int MAX_REPORTED_UNDELETED_FILE_COUNT = 100;
|
||||
}
|
||||
|
||||
@ -109,6 +109,9 @@ error.tool-not-found.advice=Please install "{0}"
|
||||
error.tool-old-version=Can not find "{0}" {1} or newer
|
||||
error.tool-old-version.advice=Please install "{0}" {1} or newer
|
||||
|
||||
warning.tempdir.cleanup-failed=Warning: Failed to clean-up temporary directory {0}
|
||||
warning.tempdir.cleanup-file-failed=Warning: Failed to delete "{0}" file in the temporary directory
|
||||
|
||||
error.output-bundle-cannot-be-overwritten=Output package file "{0}" exists and can not be removed.
|
||||
|
||||
error.blocked.option=jlink option [{0}] is not permitted in --jlink-options
|
||||
|
||||
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package jdk.jpackage.test;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import jdk.internal.util.OperatingSystem;
|
||||
import jdk.jpackage.internal.util.SetBuilder;
|
||||
|
||||
/**
|
||||
* Path deletion preventer. Encapsulates platform-specifics of how to make a
|
||||
* file or a directory non-deletable.
|
||||
* <p>
|
||||
* Implementation should be sufficient to make {@code Files#delete(Path)}
|
||||
* applied to the path protected from deletion throw.
|
||||
*/
|
||||
public sealed interface PathDeletionPreventer {
|
||||
|
||||
enum Implementation {
|
||||
/**
|
||||
* Uses Java file lock to prevent deletion of a file.
|
||||
* Works on Windows. Doesn't work on Linux and macOS.
|
||||
*/
|
||||
FILE_CHANNEL_LOCK,
|
||||
|
||||
/**
|
||||
* Removes write permission from a non-empty directory to prevent its deletion.
|
||||
* Works on Linux and macOS. Doesn't work on Windows.
|
||||
*/
|
||||
READ_ONLY_NON_EMPTY_DIRECTORY,
|
||||
;
|
||||
}
|
||||
|
||||
Implementation implementation();
|
||||
|
||||
Closeable preventPathDeletion(Path path) throws IOException;
|
||||
|
||||
enum FileChannelLockPathDeletionPreventer implements PathDeletionPreventer {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Implementation implementation() {
|
||||
return Implementation.FILE_CHANNEL_LOCK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Closeable preventPathDeletion(Path path) throws IOException {
|
||||
return new UndeletablePath(path);
|
||||
}
|
||||
|
||||
private static final class UndeletablePath implements Closeable {
|
||||
|
||||
UndeletablePath(Path file) throws IOException {
|
||||
var fos = new FileOutputStream(Objects.requireNonNull(file).toFile());
|
||||
boolean lockCreated = false;
|
||||
try {
|
||||
this.lock = fos.getChannel().lock();
|
||||
this.fos = fos;
|
||||
lockCreated = true;
|
||||
} finally {
|
||||
if (!lockCreated) {
|
||||
fos.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
lock.close();
|
||||
} finally {
|
||||
fos.close();
|
||||
}
|
||||
}
|
||||
|
||||
private final FileOutputStream fos;
|
||||
private final FileLock lock;
|
||||
}
|
||||
}
|
||||
|
||||
enum ReadOnlyDirectoryPathDeletionPreventer implements PathDeletionPreventer {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Implementation implementation() {
|
||||
return Implementation.READ_ONLY_NON_EMPTY_DIRECTORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Closeable preventPathDeletion(Path path) throws IOException {
|
||||
return new UndeletablePath(path);
|
||||
}
|
||||
|
||||
private static final class UndeletablePath implements Closeable {
|
||||
|
||||
UndeletablePath(Path dir) throws IOException {
|
||||
this.dir = Objects.requireNonNull(dir);
|
||||
|
||||
// Deliberately don't use Files#createDirectories() as don't want to create missing directories.
|
||||
try {
|
||||
Files.createDirectory(dir);
|
||||
Files.createFile(dir.resolve("empty"));
|
||||
} catch (FileAlreadyExistsException ex) {
|
||||
}
|
||||
|
||||
perms = Files.getPosixFilePermissions(dir);
|
||||
if (perms.contains(PosixFilePermission.OWNER_WRITE)) {
|
||||
Files.setPosixFilePermissions(dir, SetBuilder.<PosixFilePermission>build()
|
||||
.add(perms)
|
||||
.remove(PosixFilePermission.OWNER_WRITE)
|
||||
.emptyAllowed(true)
|
||||
.create());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (perms.contains(PosixFilePermission.OWNER_WRITE)) {
|
||||
Files.setPosixFilePermissions(dir, perms);
|
||||
}
|
||||
}
|
||||
|
||||
private final Path dir;
|
||||
private final Set<PosixFilePermission> perms;
|
||||
}
|
||||
}
|
||||
|
||||
static final PathDeletionPreventer DEFAULT = new Supplier<PathDeletionPreventer>() {
|
||||
|
||||
@Override
|
||||
public PathDeletionPreventer get() {
|
||||
switch (OperatingSystem.current()) {
|
||||
case WINDOWS -> {
|
||||
return FileChannelLockPathDeletionPreventer.INSTANCE;
|
||||
}
|
||||
default -> {
|
||||
return ReadOnlyDirectoryPathDeletionPreventer.INSTANCE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}.get();
|
||||
}
|
||||
@ -0,0 +1,570 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package jdk.jpackage.internal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import jdk.internal.util.OperatingSystem;
|
||||
import jdk.jpackage.internal.cli.Options;
|
||||
import jdk.jpackage.internal.cli.StandardOption;
|
||||
import jdk.jpackage.internal.util.FileUtils;
|
||||
import jdk.jpackage.internal.util.RetryExecutor;
|
||||
import jdk.jpackage.internal.util.function.ThrowingFunction;
|
||||
import jdk.jpackage.internal.util.function.ThrowingSupplier;
|
||||
import jdk.jpackage.test.PathDeletionPreventer;
|
||||
import jdk.jpackage.test.PathDeletionPreventer.ReadOnlyDirectoryPathDeletionPreventer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
public class TempDirectoryTest {
|
||||
|
||||
@Test
|
||||
void test_directory_use(@TempDir Path tempDirPath) throws IOException {
|
||||
try (var tempDir = new TempDirectory(Optional.of(tempDirPath), RetryExecutorFactory.DEFAULT)) {
|
||||
assertEquals(tempDir.path(), tempDirPath);
|
||||
assertFalse(tempDir.deleteOnClose());
|
||||
|
||||
var cmdline = Options.of(Map.of());
|
||||
assertSame(cmdline, tempDir.map(cmdline));
|
||||
}
|
||||
|
||||
assertTrue(Files.isDirectory(tempDirPath));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_directory_new() throws IOException {
|
||||
var tempDir = new TempDirectory(Optional.empty(), RetryExecutorFactory.DEFAULT);
|
||||
try (tempDir) {
|
||||
assertTrue(Files.isDirectory(tempDir.path()));
|
||||
assertTrue(tempDir.deleteOnClose());
|
||||
|
||||
var cmdline = Options.of(Map.of());
|
||||
var mappedCmdline = tempDir.map(cmdline);
|
||||
assertEquals(tempDir.path(), StandardOption.TEMP_ROOT.getFrom(mappedCmdline));
|
||||
}
|
||||
|
||||
assertFalse(Files.isDirectory(tempDir.path()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("try")
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void test_close(CloseType closeType, @TempDir Path root) {
|
||||
Globals.main(ThrowingSupplier.toSupplier(() -> {
|
||||
test_close_impl(closeType, root);
|
||||
return 0;
|
||||
}));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void test_DirectoryListing_listFilesAndEmptyDirectories(
|
||||
ListFilesAndEmptyDirectoriesTestSpec test, @TempDir Path root) throws IOException {
|
||||
test.run(root);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_DirectoryListing_listFilesAndEmptyDirectories_negative(@TempDir Path root) throws IOException {
|
||||
assertThrowsExactly(IllegalArgumentException.class, () -> {
|
||||
TempDirectory.DirectoryListing.listFilesAndEmptyDirectories(root, -1);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"100", "101", "1", "0"})
|
||||
void test_DirectoryListing_listFilesAndEmptyDirectories_nonexistent(int limit, @TempDir Path root) throws IOException {
|
||||
|
||||
var path = root.resolve("foo");
|
||||
|
||||
var listing = TempDirectory.DirectoryListing.listFilesAndEmptyDirectories(path, limit);
|
||||
assertTrue(listing.complete());
|
||||
|
||||
List<Path> expected;
|
||||
if (limit == 0) {
|
||||
expected = List.of();
|
||||
} else {
|
||||
expected = List.of(path);
|
||||
}
|
||||
assertEquals(expected, listing.paths());
|
||||
}
|
||||
|
||||
private static Stream<CloseType> test_close() {
|
||||
switch (PathDeletionPreventer.DEFAULT.implementation()) {
|
||||
case READ_ONLY_NON_EMPTY_DIRECTORY -> {
|
||||
return Stream.of(CloseType.values());
|
||||
}
|
||||
default -> {
|
||||
return Stream.of(CloseType.values())
|
||||
.filter(Predicate.not(Set.of(
|
||||
CloseType.FAIL_NO_LEFTOVER_FILES,
|
||||
CloseType.FAIL_NO_LEFTOVER_FILES_VERBOSE)::contains));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "try" })
|
||||
private void test_close_impl(CloseType closeType, Path root) throws IOException {
|
||||
var logSink = new StringWriter();
|
||||
var logPrintWriter = new PrintWriter(logSink, true);
|
||||
Globals.instance().loggerOutputStreams(logPrintWriter, logPrintWriter);
|
||||
if (closeType.isVerbose()) {
|
||||
Globals.instance().loggerVerbose();
|
||||
}
|
||||
|
||||
final var workDir = root.resolve("workdir");
|
||||
Files.createDirectories(workDir);
|
||||
|
||||
final Path leftoverPath;
|
||||
final TempDirectory tempDir;
|
||||
|
||||
switch (closeType) {
|
||||
case FAIL_NO_LEFTOVER_FILES_VERBOSE, FAIL_NO_LEFTOVER_FILES -> {
|
||||
leftoverPath = workDir;
|
||||
tempDir = new TempDirectory(workDir, true, new RetryExecutorFactory() {
|
||||
@Override
|
||||
public <T, E extends Exception> RetryExecutor<T, E> retryExecutor(Class<? extends E> exceptionType) {
|
||||
return new RetryExecutor<T, E>(exceptionType).setSleepFunction(_ -> {});
|
||||
}
|
||||
});
|
||||
|
||||
// Lock the parent directory of the work directory and don't create any files in the work directory.
|
||||
// This should trigger the error message about the failure to delete the empty work directory.
|
||||
try (var lockWorkDir = ReadOnlyDirectoryPathDeletionPreventer.INSTANCE.preventPathDeletion(workDir.getParent())) {
|
||||
tempDir.close();
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
Files.createFile(workDir.resolve("b"));
|
||||
|
||||
final var lockedPath = workDir.resolve("a");
|
||||
switch (PathDeletionPreventer.DEFAULT.implementation()) {
|
||||
case FILE_CHANNEL_LOCK -> {
|
||||
Files.createFile(lockedPath);
|
||||
leftoverPath = lockedPath;
|
||||
}
|
||||
case READ_ONLY_NON_EMPTY_DIRECTORY -> {
|
||||
Files.createDirectories(lockedPath);
|
||||
leftoverPath = lockedPath.resolve("a");
|
||||
Files.createFile(leftoverPath);
|
||||
}
|
||||
default -> {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
tempDir = new TempDirectory(workDir, true, new RetryExecutorFactory() {
|
||||
@Override
|
||||
public <T, E extends Exception> RetryExecutor<T, E> retryExecutor(Class<? extends E> exceptionType) {
|
||||
var config = new RetryExecutorMock.Config(lockedPath, closeType.isSuccess());
|
||||
return new RetryExecutorMock<>(exceptionType, config);
|
||||
}
|
||||
});
|
||||
|
||||
tempDir.close();
|
||||
}
|
||||
}
|
||||
|
||||
logPrintWriter.flush();
|
||||
var logMessages = new BufferedReader(new StringReader(logSink.toString())).lines().toList();
|
||||
|
||||
assertTrue(Files.isDirectory(root));
|
||||
|
||||
if (closeType.isSuccess()) {
|
||||
assertFalse(Files.exists(tempDir.path()));
|
||||
assertEquals(List.of(), logMessages);
|
||||
} else {
|
||||
assertTrue(Files.isDirectory(tempDir.path()));
|
||||
assertTrue(Files.exists(leftoverPath));
|
||||
assertFalse(Files.exists(tempDir.path().resolve("b")));
|
||||
|
||||
String errMessage;
|
||||
switch (closeType) {
|
||||
case FAIL_SOME_LEFTOVER_FILES_VERBOSE, FAIL_SOME_LEFTOVER_FILES -> {
|
||||
errMessage = "warning.tempdir.cleanup-file-failed";
|
||||
}
|
||||
case FAIL_NO_LEFTOVER_FILES_VERBOSE, FAIL_NO_LEFTOVER_FILES -> {
|
||||
errMessage = "warning.tempdir.cleanup-failed";
|
||||
}
|
||||
default -> {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
assertEquals(List.of(I18N.format(errMessage, leftoverPath)), logMessages.subList(0, 1));
|
||||
|
||||
if (closeType.isVerbose()) {
|
||||
// Check the log contains a stacktrace
|
||||
assertNotEquals(1, logMessages.size());
|
||||
}
|
||||
FileUtils.deleteRecursive(tempDir.path());
|
||||
}
|
||||
}
|
||||
|
||||
private static Collection<ListFilesAndEmptyDirectoriesTestSpec> test_DirectoryListing_listFilesAndEmptyDirectories() {
|
||||
|
||||
var testCases = new ArrayList<ListFilesAndEmptyDirectoriesTestSpec>();
|
||||
|
||||
Supplier<ListFilesAndEmptyDirectoriesTestSpec.Builder> builder = ListFilesAndEmptyDirectoriesTestSpec::build;
|
||||
|
||||
Stream.of(
|
||||
builder.get().dirs("").complete(),
|
||||
builder.get().dirs("").limit(0),
|
||||
builder.get().dirs("foo").complete(),
|
||||
builder.get().dirs("foo").limit(0),
|
||||
builder.get().dirs("foo").limit(1).complete(),
|
||||
builder.get().dirs("foo").limit(2).complete(),
|
||||
builder.get().dirs("a/b/c").files("foo").files("b/b", "b/c").complete(),
|
||||
builder.get().dirs("a/b/c").files("foo").files("b/b", "b/c").limit(4).complete(),
|
||||
builder.get().dirs("a/b/c").files("foo").files("b/b", "b/c").limit(3)
|
||||
).map(ListFilesAndEmptyDirectoriesTestSpec.Builder::create).forEach(testCases::add);
|
||||
|
||||
if (!OperatingSystem.isWindows()) {
|
||||
Stream.of(
|
||||
// A directory with the sibling symlink pointing to this directory
|
||||
builder.get().dirs("foo").symlink("foo-symlink", "foo").complete(),
|
||||
// A file with the sibling symlink pointing to this file
|
||||
builder.get().symlink("foo-symlink", "foo").files("foo").complete(),
|
||||
// A dangling symlink
|
||||
builder.get().nonexistent("foo/bar/buz").symlink("dangling-symlink", "foo/bar/buz").complete()
|
||||
).map(ListFilesAndEmptyDirectoriesTestSpec.Builder::create).forEach(testCases::add);
|
||||
}
|
||||
|
||||
return testCases;
|
||||
}
|
||||
|
||||
enum CloseType {
|
||||
SUCCEED,
|
||||
FAIL_SOME_LEFTOVER_FILES,
|
||||
FAIL_SOME_LEFTOVER_FILES_VERBOSE,
|
||||
FAIL_NO_LEFTOVER_FILES,
|
||||
FAIL_NO_LEFTOVER_FILES_VERBOSE,
|
||||
;
|
||||
|
||||
boolean isSuccess() {
|
||||
return this == SUCCEED;
|
||||
}
|
||||
|
||||
boolean isVerbose() {
|
||||
return name().endsWith("_VERBOSE");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RetryExecutorMock<T, E extends Exception> extends RetryExecutor<T, E> {
|
||||
|
||||
RetryExecutorMock(Class<? extends E> exceptionType, Config config) {
|
||||
super(exceptionType);
|
||||
setSleepFunction(_ -> {});
|
||||
this.config = Objects.requireNonNull(config);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "try", "unchecked" })
|
||||
@Override
|
||||
public RetryExecutor<T,E> setExecutable(ThrowingFunction<Context<RetryExecutor<T, E>>, T, E> v) {
|
||||
return super.setExecutable(context -> {
|
||||
if (context.isLastAttempt() && config.unlockOnLastAttempt()) {
|
||||
return v.apply(context);
|
||||
} else {
|
||||
try (var lock = PathDeletionPreventer.DEFAULT.preventPathDeletion(config.lockedPath())) {
|
||||
return v.apply(context);
|
||||
} catch (IOException ex) {
|
||||
if (exceptionType().isInstance(ex)) {
|
||||
throw (E)ex;
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private final Config config;
|
||||
|
||||
record Config(Path lockedPath, boolean unlockOnLastAttempt) {
|
||||
Config {
|
||||
Objects.requireNonNull(lockedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface FileSpec extends Comparable<FileSpec> {
|
||||
Path path();
|
||||
Path create(Path root) throws IOException;
|
||||
public default int compareTo(FileSpec other) {
|
||||
return path().compareTo(other.path());
|
||||
}
|
||||
|
||||
static File file(Path path) {
|
||||
return new File(path);
|
||||
}
|
||||
|
||||
static Directory dir(Path path) {
|
||||
return new Directory(path);
|
||||
}
|
||||
|
||||
static Nonexistent nonexistent(Path path) {
|
||||
return new Nonexistent(path);
|
||||
}
|
||||
|
||||
static Symlink symlink(Path path, FileSpec target) {
|
||||
return new Symlink(path, target);
|
||||
}
|
||||
};
|
||||
|
||||
record File(Path path) implements FileSpec {
|
||||
File {
|
||||
path = normalizePath(path);
|
||||
if (path.getNameCount() == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path create(Path root) throws IOException {
|
||||
var resolvedPath = root.resolve(path);
|
||||
if (!Files.isRegularFile(resolvedPath)) {
|
||||
Files.createDirectories(resolvedPath.getParent());
|
||||
Files.createFile(resolvedPath);
|
||||
}
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("f:%s", path);
|
||||
}
|
||||
}
|
||||
|
||||
record Nonexistent(Path path) implements FileSpec {
|
||||
Nonexistent {
|
||||
path = normalizePath(path);
|
||||
if (path.getNameCount() == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path create(Path root) throws IOException {
|
||||
return root.resolve(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("x:%s", path);
|
||||
}
|
||||
}
|
||||
|
||||
record Symlink(Path path, FileSpec target) implements FileSpec {
|
||||
Symlink {
|
||||
path = normalizePath(path);
|
||||
if (path.getNameCount() == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
Objects.requireNonNull(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path create(Path root) throws IOException {
|
||||
var resolvedPath = root.resolve(path);
|
||||
var targetPath = target.create(root);
|
||||
Files.createDirectories(resolvedPath.getParent());
|
||||
return Files.createSymbolicLink(resolvedPath, targetPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("s:%s->%s", path, target);
|
||||
}
|
||||
}
|
||||
|
||||
record Directory(Path path) implements FileSpec {
|
||||
Directory {
|
||||
path = normalizePath(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path create(Path root) throws IOException {
|
||||
return Files.createDirectories(root.resolve(path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("d:%s", path);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path normalizePath(Path path) {
|
||||
path = path.normalize();
|
||||
if (path.isAbsolute()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private record ListFilesAndEmptyDirectoriesTestSpec(Set<FileSpec> input, int limit, boolean complete) {
|
||||
|
||||
ListFilesAndEmptyDirectoriesTestSpec {
|
||||
Objects.requireNonNull(input);
|
||||
|
||||
if (!(input instanceof SortedSet)) {
|
||||
input = new TreeSet<>(input);
|
||||
}
|
||||
}
|
||||
|
||||
static Builder build() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static final class Builder {
|
||||
|
||||
ListFilesAndEmptyDirectoriesTestSpec create() {
|
||||
return new ListFilesAndEmptyDirectoriesTestSpec(Set.copyOf(input), limit, complete);
|
||||
}
|
||||
|
||||
Builder files(String... paths) {
|
||||
Stream.of(paths).map(Path::of).map(FileSpec::file).forEach(input::add);
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder dirs(String... paths) {
|
||||
Stream.of(paths).map(Path::of).map(FileSpec::dir).forEach(input::add);
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder nonexistent(String... paths) {
|
||||
Stream.of(paths).map(Path::of).map(FileSpec::nonexistent).forEach(input::add);
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder symlink(String path, String target) {
|
||||
Objects.requireNonNull(target);
|
||||
|
||||
var targetSpec = input.stream().filter(v -> {
|
||||
return v.path().equals(Path.of(target));
|
||||
}).findFirst();
|
||||
|
||||
if (targetSpec.isEmpty()) {
|
||||
var v = FileSpec.file(Path.of(target));
|
||||
input.add(v);
|
||||
targetSpec = Optional.ofNullable(v);
|
||||
}
|
||||
|
||||
input.add(FileSpec.symlink(Path.of(path), targetSpec.get()));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder limit(int v) {
|
||||
limit = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder complete(boolean v) {
|
||||
complete = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder complete() {
|
||||
return complete(true);
|
||||
}
|
||||
|
||||
private final Set<FileSpec> input = new HashSet<>();
|
||||
private int limit = Integer.MAX_VALUE;
|
||||
private boolean complete;
|
||||
}
|
||||
|
||||
void run(Path root) throws IOException {
|
||||
for (var v : input) {
|
||||
v.create(root);
|
||||
}
|
||||
|
||||
for (var v : input) {
|
||||
Predicate<Path> validator;
|
||||
switch (v) {
|
||||
case File _ -> {
|
||||
validator = Files::isRegularFile;
|
||||
}
|
||||
case Directory _ -> {
|
||||
validator = Files::isDirectory;
|
||||
}
|
||||
case Symlink _ -> {
|
||||
validator = Files::isSymbolicLink;
|
||||
}
|
||||
case Nonexistent _ -> {
|
||||
validator = Predicate.not(Files::exists);
|
||||
}
|
||||
}
|
||||
assertTrue(validator.test(root.resolve(v.path())));
|
||||
}
|
||||
|
||||
var listing = TempDirectory.DirectoryListing.listFilesAndEmptyDirectories(root, limit);
|
||||
assertEquals(complete, listing.complete());
|
||||
|
||||
if (complete) {
|
||||
var actual = listing.paths().stream().peek(p -> {
|
||||
assertTrue(p.startsWith(root));
|
||||
}).map(root::relativize).sorted().toList();
|
||||
var expected = input.stream()
|
||||
.filter(Predicate.not(Nonexistent.class::isInstance))
|
||||
.map(FileSpec::path)
|
||||
.sorted()
|
||||
.toList();
|
||||
assertEquals(expected, actual);
|
||||
} else {
|
||||
assertEquals(limit, listing.paths().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s; limit=%d; complete=%s", input, limit, complete);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user