From ce97823d4567dc3bd890e4caa9dce388a1d49b48 Mon Sep 17 00:00:00 2001 From: itsTyrion Date: Sun, 31 Jul 2022 15:10:51 +0200 Subject: [PATCH] Native: Clean up left over temp files On Windows the native libraries weren't deleted after jvm shutdown as expected. This was reported by itsTyrion in #325. This implementation is based on #326 but makes it more robust against multiple simultaneous uses of the NativeUtil library (e.g. the auto-dark-mode plugin for IntelliJ also makes use of it). --- .../darklaf/nativeutil/AbstractLibrary.java | 9 ++- .../weisj/darklaf/nativeutil/NativeUtil.java | 72 +++++++++++++++---- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/native-utils/src/main/java/com/github/weisj/darklaf/nativeutil/AbstractLibrary.java b/native-utils/src/main/java/com/github/weisj/darklaf/nativeutil/AbstractLibrary.java index 87d86ddb..abb06723 100644 --- a/native-utils/src/main/java/com/github/weisj/darklaf/nativeutil/AbstractLibrary.java +++ b/native-utils/src/main/java/com/github/weisj/darklaf/nativeutil/AbstractLibrary.java @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2019-2021 Jannis Weis + * Copyright (c) 2019-2022 Jannis Weis * * 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, @@ -27,6 +27,7 @@ import java.util.logging.Logger; public abstract class AbstractLibrary { + private static final String ILLEGAL_PATH_CHARACTERS = "[\\\\/:*?\"<>|]"; private final String name; protected final Logger logger; private boolean loaded; @@ -60,10 +61,12 @@ public abstract class AbstractLibrary { String path = getLibraryPath(); if (path != null && !path.isEmpty()) { List resources = getResourcePaths(); + String libraryIdentifier = name.replaceAll(ILLEGAL_PATH_CHARACTERS, ""); if (resources == null || resources.isEmpty()) { - NativeUtil.loadLibraryFromJar(getLoaderClass(), path); + NativeUtil.loadLibraryFromJar(getLoaderClass(), path, libraryIdentifier); } else { - NativeUtil.loadLibraryFromJarWithExtraResources(getLoaderClass(), path, resources); + NativeUtil.loadLibraryFromJarWithExtraResources(getLoaderClass(), path, resources, + libraryIdentifier); } loaded = true; info("Loaded " + name + " at " + path + "."); diff --git a/native-utils/src/main/java/com/github/weisj/darklaf/nativeutil/NativeUtil.java b/native-utils/src/main/java/com/github/weisj/darklaf/nativeutil/NativeUtil.java index ea888d45..706e0c75 100644 --- a/native-utils/src/main/java/com/github/weisj/darklaf/nativeutil/NativeUtil.java +++ b/native-utils/src/main/java/com/github/weisj/darklaf/nativeutil/NativeUtil.java @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2019-2021 Jannis Weis + * Copyright (c) 2019-2022 Jannis Weis * * 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, @@ -26,10 +26,12 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.*; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; /** @@ -45,7 +47,7 @@ import java.util.logging.Logger; public final class NativeUtil { private static final Logger LOGGER = Logger.getLogger(NativeUtil.class.getName()); - public static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils"; + public static final String NATIVE_FOLDER_PATH_PREFIX = "com-weisj-darklaf-nativeutils"; /** * The minimum length a prefix for a file has to have according to * {@link File#createTempFile(String, String)}}. @@ -77,6 +79,7 @@ public final class NativeUtil { * @param loaderClass the class to use for loading. * @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. * /package/File.ext + * @param identifier The library identifier for clean-up purposes * @throws IOException If temporary file creation or read/write operation fails * @throws IllegalArgumentException If source file (param path) does not exist * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than @@ -85,20 +88,25 @@ public final class NativeUtil { * @throws FileNotFoundException If the file could not be found inside the JAR. */ public static void loadLibraryFromJarWithExtraResources(final Class loaderClass, final String path, - final List resources) + final List resources, final String identifier) throws IOException { - List resourcePaths = extractResources(loaderClass, resources); + + String libraryIdentifier = getFullLibraryIdentifier(identifier); + Path tempDir = getTemporaryDirectory(libraryIdentifier); + + List resourcePaths = extractResources(loaderClass, resources, tempDir); try { - loadLibraryFromJar(loaderClass, path); + doLoadLibraryFromJar(loaderClass, path, identifier, tempDir); } finally { resourcePaths.forEach(NativeUtil::releaseResource); } } - private static List extractResources(final Class caller, final List resources) + private static List extractResources(final Class caller, final List resources, + final Path tempDir) throws IOException { List paths = new ArrayList<>(resources.size()); - Path tempDir = getTemporaryDirectory(); + for (Resource resource : resources) { String filename = getFileNameFromPath(resource.filePath); Path destinationDir = tempDir.resolve(resource.destinationDirectoryPath); @@ -126,6 +134,7 @@ public final class NativeUtil { * @param loaderClass the class to use for loading. * @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. * /package/File.ext + * @param identifier The library identifier for clean-up purposes * @throws IOException If temporary file creation or read/write operation fails * @throws IllegalArgumentException If source file (param path) does not exist * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than @@ -133,7 +142,15 @@ public final class NativeUtil { * {@link File#createTempFile(java.lang.String, java.lang.String)}). * @throws FileNotFoundException If the file could not be found inside the JAR. */ - public static void loadLibraryFromJar(final Class loaderClass, final String path) throws IOException { + public static void loadLibraryFromJar(final Class loaderClass, final String path, final String identifier) + throws IOException { + String libraryIdentifier = getFullLibraryIdentifier(identifier); + Path tempDir = getTemporaryDirectory(libraryIdentifier); + doLoadLibraryFromJar(loaderClass, path, identifier, tempDir); + } + + private static void doLoadLibraryFromJar(final Class loaderClass, final String path, final String identifier, + final Path tempDir) throws IOException { String filename = getFileNameFromPath(path); // Check if the filename is okay @@ -142,9 +159,12 @@ public final class NativeUtil { } // Prepare temporary file - Path tempDir = getTemporaryDirectory(); + String libraryIdentifier = getFullLibraryIdentifier(identifier); Path temp = tempDir.resolve(filename); + if (!isPosixCompliant()) { + deleteLeftoverTempFiles(tempDir, libraryIdentifier); + } extractFile(loaderClass, path, tempDir, temp); try { @@ -161,7 +181,7 @@ public final class NativeUtil { if (is == null) throw new FileNotFoundException("File " + path + " was not found inside JAR."); if (!destinationDir.toFile().canWrite()) throw new IOException("Can't write to temporary directory."); if (!Files.exists(destinationPath)) { - // Otherwise the file is already existent and most probably loaded. + // Otherwise, the file is already existent and most probably loaded. Files.copy(is, destinationPath.toAbsolutePath(), StandardCopyOption.REPLACE_EXISTING); } } catch (final IOException e) { @@ -170,6 +190,30 @@ public final class NativeUtil { } } + private static void deleteLeftoverTempFiles(Path tempDir, String identifier) throws IOException { + try (Stream files = Files.list(tempDir.getParent())) { + files.filter(Files::isDirectory) + .filter(p -> !tempDir.equals(p)) + .filter(p -> p.getFileName().toString().startsWith(identifier)) + .forEach(NativeUtil::deleteFolder); + } + } + + /** + * Recursively deletes a folder and it's files + * + * @param folder the target folder as {@link File} + */ + private static void deleteFolder(Path folder) { + LOGGER.fine("Removing " + folder); + try (Stream walk = Files.walk(folder)) { + walk.sorted(Comparator.reverseOrder()) + .forEach(NativeUtil::delete); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not delete directory", e); + } + } + private static String getFileNameFromPath(final String path) { checkPath(path); String[] parts = path.split("/"); @@ -182,9 +226,13 @@ public final class NativeUtil { } } - private static Path getTemporaryDirectory() throws IOException { + private static String getFullLibraryIdentifier(final String identifier) { + return NATIVE_FOLDER_PATH_PREFIX + "-" + identifier; + } + + private static Path getTemporaryDirectory(final String libraryIdentifier) throws IOException { if (temporaryDir == null) { - temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX); + temporaryDir = createTempDirectory(libraryIdentifier); temporaryDir.toFile().deleteOnExit(); } return temporaryDir;