fix: release files through git-cdn.magicbean.dev, falling back to git.magicbean.dev #20
No reviewers
Labels
No labels
area/build
area/install
area/saves
area/tooling
area/ui
area/updates
ci: approved
good first issue
gpu/amd
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
platform/linux
platform/steam-deck
platform/windows
priority
critical
priority
high
priority
low
risk
high
risk
low
risk
medium
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/LongshotLauncher!20
Loading…
Reference in a new issue
No description provided.
Delete branch "HumanBean/LongshotLauncher:cdn-downloads"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
The launcher downloads every release file (the builder,
SHA256SUMS, its signature,tools.lock, its own update) straight from git.magicbean.dev, the home server.git-cdn.magicbean.devserves the same files from Cloudflare's cache, so downloads should come from there first and leave the server alone.What changed
Core/CdnFetcher.cs(new) wraps the fetcher.CdnOf()rewrites ahttps://git.magicbean.devlink whose path is a release file (/<owner>/<repo>/releases/download/...) or a package (/api/packages/...) tohttps://git-cdn.magicbean.dev+ the same path.DownloadAsynctries that first, then the original link. It falls back only on a failure of the CDN (FallsBack()): no answer (connection, TLS, timeout, stall:Statusnull), or HTTP 403, 404 or 5xx. API calls (GetTextAsync,/api/v1/...), other hosts, and links already on git-cdn are left as they are.ReleaseManifest.FetchAsync/FetchUncheckedAsync,SelfUpdate.DownloadAsync,Toolchains.DownloadFrom) and throwInvalidDataExceptionas they did before. The fallback catches onlyFetchException.SetLength(start),Position = start), so nothing of the CDN's half stays. If the stream can't seek back, the CDN's error is rethrown. All the callers' streams can seek:MemoryStream, the update'sFileStream, andToolchains'CheckedStream, which now seeks back only to its start and resets its hash and count (Core/Toolchains.cs).tools.lockalready list their git-cdn link first (Longshot #160).Toolchains.Downloadtries each tool's own links once, in order, throughCdnFetcher.Inner, usingCdnFetcher.Expand(). The git-cdn link is added in front of an older list that lacks it, and is never tried twice. A link that gives other bytes moves on to the next one, as before. The builder zip and AppImage go through the strict fallback.App/LauncherService.csputs both fetchers (downloads and the update check) behindCdnFetcher.LONGSHOT_LAUNCHER_NO_CDN=1.tests/launcher_core/CdnChecks.cs(38 checks).Flags and settings
LONGSHOT_LAUNCHER_NO_CDN=1Measurements
main:
22983c82(2026-11-13)this PR: cdn-downloads @
b9c621b3Hardware: Linux x86_64 desktop (CachyOS), wired home connection
Scene: no game; the launcher's own
v1.0.0-rc.1release,LongshotLauncher-v1.0.0-rc.1-x86_64.AppImage(40,049,144 bytes)Method: a small harness that compiles
Core/and runsReleases.VersionAsync("v1.0.0-rc.1")(signedSHA256SUMSand.sig) and thenSelfUpdate.DownloadAsyncon the AppImage (size and SHA-256 checked). It times the AppImage download and logs each URL the inner fetcher fetched. 2 runs each, interleaved (CDN, no CDN, CDN, no CDN); the median of 2 is their mean.How to reproduce: build the harness against
Core/and run it withcdn/nocdn(orLONGSHOT_LAUNCHER_NO_CDN=1)Baseline: this PR @
b9c621b3, LONGSHOT_LAUNCHER_NO_CDN=1 (baseline). With the CDN off, the code takes main's download path (the same links on git.magicbean.dev, the sameHttpFetcher), so it compares the two hosts with the same harness.b9c621b3, LONGSHOT_LAUNCHER_NO_CDN=1 (baseline)b9c621b3(CDN)From this PC the two hosts are close. The CDN's gain is mostly for players far from the server, and in taking the load off it.
Testing
Tested:
python -I tests/test_launcher_core.py --strict --longshot <Longshot checkout>: 568 checks on main, 606 here. The new ones cover: the rewrite (a release file, a package, the API, a release page, other hosts, already-git-cdn, http, another port); the fallback for no answer, 403, 404, 500 and 503 before any byte, and for a file the CDN lacks; 400 not falling back; both hosts failing (git.magicbean.dev's error is the one reported); cut short half way into aMemoryStream, the update's file andToolchains' checked stream; a stream that can't seek, which does not fall back. Also: no fallback on another SHA-256 or an oversize file (FetchAsync,FetchUncheckedAsync,SelfUpdate, the builder zip); each of a tool's links once, in order; andLONGSHOT_LAUNCHER_NO_CDN. Removing the stream reset makes the checks fail.python -I tests/test_release.py: 28 tests, OK (unchanged).dotnet build LongshotLauncher.slnx -c Release -warnaserror: 0 warnings, 0 errors (Linux).v1.0.0-rc.1SHA256SUMS,.sigand AppImage came through git-cdn (log above). The signature and the AppImage's size and SHA-256 verified.Not tested:
Needs other testers: None
Screenshots
Nothing on screen
Risk and rollback
If git-cdn serves a stale or wrong file, the download fails its SHA-256 check as it would have from git.magicbean.dev. It does not fall back silently (except for a tool, whose
tools.locklinks already moved on after bad bytes). If the CDN is down, each download waits for its failure (the connection error, or at worst the 60 s timeout) before going to git.magicbean.dev.LONGSHOT_LAUNCHER_NO_CDN=1turns it off for one player; reverting the commit turns it off for all.Reviewer notes
Start with
Core/CdnFetcher.cs(CdnOf,FallsBack,DownloadAsync), thenToolchains.Downloadand theCheckedStreamreset.Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above). (This repository's set:tests/test_launcher_core.pyandtests/test_release.py, both passing.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
mainand passing here.