Show real download progress for Windows updates via a loopback proxy - #2468
Show real download progress for Windows updates via a loopback proxy#2468hahn-kev-bot wants to merge 1 commit into
Conversation
The Windows updater handed the GitHub .msixbundle URL straight to PackageManager.AddPackageByUriAsync, which fuses download + staging + registration into one opaque operation whose only feedback was an unreliable percentage the UI distrusted. Instead, run a short-lived loopback reverse proxy (System.Net.HttpListener, BCL-only) between PackageManager and GitHub. Windows still owns the download/staging/install, but we sit in the byte path to report real progress. The proxy resolves GitHub's 302 once, forwards Range requests (preserving Windows' differential download), and reports bytes streamed. On any failure it falls back to the previous direct install so update capability can't regress. The progress event now carries bytes downloaded + speed (throttled to ~4/sec for the small JS event channel); the update dialog shows an honest "Downloading update... X MB (Y MB/s)" readout instead of a fake percentage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 20 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (16)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
|
In addition to gaining better update reporting, this also seems to have boosted update speed, from 10s, to less than 1s when tested via a prototype. Unfortunately it's really hard to test this kind of thing in production because it requires having this app installed and built, then getting that version to update. |
| { | ||
| var progress = new DownloadProgressReporter(eventBus, latestRelease); | ||
| await using var proxy = await UpdateDownloadProxy.StartAsync(latestRelease.Url, logger, progress.Report); | ||
| return await Deploy(proxy.LocalUri, latestRelease, quitOnUpdate); |
There was a problem hiding this comment.
Devin AI pointed out that Deploy runs for max 2 minutes due to:
var completedTask = await Task.WhenAny(updateTask, Task.Delay(TimeSpan.FromMinutes(2)));
at which point it simply does return UpdateResult.Started;
That then results in await using var proxy = being disposed, which presumably "cleans up"/cancels the download.
So, I think this needs some work.
| downloadProgress | ||
| }: Props = $props(); | ||
|
|
||
| function formatBytes(bytes: number): string { |
There was a problem hiding this comment.
Should arguably share code with media-manager which currently has:
const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
export function formatFileSize(bytes?: number): string | undefined {
if (bytes == null) return undefined;
let size = bytes;
let unit = 0;
while (size >= 1024 && unit < BYTE_UNITS.length - 1) {
size /= 1024;
unit++;
}
return `${formatNumber(size, {maximumFractionDigits: unit === 0 ? 0 : 1})} ${BYTE_UNITS[unit]}`;
}
What & why
The Windows updater handed the GitHub
.msixbundleURL straight toPackageManager.AddPackageByUriAsync, which fuses network download + staging + registration into one opaque operation. Its only feedback was an unreliableuint percentagethe frontend distrusted (it hid0and had a "not sure if the % actually works" comment), and the op could appear to hang (the code races a 2-minute timeout).This change runs a short-lived loopback reverse proxy (
System.Net.HttpListener, BCL-only — no ASP.NET shipped in MAUI) betweenPackageManagerand GitHub. Windows still owns the download/staging/install; we just sit in the byte path to report real progress.How it works
UpdateDownloadProxy(new,Platforms/Windows): resolves GitHub's 302 redirect once to a terminal asset URL, binds a randomhttp://localhost:{port}/{guid}/prefix (no admin / no URL-ACL needed for alocalhostprefix), forwardsRangerequests (echoing206/Content-Range/Accept-Ranges), 404s unknown paths, reuses one keep-alive upstream connection, and reports the running byte total. Re-resolves + retries once on401/403(signed-URL expiry).AppUpdateService.ApplyUpdateuses the proxy path and falls back to the previous directAddPackageByUriAsync(release.Url)on any failure — update capability can't regress. Deployment options (DeferRegistrationWhenPackagesAreInUse,ForceUpdateFromAnyVersion,ForceAppShutdown) and the result mapping are unchanged.AppUpdateProgressEventnow carriesBytesDownloaded+BytesPerSecond(throttled to ~4/sec for the smallJsEventListenerchannel) instead of the distrustedPercentage. The update dialog shows an honestDownloading update... 12.3 MB (2.1 MB/s)readout, falling back toInstalling Update...when no byte events arrive (the direct-install fallback path).Notes for reviewers
AddPackageByUriAsyncbehavior is byte-identical with/without rewriting GitHub'sapplication/octet-stream; the differential behavior comes from Range support, not MIME. (Content-Type only matters for the dormant.appinstallerpath, which is out of scope here.)IHttpClientFactorywas intentionally not injected intoAppUpdateService: the proxy owns a purpose-builtHttpClient(AllowAutoRedirect=false+ keep-alive), which fights a factory client.HttpListeneris Windows-only (http.sys) — fine, since this all lives underPlatforms/Windows.Test plan
FwLiteSharedregenerates the TS event type.UpdateDownloadProxyTests(3 tests,#if WINDOWS): full-download forwarding + byte total, Range →206+Content-Rangepassthrough, 404 for unknown paths. All pass.svelte-checkclean; eslint clean on changed files; i18n extracted (newDownloading update...string across 8 locales).🤖 Generated with Claude Code