Keep bounded Epic downloads in a stable UI stage

This commit is contained in:
Juan Marti Mercado committed 2026-10-13 19:15:34 -04:00
1 parent 78b3f27816
commit c8a49e853a
3 files changed
+30 -4

No files matched your search

@@ -146,6 +146,8 @@ object EpicBackend : StoreBackend, EpicLaunchSupport {
EpicLibrary.rememberSize(app, item.id, diskBytes)
}
override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) {
// Assembly overlaps chunk fetching; only the final install event advances the UI.
if (stage == "stream") return
handle.stage(when (stage) { "check" -> DownloadStage.MANIFEST; "verify" -> DownloadStage.VERIFY; else -> DownloadStage.INSTALL })
handle.stageProgress(done, total, items, itemsTotal, bytes = stage == "install")
}
@@ -55,6 +55,7 @@ public final class EpicDownloadManager {
/**
* The active stage's own count: {@code stage} is "check" (the files already there, before
* anything is fetched), "verify" or "install" (bytes written), {@code done} of
* "stream" reports assembly during a bounded download without completing the install stage.
* {@code total} an amount (bytes, or items when there are no bytes), {@code items} of
* {@code itemsTotal} the files, when it counts files.
*/
@@ -247,7 +248,7 @@ public final class EpicDownloadManager {
if (cancel.get()) return null;
String relPath = file.filename.replace("\\", "/");
File outFile = new File(installDir, relPath);
cb.onProgress("Writing: " + (relPath.contains("/") ? relPath.substring(relPath.lastIndexOf('/') + 1) : relPath), 80 + (int) (doneFiles * 20L / totalFiles));
cb.onProgress("Downloading chunks: " + (relPath.contains("/") ? relPath.substring(relPath.lastIndexOf('/') + 1) : relPath), (int) (completedBytes * 80L / Math.max(1, fTotalBytes)));
// Like the native assembler, write to the final path and remove it on failure.
// A sibling "<name>.part" can itself be a legitimate file in the manifest.
File partFile = outFile;
@@ -308,9 +309,10 @@ public final class EpicDownloadManager {
}
doneFiles++;
writtenBytes += file.fileSize();
cb.onStage("install", writtenBytes, pendingBytes, doneFiles, totalFiles);
cb.onStage("stream", writtenBytes, pendingBytes, doneFiles, totalFiles);
}
deleteDir(chunkCacheDir);
cb.onStage("install", writtenBytes, pendingBytes, doneFiles, totalFiles);
cb.onProgress("Complete", 100);
return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest);
} catch (InstallException e) {
@@ -106,7 +106,7 @@ class EpicBoundedInstallTest {
val callback = object : EpicDownloadManager.Callback {
override fun onProgress(message: String, pct: Int) {}
override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) {
if (stage == "install" && items == 1) cancel.set(true)
if (stage == "stream" && items == 1) cancel.set(true)
}
}
assertNull(install(cancel, callback))
@@ -121,7 +121,7 @@ class EpicBoundedInstallTest {
val cancel = AtomicBoolean()
val callback = object : EpicDownloadManager.Callback {
override fun onProgress(message: String, pct: Int) {
if (message.startsWith("Downloading chunks")) cancel.set(true)
if (message.startsWith("Downloading chunks (")) cancel.set(true)
}
}
assertNull(install(cancel, callback))
@@ -156,6 +156,28 @@ class EpicBoundedInstallTest {
assertNotNull(install())
}
@Test fun boundedProgressNeverReentersDownloadAfterEnteringInstall() {
val events = mutableListOf<String>()
val callback = object : EpicDownloadManager.Callback {
override fun onProgress(message: String, pct: Int) {
if (message.startsWith("Downloading chunks")) events.add("download")
if (message.startsWith("Writing")) events.add("install")
}
override fun onBytes(done: Long, total: Long, speedBps: Long) { events.add("download") }
override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) {
if (stage == "install") {
assertEquals(total, done)
assertEquals(itemsTotal, items)
events.add("install")
}
}
}
assertNotNull(install(callback = callback))
val firstInstall = events.indexOf("install")
assertTrue(firstInstall > 0)
assertFalse(events.drop(firstInstall).contains("download"))
}
@Test fun hogwartsSizedSequentialPlanFitsAvailableSpaceWithoutFullChunkCache() {
val gib = 1024L * 1024 * 1024
val file = EpicDownloadManager.FileInfo()