Skip to content

Commit 725dbeb

Browse files
committed
fix(mods): stop duplicate jars when update/downgrade renames the file
Old and new jar were replaced with two independent, unchecked File.delete()/renameTo() calls. When the file name changes between versions, these are separate filesystem entries; if delete() of the old jar transiently failed (e.g. briefly held open by an icon/name resolution read) the failure was silently ignored and the new file still got created, leaving both jars on disk as a duplicate on next scan. Same-name updates never showed this because renameTo() onto an existing path atomically replaces it, masking the ignored delete failure. Added a shared replaceModFile() helper used by both applyUpdate() and applySwitchVersion(): renames the new file into place first, then explicitly deletes the old one (skipped when the name didn't change, since rename already replaced it), retrying the delete a few times with backoff before giving up and logging a warning.
1 parent 2c2981b commit 725dbeb

1 file changed

Lines changed: 60 additions & 5 deletions

File tree

app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/InstalledModAdapter.java

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ private void applyUpdate(Context context, ModEntry entry, int position) {
288288
boolean wasDisabled = entry.file.getName().endsWith(".disabled");
289289
String targetName = wasDisabled ? updateName + ".disabled" : updateName;
290290
File targetFile = new File(entry.file.getParent(), targetName);
291+
final File oldFile = entry.file;
291292

292293
Toast.makeText(context,
293294
context.getString(R.string.mod_updating, entry.displayName()),
@@ -304,9 +305,7 @@ private void applyUpdate(Context context, ModEntry entry, int position) {
304305
File tmpFile = new File(entry.file.getParent(), targetName + ".tmp");
305306
DownloadUtils.downloadFile(updateUrl, tmpFile);
306307

307-
// Delete old file, rename temp to final
308-
entry.file.delete();
309-
tmpFile.renameTo(targetFile);
308+
replaceModFile(oldFile, tmpFile, targetFile);
310309

311310
mMainHandler.post(() -> {
312311
entry.file = targetFile;
@@ -332,6 +331,63 @@ private void applyUpdate(Context context, ModEntry entry, int position) {
332331
});
333332
}
334333

334+
/**
335+
* Replaces {@code oldFile} with the freshly downloaded {@code tmpFile}, ending
336+
* up at {@code targetFile}. Handles both cases:
337+
* <ul>
338+
* <li>Same file name (old == target): {@code renameTo} atomically replaces
339+
* the destination on most Android filesystems, so this always works
340+
* even if deleting the old file first happens to fail.</li>
341+
* <li>Different file name (old != target): these are two independent
342+
* filesystem entries, so the old one needs an explicit, verified
343+
* delete — otherwise a transient failure (e.g. the jar being briefly
344+
* held open by an icon/name-resolution read on another thread) leaves
345+
* it behind and it shows up as a duplicate mod next time the list is
346+
* rescanned.</li>
347+
* </ul>
348+
* Old-file deletion is retried a few times with a short backoff before
349+
* giving up, since these locks are normally released within milliseconds.
350+
*/
351+
private static void replaceModFile(File oldFile, File tmpFile, File targetFile) {
352+
boolean sameName = oldFile.getAbsolutePath().equals(targetFile.getAbsolutePath());
353+
354+
if (sameName) {
355+
// renameTo() onto an existing path replaces it atomically, so the
356+
// old file is gone the moment this succeeds — no separate delete needed.
357+
if (!tmpFile.renameTo(targetFile)) {
358+
Log.w(TAG, "Failed to rename downloaded update into place: " + targetFile);
359+
}
360+
return;
361+
}
362+
363+
// Different names: rename the new file into place first, then clean up
364+
// the old one — never leave the mods folder without a working jar even
365+
// if the delete step below has trouble.
366+
if (!tmpFile.renameTo(targetFile)) {
367+
Log.w(TAG, "Failed to rename downloaded update into place: " + targetFile);
368+
return;
369+
}
370+
371+
if (!deleteWithRetry(oldFile)) {
372+
Log.w(TAG, "Failed to delete superseded jar, it will linger as a duplicate: " + oldFile);
373+
}
374+
}
375+
376+
/** Deletes a file, retrying briefly if it's transiently locked by another thread. */
377+
private static boolean deleteWithRetry(File file) {
378+
if (!file.exists()) return true;
379+
for (int attempt = 0; attempt < 5; attempt++) {
380+
if (file.delete() || !file.exists()) return true;
381+
try {
382+
Thread.sleep(60L * (attempt + 1));
383+
} catch (InterruptedException e) {
384+
Thread.currentThread().interrupt();
385+
break;
386+
}
387+
}
388+
return !file.exists();
389+
}
390+
335391
// ── Switch version ───────────────────────────────────────────────────────
336392

337393
/**
@@ -533,8 +589,7 @@ private void applySwitchVersion(Context context, ModEntry entry, int position,
533589
File tmpFile = new File(entry.file.getParent(), targetName + ".tmp");
534590
DownloadUtils.downloadFile(url, tmpFile);
535591

536-
oldFile.delete();
537-
tmpFile.renameTo(targetFile);
592+
replaceModFile(oldFile, tmpFile, targetFile);
538593

539594
mMainHandler.post(() -> {
540595
entry.file = targetFile;

0 commit comments

Comments
 (0)