Copilot commented on code in PR #112:
URL: https://github.com/apache/maven-shared-io/pull/112#discussion_r3695495151


##########
src/main/java/org/apache/maven/shared/io/download/DefaultDownloadManager.java:
##########
@@ -128,35 +142,44 @@ public File download(String url, List<TransferListener> 
transferListeners, Messa
 
         messageHolder.addMessage("Connecting to: " + repo.getHost() + 
"(baseUrl: " + repo.getUrl() + ")");
 
+        boolean success = false;
+        boolean connected = false;
         try {
             wagon.connect(
                     repo,
                     wagonManager.getAuthenticationInfo(repo.getId()),
                     wagonManager.getProxy(sourceUrl.getProtocol()));
-        } catch (ConnectionException e) {
-            throw new DownloadFailedException(url, "Download failed", e);
-        } catch (AuthenticationException e) {
-            throw new DownloadFailedException(url, "Download failed", e);
-        }
+            connected = true;
 
-        messageHolder.addMessage("Getting: " + remotePath);
+            messageHolder.addMessage("Getting: " + remotePath);
 
-        try {
             wagon.get(remotePath, downloaded);
 
             // cache this for later download requests to the same instance...
             cache.put(url, downloaded);
 
+            success = true;
             return downloaded;
+        } catch (ConnectionException e) {
+            throw new DownloadFailedException(url, "Download failed", e);
+        } catch (AuthenticationException e) {
+            throw new DownloadFailedException(url, "Download failed", e);
         } catch (TransferFailedException e) {
             throw new DownloadFailedException(url, "Download failed", e);
         } catch (ResourceDoesNotExistException e) {
             throw new DownloadFailedException(url, "Download failed", e);
         } catch (AuthorizationException e) {
             throw new DownloadFailedException(url, "Download failed", e);
         } finally {
-            // ensure the Wagon instance is closed out properly.
-            if (wagon != null) {
+            // On failure, delete the temp file immediately to avoid leaving 
orphaned files.
+            // Successfully downloaded files are cleaned up by the shutdown 
hook registered
+            // in registerShutdownHook().
+            if (!success && downloaded != null) {
+                downloaded.delete();
+            }
+
+            // ensure the Wagon instance is closed out properly (only if 
connect succeeded)
+            if (wagon != null && connected) {
                 try {

Review Comment:
   TransferListeners are added to the Wagon before connect(), but the current 
finally block only removes them when connected==true. If connect() fails, the 
listeners remain attached to the Wagon instance, which can leak listeners and 
affect subsequent uses of the same Wagon. Disconnect should be guarded by 
connected, but listener removal should happen whenever wagon != null.



##########
src/main/java/org/apache/maven/shared/io/download/DefaultDownloadManager.java:
##########
@@ -59,13 +59,30 @@ public class DefaultDownloadManager implements 
DownloadManager {
     /**
      * Create an instance of the {@code DefaultDownloadManager}.
      */
-    public DefaultDownloadManager() {}
+    public DefaultDownloadManager() {
+        registerShutdownHook();
+    }
 
     /**
      * @param wagonManager {@link 
org.apache.maven.repository.legacy.WagonManager}
      */
     public DefaultDownloadManager(WagonManager wagonManager) {
         this.wagonManager = wagonManager;
+        registerShutdownHook();
+    }
+
+    /**
+     * Registers a single JVM shutdown hook per manager instance that deletes 
all
+     * cached temporary download files at JVM exit. This avoids the memory leak
+     * caused by {@code File.deleteOnExit()}, which accumulates entries in the
+     * JVM-wide {@code DeleteOnExitHook} static set on every download 
invocation.
+     */
+    private void registerShutdownHook() {
+        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+            for (File file : cache.values()) {
+                file.delete();
+            }
+        }));
     }

Review Comment:
   The shutdown hook deletes files via File.delete() and ignores failures; 
addShutdownHook can also throw IllegalStateException/SecurityException (e.g., 
shutdown in progress or restricted runtime), which would currently fail 
construction. Consider making cleanup best-effort with 
Files.deleteIfExists(...) and guarding hook registration so the manager remains 
usable even if a hook cannot be installed.



##########
src/main/java/org/apache/maven/shared/io/download/DefaultDownloadManager.java:
##########
@@ -128,35 +142,44 @@ public File download(String url, List<TransferListener> 
transferListeners, Messa
 
         messageHolder.addMessage("Connecting to: " + repo.getHost() + 
"(baseUrl: " + repo.getUrl() + ")");
 
+        boolean success = false;
+        boolean connected = false;
         try {
             wagon.connect(
                     repo,
                     wagonManager.getAuthenticationInfo(repo.getId()),
                     wagonManager.getProxy(sourceUrl.getProtocol()));
-        } catch (ConnectionException e) {
-            throw new DownloadFailedException(url, "Download failed", e);
-        } catch (AuthenticationException e) {
-            throw new DownloadFailedException(url, "Download failed", e);
-        }
+            connected = true;
 
-        messageHolder.addMessage("Getting: " + remotePath);
+            messageHolder.addMessage("Getting: " + remotePath);
 
-        try {
             wagon.get(remotePath, downloaded);
 
             // cache this for later download requests to the same instance...
             cache.put(url, downloaded);
 
+            success = true;
             return downloaded;

Review Comment:
   cache.put(url, downloaded) can overwrite an existing cached file during 
concurrent downloads of the same URL. In that case, the overwritten temp file 
is no longer tracked in cache and (since deleteOnExit was removed) will not be 
cleaned up by the shutdown hook, leaving an orphaned temp file on disk. Use 
putIfAbsent (or similar) and delete the redundant temp file when another thread 
has already populated the cache.
   
   This issue also appears on line 177 of the same file.



##########
src/test/java/org/apache/maven/shared/io/download/DefaultDownloadManagerTest.java:
##########
@@ -357,6 +364,82 @@ void shouldDownloadConcurrentlyAndCacheResults() throws 
Exception {
         verify(wagon, wagonManager);
     }
 
+    @Test
+    void shouldDeleteTempFileOnConnectionFailure() throws Exception {
+        File tempFile = Files.createTempFile("download-source", 
"test").toFile();
+        tempFile.deleteOnExit();
+
+        setupMocksWithWagonConnectionException(new 
ConnectionException("connect error"));
+
+        replay(wagon, wagonManager);
+
+        DownloadManager downloadManager = new 
DefaultDownloadManager(wagonManager);
+
+        File tempDir = new File(System.getProperty("java.io.tmpdir"));
+        Set<String> filesBefore = listDownloadTempFiles(tempDir);
+
+        try {
+            downloadManager.download(tempFile.toURI().toASCIIString(), new 
DefaultMessageHolder());
+            fail("should have failed to connect wagon.");
+        } catch (DownloadFailedException e) {
+            
assertTrue(ExceptionUtils.getStackTrace(e).contains("ConnectionException"));
+        }
+
+        Set<String> filesAfter = listDownloadTempFiles(tempDir);
+        filesAfter.removeAll(filesBefore);
+        assertTrue(filesAfter.isEmpty(), "Temp file must be deleted 
immediately when connection fails, not leaked");

Review Comment:
   This test scans the global java.io.tmpdir for download-*.tmp before/after 
the call. That can be flaky if other tests (or external processes) create 
matching files concurrently. Consider running the download in an isolated temp 
directory by temporarily overriding java.io.tmpdir for the duration of this 
test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to