From 0f4fb9e7198d47f25c81f06c10ad1935a24bce62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kohlschu=CC=88tter?= Date: Mon, 26 Aug 2024 18:52:39 +0200 Subject: [PATCH] Clean up after JVMs unable to delete temporary shared library files On Windows, File.deleteOnExit appears to be unable to delete our temporary native library DLL files -- most likely because at the point of deletion the process still has a reference to the loaded DLL, and Windows doesn't allow deleting DLLs that are still referenced by a live process. Work around this limitation by deleting after the fact, i.e., clean up old temp files when we know they should be deleted. Keep marker files in the event that the library file cannot be deleted upon shutdown. Check for these marker files upon starting a new VM using junixsocket, and delete both the marker file and the associated shared library file. The code is written in a way that it should also do the right thing on other platforms, or at least fail silently. https://github.com/kohlschutter/junixsocket/issues/160 --- .../net/unix/NativeLibraryLoader.java | 83 +++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/NativeLibraryLoader.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/NativeLibraryLoader.java index 33dc622e0..fb681e3d6 100644 --- a/junixsocket-common/src/main/java/org/newsclub/net/unix/NativeLibraryLoader.java +++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/NativeLibraryLoader.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -52,7 +53,8 @@ final class NativeLibraryLoader implements Closeable { private static final boolean IS_ANDROID = checkAndroid(); static { - String dir = System.getProperty(PROP_LIBRARY_TMPDIR, null); + String dir = System.getProperty(PROP_LIBRARY_TMPDIR, System.getProperty("java.io.tmpdir", + null)); TEMP_DIR = (dir == null) ? null : new File(dir); } @@ -167,13 +169,64 @@ private static final class ClasspathLibraryCandidate extends LibraryCandidate { this.library = library; } + /** + * Even though we ask the JVM to delete the library file upon VM exit, this may not be honored + * in all cases (crash, Windows, etc.) + * + * Therefore, we attempt to delete these files whenever another JVM using junixsocket starts up. + * This is simplified by keeping empty marker files next to the temporary shared library file. + * + * @param libDir The directory to check. + */ + private void deleteLibTmpDelFiles(File libDir) { + if (libDir == null) { + try { + File tempFile = File.createTempFile("libtmp", ".del"); + libDir = tempFile.getParentFile(); + tryDelete(tempFile); + } catch (IOException e) { + return; + } + } + File[] filesToDelete = libDir.listFiles((File f) -> { + if (!f.isFile()) { + return false; + } + String name = f.getName(); + return name.startsWith("libtmp") && name.endsWith(".del"); + }); + if (filesToDelete == null || filesToDelete.length == 0) { + return; + } + + for (File f : filesToDelete) { + tryDelete(f); + String n = f.getName(); + n = n.substring(0, n.length() - ".del".length()); + File libFile = new File(f.getParentFile(), n); + tryDelete(libFile); + } + } + @Override + @SuppressWarnings("PMD.CognitiveComplexity") synchronized String load() throws IOException, LinkageError { if (libraryNameAndVersion == null) { return null; } File libDir = TEMP_DIR; + File userHomeDir = new File(System.getProperty("user.home", ".")); + File userDirOrNull = new File(System.getProperty("user.dir", ".")); + if (userHomeDir.equals(userDirOrNull)) { + userDirOrNull = null; + } + + deleteLibTmpDelFiles(libDir); + deleteLibTmpDelFiles(userHomeDir); + if (userDirOrNull != null) { + deleteLibTmpDelFiles(userDirOrNull); + } for (int attempt = 0; attempt < 3; attempt++) { File libFile; @@ -200,19 +253,34 @@ synchronized String load() throws IOException, LinkageError { switch (attempt) { case 0: - libDir = new File(System.getProperty("user.home", ".")); + libDir = userHomeDir; break; case 1: - libDir = new File(System.getProperty("user.dir", ".")); - break; + if (userDirOrNull != null) { + libDir = userDirOrNull; + break; + } + // fall-through default: throw e; } continue; } finally { - if (!libFile.delete()) { + if (!libFile.delete() && libFile.exists()) { libFile.deleteOnExit(); + + File markerFile = new File(libFile.getParentFile(), libFile.getName() + ".del"); + try { + Files.createFile(markerFile.toPath()); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (!libFile.exists() || libFile.delete()) { + tryDelete(markerFile); + } + })); + } catch (IOException | UnsupportedOperationException e) { + // ignore + } } } @@ -222,6 +290,11 @@ synchronized String load() throws IOException, LinkageError { return artifactName + "/" + libraryNameAndVersion; } + @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE") + private static void tryDelete(File f) { + f.delete(); // NOPMD + } + @Override public void close() { }