From 9603e76ceac7d1817e105df2a354ac5130cfb859 Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Fri, 7 Aug 2026 22:24:10 +0700 Subject: [PATCH] maybe improve visual code bundling --- .github/workflows/oneclient_nightly.yml | 56 +++-- .github/workflows/oneclient_release.yml | 56 +++-- distribution/windows/bundle-crt.py | 117 ++++++++++ distribution/windows/bundle-vcredist.py | 81 ------- packages/oneclient_app/Cargo.toml | 12 +- packages/oneclient_app/build.rs | 1 - .../src/components/account_switcher.rs | 1 - .../oneclient_app/src/components/button.rs | 4 +- .../oneclient_app/src/components/charts.rs | 27 +-- .../src/components/cluster_update_popup.rs | 19 +- .../src/components/context_menu.rs | 2 +- .../src/components/dynamic_art.rs | 15 +- .../oneclient_app/src/components/file_drop.rs | 39 +--- .../src/components/generic_prompt.rs | 31 +-- .../oneclient_app/src/components/icons.rs | 3 +- .../src/components/java_install_manager.rs | 5 +- .../src/components/java_prompt.rs | 11 +- .../src/components/microsoft_login.rs | 8 +- .../src/components/navbar/app_navbar.rs | 11 +- .../src/components/notifications.rs | 1 - .../src/components/overlay_popup.rs | 1 - .../src/components/package_row.rs | 13 +- .../src/components/package_update_popup.rs | 29 +-- .../src/components/pagination.rs | 3 +- .../src/components/player_model.rs | 8 +- .../src/components/recents_row.rs | 28 +-- .../src/components/scrollview.rs | 3 - .../src/components/splash_curtain.rs | 9 +- .../src/components/status_bar.rs | 18 +- .../oneclient_app/src/components/toasts.rs | 14 +- .../src/components/update_prompt.rs | 3 +- packages/oneclient_app/src/events.rs | 36 +-- packages/oneclient_app/src/hooks/actions.rs | 220 +++++------------- .../oneclient_app/src/hooks/active_cluster.rs | 18 +- packages/oneclient_app/src/hooks/debounce.rs | 7 +- packages/oneclient_app/src/hooks/mod.rs | 15 +- .../oneclient_app/src/hooks/queries/auth.rs | 12 +- .../src/hooks/queries/clusters.rs | 7 +- .../oneclient_app/src/hooks/queries/image.rs | 6 +- .../src/hooks/queries/migration.rs | 5 +- .../oneclient_app/src/hooks/queries/mod.rs | 8 +- .../src/hooks/queries/mutations.rs | 25 +- .../src/hooks/queries/package_updates.rs | 7 +- .../src/hooks/queries/packages.rs | 18 +- .../src/hooks/queries/player_profile.rs | 3 +- .../oneclient_app/src/hooks/queries/skin.rs | 1 - .../oneclient_app/src/hooks/queries/state.rs | 32 +-- .../src/hooks/queries/storage.rs | 12 +- .../src/hooks/queries/version_metadata.rs | 1 - .../src/hooks/queries/versions.rs | 7 +- packages/oneclient_app/src/install.rs | 35 +-- packages/oneclient_app/src/launcher.rs | 21 +- .../src/layout/animated_outlet.rs | 5 +- .../oneclient_app/src/layout/app_shell.rs | 31 +-- .../oneclient_app/src/layout/cluster_shell.rs | 5 +- .../oneclient_app/src/layout/root_layout.rs | 12 +- .../src/layout/settings_shell.rs | 23 +- packages/oneclient_app/src/lib.rs | 4 +- packages/oneclient_app/src/main.rs | 19 +- packages/oneclient_app/src/notifications.rs | 111 +++------ packages/oneclient_app/src/state.rs | 56 ++--- packages/oneclient_app/src/theme/mod.rs | 2 - packages/oneclient_app/src/transfer.rs | 31 +-- packages/oneclient_app/src/ui.rs | 29 +-- packages/oneclient_app/src/updater.rs | 18 +- packages/oneclient_app/src/utils.rs | 35 ++- .../src/view/app/analytics/mod.rs | 13 +- .../src/view/app/analytics/servers.rs | 9 +- .../src/view/app/browser/index/cards.rs | 31 +-- .../src/view/app/browser/index/mod.rs | 20 +- .../oneclient_app/src/view/app/browser/mod.rs | 56 ++--- .../src/view/app/browser/package/gallery.rs | 21 +- .../src/view/app/browser/package/panels.rs | 10 +- .../src/view/app/browser/package/sidebar.rs | 16 +- .../src/view/app/cluster/cluster_settings.rs | 4 +- .../src/view/app/cluster/logs.rs | 21 +- .../view/app/cluster/package_manager/mod.rs | 54 ++--- .../view/app/cluster/package_manager/views.rs | 23 +- .../src/view/app/clusters/page.rs | 5 +- packages/oneclient_app/src/view/app/debug.rs | 71 +----- packages/oneclient_app/src/view/app/mod.rs | 5 +- .../src/view/app/settings/accounts.rs | 26 +-- .../src/view/app/settings/changelog.rs | 11 +- .../src/view/app/settings/minecraft.rs | 4 +- .../src/view/app/settings/mod.rs | 2 - .../src/view/app/settings/storage.rs | 17 +- packages/oneclient_app/src/view/console.rs | 45 +--- .../src/view/onboarding/bundles/mod.rs | 5 +- .../view/onboarding/downloading/backdrop.rs | 5 +- .../src/view/onboarding/downloading/mod.rs | 62 ++--- .../view/onboarding/downloading/progress.rs | 5 +- .../src/view/onboarding/downloading/tips.rs | 3 +- .../src/view/onboarding/migration.rs | 23 +- .../oneclient_app/src/view/onboarding/mod.rs | 3 +- packages/oneclient_app/src/view/startup.rs | 8 +- packages/oneclient_auth/src/diagnostics.rs | 13 +- packages/oneclient_auth/src/error.rs | 13 +- packages/oneclient_auth/src/lib.rs | 9 +- packages/oneclient_auth/src/msa.rs | 1 - packages/oneclient_auth/src/service.rs | 117 +++------- packages/oneclient_auth/src/store.rs | 10 +- packages/oneclient_auth/tests/offline.rs | 1 - packages/oneclient_cluster/src/lib.rs | 14 +- packages/oneclient_cluster/src/logs/manage.rs | 10 +- packages/oneclient_cluster/src/logs/mod.rs | 7 +- packages/oneclient_cluster/src/manager.rs | 25 +- packages/oneclient_cluster/src/profile.rs | 11 +- packages/oneclient_cluster/src/profiles.rs | 21 +- .../oneclient_cluster/src/screenshots/mod.rs | 2 +- packages/oneclient_common/src/domain.rs | 25 +- packages/oneclient_common/src/error.rs | 5 +- packages/oneclient_common/src/lib.rs | 9 +- packages/oneclient_common/src/paths.rs | 13 +- packages/oneclient_common/src/search.rs | 114 +++------ .../oneclient_content/src/bundles/install.rs | 142 ++++------- .../oneclient_content/src/bundles/manager.rs | 14 +- .../src/bundles/overrides.rs | 7 - .../oneclient_content/src/bundles/runtime.rs | 4 +- .../oneclient_content/src/bundles/types.rs | 8 +- .../oneclient_content/src/bundles/updates.rs | 58 ++--- packages/oneclient_content/src/ctx.rs | 15 +- packages/oneclient_content/src/error.rs | 10 - packages/oneclient_content/src/lib.rs | 9 - .../src/packages/activity.rs | 40 +--- .../src/packages/dependencies.rs | 42 ++-- .../src/packages/metadata_cache.rs | 3 +- .../oneclient_content/src/packages/mod.rs | 3 +- .../src/packages/provider/curseforge.rs | 7 +- .../src/packages/provider/mod.rs | 7 +- .../src/packages/provider/modrinth.rs | 8 +- .../src/packages/store/download.rs | 2 +- .../src/packages/store/gc.rs | 49 +--- .../src/packages/store/link.rs | 25 +- .../src/packages/store/manifest.rs | 21 +- .../src/packages/store/mod.rs | 35 +-- .../oneclient_content/src/packages/types.rs | 16 +- .../oneclient_content/src/packages/updates.rs | 129 ++++------ packages/oneclient_core/src/clusters/mod.rs | 10 - .../oneclient_core/src/clusters/prepare.rs | 15 +- .../src/clusters/unlink_legacy.rs | 30 +-- packages/oneclient_core/src/dev.rs | 5 +- packages/oneclient_core/src/error.rs | 51 +--- packages/oneclient_core/src/game/diagnosis.rs | 59 ++--- packages/oneclient_core/src/game/launch.rs | 96 +++----- .../oneclient_core/src/game/log_replay.rs | 52 ++--- packages/oneclient_core/src/game/mod.rs | 2 - packages/oneclient_core/src/game/process.rs | 21 +- packages/oneclient_core/src/game/reattach.rs | 88 +++---- packages/oneclient_core/src/game/session.rs | 18 +- .../oneclient_core/src/game/shared_dir.rs | 194 ++++++--------- packages/oneclient_core/src/game/tail.rs | 50 ++-- packages/oneclient_core/src/java_store.rs | 10 +- packages/oneclient_core/src/lib.rs | 1 - packages/oneclient_core/src/logger.rs | 41 ++-- packages/oneclient_core/src/logger/console.rs | 47 ++-- packages/oneclient_core/src/migration/mod.rs | 15 +- .../src/migration/oneclient_v1.rs | 2 +- .../oneclient_core/src/migration/vanilla.rs | 7 +- packages/oneclient_core/src/recovery.rs | 17 +- packages/oneclient_core/src/reporting.rs | 6 +- packages/oneclient_core/src/settings/mod.rs | 10 +- packages/oneclient_core/src/settings/store.rs | 10 +- packages/oneclient_core/src/simulate.rs | 74 ++---- packages/oneclient_core/src/state.rs | 39 +--- packages/oneclient_core/src/storage.rs | 58 +---- packages/oneclient_core/src/verify.rs | 95 ++------ .../oneclient_core/src/versions/manager.rs | 9 +- .../oneclient_core/src/versions/manifest.rs | 24 +- .../oneclient_core/tests/bundle_updates.rs | 10 +- packages/oneclient_core/tests/clusters.rs | 7 +- .../oneclient_core/tests/download_verify.rs | 26 +-- packages/oneclient_db/src/console.rs | 19 +- packages/oneclient_db/src/dao/artifact.rs | 21 +- .../src/dao/browser_package_update.rs | 18 +- .../oneclient_db/src/dao/cluster_bundle.rs | 5 +- packages/oneclient_db/src/dao/game_session.rs | 7 +- .../src/models/browser_package_update.rs | 7 +- .../oneclient_db/src/models/cluster_bundle.rs | 2 +- .../src/models/setting_profile.rs | 2 +- packages/oneclient_discord/src/lib.rs | 4 - packages/oneclient_events/src/bus.rs | 52 +---- packages/oneclient_events/src/error.rs | 6 +- packages/oneclient_events/src/event.rs | 39 +--- packages/oneclient_events/src/lib.rs | 14 +- packages/oneclient_events/src/progress.rs | 50 ++-- packages/oneclient_events/src/prompt.rs | 54 ++--- .../examples/install_provider.rs | 9 +- .../oneclient_java/examples/list_providers.rs | 6 +- packages/oneclient_java/src/checker.rs | 12 +- packages/oneclient_java/src/data.rs | 24 +- packages/oneclient_java/src/error.rs | 14 +- packages/oneclient_java/src/install.rs | 12 +- packages/oneclient_java/src/lib.rs | 8 +- packages/oneclient_java/src/locate.rs | 17 +- packages/oneclient_java/src/platform.rs | 20 +- packages/oneclient_java/src/service.rs | 52 +---- packages/oneclient_java/src/store.rs | 20 +- .../oneclient_java/src/vendors/adoptium.rs | 17 +- .../oneclient_java/src/vendors/corretto.rs | 11 +- .../oneclient_java/src/vendors/liberica.rs | 10 +- packages/oneclient_java/src/vendors/zulu.rs | 8 +- .../oneclient_java/tests/vendor_checksums.rs | 6 +- packages/oneclient_mc/src/arguments.rs | 8 +- packages/oneclient_mc/src/download.rs | 22 +- packages/oneclient_mc/src/error.rs | 9 +- packages/oneclient_mc/src/install.rs | 176 ++++---------- packages/oneclient_mc/src/lib.rs | 14 +- packages/oneclient_mc/src/profile.rs | 8 +- packages/oneclient_net/src/cache.rs | 40 +--- packages/oneclient_net/src/config.rs | 14 +- packages/oneclient_net/src/download.rs | 73 ++---- packages/oneclient_net/src/error.rs | 27 +-- packages/oneclient_net/src/lib.rs | 9 +- packages/oneclient_net/src/response.rs | 18 +- packages/oneclient_net/src/service.rs | 32 +-- packages/oneclient_net/src/status.rs | 13 +- packages/oneclient_polyplus/src/lib.rs | 22 +- packages/polyio/src/archive.rs | 14 +- packages/polyio/src/file.rs | 140 +++-------- packages/polyio/src/hash.rs | 35 +-- packages/polyio/src/system.rs | 4 +- packages/polyio/src/testing.rs | 16 +- 222 files changed, 1635 insertions(+), 3962 deletions(-) create mode 100644 distribution/windows/bundle-crt.py delete mode 100644 distribution/windows/bundle-vcredist.py diff --git a/.github/workflows/oneclient_nightly.yml b/.github/workflows/oneclient_nightly.yml index 242edc38..7e77c5be 100644 --- a/.github/workflows/oneclient_nightly.yml +++ b/.github/workflows/oneclient_nightly.yml @@ -132,25 +132,28 @@ jobs: rm -f "$out/OneClient.icns" test -f "$out/Assets.car" || { echo "::error::actool produced no Assets.car"; exit 1; } - - name: (Windows) Bundle VC++ redistributable into the installer + - name: (Windows) Stage app-local VC++ runtime if: matrix.platform == 'windows-latest' shell: bash - run: python3 distribution/windows/bundle-vcredist.py + run: python3 distribution/windows/bundle-crt.py - - name: (Windows) Verify bundled redistributable is Microsoft-signed + - name: (Windows) Verify staged runtime is Microsoft-signed if: matrix.platform == 'windows-latest' shell: pwsh run: | - $sig = Get-AuthenticodeSignature 'target/vcredist/vc_redist.x64.exe' - Write-Host "Authenticode status: $($sig.Status)" - Write-Host "Signer: $($sig.SignerCertificate.Subject)" - if ($sig.Status -ne 'Valid') { - Write-Error "vc_redist.x64.exe signature is $($sig.Status), refusing to bundle it" - exit 1 - } - if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') { - Write-Error "vc_redist.x64.exe is not signed by Microsoft: $($sig.SignerCertificate.Subject)" - exit 1 + $dlls = @(Get-ChildItem 'target/crt/*.dll') + if ($dlls.Count -eq 0) { Write-Error 'no CRT DLLs staged'; exit 1 } + foreach ($dll in $dlls) { + $sig = Get-AuthenticodeSignature $dll.FullName + Write-Host "$($dll.Name): $($sig.Status) $($sig.SignerCertificate.Subject)" + if ($sig.Status -ne 'Valid') { + Write-Error "$($dll.Name) signature is $($sig.Status), refusing to ship it" + exit 1 + } + if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') { + Write-Error "$($dll.Name) is not signed by Microsoft" + exit 1 + } } - name: Package (unsigned) @@ -189,6 +192,33 @@ jobs: cargo generate-rpm -p packages/oneclient_app cp target/generate-rpm/*.rpm "${{ steps.outdir.outputs.dir }}/" + - name: (Windows) Smoke-test installer layout + if: matrix.platform == 'windows-latest' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $dest = 'C:\oneclient-smoke' + Remove-Item -Recurse -Force $dest -ErrorAction SilentlyContinue + $exe = Resolve-Path (Get-ChildItem '${{ steps.outdir.outputs.dir }}/*-setup.exe')[0].FullName + # NSIS: /S is silent, /D= sets the install dir and must be the last arg. + $p = Start-Process -FilePath $exe -ArgumentList '/S', "/D=$dest" -Wait -PassThru + if ($p.ExitCode -ne 0) { Write-Error "installer exited $($p.ExitCode)"; exit 1 } + + Write-Host '--- installed layout ---' + Get-ChildItem -Recurse $dest | ForEach-Object { Write-Host $_.FullName } + + if (-not (Test-Path "$dest\oneclient_app.exe")) { + Write-Error "oneclient_app.exe is not at the root of $dest" + exit 1 + } + foreach ($dll in @(Get-ChildItem 'target/crt/*.dll')) { + if (-not (Test-Path (Join-Path $dest $dll.Name))) { + Write-Error "$($dll.Name) did not land beside oneclient_app.exe — cargo-packager put resources elsewhere, so app-local CRT loading will not work" + exit 1 + } + } + Write-Host "OK: $($(Get-ChildItem 'target/crt/*.dll').Count) CRT DLLs sit beside oneclient_app.exe" + # Upload debug info (dSYM / .dwp / .pdb produced by the release profile's # split-debuginfo) to Sentry so nightly crash reports get real file/function/ # line stack traces instead of raw addresses. Runs on every main push, so diff --git a/.github/workflows/oneclient_release.yml b/.github/workflows/oneclient_release.yml index 1bd43898..1ebcd22c 100644 --- a/.github/workflows/oneclient_release.yml +++ b/.github/workflows/oneclient_release.yml @@ -345,25 +345,28 @@ jobs: rm -f "$out/OneClient.icns" test -f "$out/Assets.car" || { echo "::error::actool produced no Assets.car"; exit 1; } - - name: (Windows) Bundle VC++ redistributable into the installer + - name: (Windows) Stage app-local VC++ runtime if: matrix.platform == 'windows-latest' shell: bash - run: python3 distribution/windows/bundle-vcredist.py + run: python3 distribution/windows/bundle-crt.py - - name: (Windows) Verify bundled redistributable is Microsoft-signed + - name: (Windows) Verify staged runtime is Microsoft-signed if: matrix.platform == 'windows-latest' shell: pwsh run: | - $sig = Get-AuthenticodeSignature 'target/vcredist/vc_redist.x64.exe' - Write-Host "Authenticode status: $($sig.Status)" - Write-Host "Signer: $($sig.SignerCertificate.Subject)" - if ($sig.Status -ne 'Valid') { - Write-Error "vc_redist.x64.exe signature is $($sig.Status), refusing to bundle it" - exit 1 - } - if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') { - Write-Error "vc_redist.x64.exe is not signed by Microsoft: $($sig.SignerCertificate.Subject)" - exit 1 + $dlls = @(Get-ChildItem 'target/crt/*.dll') + if ($dlls.Count -eq 0) { Write-Error 'no CRT DLLs staged'; exit 1 } + foreach ($dll in $dlls) { + $sig = Get-AuthenticodeSignature $dll.FullName + Write-Host "$($dll.Name): $($sig.Status) $($sig.SignerCertificate.Subject)" + if ($sig.Status -ne 'Valid') { + Write-Error "$($dll.Name) signature is $($sig.Status), refusing to ship it" + exit 1 + } + if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') { + Write-Error "$($dll.Name) is not signed by Microsoft" + exit 1 + } } - name: Package @@ -441,6 +444,33 @@ jobs: shell: bash run: echo "path=$(ls ${{ steps.outdir.outputs.dir }}/*-setup.exe | head -n1)" >> "$GITHUB_OUTPUT" + - name: (Windows) Smoke-test installer layout + if: matrix.platform == 'windows-latest' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $dest = 'C:\oneclient-smoke' + Remove-Item -Recurse -Force $dest -ErrorAction SilentlyContinue + $exe = Resolve-Path '${{ steps.win_installer.outputs.path }}' + # NSIS: /S is silent, /D= sets the install dir and must be the last arg. + $p = Start-Process -FilePath $exe -ArgumentList '/S', "/D=$dest" -Wait -PassThru + if ($p.ExitCode -ne 0) { Write-Error "installer exited $($p.ExitCode)"; exit 1 } + + Write-Host '--- installed layout ---' + Get-ChildItem -Recurse $dest | ForEach-Object { Write-Host $_.FullName } + + if (-not (Test-Path "$dest\oneclient_app.exe")) { + Write-Error "oneclient_app.exe is not at the root of $dest" + exit 1 + } + foreach ($dll in @(Get-ChildItem 'target/crt/*.dll')) { + if (-not (Test-Path (Join-Path $dest $dll.Name))) { + Write-Error "$($dll.Name) did not land beside oneclient_app.exe — cargo-packager put resources elsewhere, so app-local CRT loading will not work" + exit 1 + } + } + Write-Host "OK: $($(Get-ChildItem 'target/crt/*.dll').Count) CRT DLLs sit beside oneclient_app.exe" + - name: (Windows) Upload unsigned installer artifact if: matrix.platform == 'windows-latest' id: win_upload_unsigned diff --git a/distribution/windows/bundle-crt.py b/distribution/windows/bundle-crt.py new file mode 100644 index 00000000..09f5b8d4 --- /dev/null +++ b/distribution/windows/bundle-crt.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + +BEGIN_RES = "# >>> CRT_RESOURCES >>>" +END_RES = "# <<< CRT_RESOURCES <<<" +BEGIN_SEC = "; >>> VC_REDIST_SECTION >>>" +END_SEC = "; <<< VC_REDIST_SECTION <<<" + +REPO_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_CARGO_TOML = REPO_ROOT / "packages" / "oneclient_app" / "Cargo.toml" +DEFAULT_OUT = REPO_ROOT / "target" / "crt" + +VSWHERE = Path( + r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" +) +CRT_GLOB = r"VC\Redist\MSVC\**\x64\Microsoft.VC*.CRT\*.dll" + +SECTION_REPLACEMENT = """; >>> VC_REDIST_SECTION >>> +; <<< VC_REDIST_SECTION <<<""" + + +def region(begin: str, end: str) -> re.Pattern: + return re.compile( + r"^[ \t]*" + re.escape(begin) + r".*?^[ \t]*" + re.escape(end) + r"[ \t]*$", + re.DOTALL | re.MULTILINE, + ) + + +REGION_RES = region(BEGIN_RES, END_RES) +REGION_SEC = region(BEGIN_SEC, END_SEC) + + +def find_crt_dir() -> Path: + """Locate the newest x64 Microsoft.VC*.CRT redist directory via vswhere.""" + if not VSWHERE.is_file(): + sys.exit(f"error: vswhere not found at {VSWHERE}; pass --crt-dir explicitly") + + out = subprocess.run( + [str(VSWHERE), "-latest", "-products", "*", "-find", CRT_GLOB], + capture_output=True, + text=True, + check=True, + ).stdout + dirs = {Path(line).parent for line in out.splitlines() if line.strip()} + if not dirs: + sys.exit(f"error: vswhere found no DLLs matching {CRT_GLOB}") + + newest = sorted(dirs, key=lambda p: p.parts)[-1] + print(f"CRT redist dir: {newest}") + return newest + + +def collect(crt_dir: Path, out_dir: Path) -> list[Path]: + dlls = sorted(crt_dir.glob("*.dll")) + if not dlls: + sys.exit(f"error: no DLLs in {crt_dir}") + + if out_dir.exists(): + shutil.rmtree(out_dir) + out_dir.mkdir(parents=True) + + copied = [] + for dll in dlls: + dest = out_dir / dll.name + shutil.copy2(dll, dest) + copied.append(dest) + print(f" {dll.name} ({dest.stat().st_size} bytes)") + return copied + + +def patch(cargo_toml: Path, dlls: list[Path]) -> None: + text = cargo_toml.read_text(encoding="utf-8") + for pat, name in ((REGION_RES, "CRT_RESOURCES"), (REGION_SEC, "VC_REDIST_SECTION")): + if not pat.search(text): + sys.exit(f"error: {name} region not found in {cargo_toml}") + + entries = "\n".join( + f' {{ src = "{str(d).replace(chr(92), chr(92) * 2)}", target = "{d.name}" }},' + for d in dlls + ) + block = ( + f" {BEGIN_RES}\n" + f" # Generated by distribution/windows/bundle-crt.py -- do not edit by hand.\n" + f"{entries}\n" + f" {END_RES}" + ) + + text = REGION_RES.sub(lambda _: block, text, count=1) + text = REGION_SEC.sub(lambda _: SECTION_REPLACEMENT, text, count=1) + cargo_toml.write_text(text, encoding="utf-8") + print(f"patched {cargo_toml} with {len(dlls)} app-local CRT DLLs") + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--cargo-toml", type=Path, default=DEFAULT_CARGO_TOML) + ap.add_argument("--out-dir", type=Path, default=DEFAULT_OUT) + ap.add_argument( + "--crt-dir", + type=Path, + help="Microsoft.VC*.CRT directory to copy from (default: ask vswhere)", + ) + args = ap.parse_args() + + crt_dir = args.crt_dir.resolve() if args.crt_dir else find_crt_dir() + dlls = collect(crt_dir, args.out_dir.resolve()) + patch(args.cargo_toml.resolve(), dlls) + + +if __name__ == "__main__": + main() diff --git a/distribution/windows/bundle-vcredist.py b/distribution/windows/bundle-vcredist.py deleted file mode 100644 index 0d040c8b..00000000 --- a/distribution/windows/bundle-vcredist.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import hashlib -import re -import sys -import urllib.request -from pathlib import Path - -REDIST_URL = "https://aka.ms/vs/17/release/vc_redist.x64.exe" -BEGIN = "; >>> VC_REDIST_INSTALL >>>" -END = "; <<< VC_REDIST_INSTALL <<<" - -REPO_ROOT = Path(__file__).resolve().parents[2] -DEFAULT_CARGO_TOML = REPO_ROOT / "packages" / "oneclient_app" / "Cargo.toml" -DEFAULT_REDIST = REPO_ROOT / "target" / "vcredist" / "vc_redist.x64.exe" - -BLOCK = """ {begin} - ; Generated by distribution/windows/bundle-vcredist.py -- do not edit by hand. - DetailPrint "Installing Microsoft Visual C++ Redistributable (x64)..." - InitPluginsDir - File "/oname=$PLUGINSDIR\\\\vc_redist.x64.exe" "{redist}" - ExecWait '"$PLUGINSDIR\\\\vc_redist.x64.exe" /quiet /norestart' $R3 - ${{If}} $R3 == 3010 - SetRebootFlag true - ${{EndIf}} - {end}""" - -REGION = re.compile( - r"^[ \t]*" + re.escape(BEGIN) + r".*?^[ \t]*" + re.escape(END) + r"[ \t]*$", - re.DOTALL | re.MULTILINE, -) - - -def download(dest: Path) -> None: - dest.parent.mkdir(parents=True, exist_ok=True) - print(f"downloading {REDIST_URL} -> {dest}") - with urllib.request.urlopen(REDIST_URL) as resp, dest.open("wb") as out: - while chunk := resp.read(1 << 20): - out.write(chunk) - - data = dest.read_bytes() - if data[:2] != b"MZ": - sys.exit(f"error: {dest} is not a PE image (no MZ header)") - if len(data) < 10 * 1024 * 1024: - sys.exit(f"error: {dest} is only {len(data)} bytes, expected >10MB") - print(f" {len(data)} bytes, sha256={hashlib.sha256(data).hexdigest()}") - - -def patch(cargo_toml: Path, redist: Path) -> None: - text = cargo_toml.read_text(encoding="utf-8") - if not REGION.search(text): - sys.exit(f"error: {BEGIN} .. {END} region not found in {cargo_toml}") - - block = BLOCK.format(begin=BEGIN, end=END, redist=str(redist).replace("\\", "\\\\")) - cargo_toml.write_text(REGION.sub(lambda _: block, text, count=1), encoding="utf-8") - print(f"patched {cargo_toml} to embed {redist}") - - -def main() -> None: - ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("--cargo-toml", type=Path, default=DEFAULT_CARGO_TOML) - ap.add_argument("--redist", type=Path, default=DEFAULT_REDIST) - ap.add_argument( - "--skip-download", - action="store_true", - help="reuse an already-downloaded --redist instead of fetching it", - ) - args = ap.parse_args() - - redist = args.redist.resolve() - if args.skip_download: - if not redist.is_file(): - sys.exit(f"error: --skip-download given but {redist} does not exist") - else: - download(redist) - - patch(args.cargo_toml.resolve(), redist) - - -if __name__ == "__main__": - main() diff --git a/packages/oneclient_app/Cargo.toml b/packages/oneclient_app/Cargo.toml index d9869174..107dc427 100644 --- a/packages/oneclient_app/Cargo.toml +++ b/packages/oneclient_app/Cargo.toml @@ -77,7 +77,11 @@ icons = [ "icons/icon.icns", "icons/icon.ico", ] -resources = [{ src = "icons/generated/Assets.car", target = "Assets.car" }] +resources = [ + { src = "icons/generated/Assets.car", target = "Assets.car" }, + # >>> CRT_RESOURCES >>> + # <<< CRT_RESOURCES <<< +] [package.metadata.packager.macos] minimum-system-version = "10.14" @@ -104,6 +108,7 @@ Section "-OneClientTauriMigration" Delete /REBOOTOK "$INSTDIR\\OneClient.exe" SectionEnd +; >>> VC_REDIST_SECTION >>> Section "-VCRedist" SetRegView 64 ReadRegDWORD $R0 HKLM "SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64" "Installed" @@ -115,14 +120,13 @@ Section "-VCRedist" Goto vcredist_done ${EndIf} - ; >>> VC_REDIST_INSTALL >>> ${If} $PassiveMode != 1 - MessageBox MB_OK|MB_ICONEXCLAMATION "OneClient needs the Microsoft Visual C++ Redistributable (x64) 14.40 or newer. Install it from https://aka.ms/vs/17/release/vc_redist.x64.exe, then run this installer again." + MessageBox MB_OK|MB_ICONEXCLAMATION "This build of OneClient needs the Microsoft Visual C++ Redistributable (x64) 14.40 or newer. Install it from https://aka.ms/vs/17/release/vc_redist.x64.exe, then run this installer again." ${EndIf} - ; <<< VC_REDIST_INSTALL <<< vcredist_done: SectionEnd +; <<< VC_REDIST_SECTION <<< """ [package.metadata.packager.deb] diff --git a/packages/oneclient_app/build.rs b/packages/oneclient_app/build.rs index 5918f392..984deef0 100644 --- a/packages/oneclient_app/build.rs +++ b/packages/oneclient_app/build.rs @@ -1,5 +1,4 @@ fn main() { - // Embeds the app icon and VERSIONINFO into the Windows executable. #[cfg(windows)] { let mut res = winresource::WindowsResource::new(); diff --git a/packages/oneclient_app/src/components/account_switcher.rs b/packages/oneclient_app/src/components/account_switcher.rs index d331000e..8e81a335 100644 --- a/packages/oneclient_app/src/components/account_switcher.rs +++ b/packages/oneclient_app/src/components/account_switcher.rs @@ -54,7 +54,6 @@ impl Component for AccountPanel { }); let progress = intro.read().value(); - // active account first, then the rest let mut ordered: Vec = Vec::new(); if let Some(account) = default_account.clone() { ordered.push(account); diff --git a/packages/oneclient_app/src/components/button.rs b/packages/oneclient_app/src/components/button.rs index cefb3681..bb2fb314 100644 --- a/packages/oneclient_app/src/components/button.rs +++ b/packages/oneclient_app/src/components/button.rs @@ -26,7 +26,6 @@ pub enum ButtonSize { Icon, } -/// How much a disabled button is faded out. const DISABLED_OPACITY: f32 = 0.5; struct ButtonColors { @@ -198,8 +197,7 @@ impl Button { self } - /// The name a button carrying only an icon goes by, since there is no label - /// for accessibility to read out. + /// Accessible name for icon-only buttons which have no label to read out pub fn alt(mut self, alt: impl Into>) -> Self { self.alt = Some(alt.into()); self diff --git a/packages/oneclient_app/src/components/charts.rs b/packages/oneclient_app/src/components/charts.rs index 062b94d0..3c5a65ce 100644 --- a/packages/oneclient_app/src/components/charts.rs +++ b/packages/oneclient_app/src/components/charts.rs @@ -22,7 +22,6 @@ impl ValueUnit { const DEFAULT_HEIGHT: f32 = 150.; const Y_AXIS_WIDTH: f32 = 44.; -/// Slice colors, cycled when there are more slices than entries. pub const SLICE_COLORS: [Color; 10] = [ Color::from_rgb(43, 75, 255), Color::from_rgb(27, 217, 106), @@ -41,11 +40,9 @@ pub fn slice_color(i: usize) -> Color { } const PIE_SIZE: f32 = 190.; -/// Inner hole as a fraction of the radius, making it a donut. +/// Fraction of the radius const PIE_HOLE: f32 = 0.58; -/// A donut chart. Hovering a slice reports it via `on_hover` so callers can -/// sync an external legend; the hovered slice is also expanded slightly. pub struct PieChart { values: Vec, labels: Vec, @@ -70,7 +67,6 @@ impl PieChart { self } - /// Shares the hovered slice index with the caller (e.g. to highlight a legend row). pub fn hovered(mut self, hovered: State>) -> Self { self.hovered = Some(hovered); self @@ -114,7 +110,6 @@ impl Component for PieChart { paint.set_anti_alias(true); paint.set_style(PaintStyle::Fill); - // With nothing recorded, draw an empty track so the ring is still there. if total <= 0 { paint.set_color(SkColor::from(colors::component_bg_hover())); ctx.canvas.draw_circle((cx, cy), radius - 5.0, &paint); @@ -130,7 +125,6 @@ impl Component for PieChart { continue; } let sweep = (v as f32 / total as f32) * 360.0; - // Grow the hovered slice a touch, and dim the rest. let is_active = Some(i) == active; let r = if is_active { radius } else { radius - 5.0 }; let color = slice_color(i); @@ -151,7 +145,6 @@ impl Component for PieChart { start += sweep; } - // Punch the donut hole using the card background. paint.set_color(SkColor::from(colors::page_elevated())); ctx.canvas .draw_circle((cx, cy), (radius - 5.0) * PIE_HOLE, &paint); @@ -189,8 +182,7 @@ impl Component for PieChart { .height(Size::fill()) .position(Position::new_absolute()) .interactive(false) - // The canvas defaults to auto-sizing and has no children, so - // without an explicit size it measures 0x0 and draws nothing. + // Childless canvas auto-sizes to 0x0 without an explicit size .child(canvas(render_cb).width(Size::fill()).height(Size::fill())), ) .child( @@ -199,11 +191,9 @@ impl Component for PieChart { .height(Size::fill()) .position(Position::new_absolute()) .interactive(false) - // Sibling absolute rects on an equal layer paint in undefined - // order, so pin the readout above the canvas. + // Sibling absolute rects on an equal layer paint in undefined order .layer(Layer::Relative(1)) .center() - // Keeps the readout inside the donut hole (Gaps is vertical, horizontal). .padding(Gaps::new_symmetric(0., size * 0.25)) .child( rect() @@ -235,7 +225,6 @@ impl Component for PieChart { } } -/// Maps a pointer position inside the chart box to a slice index. fn slice_at(values: &[i64], x: f32, y: f32, size: f32) -> Option { let total: i64 = values.iter().sum(); if total <= 0 { @@ -251,7 +240,7 @@ fn slice_at(values: &[i64], x: f32, y: f32, size: f32) -> Option { return None; } - // Angle measured clockwise from 12 o'clock, matching the drawing order. + // Clockwise from 12 o'clock matching the drawing order let mut angle = dy.atan2(dx).to_degrees() + 90.0; if angle < 0.0 { angle += 360.0; @@ -451,15 +440,13 @@ impl Component for BarChart { } } -/// How many bars to skip between shown labels so text never overlaps. -/// Derived from the measured plot width: a wider chart (e.g. maximised window) -/// fits more labels and returns a smaller stride. +/// Bars to skip between shown labels derived from measured plot width fn label_stride(labels: &[String], plot_width: f32, gap: f32) -> usize { let n = labels.len(); if n == 0 { return 1; } - // Before the first measurement, thin conservatively to avoid an overlap flash. + // Before the first measurement thin conservatively to avoid an overlap flash if plot_width <= 0.0 { return (n / 12).max(1); } @@ -470,7 +457,7 @@ fn label_stride(labels: &[String], plot_width: f32, gap: f32) -> usize { .max() .unwrap_or(0) .max(1); - // ~6.2px per char at font_size 10, plus breathing room between labels. + // ~6.2px per char at font_size 10 let slot_px = max_chars as f32 * 6.2 + gap.max(8.0) + 8.0; let max_labels = (plot_width / slot_px).floor().max(1.0) as usize; diff --git a/packages/oneclient_app/src/components/cluster_update_popup.rs b/packages/oneclient_app/src/components/cluster_update_popup.rs index 9f211727..64df1c52 100644 --- a/packages/oneclient_app/src/components/cluster_update_popup.rs +++ b/packages/oneclient_app/src/components/cluster_update_popup.rs @@ -34,7 +34,6 @@ impl UpdateTab { } } - /// Empty-state copy; the tab labels don't read as sentences on their own. fn empty_text(self) -> &'static str { match self { UpdateTab::Updates => "Nothing was updated.", @@ -68,7 +67,6 @@ impl UpdateTab { } } -/// One cluster's resolved package names for the active tab. struct ClusterGroup { cluster_id: i64, cluster_name: String, @@ -86,9 +84,7 @@ impl Component for ClusterUpdatePopup { let summaries = snapshot.cluster_update.clone(); - // Resolve pretty display names through the package-meta cache. Hooks - // must run unconditionally, so gather ids (empty when no summary) and - // query every remote provider before the early return below. + // Hooks must run unconditionally so query every provider before the early return below let all_items: Vec<&ClusterUpdateItem> = summaries .iter() .flatten() @@ -170,8 +166,7 @@ fn content( let total: usize = summaries.iter().map(|s| s.total()).sum(); let active_tab = *active.read(); - // A batch sync can touch several clusters; a single-cluster update keeps the - // plain wording and the "Open cluster" shortcut in the footer. + // A batch sync can touch several clusters single-cluster keeps the footer shortcut let single = match summaries { [only] => Some((only.cluster_id, only.cluster_name.clone())), _ => None, @@ -189,8 +184,7 @@ fn content( ), }; - // Tab row: every category always shown with a count pill, even at zero, so - // the modal shape stays stable across clusters. + // Every category is shown even at zero so the modal shape stays stable let tabs = TabBar::new() .width(Size::fill()) .height(Size::px(30.)) @@ -203,8 +197,7 @@ fn content( .on_press(move |_| *set.write() = tab) })); - // Clusters with nothing in the active category drop out, so the list never - // shows an empty section header. + // Clusters empty in the active category drop out avoiding empty section headers let groups: Vec = summaries .iter() .filter_map(|summary| { @@ -344,9 +337,7 @@ fn change_list( scroll.into_element() } -/// Section header naming the cluster a run of changes belongs to. Pressing it -/// jumps straight to that cluster, which is the multi-cluster stand-in for the -/// footer's "Open cluster" shortcut. +/// Multi-cluster stand-in for the footer's "Open cluster" shortcut fn cluster_header( group: &ClusterGroup, first: bool, diff --git a/packages/oneclient_app/src/components/context_menu.rs b/packages/oneclient_app/src/components/context_menu.rs index 5bdb882f..c8389f03 100644 --- a/packages/oneclient_app/src/components/context_menu.rs +++ b/packages/oneclient_app/src/components/context_menu.rs @@ -78,7 +78,7 @@ impl ContextMenu { impl PartialEq for ContextMenu { fn eq(&self, _other: &Self) -> bool { - // the menu is rebuilt from scratch whenever it is reopened (kind of a bad idea but who cares lol) + // Always unequal the menu is rebuilt from scratch whenever it is reopened false } } diff --git a/packages/oneclient_app/src/components/dynamic_art.rs b/packages/oneclient_app/src/components/dynamic_art.rs index 8a6176a7..8564e71b 100644 --- a/packages/oneclient_app/src/components/dynamic_art.rs +++ b/packages/oneclient_app/src/components/dynamic_art.rs @@ -9,9 +9,7 @@ use crate::AppAssets; use crate::hooks::{loaded_image, use_cached_image, use_version_metadata}; use crate::layout::HOME_BACKGROUND_ASSET; -/// The size the cluster cards ask for, and so the one variant of a version's -/// art that is usually already cached by the time anything else wants it. -/// Full-screen art stands in with it while its own download is in flight. +/// The size cluster cards request so it is usually already cached when anything else wants it pub const ART_PREVIEW_EDGE: u32 = 512; #[derive(PartialEq, Clone)] @@ -61,13 +59,7 @@ impl DynamicArt { self } - /// Stand in with a smaller variant of the same art until the full-size one - /// has downloaded. - /// - /// A full-size background is a few hundred KB and the cluster cards have - /// usually already cached the small one, so this puts the right picture on - /// screen right away and sharpens it later, instead of sitting on the - /// built-in fallback for the length of a download. + /// Stand in with a smaller cached variant until the full-size art downloads #[must_use] pub fn preview_edge(mut self, preview_edge: u32) -> Self { self.preview_edge = Some(preview_edge); @@ -98,8 +90,7 @@ pub fn use_art_bytes( let image_query = use_cached_image(art_url.clone(), max_edge); - // Subscribed unconditionally to keep the hook order stable. Without a - // preview size it is the same query as the one above, so it costs nothing. + // Subscribed unconditionally to keep hook order stable same query when no preview size let preview_edge = preview_edge.unwrap_or(max_edge); let preview_query = use_cached_image(art_url.clone(), preview_edge); diff --git a/packages/oneclient_app/src/components/file_drop.rs b/packages/oneclient_app/src/components/file_drop.rs index af93d44a..d4f6c6a8 100644 --- a/packages/oneclient_app/src/components/file_drop.rs +++ b/packages/oneclient_app/src/components/file_drop.rs @@ -1,14 +1,5 @@ -//! Window-wide file drops. -//! -//! [`FileDropOverlay`] is mounted by the app shell and owns every drop: while -//! files hover the window it shows a veil, and once they land it asks which -//! cluster they belong to and what each one should be installed as. No view -//! handles drops itself, so dropping on the mods page and dropping on the home -//! screen behave identically. -//! -//! Freya emits one `FileDrop` event per file, all within the same batch, so the -//! shell handler accumulates them and the prompt's first render already sees -//! the whole drop. +//! Freya emits one `FileDrop` event per file within a single batch so the shell +//! handler accumulates them before the prompt first renders use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -32,16 +23,13 @@ const FILE_ROW_H: f32 = 34.; const FILE_LIST_MAX_ROWS: usize = 5; const TYPE_DROPDOWN_W: f32 = 108.; -/// Content types a dropped entry can be imported as. Worlds are directories and -/// modpacks/datapacks have no view of their own, so they're deliberately absent. +/// Worlds are directories and modpacks/datapacks have no view so both are excluded const IMPORTABLE: [ContentType; 3] = [ ContentType::Mod, ContentType::ResourcePack, ContentType::Shader, ]; -/// Names that give away a shaderpack. Only a starting guess; every row has a -/// dropdown, so a wrong sniff costs the user one click. const SHADER_HINTS: [&str; 5] = ["shader", "bsl", "seus", "complementary", "sildur"]; fn content_label(content_type: ContentType) -> &'static str { @@ -62,9 +50,7 @@ fn file_name(path: &Path) -> String { .unwrap_or_else(|| path.display().to_string()) } -/// Best guess at what a dropped entry is. A `.jar` is only ever a mod; -/// everything else is ambiguous, so the page the drop landed on wins, then a -/// name sniff, then the most common case. +/// Precedence `.jar` is always a mod then the current route then a name sniff fn infer_content_type(path: &Path, route_type: Option) -> ContentType { if extension(path).as_deref() == Some("jar") { return ContentType::Mod; @@ -89,11 +75,7 @@ fn route_target(route: &Route) -> Option<(ClusterId, ContentType)> { } } -/// Renders the shell's drop affordance and, once files land, the prompt asking -/// where they should go. -/// -/// The state lives in the shell (which owns the bubbling `on_file_drop` -/// handler) and is passed down so the shell itself never re-renders on hover. +/// State lives in the shell (owner of the bubbling `on_file_drop`) so it never re-renders on hover #[derive(PartialEq)] pub struct FileDropOverlay { pub hovering: State, @@ -120,7 +102,6 @@ impl Component for FileDropOverlay { } } -/// The full-window "drop anywhere" affordance shown while files are dragged over. #[derive(PartialEq)] struct DropVeil; @@ -140,7 +121,7 @@ impl Component for DropVeil { .position(Position::new_global().top(0.).left(0.)) .width(Size::window_percent(100.)) .height(Size::window_percent(100.)) - // Purely decorative: the drop must reach the shell handler beneath. + // Purely decorative the drop must reach the shell handler beneath .interactive(false) .center() .opacity(progress) @@ -181,7 +162,6 @@ impl Component for DropVeil { } } -/// Brand-tinted circle holding an icon, used by the veil card and the dialog header. fn icon_badge(icon: IconType, size: f32, icon_size: f32) -> impl IntoElement { rect() .width(Size::px(size)) @@ -211,8 +191,7 @@ impl Component for DropPrompt { let target = route_target(&route); let initial_cluster = target.map(|(id, _)| id).or(*active_cluster.peek()); let selected_cluster = use_state(move || initial_cluster); - // Only the rows the user actually changed. Everything else falls back to - // the inferred type, so a second drop landing mid-prompt still gets one. + // Absent entries fall back to the inferred type covering drops landing mid-prompt let overrides = use_state(HashMap::::new); let clusters: Vec = settled_or_loading(&clusters_query).unwrap_or_default(); @@ -267,7 +246,7 @@ fn dialog_header(title: String, subtitle: String) -> impl IntoElement { .horizontal() .width(Size::fill()) // Without Content::Flex the flexed text column doesn't resolve and the - // badge stops centring against it. + // badge stops centring against it .content(Content::Flex) .cross_align(Alignment::Center) .spacing(12.) @@ -327,7 +306,7 @@ fn prompt_body( dispatch: crate::Actions, mut pending: State>, ) -> Element { - // The remembered cluster may have been deleted while the prompt was open. + // The remembered cluster may have been deleted while the prompt was open let cluster_id = selected_cluster .read() .filter(|id| clusters.iter().any(|c| c.id == *id)) diff --git a/packages/oneclient_app/src/components/generic_prompt.rs b/packages/oneclient_app/src/components/generic_prompt.rs index 6e6c52e4..e1468bf0 100644 --- a/packages/oneclient_app/src/components/generic_prompt.rs +++ b/packages/oneclient_app/src/components/generic_prompt.rs @@ -1,15 +1,5 @@ -//! The fallback renderer for prompts nothing else claims. -//! -//! [`Prompt`](oneclient_events::Prompt) lets any part of the core ask the user a -//! question, and two overlays render particular ones richly: the Java one and -//! the updater's. Everything else used to render as nothing at all — the core -//! would sit on `ask()` forever waiting for a reply no one could give, and -//! because the app holds a single pending prompt at a time, the stuck one also -//! kept every later prompt (the Java one included) from appearing. -//! -//! So this is a safety net, not a style: anything not claimed above is rendered -//! from the prompt's own choices, and a new prompt in the core needs no UI work -//! to be answerable. +//! Fallback renderer for prompts no dedicated overlay claims +//! Without it an unrendered prompt leaves the core blocked on `ask()` blocking all later prompts use freya::prelude::*; use oneclient_events::{Answer, ChoiceInput, ChoiceStyle}; @@ -24,8 +14,6 @@ use crate::updater::UPDATE_CHOICE_INSTALL; const CARD_BG: Color = Color::from_rgb(26, 34, 41); -/// Prompts with an overlay of their own, recognised the same way those overlays -/// recognise themselves: by the choice they offer. fn claimed_elsewhere(prompt: &PendingPromptView) -> bool { prompt.has_choice(JAVA_CHOICE_DOWNLOAD) || prompt.has_choice(UPDATE_CHOICE_INSTALL) } @@ -35,8 +23,7 @@ pub struct GenericPromptOverlay; impl Component for GenericPromptOverlay { fn render(&self) -> impl IntoElement { - // Hooks run unconditionally, before any early return, so the hook count - // stays stable whether or not a prompt is pending. + // Hooks run unconditionally before any early return to keep the hook count stable let snapshot = use_notifications_snapshot(); let dispatch = use_dispatch(); @@ -79,9 +66,7 @@ impl Component for GenericPromptOverlay { }; buttons = buttons.child(button.on_press(move |_| match &input { - // A folder choice cannot be answered until the user has picked - // one, so the answer is sent from the dialog's callback rather - // than from the press. + // Answered from the dialog callback not the press since no folder is picked yet Some(ChoiceInput::Folder { title }) => { let dispatch = dispatch.clone(); let title = title.clone(); @@ -97,9 +82,7 @@ impl Component for GenericPromptOverlay { } }); } - // A selection needs a picker only the prompt's own overlay could - // supply. Answering without one is still better than leaving the - // core waiting forever; the caller sees `selection() == None`. + // No picker here answering with `selection() == None` beats blocking the core Some(ChoiceInput::Selection { hint }) => { tracing::warn!( choice = id, @@ -179,10 +162,6 @@ mod tests { #[test] fn every_other_prompt_falls_through_to_the_generic_overlay() { - // The bug this overlay exists for: a prompt whose choices no overlay - // recognised rendered as nothing, the core waited on `ask()` forever, - // and the stuck prompt blocked every later one. Any new prompt in the - // core has to land here without UI work. for choices in [ vec![Choice::new("launch", "Launch anyway")], vec![Choice::primary("verify", "Verify and repair")], diff --git a/packages/oneclient_app/src/components/icons.rs b/packages/oneclient_app/src/components/icons.rs index f7e209b3..820ca5ec 100644 --- a/packages/oneclient_app/src/components/icons.rs +++ b/packages/oneclient_app/src/components/icons.rs @@ -38,7 +38,7 @@ impl Icon { self } - /// Rotation in degrees, clockwise. + /// Degrees clockwise pub fn rotate(mut self, degrees: f32) -> Self { self.rotation = Some(degrees); self @@ -54,7 +54,6 @@ impl Component for Icon { .show_loader(false) .width(Size::px(self.size_px)) .height(Size::px(self.size_px)) - // .fill(color) .map(self.color, |svg, color| svg.color(color)) .map(self.rotation, |svg, degrees| svg.rotation(degrees)) } diff --git a/packages/oneclient_app/src/components/java_install_manager.rs b/packages/oneclient_app/src/components/java_install_manager.rs index dc85da38..16e5987e 100644 --- a/packages/oneclient_app/src/components/java_install_manager.rs +++ b/packages/oneclient_app/src/components/java_install_manager.rs @@ -15,9 +15,7 @@ fn providers() -> Vec<(JavaVendor, &'static str)> { ] } -/// Reusable "Install Java" picker. Lists downloadable runtimes per provider and -/// calls `on_install` with the chosen `(vendor, major)`. When `suggested` is set, -/// the matching major is pulled to the top and highlighted. +/// Calls `on_install` with the chosen `(vendor, major)` #[derive(PartialEq)] pub struct JavaInstallManager { suggested: Option, @@ -68,7 +66,6 @@ impl Component for JavaInstallManager { let versions_query = use_provider_versions(vendor.clone()); let (mut versions, loading) = provider_versions(&versions_query); - // Pull the suggested major to the top so it reads as the default choice. if let Some(major) = suggested { versions.sort_by_key(|v| v.major != major); } diff --git a/packages/oneclient_app/src/components/java_prompt.rs b/packages/oneclient_app/src/components/java_prompt.rs index ec73736b..1a180caf 100644 --- a/packages/oneclient_app/src/components/java_prompt.rs +++ b/packages/oneclient_app/src/components/java_prompt.rs @@ -14,8 +14,7 @@ pub struct JavaPromptOverlay; impl Component for JavaPromptOverlay { fn render(&self) -> impl IntoElement { - // Hooks must run unconditionally, before any early return, so the hook - // count stays stable whether or not a Java prompt is pending. + // Hooks must run unconditionally before any early return to keep the hook count stable let snapshot = use_notifications_snapshot(); let dispatch = use_dispatch(); let mut show_manager = use_state(|| false); @@ -24,14 +23,12 @@ impl Component for JavaPromptOverlay { return rect().into_element(); }; - // Identified by the choices it offers rather than a prompt-kind enum. + // Identified by the choices it offers rather than a prompt-kind enum if !prompt.has_choice(JAVA_CHOICE_DOWNLOAD) { return rect().into_element(); } - // The vendor list needs a major to highlight. It is not on the prompt (the - // core only sends display text) so recover it from the folder choice's dialog - // title, which the core builds from the same number. + // The prompt carries no major so recover it from the folder choice's dialog title let major = prompt .choice(JAVA_CHOICE_FOLDER) .and_then(|choice| match &choice.input { @@ -42,13 +39,11 @@ impl Component for JavaPromptOverlay { }) .unwrap_or(0); - // Download opens the install manager with the required major highlighted. if *show_manager.read() { let install = dispatch.clone(); return JavaInstallManager::new() .suggested(Some(major)) .on_install(move |(vendor, _row_major): (JavaVendor, u32)| { - // The launch needs `major`; the manager only picks the vendor. install.answer_prompt( Answer::new(JAVA_CHOICE_DOWNLOAD).with_selection(vendor.to_string()), ); diff --git a/packages/oneclient_app/src/components/microsoft_login.rs b/packages/oneclient_app/src/components/microsoft_login.rs index 99cc1e45..aabe3044 100644 --- a/packages/oneclient_app/src/components/microsoft_login.rs +++ b/packages/oneclient_app/src/components/microsoft_login.rs @@ -87,7 +87,6 @@ pub fn use_microsoft_login() -> MicrosoftLogin { return; } - // open the browser immediately platform::open_url(session.auth_url()); finish.mutate(session.clone()); pending_login.set(Some(session)); @@ -103,7 +102,7 @@ pub fn use_microsoft_login() -> MicrosoftLogin { } }); - // is true once the flow has some status updates (just to ensure the user has seen the popup) and the flow is still in progress + // True once the flow reports progress ensuring the user has seen the popup let locked = status.as_ref().is_some_and(|s| s.current > 0); let in_flight = begin.read().state().is_loading() || finish.read().state().is_loading(); let pending = in_flight && !*cancelled.read(); @@ -141,10 +140,7 @@ where | MutationStateData::Loading { res: Some(Err(err)), } => { - // A cancellation is the user's own decision arriving back as an - // error. Reporting it would put a red line under a button they - // pressed on purpose, and it would still be there when they start - // the next sign-in. + // Cancellation arrives as an error surfacing it would persist into the next sign-in if err.is_login_cancelled() { return None; } diff --git a/packages/oneclient_app/src/components/navbar/app_navbar.rs b/packages/oneclient_app/src/components/navbar/app_navbar.rs index 3a1145eb..cdbf42d3 100644 --- a/packages/oneclient_app/src/components/navbar/app_navbar.rs +++ b/packages/oneclient_app/src/components/navbar/app_navbar.rs @@ -117,9 +117,7 @@ fn navbar_center() -> impl IntoElement { }) } -/// Browsing always happens for some cluster, so the navbar picks the one the -/// shell has active and falls back to the most recently played one. With no -/// clusters at all there is nothing to browse for, so it points at Versions. +/// Falls back active cluster then most recently played then Versions when none exist fn browse_target() -> Route { let clusters = settled_or_loading(&use_clusters()).unwrap_or_default(); let active = *use_active_cluster_id().read(); @@ -178,8 +176,8 @@ impl Component for NavLink { .cross_align(Alignment::Center) .spacing(2.) .width(Size::px(nav_label.len() as f32 * 10. + 10.)) - // TODO: Hacky workaround for weird freya measurement bug, where if a rectangle has a transparent background (so in dec = 0), it will be measured weirdly. - // This basically fools freya into thinking it indeed has a box and properly handles pointer events + // TODO workaround for a Freya measurement bug a fully transparent background + // measures wrongly so give it alpha 0 red to keep pointer events working .background(Color::RED.with_a(0)) .a11y_id(a11y_id) .a11y_focusable(true) @@ -230,18 +228,15 @@ impl Component for NavbarRight { .map(|account| account.id.to_string()) .unwrap_or_else(|| uuid::Uuid::nil().to_string()); - // open notification center let notif_dispatch = dispatch.clone(); let open_notifications = move |_| { notif_dispatch.toggle_notification_center(); }; - // open account switcher let open_account_switcher = move |_| { dispatch.toggle_account_switcher(); }; - // open settings let open_settings = |_| { let _ = RouterContext::get().push(Route::SettingsLauncher {}); }; diff --git a/packages/oneclient_app/src/components/notifications.rs b/packages/oneclient_app/src/components/notifications.rs index f1a5fb35..77125538 100644 --- a/packages/oneclient_app/src/components/notifications.rs +++ b/packages/oneclient_app/src/components/notifications.rs @@ -279,7 +279,6 @@ fn task_row(task: &crate::notifications::TaskView) -> impl IntoElement { .into_element() } -/// A right-facing chevron that rotates 90° down when `expanded`, animated. #[derive(PartialEq)] struct ChevronToggle { expanded: bool, diff --git a/packages/oneclient_app/src/components/overlay_popup.rs b/packages/oneclient_app/src/components/overlay_popup.rs index 956b27a4..3b2f776c 100644 --- a/packages/oneclient_app/src/components/overlay_popup.rs +++ b/packages/oneclient_app/src/components/overlay_popup.rs @@ -36,7 +36,6 @@ impl OverlayPopup { self } - /// Whether to darken everything behind the popup pub fn backdrop(mut self, backdrop: bool) -> Self { self.backdrop = backdrop; self diff --git a/packages/oneclient_app/src/components/package_row.rs b/packages/oneclient_app/src/components/package_row.rs index 61cedc4c..65c0047b 100644 --- a/packages/oneclient_app/src/components/package_row.rs +++ b/packages/oneclient_app/src/components/package_row.rs @@ -44,12 +44,9 @@ pub struct PackageEntry { pub installed: bool, pub hash: Option, pub manifest_default: bool, - /// The bundle ships this as a private dependency. Only ever set for bundle - /// rows, and only surfaced when the user asks for hidden packages. + /// Private bundle dependency only set for bundle rows pub hidden: bool, - /// The launch-time check found a newer version for this artifact. Only ever - /// set for browser-installed content: bundle packages report their staleness - /// through the bundle update flow instead. + /// Only set for browser-installed content bundle packages use the bundle update flow pub update_available: bool, } @@ -62,9 +59,7 @@ impl PackageEntry { self.bundle_name.is_some() } - /// Whether to mark the row out of date. A bundle package is never marked - /// here even if a row somehow says so, so the two update flows cannot - /// contradict each other on screen. + /// Bundle packages are never marked here so the two update flows cannot contradict pub fn is_outdated(&self) -> bool { self.update_available && self.is_remote() && !self.in_bundle() } @@ -463,8 +458,6 @@ pub fn provider_badge(provider: ProviderId) -> Element { ) } -/// Says the installed version is behind the provider's newest compatible one. -/// The update itself is offered by the launch-time modal, not from here. fn outdated_badge() -> Element { accent_badge( Icon::new(IconType::RefreshCw01) diff --git a/packages/oneclient_app/src/components/package_update_popup.rs b/packages/oneclient_app/src/components/package_update_popup.rs index 07cea084..45ea8cbe 100644 --- a/packages/oneclient_app/src/components/package_update_popup.rs +++ b/packages/oneclient_app/src/components/package_update_popup.rs @@ -1,12 +1,4 @@ -//! The launch-time "these packages are out of date" modal. -//! -//! Only ever shows packages the user installed from the browser; bundle content -//! has its own flow and is never listed here. -//! -//! Every row carries its own dropdown, so a user who wants a single mod moved -//! forward is not made to take the rest with it. Nothing happens until Proceed: -//! the choices are collected first and applied in one pass, which is why the -//! rows stay put while they are being answered. +//! Only lists packages installed from the browser bundle content has its own flow use std::collections::HashMap; @@ -30,12 +22,7 @@ const CHOICE_DROPDOWN_W: f32 = 150.; type MetaMap = HashMap<(ProviderId, String), CachedPackageMeta>; -/// What Proceed should do with one row. -/// -/// The two skips differ only in whether the answer outlives the modal: a plain -/// skip leaves the package stale and it is offered again next launch, while -/// [`RowChoice::SkipVersion`] records that this exact version was declined and -/// stops it being raised until a newer one appears. +/// `Skip` is re-offered next launch `SkipVersion` persists the decline until a newer version #[derive(Clone, Copy, Default, PartialEq, Eq)] enum RowChoice { #[default] @@ -60,7 +47,6 @@ impl RowChoice { } } -/// Identifies a row across the whole modal, which may span several clusters. type RowKey = (ClusterId, String); fn row_key(update: &BrowserPackageUpdate) -> RowKey { @@ -75,15 +61,12 @@ impl Component for PackageUpdatePopup { let snapshot = use_notifications_snapshot(); let dispatch = use_dispatch(); - // Only the rows the user actually touched. Everything absent is an - // Update, so a group that arrives while the modal is open still gets a - // sensible default without being reconciled into the map. + // Absent entries default to Update so groups arriving mid-modal need no reconciliation let choices = use_state(HashMap::::new); let groups = snapshot.package_updates.clone(); - // Hooks run unconditionally, so the meta lookup is built from a possibly - // empty id list and happens before the early return below. + // Hooks run unconditionally so this must precede the early return below let mut meta = MetaMap::new(); for provider in ProviderId::REMOTE_PROVIDERS.iter().copied() { let ids: Vec = groups @@ -233,8 +216,6 @@ fn content( RowChoice::Update => { proceed_dispatch.apply_package_update(update.clone()); } - // Nothing to record: the package stays - // stale and is offered again next launch. RowChoice::Skip => {} RowChoice::SkipVersion => proceed_dispatch .skip_package_update( @@ -349,8 +330,6 @@ impl Component for UpdateRow { let mut choices = self.choices; let key = row_key(&update); - // "1.0.2 to 1.1.0" when both are known; a provider that records neither - // still gets a usable row rather than an empty line. let versions = match ( update.installed_version_name.is_empty(), update.latest_version_name.is_empty(), diff --git a/packages/oneclient_app/src/components/pagination.rs b/packages/oneclient_app/src/components/pagination.rs index 29dd8634..ef5a7640 100644 --- a/packages/oneclient_app/src/components/pagination.rs +++ b/packages/oneclient_app/src/components/pagination.rs @@ -37,8 +37,7 @@ impl Pagination { self } - /// Greys out every control without unmounting the pager, for callers that - /// are still loading the page the user just asked for. + /// Greys out controls without unmounting the pager pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = enabled; self diff --git a/packages/oneclient_app/src/components/player_model.rs b/packages/oneclient_app/src/components/player_model.rs index 9fdd1baa..70de4f80 100644 --- a/packages/oneclient_app/src/components/player_model.rs +++ b/packages/oneclient_app/src/components/player_model.rs @@ -108,9 +108,8 @@ impl Component for PlayerModel { let mut drag = use_state(|| None::<(f32, f32, f32, f32)>); let mut hovering = use_state(|| false); - // Always derive the cursor from both states instead of setting it per event: - // Freya re-emits `pointer enter` on the first move after a press, so a plain - // `Grab` there would drop the grabbing cursor as soon as the drag starts. + // Derive from both states Freya re-emits `pointer enter` after a press which + // would otherwise drop the grabbing cursor as the drag starts let apply_cursor = move || { Cursor::set(if drag.peek().is_some() { CursorIcon::Grabbing @@ -203,8 +202,7 @@ impl Component for PlayerModel { Platform::get().send(UserEvent::RequestRedraw); }) - // Global so a release outside the widget still ends the drag and, - // since the pointer is no longer over us, restores the default cursor. + // Global so a release outside the widget still ends the drag .on_global_pointer_press(move |_: Event| { if drag.peek().is_some() { drag.set(None); diff --git a/packages/oneclient_app/src/components/recents_row.rs b/packages/oneclient_app/src/components/recents_row.rs index f26ec59a..dbf59978 100644 --- a/packages/oneclient_app/src/components/recents_row.rs +++ b/packages/oneclient_app/src/components/recents_row.rs @@ -18,7 +18,6 @@ const MAX_CARD_WIDTH_PX: f32 = 480.0; const CARD_MS: u64 = 460; const STAGGER_MS: u64 = 85; const CARD_RISE_PX: f32 = 48.0; -/// Items the intro staggers individually; past this they rise together. const INTRO_ITEMS: usize = 6; #[derive(PartialEq)] @@ -34,12 +33,8 @@ impl Component for RecentsRow { let sorted: Vec = sort_clusters_for_home(clusters); let slots = *visible_slots.read(); - // Budgeted off the clusters we have, never off the slots we can show. An - // offset is the one thing a card must not change on a frame where the row - // is re-measured: the layout engine treats an offset-only change as "inner - // layout" and hands back the card's *cached* area instead of measuring it, - // so a rerun triggered by the slot count would freeze the surviving cards - // at their pre-resize widths while the new ones come in at the new width. + // Budget off cluster count not slot count an offset-only change makes the layout + // engine reuse a card's cached area freezing survivors at pre-resize widths let items = intro_items(sorted.len()); let display = sorted.into_iter().take(slots).collect::>(); @@ -82,7 +77,6 @@ impl Component for RecentsRow { } } -/// Items to stagger for `clusters` recents, counting the "other versions" tile. fn intro_items(clusters: usize) -> usize { (clusters + 1).min(INTRO_ITEMS) } @@ -93,8 +87,7 @@ fn intro_ms(items: usize) -> u64 { fn stagger_eased(progress: f32, index: usize, items: usize) -> f32 { let elapsed = progress * intro_ms(items) as f32; - // Anything past the budget shares the last slot, so every item still lands on - // a finished 1.0 rather than freezing part-way with a leftover offset. + // Items past the budget share the last slot so every one still lands on a finished 1.0 let start = index.min(items.saturating_sub(1)) as f32 * STAGGER_MS as f32; let local = ((elapsed - start) / CARD_MS as f32).clamp(0., 1.); 1.0 - (1.0 - local).powi(3) @@ -300,14 +293,9 @@ impl Component for OtherVersionsTile { } } -/// How many cards fit beside the "other versions" tile at `row_width_px`. -/// -/// `n` cards sit in `n` gaps' worth of spacing — `n - 1` between the cards and -/// one before the tile — so `n * (MIN + GAP) + MORE` has to fit. One card is -/// always shown: below that width it just renders narrower than the minimum, -/// which is why the card carries no `min_width`. With one, a stale count during -/// a live resize used to push the row past its bounds and leave a card cut off -/// or stretched until the next measurement landed. +/// `n` cards occupy `n` gaps (n-1 between cards one before the tile) so `n * (MIN + GAP) + MORE` +/// must fit +/// Always returns at least 1 the card carries no `min_width` and just renders narrower fn recent_card_slots_for_width(row_width_px: f32) -> usize { if !row_width_px.is_finite() { return 1; @@ -348,8 +336,6 @@ mod tests { #[test] fn every_item_settles_when_the_intro_ends() { - // A card left with a leftover offset keeps changing it, and an offset-only - // change is what makes the layout engine reuse a card's cached width. for clusters in 0..24 { let items = intro_items(clusters); for index in 0..=clusters { @@ -364,8 +350,6 @@ mod tests { #[test] fn the_intro_runs_the_same_length_whatever_fits_on_screen() { - // The window width decides how many cards show, so anything the intro is - // measured against has to be independent of it. assert_eq!(intro_items(3), intro_items(3)); assert_eq!(intro_ms(intro_items(30)), intro_ms(INTRO_ITEMS)); assert_eq!(intro_ms(intro_items(0)), CARD_MS); diff --git a/packages/oneclient_app/src/components/scrollview.rs b/packages/oneclient_app/src/components/scrollview.rs index 471e9737..8b573159 100644 --- a/packages/oneclient_app/src/components/scrollview.rs +++ b/packages/oneclient_app/src/components/scrollview.rs @@ -150,7 +150,6 @@ impl ScrollArea { self } - /// Called every render pub fn on_ctx(mut self, handler: impl FnMut(ScrollAreaCtx) + 'static) -> Self { self.on_ctx = Some(EventHandler::new(handler)); self @@ -237,7 +236,6 @@ impl ScrollArea { let mut drag_start_x = use_state::>(|| None); let mut shift_held = use_state(|| false); - // used to jump to the top when the reset key changes let reset_key = self.reset_key; let mut last_reset = use_state(|| reset_key); if reset_key.is_some() && *last_reset.peek() != reset_key { @@ -327,7 +325,6 @@ impl ScrollArea { let do_h = h_delta != 0.0 && can_scroll_h; let do_v = v_delta != 0.0 && can_scroll_v; - // check if scrollable if !do_h && !do_v { return; } diff --git a/packages/oneclient_app/src/components/splash_curtain.rs b/packages/oneclient_app/src/components/splash_curtain.rs index 8b5be9c4..7f2ee599 100644 --- a/packages/oneclient_app/src/components/splash_curtain.rs +++ b/packages/oneclient_app/src/components/splash_curtain.rs @@ -6,9 +6,8 @@ use freya::prelude::*; use crate::hooks::use_splash; use crate::theme::colors; -/// How long the curtain takes to fade away once home is ready. const FADE_MS: u64 = 460; -/// Safety net: reveal the app even if the "ready" signal never arrives. +/// Safety net reveal the app even if the "ready" signal never arrives const FALLBACK_MS: u64 = 8000; #[derive(PartialEq)] @@ -22,8 +21,7 @@ impl Component for SplashCurtain { return rect().into_element(); } - // Fresh mount whenever the curtain is raised, so `CurtainFade`'s - // animation and timers start from the beginning each time. + // Fresh mount so `CurtainFade`'s animation and timers restart each time CurtainFade.into_element() } } @@ -43,7 +41,7 @@ impl Component for CurtainFade { .function(Function::Cubic) }); - // Force-reveal after a while if home never reports ready. + // Force-reveal after a while if home never reports ready use_hook(move || { let mut home_ready = splash.home_ready; spawn(async move { @@ -54,7 +52,6 @@ impl Component for CurtainFade { }); }); - // When home is ready, play the fade-out then drop the curtain. use_side_effect_with_deps(&ready, move |&ready| { if !ready { return; diff --git a/packages/oneclient_app/src/components/status_bar.rs b/packages/oneclient_app/src/components/status_bar.rs index 8b9054c5..6b616de8 100644 --- a/packages/oneclient_app/src/components/status_bar.rs +++ b/packages/oneclient_app/src/components/status_bar.rs @@ -10,12 +10,9 @@ use crate::theme::colors; const BAR_HEIGHT: f32 = 34.; const AMBER: Color = Color::from_rgb(191, 122, 26); -/// `Layer::RelativeOverlay(n)` resolves to `n * (i16::MAX / 16)`, so any `n` -/// past 16 pins to `i16::MAX`. Children resolve to `parent + 1`, which saturates -/// to the *same* layer — and a layer is an unordered set, so the bar's own -/// background painted over its contents about as often as not. That is what -/// made the message and the close button come and go. 14 keeps the bar above -/// every popup while leaving its children room to sit in front of it. +/// `Layer::RelativeOverlay(n)` saturates at `n = 16` putting children on the same +/// (unordered) layer as the bar background +/// 14 leaves headroom above every popup const BAR_LAYER: u8 = 14; #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -131,8 +128,7 @@ impl Component for StatusBanner { let mut close_hover = use_state(|| false); - // Pressing close unmounts the banner from under the pointer, so there is - // no `leave` to put the cursor back. + // Close unmounts the banner from under the pointer so no `leave` fires use_drop(|| Cursor::set(CursorIcon::default())); let close = issue.closeable().then(|| { @@ -161,10 +157,8 @@ impl Component for StatusBanner { .into_element() }); - // The message is centred by a flex spacer on each side, and the close - // button rides the end of the trailing one. It used to be absolutely - // positioned, which pinned it to the top of the content box — inside the - // bar's horizontal padding, so it sat high and well short of the edge. + // Flex spacers centre the message absolute positioning pinned the close button + // to the top of the content box inside the bar's padding let leading = rect().width(Size::flex(1.0)).height(Size::fill()); let message = rect() diff --git a/packages/oneclient_app/src/components/toasts.rs b/packages/oneclient_app/src/components/toasts.rs index 66391f66..2c5718e6 100644 --- a/packages/oneclient_app/src/components/toasts.rs +++ b/packages/oneclient_app/src/components/toasts.rs @@ -20,9 +20,7 @@ use crate::{ const TOAST_W: f32 = 300.; const TRAVEL: f32 = 320.; const TTL_MS: f32 = 5000.; -/// Toasts sit below the navbar rather than on top of it: the startup/bundle-sync -/// progress toast is on screen exactly when Home first appears, and at any -/// smaller offset its card covered the nav links and account controls. +/// Below the navbar a smaller offset let the startup progress toast cover the nav links const TOP_PX: f32 = crate::theme::NAVBAR_HEIGHT_PX + 12.; #[derive(Clone, Copy, PartialEq)] @@ -61,7 +59,7 @@ pub struct Toasts; impl Component for Toasts { fn render(&self) -> impl IntoElement { let snapshot = use_notifications_snapshot(); - // How many toasts the pointer is over; while any is hovered, every toast's timer pauses. + // While any toast is hovered every toast's timer pauses let hovering = use_state(|| 0usize); let entries: Vec = snapshot @@ -99,7 +97,6 @@ impl Component for Toasts { #[derive(PartialEq)] struct ToastCard { entry: InboxEntry, - /// Shared across every toast: how many of them the pointer is currently over. hovering: State, key: DiffKey, } @@ -129,7 +126,6 @@ impl Component for ToastCard { AnimNum::new(0., 1.).time(260).ease(Ease::Out) }); - // Hovering any toast freezes this bar where it is; leaving runs out the remaining TTL. let paused = *self.hovering.read() > 0; let mut was_paused = use_state(|| false); let mut from_pct = use_state(|| 100.); @@ -166,7 +162,7 @@ impl Component for ToastCard { let opacity = fade.read().value(); let ttl_pct = ttl_bar_anim.read().value(); - // A card can be unmounted while hovered (dismissing it), which emits no `out` event. + // A card can be unmounted while hovered which emits no `out` event let mut hovering = self.hovering; let hover_flag = use_hook(|| Rc::new(Cell::new(false))); let (over_flag, out_flag, drop_flag) = @@ -195,8 +191,8 @@ impl Component for ToastCard { .width(Size::px(TOAST_W)) .offset_x(offset) .opacity(opacity) - // `over`/`out` rather than `enter`/`leave`: the latter are exclusive to the deepest - // node, so moving onto the close button would count as leaving the toast. + // `enter`/`leave` are exclusive to the deepest node so the close button would + // read as leaving the toast .on_pointer_over(move |_| { if over_flag.replace(true) { return; diff --git a/packages/oneclient_app/src/components/update_prompt.rs b/packages/oneclient_app/src/components/update_prompt.rs index e3927852..b07a42ae 100644 --- a/packages/oneclient_app/src/components/update_prompt.rs +++ b/packages/oneclient_app/src/components/update_prompt.rs @@ -22,8 +22,7 @@ impl Component for UpdatePromptOverlay { return rect().into_element(); }; - // Recognise the update prompt by the choice it offers rather than by a - // prompt-kind enum, so the event layer needn't enumerate every prompt. + // Recognised by the choice it offers so the event layer needn't enumerate prompts if !prompt.has_choice(UPDATE_CHOICE_INSTALL) { return rect().into_element(); } diff --git a/packages/oneclient_app/src/events.rs b/packages/oneclient_app/src/events.rs index 694cc82c..15c979d2 100644 --- a/packages/oneclient_app/src/events.rs +++ b/packages/oneclient_app/src/events.rs @@ -1,12 +1,5 @@ -//! Drains the core's event stream into the radio station. -//! -//! An event drain, the toast expiry timers, and the game-log flush. It runs -//! under freya's `spawn`, not `tokio::spawn`, because radio state is `!Send`. -//! -//! The batching matters. During a download the core emits tens of thousands of -//! progress events, and a radio write wakes every subscriber to that channel, so -//! events are drained into a batch, folded, and written once per channel rather -//! than once per event. +//! Runs under freya's `spawn` not `tokio::spawn` radio state is `!Send` +//! Events are batched and folded because a radio write wakes every subscriber use std::collections::{HashMap, HashSet}; use std::time::Duration; @@ -20,13 +13,9 @@ use crate::hooks::PumpSignal; use crate::notifications::{MESSAGE_TOAST_TTL, PendingPromptView}; use crate::state::{AppChannel, AppState, LoginProgress}; -/// How long game log lines accumulate before being written. -/// -/// A launch emits lines continuously; without this every line would wake the -/// log view. +/// Quiet period before log lines are written without it every line wakes the log view const GAME_LOG_FLUSH: Duration = Duration::from_millis(120); -/// Cap on how many queued events are folded into a single write. const COALESCE_BUDGET: usize = 1024; pub struct EventPump { @@ -38,8 +27,7 @@ pub struct EventPump { impl EventPump { pub async fn run(mut self) { let mut armed: HashMap = HashMap::new(); - // Hovering any toast pauses every toast, including ones that arrive - // while hovering. + // Hovering any toast pauses every toast including ones arriving while hovering let mut paused = false; let mut game_flush: Option = None; @@ -86,8 +74,7 @@ impl EventPump { if game_flush.is_some_and(|d| d <= now) { game_flush = None; - // Nothing to write: log lines were folded in as they - // arrived; this only ends the quiet period. + // Lines were already folded in on arrival this only ends the quiet period self.station.write_channel(AppChannel::Game); } } @@ -133,7 +120,6 @@ impl EventPump { } } - /// Folds a batch of events into the station, one write per channel touched. fn apply(&mut self, batch: Vec) -> Folded { let mut folded = Folded::default(); let mut engine_events = Vec::new(); @@ -162,8 +148,7 @@ impl EventPump { cluster_id, message, }) => failed = Some((cluster_id, message)), - // The sign-in modal renders this inline rather than as a toast, - // so it is lifted out here and never reaches the engine. + // Lifted out so it never reaches the engine the sign-in modal renders it inline Event::Progress(ProgressEvent::Update { id, ref label, @@ -247,8 +232,6 @@ struct Folded { java: bool, } -/// Arms a timer for every toast that should have one, and drops timers for -/// toasts that have gone. fn reconcile( station: &RadioStation, armed: &mut HashMap, @@ -268,7 +251,6 @@ fn reconcile( armed.retain(|id, _| want.contains(id)); } -/// A toast's time-to-live, which hovering pauses and un-hovering resumes. enum ToastTimer { Running(tokio::time::Instant), Paused(Duration), @@ -283,7 +265,7 @@ impl ToastTimer { } } - /// The instant this toast expires, or `None` while paused. + /// `None` while paused fn deadline(&self) -> Option { match self { Self::Running(deadline) => Some(*deadline), @@ -306,7 +288,6 @@ impl ToastTimer { } } -/// Builds the view the prompt overlays render. #[must_use] pub fn prompt_view(state: &AppState) -> Option { state.prompt.as_ref().map(|prompt| PendingPromptView { @@ -317,8 +298,6 @@ pub fn prompt_view(state: &AppState) -> Option { }) } -/// Everything the app needs before the UI can do anything: build the launcher, -/// publish the handle, then start the background work. pub async fn start_launcher( station: RadioStation, events: oneclient_events::EventBus, @@ -359,7 +338,6 @@ pub async fn start_launcher( Ok(()) } -/// Records a startup failure where the user can see it. pub fn report_startup_failure( station: &RadioStation, err: &anyhow::Error, diff --git a/packages/oneclient_app/src/hooks/actions.rs b/packages/oneclient_app/src/hooks/actions.rs index b05b3172..7afd5087 100644 --- a/packages/oneclient_app/src/hooks/actions.rs +++ b/packages/oneclient_app/src/hooks/actions.rs @@ -1,14 +1,7 @@ -//! Everything the UI can ask for. +//! Core operations go onto freya's UI-thread executor radio state is `!Send` //! -//! A method per operation, each free to return a result. Core operations go -//! onto freya's UI-thread executor, since radio state is `!Send` and anything -//! that writes it must run there; pure UI toggles write it immediately. -//! -//! Always `spawn_forever`, never `spawn`. Freya's `spawn` binds a task to the -//! calling component's scope and cancels it on unmount; these are started from -//! event handlers, so a launch kicked off from a panel that then re-renders -//! would have its log tail and exit watcher killed underneath it. The work is -//! app-scoped, not component-scoped. +//! Always `spawn_forever` never `spawn` Freya's `spawn` cancels the task when +//! the calling component unmounts this work is app-scoped not component-scoped use std::path::PathBuf; use std::sync::Arc; @@ -33,38 +26,25 @@ use crate::notifications::{ }; use crate::state::{AppChannel, AppState, AsyncStatus}; -/// How long the launch button stays disabled after a click, at minimum. -/// -/// Over-disabling is the cheaper mistake: a button that is dead for two seconds -/// after a failed launch beats one that lets a second game start. +/// Over-disabling is the cheaper mistake a dead button beats a second game const LAUNCH_HOLD: Duration = Duration::from_secs(2); -/// How long the pre-launch update check may take before the launch goes ahead -/// without it. -/// -/// Mirrors `oneclient_net`'s reachability probe rather than reqwest's own -/// per-request timeouts, which are minutes long and sized for downloads. This -/// one sits between the user pressing Play and the game starting. +/// Mirrors `oneclient_net`'s reachability probe not reqwest's per-request +/// timeouts which are minutes long This sits between Play and the game const UPDATE_CHECK_BUDGET: Duration = Duration::from_secs(8); -/// What a launch should do about the updates its check turned up. #[derive(Debug, Clone, PartialEq)] enum LaunchUpdatePlan { - /// Launch straight away. The cache has still been written. Nothing, Apply(Vec), Prompt(Vec), } -/// Decides what a launch does with its pending updates. -/// -/// Pure, and deliberately separate from the work it describes: the mode is the -/// one piece of this flow with three-way behaviour worth pinning down in tests, -/// and none of it needs a launcher, a database or a provider to be true. +/// Kept pure and separate from the work it describes so the three-way mode is +/// testable without a launcher database or provider /// /// `pending` has already had declined versions filtered out by -/// `BrowserUpdateCheck::pending`, so nothing here re-asks a question the user -/// has answered. +/// `BrowserUpdateCheck::pending` fn plan_launch_updates( mode: PackageUpdateMode, pending: Vec, @@ -74,22 +54,17 @@ fn plan_launch_updates( } match mode { - // The check ran and the cache was written before this point; skipping - // is only ever about what the user is shown. + // The cache was already written skipping only affects what is shown PackageUpdateMode::Skip => LaunchUpdatePlan::Nothing, PackageUpdateMode::Automatic => LaunchUpdatePlan::Apply(pending), PackageUpdateMode::Prompt => LaunchUpdatePlan::Prompt(pending), } } -/// Asks the event pump to do something it alone owns. -/// -/// The pump holds the toast timers, so an action that adds or removes a toast -/// has to tell it to re-arm; and hover-pause is a property of the timers, not of -/// any state a component reads. +/// The pump alone owns the toast timers so adding or removing a toast has to +/// tell it to re-arm hover-pause is a timer property not component state #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PumpSignal { - /// The notification engine changed; re-arm toast timers. Reconcile, PauseToasts, ResumeToasts, @@ -114,7 +89,6 @@ impl Actions { let _ = self.pump.send(signal); } - /// Folds a change into the notification engine, then re-arms toast timers. fn with_engine(&self, mutate: impl FnOnce(&mut AppState)) { { let mut guard = self.station.clone().write_channel(AppChannel::Notifications); @@ -131,8 +105,6 @@ impl Actions { .error = error; } - // --- launcher settings ------------------------------------------------- - pub fn reload_settings(&self) { let station = self.station; spawn_forever(async move { @@ -178,8 +150,6 @@ impl Actions { }); } - /// Applies a settings change in memory and publishes it, returning the - /// snapshot to persist. fn mutate_settings(&self, mutate: impl FnOnce(&mut LauncherSettings)) -> Option { let state = launcher::state().ok()?; let updated = { @@ -214,8 +184,7 @@ impl Actions { self.persist(updated); } - /// Not persisted: "seen" versions are only used to decide whether to show - /// the changelog this run, and the next real save carries them along. + /// Not persisted the next real save carries the seen versions along pub fn record_seen_version(&self, version: impl Into) { let version = version.into(); self.mutate_settings(|settings| { @@ -255,8 +224,6 @@ impl Actions { } } - // --- settings profiles ------------------------------------------------- - pub fn save_global_profile(&self, profile: GameSettingsProfile) { let actions = self.clone(); spawn_forever(async move { @@ -411,8 +378,7 @@ impl Actions { tracing::error!("failed to set loader version: {err:#}"); return; } - // Best effort: the cluster is still usable if the stage reset fails, - // it just will not re-download on next launch. + // Best effort on failure the cluster just will not re-download let _ = state .clusters .set_stage(cluster_id, ClusterStage::NotReady) @@ -421,8 +387,6 @@ impl Actions { }); } - // --- migration --------------------------------------------------------- - pub fn import_launcher( &self, source: oneclient_core::MigrationSource, @@ -452,8 +416,6 @@ impl Actions { }); } - // --- java -------------------------------------------------------------- - pub fn install_java_runtime(&self, vendor: oneclient_java::JavaVendor, major: u32) { spawn_forever(async move { let Ok(state) = launcher::state() else { return }; @@ -501,8 +463,6 @@ impl Actions { }); } - // --- notifications (pure UI) ------------------------------------------- - pub fn toggle_notification_center(&self) { self.with_engine(|state| { state.center_open = state @@ -568,8 +528,8 @@ impl Actions { self.with_engine(|state| state.notifications.close_package_updates()); } - /// Hovering any toast pauses every toast, including ones that arrive while - /// hovering, which is why this is a pump signal rather than a state flag. + /// Hovering any toast pauses every toast including ones arriving while + /// hovering hence a pump signal rather than a state flag pub fn pause_toasts(&self) { self.nudge(PumpSignal::PauseToasts); } @@ -623,7 +583,6 @@ impl Actions { }); } - /// Debug-only: drives a fake progress notification through the real path. pub fn send_test_progress(&self, current: u64, total: u64) { let Ok(state) = launcher::state() else { return }; let id = uuid::Uuid::from_u128(0x0CE0_0CE0_0CE0_0CE0_0CE0_0CE0_0CE0_0CE0); @@ -633,19 +592,8 @@ impl Actions { .progress(id, "Downloading assets", current, total); } - // --- game -------------------------------------------------------------- - - /// The Play button. Starts a launch, at most one per cluster. - /// - /// The pending flag is raised synchronously, before the first `await`, so - /// the button is already disabled by the time a second click of a - /// double-click could be dispatched; waiting for core to report `Checking` - /// left a few hundred ms where every click spawned its own game. The claim - /// itself is the guard, so a click that bypasses the disabled attribute - /// (keyboard activation, replayed events) is dropped here too. It also - /// covers the pre-launch work in [`launch`], which the core does not own — - /// otherwise the update modal could sit open above a Play button still - /// inviting a second press. + /// The pending flag is raised synchronously before the first `await` the + /// claim itself is the guard against a double-click spawning two games pub fn launch_cluster(&self, cluster_id: ClusterId) { let claimed = self .station @@ -663,8 +611,8 @@ impl Actions { let started = Instant::now(); launch(&actions, cluster_id).await; - // A launch that fails instantly would hand the button back inside - // the same click burst, so hold it for the floor either way. + // An instant failure would hand the button back inside the same + // click burst so hold it for the floor either way if let Some(remaining) = LAUNCH_HOLD.checked_sub(started.elapsed()) { tokio::time::sleep(remaining).await; } @@ -691,8 +639,6 @@ impl Actions { .error = None; } - // --- content ----------------------------------------------------------- - pub fn import_local_file(&self, cluster_id: ClusterId, content_type: ContentType, path: PathBuf) { spawn_forever(async move { let Ok(state) = launcher::state() else { return }; @@ -761,8 +707,8 @@ impl Actions { ) .await; - // The download already raised a progress notification; the outcome - // replaces it in place rather than arriving as a second one. + // Replaces the download's progress notification in place rather + // than arriving as a second one let spec = match &install.result { Ok(name) => NotificationSpec { title: "Installed".to_string(), @@ -793,10 +739,8 @@ impl Actions { }); if install.result.is_ok() { - // Installing a second version of something the cluster already - // had leaves the newest live and the rest switched off. Done - // before the refresh below so the version list never renders the - // moment where both copies read as active. + // Before the refresh below so the version list never renders + // the moment where both copies read as active if let Err(err) = oneclient_content::bundles::reconcile_duplicate_activity( cluster_id, &state.services.content(), @@ -808,8 +752,6 @@ impl Actions { super::invalidate_cluster_content_queries().await; - // Everything else the install touched is nobody's blocker and - // catches up on the usual signal. state .services .events @@ -820,15 +762,11 @@ impl Actions { }); } - /// Drops one installed version of a browsed package. + /// Shares the installs channel with [`Self::install_package`] so every row's + /// button reads as busy while one is being acted on /// - /// Shares the installs channel with [`Self::install_package`] so the whole - /// project reads as busy while this runs: the version list has a button per - /// row, and none of them should be pressable while one is being acted on. - /// - /// The removal is recorded as a bundle override when a bundle owns the - /// artifact — the same thing the package manager's remove does. Without it - /// the next sync puts the version straight back. + /// Recorded as a bundle override when a bundle owns the artifact otherwise + /// the next sync puts the version straight back pub fn remove_package_version( &self, cluster_id: ClusterId, @@ -874,9 +812,8 @@ impl Actions { Ok(()) => { events.notify("Removed").body(display_name).send(); - // Same reasoning as the install path: what the row turns - // back into is read straight off the cluster's content, so - // that is refreshed before the busy flag drops. + // The row's next state is read off the cluster's content + // so refresh that before the busy flag drops super::invalidate_cluster_content_queries().await; events.signal(oneclient_events::Signal::ClustersChanged); } @@ -960,11 +897,8 @@ impl Actions { }); } - /// Syncs the bundle catalog and brings every cluster up to date. - /// - /// `syncing_bundles` gates every launch button, so the flag must not be - /// raised unless this can actually clear it, hence the readiness check before - /// the write rather than inside the task. + /// `syncing_bundles` gates every launch button so the readiness check must + /// happen before the flag is raised not inside the task pub fn sync_bundles(&self) { let state = match launcher::state() { Ok(state) => state, @@ -993,10 +927,8 @@ impl Actions { tracing::error!("bundle cluster provisioning failed: {err:#}"); } - // One shared grouped session for the whole batch, so every cluster's - // downloads surface as a single "update in progress" notification. - // `detach` hands that entry over so it can be converted to the - // finished "view changes" state in place rather than replaced. + // One grouped session for the batch so all downloads surface as a + // single notification `detach` lets it be converted in place let session = oneclient_events::GroupedProgressSession::start( &state.services.events, "Updating mods", @@ -1025,24 +957,11 @@ impl Actions { }); } - // --- browser package updates ------------------------------------------- - - /// Brings the launching cluster's browser-installed packages up to date, - /// or asks about them, before the game starts. + /// The check runs and the cache is written whatever the mode says keeping + /// the package manager's "Update available" markers honest /// - /// The check runs and the cache is written whatever the mode says — that is - /// what keeps the "Update available" markers in the package manager honest - /// for users who never want the modal. The mode only decides what the user - /// sees and whether anything is installed. - /// - /// Returns once the launch may proceed. In `Prompt` mode that means once - /// the user has answered every row or dismissed the modal; the applies - /// themselves complete before their rows disappear, so nothing is still - /// downloading into the cluster when the game starts. - /// - /// Never fails the launch. An unreachable provider, a check that runs long, - /// a failed install: all of them log and let the game start on what is - /// already installed. + /// Returns once the launch may proceed nothing is still downloading then + /// Never fails the launch every error logs and lets the game start async fn resolve_package_updates_before_launch( &self, state: &Arc, @@ -1050,11 +969,8 @@ impl Actions { ) { let content = state.services.content(); - // Bounded because this sits between the user pressing Play and the game - // starting. A network that hangs rather than refuses would otherwise - // hold the launch for reqwest's own much longer timeouts, so the check - // gets the same budget the reachability probe uses and the launch - // continues on whatever is already installed. + // Bounded a hanging network would otherwise hold the launch for + // reqwest's much longer timeouts let check = match tokio::time::timeout( UPDATE_CHECK_BUDGET, oneclient_core::refresh_browser_package_updates(cluster_id, &content), @@ -1099,9 +1015,8 @@ impl Actions { } } - /// The launching cluster's effective mode, with the usual profile-over- - /// global inheritance. Anything unreadable falls back to the default rather - /// than blocking the launch on a settings lookup. + /// Anything unreadable falls back to the default rather than blocking the + /// launch on a settings lookup async fn cluster_update_mode( &self, state: &Arc, @@ -1122,8 +1037,7 @@ impl Actions { .unwrap_or_default() } - /// Installs every pending update, in one grouped progress notification so - /// the pre-launch pause has something to show for itself. + /// One grouped progress notification so the pre-launch pause shows something async fn apply_updates_for_launch( &self, state: &Arc, @@ -1150,8 +1064,8 @@ impl Actions { match oneclient_core::apply_browser_package_update(update, Some(&child), &content).await { Ok(_) => applied += 1, - // One package failing is not a reason to hold the game back; - // the cache row survives, so the next launch offers it again. + // Not a reason to hold the game back the cache row survives + // so the next launch offers it again Err(err) => tracing::warn!( package = %update.display_name, error = %err, @@ -1184,7 +1098,6 @@ impl Actions { } } - /// Opens the modal and waits for the user to be done with it. async fn prompt_package_updates(&self, group: PackageUpdateGroup) { let (done, wait) = tokio::sync::oneshot::channel(); @@ -1193,12 +1106,10 @@ impl Actions { state.center_open = false; }); - // An error means the modal state was replaced or torn down, which is - // equally a reason to stop waiting and get on with the launch. + // A replaced or torn-down modal is equally a reason to stop waiting let _ = wait.await; } - /// Applies one update from the modal, then drops that row from it. pub fn apply_package_update(&self, update: oneclient_core::BrowserPackageUpdate) { let actions = self.clone(); spawn_forever(async move { @@ -1252,8 +1163,7 @@ impl Actions { .finish_grouped_as_actions(&mut app.inbox, session_id, Some(spec)); }); - // A failed update stays in the list: the row is still out of date, - // and the user may want to try again. + // A failed update stays in the list so the user can retry if result.is_ok() { actions.with_engine(|app| { app.notifications @@ -1264,8 +1174,8 @@ impl Actions { }); } - /// Declines one update. The package stays marked out of date; only the - /// modal stops asking, and only for this version. + /// The package stays marked out of date only the modal stops asking and + /// only for this version pub fn skip_package_update(&self, cluster_id: ClusterId, hash: impl Into) { let hash = hash.into(); let actions = self.clone(); @@ -1289,19 +1199,14 @@ impl Actions { } } -/// The launch itself. Failures are reported as game events, so the caller only -/// has to know that the attempt is over. -/// -/// Everything before [`oneclient_core::launch_cluster`] is pre-launch work the -/// core does not own: renewing the account token, and bringing -/// browser-installed packages up to date. +/// Failures are reported as game events so the caller only has to know the +/// attempt is over Everything before `launch_cluster` is pre-launch work async fn launch(actions: &Actions, cluster_id: ClusterId) { let Ok(state) = launcher::state() else { return }; let events = state.services.events.clone(); events.game_stage(cluster_id, oneclient_events::LaunchStage::Checking); - // Renews a lapsed token before launching. let account = match state.auth.default_account_for_launch().await { Ok(account) => account, Err(err) => { @@ -1318,18 +1223,15 @@ async fn launch(actions: &Actions, cluster_id: ClusterId) { return; }; - // Before the game process, never after: Minecraft reads its mods once at - // startup, so an update applied to a live session would do nothing until - // the next launch anyway. + // Before the game process never after Minecraft reads its mods once at + // startup actions .resolve_package_updates_before_launch(&state, cluster_id) .await; if let Err(err) = oneclient_core::launch_cluster(&state, cluster_id, &account, true).await { - // A launch that failed because a file simply is not there is the one - // failure the launcher can fix by itself. Showing the user a path inside - // our own metadata folder and stopping leaves them with nothing to act - // on, so repair first and only report if that does not help. + // A missing file is the one failure the launcher can fix itself and a + // path inside our metadata folder gives the user nothing to act on if err.indicates_missing_files() { repair_and_relaunch(&state, cluster_id, &account, err).await; return; @@ -1339,12 +1241,8 @@ async fn launch(actions: &Actions, cluster_id: ClusterId) { } } -/// Runs a verify-and-repair pass, then tries the launch once more. -/// -/// Only ever one retry: if the game still cannot start after every file has -/// been hashed and the broken ones re-downloaded, the problem is not missing -/// files, and trying again would just spend another few minutes to show the -/// same error. +/// Only ever one retry if the game still will not start after a full rehash and +/// repair the problem is not missing files async fn repair_and_relaunch( state: &std::sync::Arc, cluster_id: ClusterId, @@ -1366,8 +1264,8 @@ async fn repair_and_relaunch( Ok(report) => report, Err(repair_err) => { tracing::error!(cluster_id, "repair failed: {repair_err:#}"); - // The original failure is what the user was trying to do, so lead - // with it; the repair failure explains why it was not fixed. + // Lead with the original failure the repair failure explains why + // it was not fixed events.game_failed( cluster_id, format!("{original:#} (repair also failed: {repair_err:#})"), diff --git a/packages/oneclient_app/src/hooks/active_cluster.rs b/packages/oneclient_app/src/hooks/active_cluster.rs index 4235eb52..54627eb8 100644 --- a/packages/oneclient_app/src/hooks/active_cluster.rs +++ b/packages/oneclient_app/src/hooks/active_cluster.rs @@ -15,14 +15,11 @@ pub fn use_active_cluster_id() -> State> { consume_root_context::().0 } -/// Drives the startup splash "curtain": a full-screen cover that stays up after -/// the router leaves the Startup route until the home view's clusters/art have -/// settled, then fades out, so the app never reveals a half-populated home. +/// Startup curtain stays up past the Startup route until home content settles +/// so the app never reveals a half-populated home #[derive(Clone, Copy)] pub struct SplashState { - /// The curtain is covering the screen (raised when heading into Home). pub active: State, - /// Home content (clusters + background) has finished loading. pub home_ready: State, } @@ -89,20 +86,17 @@ pub fn use_browser_state_store() -> State> { #[derive(Clone)] pub struct OnboardingSelectionState { pub selected: State>, - /// Set once the user changes the selection themselves. Until then the shell - /// keeps re-deriving `selected` from the catalog, so a bundle list that - /// settles late still gets the right defaults. + /// Until set the shell keeps re-deriving `selected` from the catalog so a + /// late-settling bundle list still gets the right defaults pub user_touched: State, - /// Bundle categories the user had in their v1 install, once the migration - /// step has been answered. `None` means "no migration decision yet". + /// `None` means "no migration decision yet" pub migrated_categories: State>>, pub language: State, pub reduce_motion: State, pub predownload: State, pub setup_started: State, - /// v1-migration: source folder to import files from (`None` = don't import). + /// `None` = don't import pub import_folder: State>, - /// v1-migration: import into the matching cluster's own dir instead of shared. pub import_dedicated: State, } diff --git a/packages/oneclient_app/src/hooks/debounce.rs b/packages/oneclient_app/src/hooks/debounce.rs index 98c8d432..b0c71857 100644 --- a/packages/oneclient_app/src/hooks/debounce.rs +++ b/packages/oneclient_app/src/hooks/debounce.rs @@ -2,16 +2,13 @@ use std::time::Duration; use freya::prelude::*; -/// Returns a debounced mirror of `value`. The returned state only updates to a -/// new `value` once `delay` has elapsed without any further change. Rapid -/// changes (e.g. per-keystroke) collapse into a single trailing update. +/// Trailing-edge debounce updates only after `delay` with no further change pub fn use_debounced(value: T, delay: Duration) -> State where T: Clone + PartialEq + 'static, { let debounced = use_state(|| value.clone()); - // Bumped on every change; the async task commits only if its generation is - // still the latest when the delay expires, dropping superseded changes. + // Task commits only if its generation is still latest dropping superseded changes let mut generation = use_state(|| 0u64); use_side_effect_with_deps(&value, move |value| { diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs index 16068351..ec3da18a 100644 --- a/packages/oneclient_app/src/hooks/mod.rs +++ b/packages/oneclient_app/src/hooks/mod.rs @@ -62,8 +62,6 @@ use crate::state::{ use freya::prelude::*; use freya::radio::use_radio; -/// Publishes the actions handle so components can reach it without prop -/// drilling. Provided once, at the root. pub fn use_provide_actions(actions: &Actions) { let actions = actions.clone(); use_provide_root_context(move || actions.clone()); @@ -73,10 +71,8 @@ pub fn use_dispatch() -> Actions { consume_root_context::() } -/// Subscribes to one concern of the app state. -/// -/// Each of these wakes its component only when *that* channel is written, so a -/// toast timer tick does not re-render a component that reads only `data_dir`. +/// Wakes its component only when that channel is written so a toast tick does +/// not re-render a component reading only `data_dir` pub fn use_launcher() -> LauncherInit { use_radio(AppChannel::Launcher).read().launcher.clone() } @@ -85,11 +81,8 @@ pub fn use_settings_snapshot() -> SettingsState { use_radio(AppChannel::Settings).read().settings.clone() } -/// Derives the render view from the engine. -/// -/// Built on read rather than published on write: during a download the engine -/// changes tens of thousands of times, and cloning the inbox each time was the -/// snapshot channel's main cost. +/// Built on read not published on write cloning the inbox on every one of a +/// download's tens of thousands of engine changes was the channel's main cost pub fn use_notifications_snapshot() -> NotificationSnapshot { let radio = use_radio(AppChannel::Notifications); let state = radio.read(); diff --git a/packages/oneclient_app/src/hooks/queries/auth.rs b/packages/oneclient_app/src/hooks/queries/auth.rs index 28539dff..281d8bf0 100644 --- a/packages/oneclient_app/src/hooks/queries/auth.rs +++ b/packages/oneclient_app/src/hooks/queries/auth.rs @@ -160,8 +160,7 @@ impl MutationCapability for FinishMicrosoftLoginMutation { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct CancelMicrosoftLoginMutation; -/// Drops a pending browser login server-side when the user cancels before the -/// flow reaches the point of no return. Keyed by the session's CSRF state token. +/// Keyed by the session's CSRF state token #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct CancelMicrosoftLoginKeys { pub state_token: String, @@ -354,12 +353,8 @@ pub fn use_refresh_all_accounts() -> UseMutation { use_mutation(Mutation::new(RefreshAllAccountsMutation)) } -/// Whether the mutation has yet to produce a settled result. -/// -/// True *before* it has ever been run as well as while it is running — -/// `MutationStateData::Pending` means "has not loaded yet", not "in progress". -/// For driving a button's label or disabled state, use [`mutation_is_running`]; -/// this one reads as "no result to show yet". +/// True before it has ever been run as well as while running For button +/// disabled state use [`mutation_is_running`] instead pub fn mutation_is_pending(mutation: &UseMutation) -> bool { matches!( &*mutation.read().state(), @@ -367,7 +362,6 @@ pub fn mutation_is_pending(mutation: &UseMutation) -> ) } -/// Whether the mutation is actually in flight right now. pub fn mutation_is_running(mutation: &UseMutation) -> bool { mutation.read().state().is_loading() } diff --git a/packages/oneclient_app/src/hooks/queries/clusters.rs b/packages/oneclient_app/src/hooks/queries/clusters.rs index 4b6cc0b2..a8bc5bea 100644 --- a/packages/oneclient_app/src/hooks/queries/clusters.rs +++ b/packages/oneclient_app/src/hooks/queries/clusters.rs @@ -23,11 +23,8 @@ pub fn use_clusters() -> UseQuery { use_query(Query::new(ListClustersKeys, ListClustersQuery)) } -/// The one cluster a route is about, pulled out of the list query. -/// -/// There is no per-cluster query, since the list is always loaded and small, so -/// this is a lookup, not a fetch. It is a hook because it subscribes to the -/// list on the caller's behalf. +/// A lookup not a fetch there is no per-cluster query the list is always +/// loaded and small A hook because it subscribes to the list pub fn use_cluster(cluster_id: i64) -> Option { let clusters = super::state::settled_or_loading(&use_clusters()).unwrap_or_default(); clusters.into_iter().find(|c| c.id == cluster_id) diff --git a/packages/oneclient_app/src/hooks/queries/image.rs b/packages/oneclient_app/src/hooks/queries/image.rs index be6488c6..d3dd8499 100644 --- a/packages/oneclient_app/src/hooks/queries/image.rs +++ b/packages/oneclient_app/src/hooks/queries/image.rs @@ -35,10 +35,8 @@ impl QueryCapability for CachedImageQuery { } } -/// The bytes of a cached image, once they are there. -/// -/// The url comes back with them because callers key their image handle on it; -/// a stale url paired with fresh bytes renders the wrong picture for a frame. +/// Returns the url alongside the bytes callers key their image handle on it +/// and a stale url with fresh bytes renders the wrong picture for a frame pub fn loaded_image(url: Option<&str>, query: &UseQuery) -> Option<(String, Bytes)> { let url = url?; let bytes = super::state::settled_or_loading(query)?; diff --git a/packages/oneclient_app/src/hooks/queries/migration.rs b/packages/oneclient_app/src/hooks/queries/migration.rs index 1f6c023d..4a9d862c 100644 --- a/packages/oneclient_app/src/hooks/queries/migration.rs +++ b/packages/oneclient_app/src/hooks/queries/migration.rs @@ -19,9 +19,8 @@ impl QueryCapability for MigrationQuery { return Ok(None); }; - // remote cluster migrations run on startup (so before we even get to launcher migrations) - // this pretty much tries to migrate the source instance to the latest version in the migration chain, - // so that we can import it into the correct cluster + // Runs before launcher migrations walks the source instance up the + // migration chain so it can be imported into the correct cluster let rules = crate::launcher::state()?.versions.migrations().await; if !rules.is_empty() { for instance in &mut detection.instances { diff --git a/packages/oneclient_app/src/hooks/queries/mod.rs b/packages/oneclient_app/src/hooks/queries/mod.rs index d1362b34..1fdc560e 100644 --- a/packages/oneclient_app/src/hooks/queries/mod.rs +++ b/packages/oneclient_app/src/hooks/queries/mod.rs @@ -1,11 +1,7 @@ use crate::hooks::use_settings_snapshot; -/// The cache key for anything fetched from the metadata server. -/// -/// Not the url itself, just enough of the setting to notice when it changes, -/// so pointing the launcher at a different meta host refetches instead of -/// serving the old host's answer. The queries read the real base out of -/// settings when they run. +/// Not the url itself just enough to notice when the meta host setting +/// changes so a different host refetches instead of serving the old answer fn use_meta_url_key() -> String { use_settings_snapshot() .settings diff --git a/packages/oneclient_app/src/hooks/queries/mutations.rs b/packages/oneclient_app/src/hooks/queries/mutations.rs index 68759673..03e732cc 100644 --- a/packages/oneclient_app/src/hooks/queries/mutations.rs +++ b/packages/oneclient_app/src/hooks/queries/mutations.rs @@ -24,12 +24,8 @@ pub async fn invalidate_cluster_queries() { QueriesStorage::::try_invalidate_all().await; } -/// Just the query that decides whether a cluster already has a package. -/// -/// Split out of [`invalidate_cluster_queries`] so an install can wait for -/// exactly this before it stops calling itself busy. The full sweep reaches the -/// network several times over, and waiting for all of it would trade one stale -/// button for a slower one. +/// Split out of [`invalidate_cluster_queries`] so an install can wait for just +/// this before dropping its busy flag the full sweep hits the network pub async fn invalidate_cluster_content_queries() { QueriesStorage::::try_invalidate_all().await; } @@ -64,9 +60,8 @@ pub enum ClusterAction { bundle_name: String, package_id: String, enabled: bool, - /// The bundle manifest's `enabled` flag, so we know whether the user's - /// choice matches the default (clear the override) or contradicts it - /// (write `Enabled` / `Disabled`). + /// Manifest default matching it clears the override contradicting it + /// writes `Enabled` / `Disabled` manifest_default: bool, }, ImportLocalFile { @@ -78,7 +73,6 @@ pub enum ClusterAction { cluster_id: ClusterId, dedicated: bool, }, - /// Rehash every installed file and re-download whatever no longer matches. VerifyFiles { cluster_id: ClusterId, }, @@ -95,10 +89,8 @@ impl MutationCapability for ClusterMutation { let content = &state.services.content(); let result = match keys { ClusterAction::ToggleArtifact { cluster_id, hash } => { - // Recorded in the database and applied to the game folder at the - // next launch, whether or not a session is live: Minecraft reads - // its mods once at startup, so there is nothing a mid-session - // write could achieve. + // Applied to the game folder at next launch never mid-session + // Minecraft reads its mods once at startup oneclient_core::toggle_artifact_enabled(*cluster_id, hash, content) .await .map(|_| ()) @@ -154,9 +146,8 @@ impl MutationCapability for ClusterMutation { }) } ClusterAction::VerifyFiles { cluster_id } => { - // Reports its own outcome rather than going through the generic - // failure toast: a verify that finds nothing wrong is a useful - // result the user asked for, not a silent no-op. + // Reports its own outcome not the generic failure toast a + // verify that finds nothing wrong is still a useful result match oneclient_core::verify_cluster_files(&state, *cluster_id).await { Ok(report) => { let notify = services.events.notify("Verification complete"); diff --git a/packages/oneclient_app/src/hooks/queries/package_updates.rs b/packages/oneclient_app/src/hooks/queries/package_updates.rs index 8ab9c01a..8d88fd52 100644 --- a/packages/oneclient_app/src/hooks/queries/package_updates.rs +++ b/packages/oneclient_app/src/hooks/queries/package_updates.rs @@ -4,10 +4,8 @@ use freya::query::{Query, QueryCapability, UseQuery, use_query}; use oneclient_core::{BrowserPackageUpdate, LauncherError}; use oneclient_db::models::ClusterId; -/// What the last launch-time check recorded for one cluster. -/// -/// Reads the cache table only: the check itself runs on launch, so rendering a -/// package list must never be what talks to a provider. +/// Reads the cache table only rendering a package list must never talk to a +/// provider The check itself runs on launch #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct PackageUpdatesQuery { pub cluster_id: ClusterId, @@ -44,7 +42,6 @@ pub fn package_updates(query: &UseQuery) -> Vec) -> HashSet { package_updates(query) .into_iter() diff --git a/packages/oneclient_app/src/hooks/queries/packages.rs b/packages/oneclient_app/src/hooks/queries/packages.rs index 901dd321..8e7d0cb5 100644 --- a/packages/oneclient_app/src/hooks/queries/packages.rs +++ b/packages/oneclient_app/src/hooks/queries/packages.rs @@ -13,10 +13,8 @@ use oneclient_core::LauncherError; pub const BROWSE_PAGE_SIZE: usize = DEFAULT_PAGE_SIZE; pub const VERSIONS_PAGE_SIZE: usize = 20; -/// A published version list barely moves, and the browser listing now asks for -/// one per visible card to know what "latest" is. Holding them for a while -/// keeps scrolling from refiring a request per card every time one comes back -/// into view. +/// The listing asks for one per visible card holding them stops scrolling from +/// refiring a request per card each time it comes back into view const VERSIONS_STALE: Duration = Duration::from_secs(5 * 60); pub fn content_type_for_slug(slug: &str) -> ContentType { @@ -85,9 +83,8 @@ pub fn use_package_search( PackageSearchKeys { provider, content_type, - // Canonical form, because the key is also the cache key: without it - // "sodium" and "sodium " are two entries, and adding a trailing - // space throws the results away and refetches for nothing. + // The key is also the cache key without normalizing "sodium" and + // "sodium " are two entries and a trailing space refetches for nothing query: normalize_query(&query), game_versions, loaders, @@ -245,11 +242,8 @@ pub fn use_package_versions( use_package_versions_when(true, provider, project_id, game_version, loader, page) } -/// The version list, fetched only when `enabled`. -/// -/// A listing row keeps its install button after the package is in the cluster, -/// so the hook still has to run there — but there is nothing left for it to -/// install, and a request per already-installed row is pure waste. +/// A listing row keeps its install button once installed so the hook still +/// runs there but a request per already-installed row is pure waste pub fn use_package_versions_when( enabled: bool, provider: ProviderId, diff --git a/packages/oneclient_app/src/hooks/queries/player_profile.rs b/packages/oneclient_app/src/hooks/queries/player_profile.rs index 8735d3d6..742b5ad7 100644 --- a/packages/oneclient_app/src/hooks/queries/player_profile.rs +++ b/packages/oneclient_app/src/hooks/queries/player_profile.rs @@ -4,7 +4,6 @@ use freya::query::{Query, QueryCapability, UseQuery, use_query}; use oneclient_core::LauncherError; use oneclient_mc::{self as minecraft, PlayerProfileView}; -// image cache const PROFILE_STALE: Duration = Duration::from_secs(30 * 60); const PROFILE_CLEAN: Duration = Duration::from_secs(2 * 60 * 60); @@ -31,7 +30,7 @@ impl QueryCapability for FetchPlayerProfileQuery { Ok(minecraft::fetch_player_profile_view(client, &keys.uuid, access_token.as_deref()).await?) } - // TODO: Cache + // TODO Cache // fn matches(&self, keys: &Self::Keys) -> bool { // keys.uuid.as_deref().is_some_and(|id| !id.is_empty()) // } diff --git a/packages/oneclient_app/src/hooks/queries/skin.rs b/packages/oneclient_app/src/hooks/queries/skin.rs index b233e878..10ab6609 100644 --- a/packages/oneclient_app/src/hooks/queries/skin.rs +++ b/packages/oneclient_app/src/hooks/queries/skin.rs @@ -14,7 +14,6 @@ pub fn use_player_skin(uuid: String) -> (Bytes, bool) { let steve = use_memo(|| AppAssets::get_bytes("steve.png").unwrap_or_default()); let alex = use_memo(|| AppAssets::get_bytes("alex.png").unwrap_or_default()); - // No custom skin: pick alex (slim) or steve (classic) from the UUID. let default_slim = (java_string_hash(&uuid) & 1) == 1; match crate::hooks::loaded_image(skin_url.as_deref(), &skin_query) { diff --git a/packages/oneclient_app/src/hooks/queries/state.rs b/packages/oneclient_app/src/hooks/queries/state.rs index db9924fd..63e5f3ac 100644 --- a/packages/oneclient_app/src/hooks/queries/state.rs +++ b/packages/oneclient_app/src/hooks/queries/state.rs @@ -1,21 +1,12 @@ -//! Reading a query's state without spelling out the match every time. -//! -//! `QueryStateData` has three variants and the useful cases straddle two of -//! them: a query that is refetching still holds the previous value, and the UI -//! wants to keep showing it rather than flash a spinner. Written out by hand -//! that is a five-line match repeated over a hundred times, with enough small -//! divergences (does a settled `Err` read as empty or as absent? does -//! "loading" include a refetch?) that the differences were impossible to see. +//! Query state readers The useful cases straddle two `QueryStateData` +//! variants a refetch still holds the previous value the UI wants to show use std::fmt::Display; use freya::query::{QueryCapability, QueryStateData, UseQuery}; -/// The query's value, keeping the previous one visible across a refetch. -/// -/// `None` means there has never been a successful result: either the query -/// has not finished yet or the only outcome so far is an error. Callers that -/// treat "failed" the same as "empty" finish with `.unwrap_or_default()`. +/// Keeps the previous value visible across a refetch `None` means there has +/// never been a successful result (not finished yet or only errors so far) pub fn settled_or_loading(query: &UseQuery) -> Option where Q: QueryCapability, @@ -26,10 +17,8 @@ where state.ok().cloned() } -/// The error from a settled query, rendered for display. -/// -/// Not reported while loading: a refetch that is still in flight has not -/// failed yet, even if the previous attempt did. +/// Not reported while loading a refetch in flight has not failed yet even if +/// the previous attempt did pub fn query_error(query: &UseQuery) -> Option where Q: QueryCapability, @@ -43,11 +32,8 @@ where } } -/// Whether the query is working with nothing to show yet. -/// -/// A refetch that still holds a previous value is *not* loading by this -/// definition, since the UI has something to render and a spinner would be a -/// step backwards. Use [`query_is_busy`] where the distinction matters. +/// A refetch still holding a previous value is *not* loading here Use +/// [`query_is_busy`] where the distinction matters pub fn query_is_loading(query: &UseQuery) -> bool { let reader = query.read(); let state = reader.state(); @@ -57,8 +43,6 @@ pub fn query_is_loading(query: &UseQuery) -> bool { ) } -/// Whether the query is working at all, including a refetch over a value that -/// is already on screen. pub fn query_is_busy(query: &UseQuery) -> bool { let reader = query.read(); let state = reader.state(); diff --git a/packages/oneclient_app/src/hooks/queries/storage.rs b/packages/oneclient_app/src/hooks/queries/storage.rs index c52462aa..f5ba2996 100644 --- a/packages/oneclient_app/src/hooks/queries/storage.rs +++ b/packages/oneclient_app/src/hooks/queries/storage.rs @@ -24,14 +24,8 @@ impl QueryCapability for StorageReportQuery { } } -/// How long a scan stays good for. -/// -/// The report walks every file under the launcher directory, which on a -/// well-used install is tens of thousands of them. `stale_time` defaults to -/// zero, meaning the scan would re-run every single time the page is mounted — -/// so leaving the page and coming back would rescan the whole tree. Disk usage -/// does not move on its own, and the page has a refresh button for when the -/// user knows it has. +/// The report walks tens of thousands of files and the default zero stale time +/// would rescan the whole tree on every mount The page has a refresh button const STORAGE_STALE_TIME: Duration = Duration::from_secs(120); pub fn use_storage_report() -> UseQuery { @@ -48,9 +42,7 @@ pub async fn invalidate_storage_queries() { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum StorageAction { - /// Delete cached package files no artifact refers to. CleanUnreferencedCache, - /// Clear content links older launchers wrote into cluster folders. CleanLegacyClusterContent, } diff --git a/packages/oneclient_app/src/hooks/queries/version_metadata.rs b/packages/oneclient_app/src/hooks/queries/version_metadata.rs index 681cfc80..fb90e3d9 100644 --- a/packages/oneclient_app/src/hooks/queries/version_metadata.rs +++ b/packages/oneclient_app/src/hooks/queries/version_metadata.rs @@ -50,7 +50,6 @@ pub fn use_version_metadata( let versions_query = use_versions(); let major = major?; - // Borrowed, not cloned: the manifest is long and this runs per render. let reader = versions_query.read(); let state = reader.state(); let list = state.ok()?; diff --git a/packages/oneclient_app/src/hooks/queries/versions.rs b/packages/oneclient_app/src/hooks/queries/versions.rs index 8d8f478e..be744e63 100644 --- a/packages/oneclient_app/src/hooks/queries/versions.rs +++ b/packages/oneclient_app/src/hooks/queries/versions.rs @@ -28,15 +28,12 @@ pub fn use_versions() -> UseQuery { use_query(Query::new(VersionsMetadataKeys, VersionsMetadataQuery)) } -/// The manifest rows once the query has settled, `None` while it is still -/// loading. A failed fetch reads as an empty list rather than `None`, so -/// callers that block on this don't wait forever when the network is down. +/// A failed fetch reads as an empty list rather than `None` so callers that +/// block on this don't wait forever when the network is down pub fn versions_metadata(query: &UseQuery) -> Option> { let reader = query.read(); let state = reader.state(); match &*state { - // A failed fetch reads as an empty list rather than `None`, so callers - // that block on this don't wait forever when the network is down. QueryStateData::Settled { res: Err(_), .. } => Some(Vec::new()), other => other.ok().cloned(), } diff --git a/packages/oneclient_app/src/install.rs b/packages/oneclient_app/src/install.rs index 742f9009..62961989 100644 --- a/packages/oneclient_app/src/install.rs +++ b/packages/oneclient_app/src/install.rs @@ -1,8 +1,3 @@ -//! Shared helpers for content installs and the notifications they raise. -//! -//! Lifted out of the bridge runtime so the actions layer can use them without -//! the command loop existing. - use std::sync::Arc; use oneclient_core::LauncherState; @@ -16,8 +11,7 @@ use crate::notifications::{ NotificationSpec, PackageUpdateGroup, }; -/// Names a cluster for a notification or modal header, falling back rather than -/// failing: a missing row is not a reason to lose the whole message. +/// Falls back to a placeholder a missing row must not lose the whole message pub async fn cluster_display_name( cluster_id: i64, services: &oneclient_core::LauncherServices, @@ -28,8 +22,6 @@ pub async fn cluster_display_name( .unwrap_or_else(|_| "Cluster".to_string()) } -/// Turns the launching cluster's pending browser updates into what the modal -/// renders. `None` when there is nothing to offer. pub async fn package_update_group( cluster_id: i64, updates: &[oneclient_core::BrowserPackageUpdate], @@ -140,10 +132,7 @@ pub async fn cluster_update_notification( }) } -/// Builds a single notification summarising a batch bundle sync. Every changed -/// cluster rides along on one "View changes" action so the notification keeps -/// exactly two buttons no matter how many clusters moved; the modal does the -/// per-cluster breakdown. `None` when nothing changed. +/// All clusters share one "View changes" action so the notification keeps exactly two buttons pub async fn combined_cluster_update_spec( changed: &[(i64, oneclient_core::ApplyBundleUpdatesResult)], services: &oneclient_core::LauncherServices, @@ -182,23 +171,17 @@ pub async fn combined_cluster_update_spec( }) } -/// An install's outcome together with the progress session it ran under. -/// -/// The session is detached rather than finished so the caller can convert that -/// same notification into the "Installed" / "Install failed" result, instead of -/// leaving a finished progress card and raising a second notification beside it. -/// `session_id` is `None` when the install never got as far as downloading. +/// The session is detached not finished so the caller can reuse its notification +/// as the "Installed" / "Install failed" result +/// `session_id` is `None` if no download ran pub struct PackageInstall { pub session_id: Option, pub result: anyhow::Result, - /// Required dependencies pulled in alongside the package, by display name. pub dependencies: Vec, - /// Required dependencies that couldn't be resolved or downloaded. The - /// package still installs; the caller says so in the notification. + /// Unresolved or failed dependencies the package still installs pub missing_dependencies: Vec, } -/// "Added Sodium with 2 dependencies. Could not add: Fabric API." pub fn install_body(name: &str, dependencies: &[String], missing: &[String]) -> String { let mut body = format!("Added {name}"); @@ -253,8 +236,7 @@ pub async fn install_package( Err(err) => return PackageInstall::failed(err), }; - // Worked out before the session starts so its children can be announced up - // front; a failure here leaves the package itself installable. + // Resolved before the session starts so its children can be announced up front let mut resolution = oneclient_content::packages::DependencyResolution::default(); if oneclient_content::packages::resolves_dependencies(project.content_type) { match oneclient_content::packages::resolve_required( @@ -292,8 +274,7 @@ pub async fn install_package( let mut installed_dependencies = Vec::new(); let mut missing_dependencies = resolution.unresolved; - // Dependencies go in first so the package is never sitting in a cluster - // without them, however the run ends. + // Dependencies first so the package is never in a cluster without them however the run ends for dependency in &resolution.install { let child = session.child( dependency.project.name.clone(), diff --git a/packages/oneclient_app/src/launcher.rs b/packages/oneclient_app/src/launcher.rs index ca6737af..a8c7db22 100644 --- a/packages/oneclient_app/src/launcher.rs +++ b/packages/oneclient_app/src/launcher.rs @@ -1,13 +1,5 @@ -//! The app's handle on the launcher core. -//! -//! The core keeps no process-wide `OnceCell`; a library holding ambient state -//! cannot be constructed twice, or once in a test. The handle lives here -//! instead, in the executable, which is the one place a global is defensible: -//! this *is* the composition root. -//! -//! It exists because `freya::query`'s `QueryCapability::run` and -//! `MutationCapability::run` take only their keys, with no context and no -//! hooks, so there is nowhere else to thread the state through. +//! The global lives here not in the core so the library stays constructible in tests +//! It is needed because `freya::query`'s `run` methods take only keys with no context use std::sync::{Arc, OnceLock}; @@ -15,11 +7,7 @@ use oneclient_core::{LauncherError, LauncherResult, LauncherState}; static LAUNCHER: OnceLock> = OnceLock::new(); -/// Installs the launcher handle. Called once, from the startup path. -/// -/// Returns the existing handle if one was already installed, rather than -/// failing: a second call is a bug in startup ordering, not something a user -/// can trigger, and silently keeping the first is safer than panicking. +/// Returns the existing handle on a second call rather than panicking pub fn install(state: Arc) -> Arc { if let Err(existing) = LAUNCHER.set(Arc::clone(&state)) { tracing::warn!("launcher state was already installed; keeping the first"); @@ -28,8 +16,7 @@ pub fn install(state: Arc) -> Arc { state } -/// The launcher handle, or [`LauncherError::NotInitialized`] before startup has -/// finished installing it. +/// Errors with [`LauncherError::NotInitialized`] before startup installs the handle pub fn state() -> LauncherResult> { LAUNCHER .get() diff --git a/packages/oneclient_app/src/layout/animated_outlet.rs b/packages/oneclient_app/src/layout/animated_outlet.rs index dffcd969..c1474ebd 100644 --- a/packages/oneclient_app/src/layout/animated_outlet.rs +++ b/packages/oneclient_app/src/layout/animated_outlet.rs @@ -14,9 +14,8 @@ enum Enter { Fade, } -/// Every route nested under [`SettingsShell`](crate::layout::SettingsShell). The -/// shell runs its own content-only transition, so this outlet must stay still for -/// them — otherwise the sidebar animates along with the page. +/// This outlet must stay still for these the shell runs its own content-only +/// transition else the sidebar animates along with the page fn is_sidebar_route(route: &Route) -> bool { matches!( route, diff --git a/packages/oneclient_app/src/layout/app_shell.rs b/packages/oneclient_app/src/layout/app_shell.rs index de469032..c45c9a58 100644 --- a/packages/oneclient_app/src/layout/app_shell.rs +++ b/packages/oneclient_app/src/layout/app_shell.rs @@ -43,10 +43,7 @@ impl Component for AppShell { let browser_state = use_state(HashMap::new); use_provide_browser_state(BrowserStateStore(browser_state)); - // Window-wide file drops. `FileDrop` bubbles, so anything a dedicated - // drop zone doesn't claim with `stop_propagation()` ends up here and the - // user gets asked where it should go. Only the overlay reads these, so - // hovering a file doesn't re-render the shell. + // `FileDrop` bubbles so anything a drop zone doesn't `stop_propagation()` lands here let mut drop_hovering = use_state(|| false); let mut drop_pending = use_state(Vec::::new); @@ -259,8 +256,6 @@ pub(crate) fn appshell_overlay() -> Rect { pub const HOME_BACKGROUND_ASSET: &str = "backgrounds/CavesAndCliffs.jpg"; -/// The cluster the home background is drawn from: the active one, or the first -/// in the list before anything is active. fn home_cluster(clusters: &[Cluster], active: Option) -> Option<&Cluster> { active .and_then(|id| clusters.iter().find(|c| c.id == id)) @@ -273,25 +268,15 @@ fn home_art(cluster: Option<&Cluster>) -> DynamicArt { .preview_edge(ART_PREVIEW_EDGE) } -/// Loads the home background art as soon as the launcher is up, rather than -/// when Home mounts. -/// -/// The art is three async hops deep — the cluster list, then the version -/// manifest holding its url, then the image itself — and none of them start -/// until something subscribes. Mounted at the root, they overlap the rest of -/// startup while the splash curtain is still up; left to Home, they run after -/// it has lifted and show as the built-in fallback lingering on screen. -/// -/// The queries are global and outlive this component's subscription, so Home -/// finds them already settled and renders the real art on its first frame. +/// Warms the home art at the root so its three async hops overlap startup behind the +/// splash left to Home the fallback lingers on screen after the curtain lifts #[derive(PartialEq, Clone, Copy)] pub struct HomeArtPrefetch; impl Component for HomeArtPrefetch { fn render(&self) -> impl IntoElement { - // Before the launcher is ready every query errors out, and a settled - // error is not retried until a new subscriber mounts. Waiting means the - // inner component's hooks run once, when they can actually succeed. + // Before the launcher is ready every query errors and a settled error is not + // retried until a new subscriber mounts if !use_launcher().ready { return rect().into_element(); } @@ -308,8 +293,7 @@ impl Component for HomeArtWarm { let art = { let reader = clusters_query.read(); let state = reader.state(); - // No active cluster this early, so this is the first in the list — - // the same one `AppHomeBackground` picks. + // No active cluster this early so this picks the same one `AppHomeBackground` will let clusters = state.ok().map_or(&[][..], Vec::as_slice); home_art(home_cluster(clusters, None)) }; @@ -330,8 +314,7 @@ impl Component for AppHomeBackground { let launcher = use_launcher(); let splash = use_splash(); - // Once the cluster list has settled and the startup fetch is done, the - // home view is populated, so let the splash curtain fade out. + // The home view is populated once these settle which releases the splash curtain let clusters_settled = matches!( &*clusters_query.read().state(), QueryStateData::Settled { .. } diff --git a/packages/oneclient_app/src/layout/cluster_shell.rs b/packages/oneclient_app/src/layout/cluster_shell.rs index 2d824316..a5458e7e 100644 --- a/packages/oneclient_app/src/layout/cluster_shell.rs +++ b/packages/oneclient_app/src/layout/cluster_shell.rs @@ -105,9 +105,8 @@ impl Component for ClusterShell { let show_game_log = game.is_active(cluster_id); let launch_state = launch_button_state(&game, cluster_id, syncing); - // Queried out here rather than where it is used: the shell can mount - // before the cluster list settles, and a hook that only runs once the - // cluster resolves would change this component's hook count mid-life. + // Queried unconditionally the shell can mount before the cluster list settles + // and a conditional hook would change this component's hook count mid-life let parsed = cluster.as_ref().and_then(|c| parse_mc_version(&c.mc_version)); let metadata = use_version_metadata( parsed.as_ref().map(|p| p.major), diff --git a/packages/oneclient_app/src/layout/root_layout.rs b/packages/oneclient_app/src/layout/root_layout.rs index f44b049f..7489dcaf 100644 --- a/packages/oneclient_app/src/layout/root_layout.rs +++ b/packages/oneclient_app/src/layout/root_layout.rs @@ -21,15 +21,12 @@ impl Component for RootLayout { let home_ready = use_state(|| false); use_provide_splash(SplashState { active, home_ready }); - // macOS draws the corners natively (rounded window + shadow via the - // window attributes), so Freya must not round on top of it. + // macOS rounds the window natively so Freya must not round on top of it #[cfg(target_os = "macos")] let corner = 0.; - // Other platforms are borderless: Freya rounds, squared when maximized so - // there's no gap to the screen edge. No reactive maximized signal exists, - // so mirror it: root_size changes on every maximize/restore/resize, which - // re-runs the effect to re-query. + // Elsewhere the window is borderless and squared when maximized + // No reactive maximized signal exists so root_size changes stand in as the trigger #[cfg(not(target_os = "macos"))] let corner = { let root_size = Platform::get().root_size; @@ -72,8 +69,7 @@ impl Component for RootLayout { .child(Toasts) .child(UpdatePromptOverlay) .child(JavaPromptOverlay) - // Last of the prompt overlays: it renders whatever the two above - // did not claim, so no prompt can reach the user as silence. + // Must stay last it renders whatever the overlays above did not claim .child(GenericPromptOverlay) .child(ClusterUpdatePopup) .child(PackageUpdatePopup) diff --git a/packages/oneclient_app/src/layout/settings_shell.rs b/packages/oneclient_app/src/layout/settings_shell.rs index 0d964860..9debe367 100644 --- a/packages/oneclient_app/src/layout/settings_shell.rs +++ b/packages/oneclient_app/src/layout/settings_shell.rs @@ -51,9 +51,8 @@ struct SearchItem { route: Route, } -// TODO: Replace this with inventory crate or something +// TODO Replace this with inventory crate or something const SEARCH_INDEX: &[SearchItem] = &[ - // Launcher SearchItem { id: "launcher.discord_rpc", icon: IconType::Link03, @@ -70,7 +69,6 @@ const SEARCH_INDEX: &[SearchItem] = &[ keywords: &["data dir", "directory", "folder", "open"], route: Route::SettingsLauncher {}, }, - // Appearance SearchItem { id: "appearance.accent_color", icon: IconType::PaintPour, @@ -103,7 +101,6 @@ const SEARCH_INDEX: &[SearchItem] = &[ keywords: &["reduce motion", "animations", "effects"], route: Route::SettingsAppearance {}, }, - // APIs SearchItem { id: "apis.modrinth_key", icon: IconType::Key01, @@ -128,7 +125,6 @@ const SEARCH_INDEX: &[SearchItem] = &[ keywords: &["endpoint", "api", "backend", "url"], route: Route::SettingsApis {}, }, - // Minecraft settings SearchItem { id: "minecraft.force_fullscreen", icon: IconType::Maximize01, @@ -177,7 +173,7 @@ const SEARCH_INDEX: &[SearchItem] = &[ keywords: &["hook", "command", "post exit", "postexit"], route: Route::SettingsMinecraft {}, }, - // Java (routing only, page isn't built from settings_row_focus yet) + // Java routing only the page is not built from settings_row_focus yet SearchItem { id: "java.install", icon: IconType::Download01, @@ -194,7 +190,6 @@ const SEARCH_INDEX: &[SearchItem] = &[ keywords: &["java", "runtime", "custom", "folder", "path"], route: Route::SettingsJava {}, }, - // Storage SearchItem { id: "storage.usage", icon: IconType::Database01, @@ -214,7 +209,6 @@ const SEARCH_INDEX: &[SearchItem] = &[ ], route: Route::SettingsStorage {}, }, - // Language SearchItem { id: "language.select", icon: IconType::Globe01, @@ -226,7 +220,6 @@ const SEARCH_INDEX: &[SearchItem] = &[ ], route: Route::SettingsLanguage {}, }, - // Developer SearchItem { id: "developer.compat_only", icon: IconType::SearchMd, @@ -259,7 +252,6 @@ const SEARCH_INDEX: &[SearchItem] = &[ keywords: &["debug", "page"], route: Route::SettingsDeveloper {}, }, - // Navigation-level items SearchItem { id: "nav.accounts", icon: IconType::Users01, @@ -505,9 +497,7 @@ fn search_box(mut search: State) -> impl IntoElement { ) } -/// How well a settings entry answers the query. The title is what the user is -/// looking for, so a hit there outranks one in the blurb or the hidden keywords -/// however well those scored — a description match is a hint, not the thing. +/// A title hit outranks a keyword or description hit however well those scored fn settings_score(item: &SearchItem, query: &SearchQuery) -> Option<(u8, MatchScore)> { if let Some(score) = query.score(item.title) { return Some((2, score)); @@ -524,8 +514,7 @@ fn search_results(query: &SearchQuery, mut search: State) -> Vec) -> Vec bool { let settings = use_settings_snapshot().settings; let query = use_changelog(); diff --git a/packages/oneclient_app/src/lib.rs b/packages/oneclient_app/src/lib.rs index 4a7716d2..412de5f4 100644 --- a/packages/oneclient_app/src/lib.rs +++ b/packages/oneclient_app/src/lib.rs @@ -1,6 +1,6 @@ -// TODO: Remove this once freya has some workaround for this +// TODO Remove this once freya has some workaround for this #![allow(float_literal_f32_fallback)] -// Deeply nested async call chains through instrumented oneclient_core fns exceed the default limit +// Instrumented oneclient_core async call chains exceed the default limit #![recursion_limit = "256"] mod assets; diff --git a/packages/oneclient_app/src/main.rs b/packages/oneclient_app/src/main.rs index 2fd66798..537b84b9 100644 --- a/packages/oneclient_app/src/main.rs +++ b/packages/oneclient_app/src/main.rs @@ -17,9 +17,8 @@ struct OneClientApp; impl App for OneClientApp { fn render(&self) -> impl IntoElement { - // The station holds all app state, and radio state is `!Send`, so - // everything that writes it runs on the UI thread. `spawn_forever`, not - // `spawn`: these must outlive any component scope. + // Radio state is `!Send` so every writer runs on the UI thread `spawn_forever` + // rather than `spawn` because these must outlive any component scope let station = use_init_radio_station::(AppState::default); let actions = use_hook(move || { @@ -27,8 +26,7 @@ impl App for OneClientApp { let (events_bus, events_rx) = oneclient_events::EventBus::channel(); let actions = Actions::new(station, signals_tx); - // Started first so nothing emitted during startup waits on a - // consumer, though the bus is unbounded either way. + // Started first so nothing emitted during startup waits on a consumer spawn_forever( EventPump { events: events_rx, @@ -41,10 +39,8 @@ impl App for OneClientApp { let startup = actions.clone(); spawn_forever(async move { match events::start_launcher(station, events_bus).await { - // Chained rather than called alongside: `sync_bundles` needs - // the launcher handle, and it sets `syncing_bundles` before - // spawning its work, so firing it early would both fail and - // leave the flag stuck, disabling every launch button. + // Must follow startup `sync_bundles` needs the launcher handle and firing + // it early leaves `syncing_bundles` stuck disabling every launch button Ok(()) => startup.sync_bundles(), Err(err) => events::report_startup_failure(&station, &err), } @@ -70,8 +66,7 @@ fn main() { let mut builder = Builder::new_multi_thread(); builder.enable_all().max_blocking_threads(64); - // Debug builds emit unoptimized async code, which can cause stack overflows in some cases. - // Default stack size is 2MB, so we'll increase it to 3MB for debug builds + // Debug builds emit unoptimized async code that can overflow the default 2MB stack #[cfg(debug_assertions)] builder.thread_stack_size(3 * 1024 * 1024); @@ -126,7 +121,7 @@ fn main() { std::env::var("ONECLIENT_GPU_CACHE_BYTES") .ok() .and_then(|v| v.parse::().ok()) - .unwrap_or(512 * 1024 * 1024), // 512 MB + .unwrap_or(512 * 1024 * 1024), ) .with_default_font(theme::DEFAULT_FONT); diff --git a/packages/oneclient_app/src/notifications.rs b/packages/oneclient_app/src/notifications.rs index 3d1346a6..25f793cf 100644 --- a/packages/oneclient_app/src/notifications.rs +++ b/packages/oneclient_app/src/notifications.rs @@ -23,17 +23,12 @@ pub struct NotificationAction { #[derive(Clone, Debug, PartialEq)] pub enum NotificationActionKind { - /// Opens the changes modal for every cluster touched by one sync. Kept as a - /// list so a batch sync stays a single "View changes" action instead of one - /// button per cluster. + /// A list so a batch sync stays a single "View changes" action not one button per cluster OpenClusterUpdate(Vec), } -/// The stale browser packages of the cluster being launched. -/// -/// Carries the core updates whole rather than a display projection: the modal's -/// Update button hands one straight back to the apply path, and a trimmed copy -/// would have to be re-looked-up to find the version it is meant to install. +/// Carries core updates whole not a display projection the modal's Update button +/// hands one straight back to the apply path #[derive(Clone, Debug, PartialEq)] pub struct PackageUpdateGroup { pub cluster_id: ClusterId, @@ -41,13 +36,11 @@ pub struct PackageUpdateGroup { pub packages: Vec, } -/// One changed package in a cluster update, carrying enough identity to -/// resolve a pretty display name through the package-meta cache in the UI. #[derive(Clone, Debug, PartialEq)] pub struct ClusterUpdateItem { pub provider: ProviderId, pub project_id: Option, - /// Name shown when the meta cache has no entry (file name / package id). + /// Used when the meta cache has no entry (file name / package id) pub fallback: String, } @@ -89,12 +82,11 @@ pub struct InboxEntry { pub created_at: Instant, pub actions: Vec, pub tasks: Vec, - /// Live transfer stats for grouped downloads (bytes/sec, seconds remaining). + /// Bytes/sec and seconds remaining for grouped downloads pub transfer: Option, } -/// One row in the expandable task list, an aggregate over all children of a -/// single [`TaskCategory`] (e.g. all libraries collapse into one "Libraries" row). +/// One aggregate row per [`TaskCategory`] (all libraries collapse into one "Libraries" row) #[derive(Clone, Debug, PartialEq)] pub struct TaskView { pub label: String, @@ -138,12 +130,10 @@ pub struct PendingPrompt { pub question: String, pub choices: Vec, pub dismiss: Option, - /// Taken when answered, so a second answer cannot fire the oneshot twice. + /// Taken when answered so a second answer cannot fire the oneshot twice pub reply_tx: Option>>, } -/// The renderable half of a [`PendingPrompt`]: everything except the reply -/// channel, which cannot be cloned into the snapshot. #[derive(Clone, Debug)] pub struct PendingPromptView { pub title: String, @@ -153,9 +143,7 @@ pub struct PendingPromptView { } impl PendingPromptView { - /// Whether this prompt offers a choice with the given id. Lets a - /// specialised overlay (the Java one) recognise a prompt it knows how to - /// render richly, without the event layer naming the subsystem. + /// Lets a specialised overlay recognise a prompt without the event layer naming it pub fn has_choice(&self, id: &str) -> bool { self.choices.iter().any(|choice| choice.id == id) } @@ -199,14 +187,12 @@ pub struct NotificationState { pending_timers: Vec, cluster_update: Option>, package_updates: Option>, - /// Resumes the launch that opened the update modal. Held here rather than - /// by the launch task because every way the modal can end — the last row - /// answered, "Not now", Escape, the scrim — goes through this state, and - /// each of them has to let the game start. + /// Resumes the launch that opened the update modal + /// Held here not by the launch task + /// because every way the modal can end goes through this state package_updates_done: Option>, } -/// Fixed display order for the category rows. const CATEGORY_ORDER: [TaskCategory; 7] = [ TaskCategory::Java, TaskCategory::Client, @@ -231,15 +217,13 @@ struct GroupedTasks { done_units: HashMap, done_count: HashMap, seen_count: HashMap, - /// Expected counts/bytes announced up-front so totals don't climb mid-download. + /// Expected counts/bytes announced up-front so totals don't climb mid-download reserved_count: HashMap, reserved_units: HashMap, - /// Transfer rate and ETA for this group's downloads. meter: TransferMeter, } impl GroupedTasks { - /// Aggregate live + finished children into one row per category, in display order. fn task_list(&self) -> Vec { CATEGORY_ORDER .iter() @@ -259,7 +243,6 @@ impl GroupedTasks { let live_current: u64 = live.iter().map(|c| c.current.min(c.total)).sum(); let live_total: u64 = live.iter().map(|c| c.total).sum(); - // Surface the most advanced phase among live children; default Downloading. let phase = live .iter() .map(|c| c.phase) @@ -270,8 +253,7 @@ impl GroupedTasks { label: cat.label().to_string(), phase, current: done_units + live_current, - // Reserved total keeps the denominator stable while children are - // still being added; fall back to what we've actually seen. + // Reserved total keeps the denominator stable while children are still added total: reserved_units.max(done_units + live_total), done_count, total_count, @@ -280,7 +262,6 @@ impl GroupedTasks { .collect() } - /// Which coarse group is currently downloading, for the notification body. fn active_body(&self) -> Option<&'static str> { let mut minecraft = false; let mut packages = false; @@ -318,7 +299,6 @@ impl NotificationState { } } - /// Entry ids with a toast currently showing. #[must_use] pub fn active_toast_entry_ids(&self) -> Vec { self.active_toasts.iter().map(|t| t.entry_id).collect() @@ -335,11 +315,8 @@ impl NotificationState { self.cluster_update = None; } - /// Opens the pre-launch update modal, with `done` as the launch's - /// continuation. - /// - /// Nothing to show is answered immediately rather than left pending: a - /// launch must never be able to wait on a modal that was never drawn. + /// `done` is the launch's continuation it fires immediately when there is nothing + /// to show so a launch never waits on a modal that was never drawn pub fn open_package_updates( &mut self, groups: Vec, @@ -357,8 +334,7 @@ impl NotificationState { return; } - // A second modal would strand the first launch, so hand the old - // continuation back before taking the new one. + // A second modal would strand the first launch so release the old continuation first self.finish_package_updates(); self.package_updates = Some(groups); self.package_updates_done = done; @@ -368,10 +344,7 @@ impl NotificationState { self.finish_package_updates(); } - /// Drops one package from the open modal, closing it once the user has - /// answered for every row. Applying and skipping are the same thing here: - /// both are answers, and neither should leave a row the user has already - /// dealt with sitting in the list. + /// Applying and skipping are both answers either drops the row and can close the modal pub fn resolve_package_update(&mut self, cluster_id: ClusterId, hash: &str) { let Some(groups) = self.package_updates.as_mut() else { return; @@ -389,14 +362,11 @@ impl NotificationState { } } - /// Closes the modal and releases the launch waiting on it. Safe to call - /// when nothing is open; that is what makes every exit path able to call it - /// without first checking which one it is. + /// Safe to call when nothing is open so every exit path can call it unconditionally fn finish_package_updates(&mut self) { self.package_updates = None; if let Some(done) = self.package_updates_done.take() { - // The launch task is gone if this fails, which is not this layer's - // problem: there is simply nothing left to resume. + // A send failure means the launch task is gone nothing left to resume let _ = done.send(()); } } @@ -405,10 +375,9 @@ impl NotificationState { inbox.iter().filter(|entry| !entry.read).count() } - /// Folds one notification into the engine state. Does not build a - /// [`NotificationSnapshot`]: during a download this runs tens of thousands - /// of times, and every snapshot deep-clones the whole inbox only for the - /// caller to discard all but the last. Callers snapshot once, after draining. + /// Deliberately does not snapshot this runs tens of thousands of times per download + /// and each snapshot deep-clones the inbox + /// Callers snapshot once after draining pub fn dispatch( &mut self, inbox: &mut Vec, @@ -428,9 +397,7 @@ impl NotificationState { ); self.push_ephemeral_toast(entry_id, MESSAGE_TOAST_TTL); } - // The Microsoft login renders its own progress in the sign-in - // modal, so it must not also become a toast. This is a front-end - // policy decision about a progress id the core merely names. + // The sign-in modal renders this progress itself it must not also become a toast Event::Progress(ProgressEvent::Update { id, .. }) if id == oneclient_auth::MICROSOFT_LOGIN_PROGRESS => {} Event::Progress(ProgressEvent::Update { @@ -447,8 +414,7 @@ impl NotificationState { Event::Progress(ProgressEvent::Grouped(event)) => { self.handle_grouped_progress(inbox, event); } - // Signals and game events drive the snapshot directly in the - // runtime loop; they never become inbox entries. + // Handled directly by the runtime loop they never become inbox entries Event::Signal(_) | Event::Game(_) => {} Event::Notification(Notification::Prompt(request)) => { let pending_prompt = Some(PendingPrompt { @@ -682,15 +648,12 @@ impl NotificationState { self.update_inbox_entry(inbox, entry_id, label, body, progress, !done); self.ensure_progress_toast(entry_id); - // The id->entry mapping is kept even once `done`, so a later - // `ProgressComplete` can convert this same card into its finished state (see - // `handle_progress_complete`). It is cleaned up when the entry is dismissed - // or the engine is reset. + // The id->entry mapping is kept even once `done` so a later `ProgressComplete` + // can convert this same card into its finished state } - /// Convert an in-flight progress card into a finished message in place. Used so an - /// operation and its "done" notice stay a single notification. Falls back to a fresh - /// message if the progress entry is already gone (e.g. dismissed mid-flight). + /// Converts the in-flight card in place so an operation and its "done" notice stay one + /// notification falls back to a fresh message if the entry was dismissed mid-flight fn handle_progress_complete( &mut self, inbox: &mut Vec, @@ -830,11 +793,9 @@ impl NotificationState { } } - /// Converts a still-open grouped-progress entry into its finished state, - /// reusing the same inbox id. `spec: None` forgets the entry entirely (used - /// when a sync completed with nothing to show). This lets a download progress - /// notification morph into a "view changes" notification without ever - /// becoming a second entry. + /// Reuses the same inbox id so a progress card morphs into its result instead of + /// becoming a second entry + /// `spec: None` forgets the entry entirely pub fn finish_grouped_as_actions( &mut self, inbox: &mut Vec, @@ -845,7 +806,6 @@ impl NotificationState { let entry_id = self.grouped_entries.remove(&session_id); let Some(spec) = spec else { - // Nothing changed: drop the progress entry if one was created. if let Some(entry_id) = entry_id { self.forget_entry(inbox, entry_id); } @@ -874,14 +834,11 @@ impl NotificationState { entry.actions = actions; entry.tasks = Vec::new(); entry.transfer = None; - // The progress toast (if any) is already armed for this entry; - // leaving it in `active_toasts` lets the loop give it a dismiss - // timer now that it is no longer loading. + // Keeping it in `active_toasts` lets the loop arm a dismiss timer + // now that the entry is no longer loading self.ensure_progress_toast(entry.id); } None => { - // Progress entry never materialised (e.g. no downloads happened); - // fall back to a fresh notification so the result still shows. self.push_custom( inbox, NotificationSpec { @@ -915,7 +872,7 @@ impl NotificationState { let completed: u64 = tasks.iter().map(|t| t.current.min(t.total)).sum(); let total: u64 = tasks.iter().map(|t| t.total).sum::().max(1); - // Below 1 B/s the estimate is noise, not information. + // Below 1 B/s the estimate is noise let transfer = group .meter .sample(completed, total) diff --git a/packages/oneclient_app/src/state.rs b/packages/oneclient_app/src/state.rs index fa4e9906..e5479294 100644 --- a/packages/oneclient_app/src/state.rs +++ b/packages/oneclient_app/src/state.rs @@ -1,13 +1,5 @@ -//! The app's own state, held in a freya-radio station. -//! -//! One struct republished over a `watch` channel woke every consumer on every -//! change (a toast timer tick re-rendering components that only read -//! `data_dir`) and deep-cloned the whole notification inbox on publish, tens of -//! thousands of times during a download. -//! -//! [`AppChannel`] gives per-concern subscription instead, and because the -//! station only requires `'static`, this owns [`NotificationState`] outright -//! rather than cloning a snapshot out of it on every event. +//! [`AppChannel`] gives per-concern subscription a single `watch` channel woke every +//! consumer and deep-cloned the inbox on every event use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -19,23 +11,15 @@ use oneclient_events::LaunchStage; use crate::notifications::{InboxEntry, NotificationState, PendingPrompt}; -/// What a component can subscribe to. Writing through a channel wakes only the -/// components that asked for it. +/// Writing through a channel wakes only the components that subscribed to it #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum AppChannel { - /// Startup progress and the resolved data directory. Launcher, - /// Launcher settings and their save state. Settings, - /// Inbox, toasts and the pending prompt. Notifications, - /// Per-cluster launch stage and live log lines. Game, - /// Whether the account switcher popover is open. AccountSwitcher, - /// Live progress of an in-flight Microsoft sign-in. MicrosoftLogin, - /// Packages currently being installed into a cluster. Installs, } @@ -45,17 +29,12 @@ impl RadioChannel for AppChannel {} pub struct AppState { pub launcher: LauncherInit, pub settings: SettingsState, - /// The notification engine itself, not a snapshot of it. - /// - /// Both the event pump and UI actions fold into this, so it has to live where - /// both can reach it, and nothing has to clone the inbox per event. + /// The engine itself not a snapshot both the pump and UI actions fold into it pub notifications: NotificationState, - /// Held beside the engine because it folds events into the inbox by `&mut` - /// reference; keeping them together lets the engine stay a plain - /// state machine with no channels of its own. + /// Held beside the engine which folds events into it by `&mut` so the engine + /// stays a plain state machine with no channels of its own pub inbox: Vec, pub prompt: Option, - /// Whether the notification centre panel is open. pub center_open: bool, pub game: GameState, pub account_switcher_open: bool, @@ -63,8 +42,7 @@ pub struct AppState { pub installs: InstallState, } -/// Package installs that are in flight, so the button that started one can stay -/// disabled until it lands — and come back if it failed. +/// In-flight installs so the button that started one stays disabled until it lands #[derive(Clone, Debug, Default, PartialEq)] pub struct InstallState { pending: HashSet<(i64, ProviderId, String)>, @@ -91,8 +69,7 @@ impl InstallState { pub struct LauncherInit { pub ready: bool, pub fetching: bool, - /// True while the background cluster-bundle download is running. Launch is - /// disabled until it clears. + /// Launch is disabled until this clears pub syncing_bundles: bool, pub error: Option, pub data_dir: String, @@ -115,9 +92,8 @@ pub struct SettingsState { pub error: Option, } -/// Live progress of an in-flight Microsoft sign-in, rendered inside the sign-in -/// modal rather than as a toast. UI state: the core reports this as ordinary -/// progress and has no opinion about where it is shown. +/// Rendered inside the sign-in modal rather than as a toast the core reports it +/// as ordinary progress and has no opinion about where it is shown #[derive(Clone, Debug, PartialEq, Eq)] pub struct LoginProgress { pub label: String, @@ -130,11 +106,8 @@ pub struct GameState { pub stages: HashMap, pub error: Option, pub logs: HashMap>>>, - /// Clusters whose launch was started from the UI but has not been answered - /// by core yet. - /// - /// Core takes a few hundred ms to report [`LaunchStage::Checking`], and - /// every click that lands in that window used to spawn its own game. + /// Launches started from the UI but not yet answered by core which takes a few + /// hundred ms every click in that window otherwise spawns its own game pending: HashSet, } @@ -144,8 +117,7 @@ impl GameState { self.stages.get(&cluster_id).copied() } - /// Claims the launch for this cluster, returning false if one is already in - /// flight — the re-entrancy guard behind the button's disabled state. + /// Returns false if a launch is already in flight the re-entrancy guard pub fn begin_launch(&mut self, cluster_id: i64) -> bool { if self.is_active(cluster_id) || self.is_launch_pending(cluster_id) { return false; @@ -223,7 +195,7 @@ mod tests { game.begin_launch(1); assert_eq!(launch_button_state(&game, 1, false), ("Launching", false)); - // Held past a failure, which parks the stage at `Exited`. + // Held past a failure which parks the stage at `Exited` game.stages.insert(1, LaunchStage::Exited); assert_eq!(launch_button_state(&game, 1, false), ("Launching", false)); diff --git a/packages/oneclient_app/src/theme/mod.rs b/packages/oneclient_app/src/theme/mod.rs index 7555ba93..11a09bd2 100644 --- a/packages/oneclient_app/src/theme/mod.rs +++ b/packages/oneclient_app/src/theme/mod.rs @@ -10,7 +10,6 @@ pub const MONO_FONT: &str = "JetBrains Mono"; pub fn load_fonts() -> Vec<(&'static str, Bytes)> { const FONTS: [(&str, &str); 11] = [ - // UI Font (DEFAULT_FONT, "fonts/Poppins/Poppins-Bold.ttf"), (DEFAULT_FONT, "fonts/Poppins/Poppins-BoldItalic.ttf"), (DEFAULT_FONT, "fonts/Poppins/Poppins-SemiBold.ttf"), @@ -21,7 +20,6 @@ pub fn load_fonts() -> Vec<(&'static str, Bytes)> { (DEFAULT_FONT, "fonts/Poppins/Poppins-Italic.ttf"), (DEFAULT_FONT, "fonts/Poppins/Poppins-Light.ttf"), (DEFAULT_FONT, "fonts/Poppins/Poppins-LightItalic.ttf"), - // Mono Font (MONO_FONT, "fonts/JetBrainsMono/JetBrainsMonoVariable.ttf"), ]; diff --git a/packages/oneclient_app/src/transfer.rs b/packages/oneclient_app/src/transfer.rs index 168a1405..85b52480 100644 --- a/packages/oneclient_app/src/transfer.rs +++ b/packages/oneclient_app/src/transfer.rs @@ -1,21 +1,10 @@ -//! Download speed and time-remaining estimation. -//! -//! One shared estimator, so the notification engine and the onboarding -//! installer show the same numbers arrived at the same way. - use std::time::Instant; -/// Weight given to the existing average when a new sample arrives. -/// -/// Downloads are bursty (a single asset finishing can spike the instantaneous -/// rate several-fold) so the average is kept sluggish on purpose. +/// Weight of the existing average per sample kept sluggish because downloads are bursty const SMOOTHING: f64 = 0.7; -/// An exponentially-weighted average of transfer rate, plus the ETA that falls -/// out of it. #[derive(Debug, Clone, Copy, Default)] pub struct TransferMeter { - /// Timestamp and byte count of the last sample. last_sample: Option<(Instant, u64)>, speed_bps: f64, } @@ -23,19 +12,14 @@ pub struct TransferMeter { #[derive(Debug, Clone, Copy, PartialEq)] pub struct TransferStats { pub speed_bps: f64, - /// `None` until there is enough signal to estimate, or when the total is - /// unknown. + /// `None` until there is enough signal to estimate or when the total is unknown pub eta_secs: Option, } impl TransferMeter { - /// Records progress and returns the current estimate. - /// - /// `completed` and `total` are cumulative byte counts. Samples closer - /// together than a few hundred milliseconds are ignored rather than folded - /// in: the elapsed divisor gets small enough to swamp the average with - /// noise, which is what the fixed-tick version was working around by - /// sampling on a timer instead. + /// `completed` and `total` are cumulative bytes + /// Samples under 250ms apart are not folded in + /// the tiny elapsed divisor would swamp the average with noise pub fn sample(&mut self, completed: u64, total: u64) -> Option { let now = Instant::now(); @@ -70,8 +54,7 @@ impl TransferMeter { let remaining = total.saturating_sub(completed); Some(TransferStats { speed_bps: self.speed_bps, - // No total means no estimate, which beats a countdown derived from a - // denominator that is still climbing. + // No total means no estimate the denominator may still be climbing eta_secs: (total > 0).then(|| (remaining as f64 / self.speed_bps) as u64), }) } @@ -111,8 +94,6 @@ mod tests { assert!(late < early, "eta should shrink: {early} -> {late}"); } - /// Rapid updates are the common case during an asset fan-out; they must not - /// divide by a near-zero elapsed and blow the average up. #[test] fn samples_taken_too_close_together_do_not_distort_the_average() { let mut meter = TransferMeter::default(); diff --git a/packages/oneclient_app/src/ui.rs b/packages/oneclient_app/src/ui.rs index e0ce5dec..0e075fe3 100644 --- a/packages/oneclient_app/src/ui.rs +++ b/packages/oneclient_app/src/ui.rs @@ -28,12 +28,8 @@ pub fn fmt_date(ts: chrono::DateTime) -> String { ts.format("%Y-%m-%d %H:%M").to_string() } -/// How long ago, for a timestamp shown next to something that happened. -/// -/// Nothing re-renders on a timer, so the shortest band is a whole minute: a -/// seconds counter would freeze at whatever it read when the row was drawn and -/// quietly lie from then on. The version this replaced had a seconds band and -/// no callers. +/// Shortest band is a minute nothing re-renders on a timer so a seconds counter +/// would freeze at whatever it read when the row was drawn pub fn relative_time(created_at: Instant) -> String { let secs = created_at.elapsed().as_secs(); match secs { @@ -44,10 +40,7 @@ pub fn relative_time(created_at: Instant) -> String { } } -/// A one-pixel rule in the component-border colour. -/// -/// Returns the `Rect` rather than an `Element` so a caller that wants it inset -/// or rounded can say so without copying the whole thing out again. +/// Returns the `Rect` not an `Element` so callers can inset or round it pub fn divider() -> Rect { rect() .width(Size::fill()) @@ -55,8 +48,6 @@ pub fn divider() -> Rect { .background(theme::colors::component_border()) } -/// A quiet line of text centred in a fixed block: "nothing here yet", or a -/// failure where a chart would otherwise be. pub fn centered_note(text: &str) -> Element { rect() .width(Size::fill()) @@ -71,10 +62,7 @@ pub fn centered_note(text: &str) -> Element { .into_element() } -/// How many columns no wider than `max_col` fit across `width`. -/// -/// One column until the grid has measured itself, so the first frame renders -/// something rather than dividing by an unknown. +/// Returns 1 until the grid has measured itself so the first frame still renders pub fn grid_columns_for_width(width: f32, max_col: f32, gap: f32) -> usize { if width <= 0. { return 1; @@ -82,13 +70,8 @@ pub fn grid_columns_for_width(width: f32, max_col: f32, gap: f32) -> usize { (((width + gap) / (max_col + gap)).ceil() as usize).max(1) } -/// Lays `items` out in `cols` equal columns, filling row by row. -/// -/// There is no wrapping flex row to lean on, so the split is done here from the -/// width the grid last measured. It reports its own size back through `width`, -/// which the caller feeds to [`grid_columns_for_width`] on the next render. -/// Short final rows are padded with empty flex cells so their tiles keep the -/// column width instead of stretching across the row. +/// Reports its measured size back through `width` for [`grid_columns_for_width`] +/// Short final rows are padded with empty flex cells so tiles keep the column width pub fn flow_grid(items: Vec, cols: usize, mut width: State, gap: f32) -> Element { let cols = cols.max(1); diff --git a/packages/oneclient_app/src/updater.rs b/packages/oneclient_app/src/updater.rs index 859b4ef4..036c2c54 100644 --- a/packages/oneclient_app/src/updater.rs +++ b/packages/oneclient_app/src/updater.rs @@ -34,7 +34,7 @@ pub fn spawn_update_check(auto_install: bool) { }); } -/// Debug-only: drives the full auto-update UX +/// Debug-only drives the full auto-update UX pub fn spawn_simulated_update() { tokio::spawn(async move { if let Err(err) = run_simulated_update().await { @@ -45,7 +45,7 @@ pub fn spawn_simulated_update() { async fn run_simulated_update() -> anyhow::Result<()> { const FAKE_VERSION: &str = "9999.9999.9999"; - const FAKE_TOTAL: u64 = 48 * 1024 * 1024; // 48 MiB + const FAKE_TOTAL: u64 = 48 * 1024 * 1024; let events = crate::launcher::state()?.services.events.clone(); @@ -77,7 +77,7 @@ async fn run_simulated_update() -> anyhow::Result<()> { async fn run_check(auto_install: bool) -> anyhow::Result<()> { let events = crate::launcher::state()?.services.events.clone(); - // `check_update` performs a blocking HTTP request, so offload it to a thread pool. + // `check_update` performs a blocking HTTP request so offload it to a thread pool let Some(update) = tokio::task::spawn_blocking(check_for_update).await?? else { tracing::info!("no update available"); return Ok(()); @@ -85,12 +85,8 @@ async fn run_check(auto_install: bool) -> anyhow::Result<()> { tracing::info!("update available: {}", update.version); - // The bundled self-updater can only replace an AppImage in place: on Linux - // cargo-packager-updater rejects every format except AppImage and locates the - // target via the `APPIMAGE` env var. deb/rpm installs run from a package-managed - // path (e.g. /usr/bin) we can't overwrite without root. Attempting an install - // would fail with a permission error, or clobber the system binary if elevated. - // Detect that case and point the user at the release page instead. + // cargo-packager-updater can only replace an AppImage in place deb/rpm installs live + // under a package-managed path that an install would fail on or clobber if !can_self_update() { tracing::info!("install is not self-updatable (non-AppImage Linux); notifying only"); events @@ -111,7 +107,6 @@ async fn run_check(auto_install: bool) -> anyhow::Result<()> { download_and_install(update, events).await } -/// Whether the running install can be updated in place by the bundled updater. fn can_self_update() -> bool { if cfg!(debug_assertions) { return false; @@ -175,8 +170,7 @@ async fn download_and_install(update: Update, events: EventBus) -> anyhow::Resul update.install(bytes)?; - // Convert the same download card into its finished state instead of emitting a - // second notification, so the user sees one notification through to completion. + // Converts the same download card into its finished state rather than adding a second events.finish_progress( progress_id, "Finished Downloading", diff --git a/packages/oneclient_app/src/utils.rs b/packages/oneclient_app/src/utils.rs index 35e26303..04295aa4 100644 --- a/packages/oneclient_app/src/utils.rs +++ b/packages/oneclient_app/src/utils.rs @@ -8,14 +8,12 @@ use oneclient_common::{ParsedMcVersion, VersionKey, format_mc_version, parse_mc_ pub type ClusterGroups = BTreeMap>; -/// One entry on the versions page. The modern scheme puts a full release in the -/// first two components (`26.1`, `26.2`), so each minor is its own line; legacy -/// `1.x` versions keep the whole major (`1.21`) as one line. A line that holds a -/// single concrete version is presented as that version (see [`line_title`]). +/// The modern scheme puts a full release in the first two components (`26.1`) so each +/// minor is its own line legacy `1.x` versions keep the whole major (`1.21`) as one line #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ReleaseLine { pub major: u32, - /// `Some` only for the modern scheme, where the minor is part of the line. + /// `Some` only for the modern scheme where the minor is part of the line pub minor: Option, } @@ -40,7 +38,7 @@ impl ReleaseLine { Self::from_version(&cluster.mc_version) } - /// The generic name of the line: `26.1`, `1.21`. + /// The generic name of the line `26.1` `1.21` pub fn pretty_name(&self) -> String { match self.minor { Some(minor) => format!("{}.{minor}", self.major), @@ -48,8 +46,6 @@ impl ReleaseLine { } } - /// Metadata/art lookup key for the line itself, when no single version - /// stands in for it. fn art_key(&self) -> Option { self.minor.map(|minor| (minor, None)) } @@ -59,7 +55,6 @@ fn cluster_key(cluster: &Cluster) -> Option { parse_mc_version(&cluster.mc_version).and_then(|parsed| parsed.key()) } -/// The one concrete version behind `clusters`, if they all share it. fn sole_version_key(clusters: &[Cluster]) -> Option { match version_keys(clusters).as_slice() { [only] => Some(*only), @@ -67,8 +62,7 @@ fn sole_version_key(clusters: &[Cluster]) -> Option { } } -/// A line with a single version is shown as that full version (`26.1.2`); -/// otherwise the generic line name (`1.21`), with the versions in a dropdown. +/// A line with a single version is shown as that full version (`26.1.2`) else `1.21` pub fn line_title(line: ReleaseLine, clusters: &[Cluster]) -> String { match sole_version_key(clusters) { Some(key) => version_label(line.major, key), @@ -76,7 +70,6 @@ pub fn line_title(line: ReleaseLine, clusters: &[Cluster]) -> String { } } -/// Metadata/art key matching [`line_title`]. pub fn line_art_key(line: ReleaseLine, clusters: &[Cluster]) -> Option { sole_version_key(clusters).or_else(|| line.art_key()) } @@ -151,7 +144,7 @@ pub fn resolve_cluster( .cloned() } -/// The line the page opens on: the active cluster's line, else the newest. +/// The line the page opens on the active cluster's line else the newest pub fn default_line(groups: &ClusterGroups, active: Option) -> Option { if let Some(cluster) = active && let Some(line) = ReleaseLine::for_cluster(&cluster) @@ -194,7 +187,6 @@ pub fn version_label(major: u32, (minor, patch): VersionKey) -> String { format_mc_version(major, minor, patch) } -/// Human-readable byte size (B / KB / MB). pub fn format_size(bytes: u64) -> String { const KB: f64 = 1024.; const MB: f64 = KB * 1024.; @@ -212,7 +204,7 @@ pub fn format_res((w, h): (u32, u32)) -> String { format!("{w}×{h}") } -/// `7384` -> `2h 3m`, `540` -> `9m`, `0` -> `0m`. +/// `7384` -> `2h 3m` `540` -> `9m` `0` -> `0m` pub fn format_duration_hm(secs: i64) -> String { if secs <= 0 { return "0m".to_string(); @@ -226,7 +218,7 @@ pub fn format_duration_hm(secs: i64) -> String { } } -/// `3723` -> `1h 2m`, `83` -> `1m 23s`, `45` -> `45s`. +/// `3723` -> `1h 2m` `83` -> `1m 23s` `45` -> `45s` pub fn format_duration_hms(secs: i64) -> String { if secs <= 0 { return "0s".to_string(); @@ -240,7 +232,7 @@ pub fn format_duration_hms(secs: i64) -> String { } } -/// Compact large counts: `1500` -> `1.5K`, `2_400_000` -> `2.4M`. +/// `1500` -> `1.5K` `2_400_000` -> `2.4M` pub fn abbreviate_number(n: u64) -> String { let f = n as f64; if f >= 1_000_000.0 { @@ -252,7 +244,6 @@ pub fn abbreviate_number(n: u64) -> String { } } -/// English plural suffix: `""` for 1, `"s"` otherwise. pub fn plural(n: i64) -> &'static str { if n == 1 { "" } else { "s" } } @@ -265,7 +256,7 @@ pub fn capitalize(s: &str) -> String { } } -/// 24-hour clock label: `9` -> `09:00`. +/// 24-hour clock label `9` -> `09:00` pub fn format_hour(hour: usize) -> String { format!("{hour:02}:00") } @@ -274,7 +265,7 @@ pub fn parse_day(date: &str) -> Option { NaiveDate::parse_from_str(date, "%Y-%m-%d").ok() } -/// `2026-07-05` -> `Jul 5`. +/// `2026-07-05` -> `Jul 5` pub fn format_day(date: NaiveDate) -> String { const MONTHS: [&str; 12] = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", @@ -289,11 +280,11 @@ pub fn sort_clusters_for_home(mut clusters: Vec) -> Vec { fn compare_last_played(a: &Cluster, b: &Cluster) -> Ordering { match (a.last_played, b.last_played) { - // Most recently played first. + // Most recently played first (Some(a), Some(b)) => b.cmp(&a), (Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, - // Never played: latest version first (major, then minor). + // Never played latest version first (major then minor) (None, None) => version_sort_key(b).cmp(&version_sort_key(a)), } } diff --git a/packages/oneclient_app/src/view/app/analytics/mod.rs b/packages/oneclient_app/src/view/app/analytics/mod.rs index 81922cbd..3d8d46a1 100644 --- a/packages/oneclient_app/src/view/app/analytics/mod.rs +++ b/packages/oneclient_app/src/view/app/analytics/mod.rs @@ -19,9 +19,7 @@ pub fn analytics_body(analytics: &Analytics) -> Element { analytics_body_inner(analytics, false) } -/// `force_all` keeps the sections that are normally hidden for lack of data -/// (session length, servers) on screen, so the zeroed empty state still shows -/// the whole layout. +/// `force_all` keeps the normally-hidden sections (session length servers) on screen fn analytics_body_inner(analytics: &Analytics, force_all: bool) -> Element { let stats = &analytics.playtime; @@ -49,10 +47,7 @@ fn analytics_body_inner(analytics: &Analytics, force_all: bool) -> Element { root.into_element() } -/// Renders the full analytics layout with zeroed data, dimmed behind a scrim -/// with `note` centered on top. Used by the cluster overview and global -/// statistics pages when nothing has been recorded yet, so the charts and stats -/// are always visible without inventing numbers. +/// Zeroed data behind a scrim with `note` on top so charts stay visible without inventing numbers pub fn analytics_placeholder(note: &str) -> Element { rect() .width(Size::fill()) @@ -88,9 +83,7 @@ pub fn analytics_placeholder(note: &str) -> Element { .into_element() } -/// Every field zeroed: real, honest "nothing recorded" data rather than -/// invented numbers. The daily series is a run of zero-second days ending today -/// so the timeline still renders its columns for each range selector. +/// A run of zero-second days ending today so the timeline still renders columns for each range fn empty_analytics() -> Analytics { const EMPTY_DAYS: i64 = 90; diff --git a/packages/oneclient_app/src/view/app/analytics/servers.rs b/packages/oneclient_app/src/view/app/analytics/servers.rs index efa1db10..61746052 100644 --- a/packages/oneclient_app/src/view/app/analytics/servers.rs +++ b/packages/oneclient_app/src/view/app/analytics/servers.rs @@ -12,7 +12,7 @@ use crate::utils::{format_duration_hm, plural}; use super::{card, card_header}; -/// Number of servers charted; the rest are grouped into an "Other" slice. +/// Number of servers charted the rest are grouped into an "Other" slice const TOP_SERVERS: usize = 10; pub(super) fn servers_section(servers: &[ServerStat]) -> Element { @@ -166,9 +166,7 @@ fn legend(values: &[i64], labels: &[String], active: Option) -> Element { list.into_element() } -/// A Component (not a plain builder) because it mounts a `ScrollArea`, whose -/// hooks run in the caller's render scope. Mounting it conditionally from -/// `ServersSection` would change that scope's hook count and panic Freya. +/// A Component not a builder mounting its `ScrollArea` conditionally would change the caller's hook count and panic Freya #[derive(PartialEq)] struct ServerDetailsPopup { servers: Vec, @@ -250,8 +248,7 @@ impl Component for ServerDetailsPopup { } } -/// IP addresses stay masked in the always-visible chart legend; the details -/// popup reveals them on hover only. +/// IPs stay masked in the always-visible legend the details popup reveals them on hover fn server_label(s: &ServerStat) -> String { if s.is_ip { return "Direct IP".to_string(); diff --git a/packages/oneclient_app/src/view/app/browser/index/cards.rs b/packages/oneclient_app/src/view/app/browser/index/cards.rs index 6bbdea32..9bddf9a6 100644 --- a/packages/oneclient_app/src/view/app/browser/index/cards.rs +++ b/packages/oneclient_app/src/view/app/browser/index/cards.rs @@ -17,7 +17,7 @@ use crate::ui::border_all_color; type InstalledMap = HashMap<(ProviderId, String), Installed>; -/// Diameter of the round install button. +/// Diameter of the round install button const INSTALL_BUTTON: f32 = 30.; fn installed_for(installed: &InstalledMap, item: &ProjectSummary) -> Option { @@ -137,9 +137,7 @@ impl Component for PackageCard { .corner_radius(CornerRadius::new(10., 10., 0., 0.)) .overflow(Overflow::Clip) .child(PackageBanner::new(icon_url, BANNER_H)) - // Sat in the banner's top-left corner rather than down in - // the footer: it is the first thing worth knowing about a - // result, and the opposite corner is the install button's. + // Top-left of the banner the first thing worth knowing about a result and the opposite corner is the install button's .maybe_child(self.installed.map(|installed| { rect() .position(Position::new_absolute().top(8.).left(8.)) @@ -298,9 +296,7 @@ impl Component for ListRow { .color(colors::fg_secondary()), ) .child(Icon::new(item.provider).size(12.)) - // Beside the attribution rather than out at the far - // right, where it drifted away from the package it - // describes and crowded the install button. + // Beside the attribution rather than far right where it drifted from the package and crowded the install button .maybe_child( self.installed.map(|installed| { installed_badge(installed, 10.).into_element() @@ -317,9 +313,7 @@ impl Component for ListRow { ), ) .child(downloads_row(item.downloads)) - // Kept in place once the package is in the cluster, rather than - // dropped: a row that loses its button leaves the ones around it - // looking misaligned. Disabled says the same thing and holds still. + // Kept once installed rather than dropped a row that loses its button leaves its neighbours misaligned .child(InstallButton::new( &self.item, self.cluster_id, @@ -329,17 +323,14 @@ impl Component for ListRow { } } -/// Installs a listing entry's latest compatible version without making the user -/// open its page first. Same version pick and same dispatch as the install -/// button on the package page. +/// Same version pick and dispatch as the install button on the package page #[derive(PartialEq)] struct InstallButton { provider: ProviderId, project_id: String, cluster_id: i64, content_type: ContentType, - /// The cluster already has this one, so there is nothing left to press. - /// Callers that hide the button entirely in that case pass `false`. + /// Cluster already has this one callers that hide the button entirely pass `false` installed: bool, } @@ -365,8 +356,7 @@ impl Component for InstallButton { let compat = *use_browser_compat().read(); let cluster = use_cluster(cluster_id); - // The same narrowing the package page does before calling the first - // entry of the list "latest", so both agree on what gets installed. + // The same narrowing the package page does so both agree on what "latest" installs let (game_version, loader) = match (compat, &cluster) { (true, Some(c)) => ( Some(c.mc_version.clone()), @@ -387,14 +377,11 @@ impl Component for InstallButton { )); let latest = versions.first().map(|v| v.version_id.clone()); - // Nothing to start twice while the install that was just kicked off is - // still running, and nothing to start before the version list arrives. + // Nothing to start twice while an install is running or before versions arrive let installing = use_installs_snapshot().is_installing(cluster_id, provider, &project_id); rect() - // The card behind this is one big press target. A press on the - // button is not a press on the card, so it stops here rather than - // opening the package page on top of the install. + // The card behind is one big press target stop here so the package page doesn't open on top of the install .on_all_press(|e: Event| e.stop_propagation()) .child( Button::new() diff --git a/packages/oneclient_app/src/view/app/browser/index/mod.rs b/packages/oneclient_app/src/view/app/browser/index/mod.rs index 9840e5f5..a5e38650 100644 --- a/packages/oneclient_app/src/view/app/browser/index/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/index/mod.rs @@ -68,8 +68,7 @@ fn encode_package_id(provider: ProviderId, id: &str) -> String { pub struct Browser { pub cluster_id: i64, pub package_type: String, - /// Entered from the navbar rather than from a cluster's content page, so - /// the cluster it browses for is picked here instead of being decided. + /// Entered from the navbar so the cluster to install into is picked here pub pick_cluster: bool, } @@ -104,7 +103,7 @@ impl Component for Browser { }); } - // Debounce the search query so typing doesn't fire a request per keystroke. + // Debounce the search query so typing doesn't fire a request per keystroke let debounced_query = use_debounced( query.read().clone(), Duration::from_millis(SEARCH_DEBOUNCE_MS), @@ -128,8 +127,7 @@ impl Component for Browser { }; let mut page_state = page; - // Normalised the same way the search key is, so a query that only picked - // up a space is not treated as a new search that resets the page. + // Normalised like the search key so a query differing only by a space does not reset the page let signature = format!( "{provider_id:?}|{}|{compat}|{}", normalize_query(&debounced_query.read()).to_lowercase(), @@ -166,9 +164,7 @@ impl Component for Browser { let pending = search_pending(&search); let current_page = *page.read(); - // A page switch re-runs the search, which reports no total until it - // settles. Holding the last count keeps the pager in place — disabled - // while the page loads — instead of dropping out and back in. + // A page switch reports no total until it settles holding the last count keeps the pager in place let live_pages = (total > 0).then(|| total.div_ceil(BROWSE_PAGE_SIZE).max(1)); let mut settled_pages = use_state(|| live_pages); if !pending { @@ -298,7 +294,7 @@ fn page_title( ) .maybe_child(match picker { Some(picker) => Some(picker.into_element()), - // Without the picker the cluster is fixed, so it's just a subtitle. + // Without the picker the cluster is fixed so it's just a subtitle None => cluster_name.map(|name| { label() .text(format!("for {name}")) @@ -309,8 +305,7 @@ fn page_title( }) } -/// The version's display name from the manifest ("Tricky Trials"), falling back -/// to the bare version while the manifest hasn't arrived or doesn't cover it. +/// Falls back to the bare version while the manifest hasn't arrived or doesn't cover it fn version_name(metadata: &[VersionMetadata], cluster: &Cluster) -> String { parse_mc_version(&cluster.mc_version) .and_then(|parsed| { @@ -325,8 +320,7 @@ fn version_name(metadata: &[VersionMetadata], cluster: &Cluster) -> String { .unwrap_or_else(|| cluster.mc_version.clone()) } -/// Switches which cluster the browser installs into. The choice lives in the -/// route, so going back and forward keeps the cluster that was being browsed. +/// The choice lives in the route so back and forward keep the cluster being browsed #[derive(PartialEq)] struct ClusterPicker { cluster_id: i64, diff --git a/packages/oneclient_app/src/view/app/browser/mod.rs b/packages/oneclient_app/src/view/app/browser/mod.rs index 6173d46f..6264cda4 100644 --- a/packages/oneclient_app/src/view/app/browser/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/mod.rs @@ -17,12 +17,9 @@ use crate::ui::border_all_color; const BANNER_BG: Color = Color::from_rgb(21, 28, 34); -/// How a browsed package is already present in the cluster. #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) enum InstallSource { - /// Installed on its own. Manual, - /// Comes from one of the cluster's bundles. Bundled, } @@ -34,8 +31,7 @@ impl InstallSource { } } - /// Bundled reads blue rather than green: the package is there, but the - /// cluster's bundle put it there, not the user. + /// Bundled reads blue rather than green the cluster's bundle put it there not the user fn color(self) -> Color { match self { Self::Manual => colors::success(), @@ -44,32 +40,22 @@ impl InstallSource { } } -/// One version of a browsed package that the cluster has. #[derive(Clone, PartialEq)] pub(crate) struct InstalledVersion { pub version_id: String, - /// The artifact to remove. `None` when a bundle names this version but - /// nothing is linked yet, which is a state the user cannot act on. + /// The artifact to remove + /// `None` when a bundle names this version but nothing is linked pub hash: Option, - /// Whether the game loads this one. Only meaningful alongside a `hash`; - /// a bundle pin with nothing linked is neither on nor off, and the badge - /// that reads this never renders for one. + /// Only meaningful alongside a `hash` a bundle pin with nothing linked is neither on nor off pub enabled: bool, pub source: InstallSource, } -/// A browsed package the cluster already has. -/// -/// More than one version is not a state the launcher aims for, but nothing -/// stops a cluster reaching it, so this holds every one it finds rather than an -/// arbitrary winner — the version list marks each of them. +/// Holds every version found rather than an arbitrary winner a cluster can end up with more than one #[derive(Clone, PartialEq)] pub(crate) struct Installed { - /// How the package as a whole got here, for the badge on cards and the - /// sidebar. A bundle claiming the project wins over a hand-installed copy. + /// A bundle claiming the project wins over a hand-installed copy pub source: InstallSource, - /// Every version in the cluster the provider recorded one for, in the order - /// the cluster reports them. pub versions: Vec, } @@ -82,19 +68,13 @@ impl Installed { self.versions.iter().find(|v| v.version_id == version_id) } - /// Whether the cluster holds more than one copy of this package. - /// - /// Counts linked artifacts only: a bundle pin the cluster never downloaded - /// is listed as a version but is not a second copy of anything. + /// Counts linked artifacts only a bundle pin the cluster never downloaded is not a second copy pub fn is_duplicated(&self) -> bool { self.versions.iter().filter(|v| v.hash.is_some()).count() > 1 } } -/// Indexes what the cluster already has by the project it came from, so a -/// search result can tell at a glance whether it's in there and how it got -/// there. Local files have no project to match a search result against and -/// are left out, as are a bundle's external (non-provider) files. +/// Local files and a bundle's external files are left out they have no project id to match against pub(crate) fn installed_map( content: Vec, bundles: &[BundleWithUpdateStatus], @@ -106,9 +86,7 @@ pub(crate) fn installed_map( continue; }; - // The entry is created even for an artifact with no recorded version: - // the package is in the cluster either way, and the card badge only - // asks that much. It just cannot be tied to a row in the version list. + // Created even without a recorded version the package is in the cluster it just cannot be tied to a version row let installed = map.entry((provider, project_id)).or_insert(Installed { source: InstallSource::Manual, versions: Vec::new(), @@ -124,11 +102,8 @@ pub(crate) fn installed_map( } } - // Bundle membership wins: a bundle's files land in the cluster as ordinary - // content, so they'd otherwise read as hand-installed. The version on disk - // is the truth when there is one; the manifest's pin is the fallback, and - // is deliberately not added alongside a linked version — a pin the cluster - // has not downloaded is not something the user has installed. + // Bundle membership wins a bundle's files would otherwise read as hand-installed + // The manifest pin is only a fallback for a missing linked version for bundle in bundles { for (file, _status) in &bundle.files { if let BundleFileKind::Managed { @@ -175,9 +150,7 @@ pub(crate) fn installed_badge(installed: InstallSource, font_size: f32) -> impl badge(installed, font_size, installed.color().with_a(38), None) } -/// The same badge sat on top of a card's artwork. The faint tint the in-card -/// badge uses has nothing to read against there, so this one brings its own -/// backdrop and outlines itself to stay legible over whatever the image is. +/// Brings its own backdrop and outline so it stays legible over card artwork pub(crate) fn installed_badge_overlay(installed: InstallSource) -> impl IntoElement { badge( installed, @@ -187,7 +160,7 @@ pub(crate) fn installed_badge_overlay(installed: InstallSource) -> impl IntoElem ) } -/// Which of several installed versions the game actually loads. +/// Which of several installed versions the game actually loads pub(crate) fn activity_badge(active: bool) -> impl IntoElement { let (text, color) = if active { ("Active", colors::brand()) @@ -195,8 +168,7 @@ pub(crate) fn activity_badge(active: bool) -> impl IntoElement { ("Inactive", colors::fg_secondary()) }; - // No icon, unlike the source badges it sits beside: three pills in a row - // all wearing a tick reads as decoration rather than as three facts. + // No icon three pills in a row all wearing a tick reads as decoration rect() .horizontal() .cross_align(Alignment::Center) diff --git a/packages/oneclient_app/src/view/app/browser/package/gallery.rs b/packages/oneclient_app/src/view/app/browser/package/gallery.rs index a0dc3b34..1e7d3562 100644 --- a/packages/oneclient_app/src/view/app/browser/package/gallery.rs +++ b/packages/oneclient_app/src/view/app/browser/package/gallery.rs @@ -1,10 +1,4 @@ -//! The package page's gallery tab. -//! -//! Laid out like the cluster's screenshots: a grid of tiles that open into a -//! full-window viewer with arrows between them. The difference is where the -//! pixels come from — these are remote images behind the shared image cache -//! rather than files on disk, so a tile and the opened view ask the cache for -//! the same url at two different sizes instead of sharing one decode. +//! Remote images behind the shared image cache a tile and the opened view ask for the same url at two different sizes use super::*; @@ -15,14 +9,12 @@ use crate::hooks::{loaded_image, query_is_busy, use_cached_image}; use crate::theme::colors; use crate::ui::{border_all_color, flow_grid, grid_columns_for_width}; -/// How wide a tile is allowed to get before the grid takes another column. +/// How wide a tile is allowed to get before the grid takes another column const MAX_COL_W: f32 = 400.; const GRID_GAP: f32 = 16.; const TILE_PREVIEW_H: f32 = 168.; -/// Downscale ceilings handed to the image cache. A tile never shows more than -/// its own width, and asking for the full thing there was what made the grid -/// slow to fill; the opened view is the one that needs the detail. +/// Downscale ceilings for the image cache full-size tiles made the grid slow to fill const TILE_EDGE: u32 = 640; const FULL_EDGE: u32 = 2048; @@ -159,8 +151,6 @@ impl Component for GalleryTile { } } -/// The opened image, filling the window with the rest of the gallery a key -/// press away. #[derive(PartialEq)] struct GalleryViewer { images: Vec, @@ -204,10 +194,7 @@ impl Component for GalleryViewer { .aspect_ratio(AspectRatio::Min) .into_element() })) - // The tile's smaller copy is already cached, but this one is a - // fresh fetch at full size, so the first moment of the opened view - // says so rather than showing an empty frame. Only while it is - // still working: a fetch that failed is not loading. + // A full-size fetch separate from the tile's cached copy a failed fetch is not loading .maybe_child(query_is_busy(&query).then(|| { label() .text("Loading image...") diff --git a/packages/oneclient_app/src/view/app/browser/package/panels.rs b/packages/oneclient_app/src/view/app/browser/package/panels.rs index 7172be03..a7f3cc61 100644 --- a/packages/oneclient_app/src/view/app/browser/package/panels.rs +++ b/packages/oneclient_app/src/view/app/browser/package/panels.rs @@ -200,9 +200,7 @@ fn version_row( cluster_id: i64, dispatch: Actions, installed: Option, - // Whether the cluster has more than one version of this package, which is - // the only situation where saying which one is live tells the user - // anything. + // Saying which version is live only tells the user anything when there are several duplicated: bool, installing: bool, ) -> impl IntoElement { @@ -275,11 +273,7 @@ fn version_row( )) } -/// Install, or remove the version that is already there. -/// -/// A bundle that names this version without the cluster having linked anything -/// yet leaves nothing to press: there is no artifact to remove, and installing -/// it by hand would make a copy the bundle does not own. +/// A bundle pin with nothing linked leaves nothing to press no artifact to remove and installing by hand would duplicate it #[allow(clippy::too_many_arguments)] fn version_button( installed: Option, diff --git a/packages/oneclient_app/src/view/app/browser/package/sidebar.rs b/packages/oneclient_app/src/view/app/browser/package/sidebar.rs index d3ac2ec1..4aa6a745 100644 --- a/packages/oneclient_app/src/view/app/browser/package/sidebar.rs +++ b/packages/oneclient_app/src/view/app/browser/package/sidebar.rs @@ -27,8 +27,7 @@ pub(super) fn sidebar( }; let project_id = project.id.clone(); - // Nothing to do when the cluster already has exactly this version, and - // nothing to do twice while the install that was just started is running. + // Nothing to do when this version is already there or while an install is running let have_latest = match (&installed, &latest_version) { (Some(installed), Some(latest)) => installed.is_version(latest), _ => false, @@ -48,10 +47,6 @@ pub(super) fn sidebar( .overflow(Overflow::Clip) .background(PANEL_BG) .border(border_all_color(1., colors::component_border())) - // The badge rides on the artwork here for the same reason it - // does on a listing card: it belongs to the package, and it - // used to sit in a strip of its own between the card and the - // install button. .child( rect() .width(Size::fill()) @@ -151,8 +146,7 @@ pub(super) fn sidebar( .into_element() } -/// The package's own page on the provider's site. Modrinth doesn't hand one -/// out, so it's rebuilt from the slug; CurseForge already lists it as "Website". +/// Modrinth doesn't hand a url out so it's rebuilt from the slug CurseForge lists it as "Website" fn provider_project_url(project: &ProjectDetail) -> Option { match project.provider { ProviderId::Modrinth => Some(format!( @@ -170,8 +164,7 @@ fn provider_project_url(project: &ProjectDetail) -> Option { } } -/// "Mod on Modrinth", linking out to the package's page there when the provider -/// has one. Plain text otherwise, so a local package doesn't look clickable. +/// Plain text when the provider has no page so a local package doesn't look clickable #[derive(PartialEq)] struct ProviderTag { text: String, @@ -225,8 +218,7 @@ fn card(title: &str, rows: Vec) -> impl IntoElement { card_spaced(title, rows, 10.) } -/// `card` with the gap between rows chosen by the caller: a list of one-line -/// links reads better tight than the multi-line author and detail rows do. +/// `card` with a caller-chosen row gap one-line link lists read better tight fn card_spaced(title: &str, rows: Vec, spacing: f32) -> impl IntoElement { rect() .vertical() diff --git a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs index 98608442..5749b611 100644 --- a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs +++ b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs @@ -259,9 +259,7 @@ impl Component for VerifyFilesRow { mutation.mutate(ClusterAction::VerifyFiles { cluster_id }); }; - // The pass reads every installed file, so it is worth saying that it - // takes a while — otherwise a button that sits there for a minute reads - // as broken rather than busy. + // The pass reads every installed file so say it takes a while otherwise the button reads as broken let button = Button::new() .small() .secondary() diff --git a/packages/oneclient_app/src/view/app/cluster/logs.rs b/packages/oneclient_app/src/view/app/cluster/logs.rs index 1f23ef88..9e4fb1cf 100644 --- a/packages/oneclient_app/src/view/app/cluster/logs.rs +++ b/packages/oneclient_app/src/view/app/cluster/logs.rs @@ -65,10 +65,7 @@ enum Confirm { Upload, } -/// Which half of the cluster's files the picker is listing. -/// -/// Crash reports and ordinary logs answer different questions, and a crash -/// report buried among thirty rotated `.log.gz` files is one nobody finds. +/// A crash report buried among thirty rotated `.log.gz` files is one nobody finds #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Category { Logs, @@ -131,9 +128,7 @@ impl Component for ClusterLogs { let mut selected = use_state(PathBuf::new); let mut handled_upload = use_state(|| None::); - // Reads `category` as well as the query, so switching the picker to - // crash reports re-picks a file instead of leaving the viewer on a - // selection that is no longer in the list. + // Reads `category` too so switching the picker re-picks a file instead of keeping a stale selection use_side_effect(move || { let files = visible_files(&logs_query, *category.read()); @@ -248,7 +243,6 @@ impl Component for ClusterLogs { } } -/// The files the picker should be showing right now. fn visible_files(query: &UseQuery, category: Category) -> Vec { try_cluster_logs(query) .unwrap_or_default() @@ -398,9 +392,7 @@ fn viewer_header( struct LogPicker { label: String, files: Vec, - /// Owned by the page rather than by this component: the page filters - /// `files` by it and re-picks a selection when it changes, so both have to - /// be looking at the same value. + /// Owned by the page it filters `files` by this so both must see the same value category: State, on_select: EventHandler, } @@ -417,9 +409,7 @@ impl Component for LogPicker { let label_text = self.label.clone(); let files = self.files.clone(); let on_select = self.on_select.clone(); - // Stays usable with an empty list: the category selector lives inside - // the panel, so a cluster with no crash reports still needs a way back - // to its logs. + // Stays usable with an empty list the category selector lives inside the panel let is_open = open(); let query = SearchQuery::new(&filter.read()); @@ -507,8 +497,7 @@ impl Component for LogPicker { rect() .margin(Gaps::new(4., 0., 0., 0.)) .width(Size::px(320.)) - // Room for the category selector above the search - // box, on top of the list's own maximum. + // Room for the category selector above the search box .max_height(Size::px(400.)) .layer(Layer::OverlayLevel(12)) .vertical() diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs index 9fc2fb04..22587ce6 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs @@ -71,14 +71,8 @@ pub fn use_content_meta( out } -/// Builds the rows for one content type. -/// -/// `stale` is the set of artifact hashes the last browser update check found a -/// newer version for. Bundle-provided rows are built before it is consulted and -/// never carry the flag, so the two update flows cannot both claim a package. -/// -/// Rows for a bundle's hidden files are included and flagged; whether they -/// reach the screen is [`HiddenFilter`]'s call. +/// `stale` is the artifact hashes with a newer version bundle rows never carry it so the two update flows cannot both claim a package +/// Hidden bundle files get flagged rows [`HiddenFilter`] decides whether they show pub fn bundle_packages( content: Vec, bundles: &[BundleWithUpdateStatus], @@ -96,9 +90,7 @@ pub fn bundle_packages( by_hash.insert(info.hash.as_str(), info); } - // A package another bundle ships as a normal, user-facing file. Hidden is a - // per-bundle property, so one bundle carrying a mod as a private dependency - // must not suppress the bundle that offers it openly. + // Hidden is per-bundle so one bundle carrying a mod as a private dependency must not suppress a bundle that offers it openly let mut shown_elsewhere: HashSet = HashSet::new(); for bundle in bundles { for (file, _status) in &bundle.files { @@ -119,8 +111,7 @@ pub fn bundle_packages( continue; } let pid = file.kind.package_id(); - // Let the bundle that lists it openly own the row, whatever the - // iteration order happens to be. + // Let the bundle that lists it openly own the row whatever the iteration order if file.hidden && shown_elsewhere.contains(&pid) { continue; } @@ -162,10 +153,7 @@ pub fn bundle_packages( meta, file.display_name(), false, - // Emitted as an ordinary row carrying the flag rather than - // dropped here: the row is what `HiddenFilter` filters on, and - // the id counting as seen keeps the loose-content pass below - // from resurrecting the installed artifact as a local file. + // Flagged rather than dropped `HiddenFilter` filters on the row and the seen id stops the loose-content pass resurrecting it as a local file file.hidden, )); } @@ -278,27 +266,20 @@ impl Tab { pub(super) fn matches(&self, p: &PackageEntry) -> bool { match self { - // Every package assigned to the cluster, regardless of origin. Tab::All => true, Tab::Category(c) => p.categories.iter().any(|pc| pc == c), - // Browser = remote provider content that is NOT provided by a bundle. Tab::Browser => p.is_remote() && !p.in_bundle(), Tab::Local => !p.is_remote(), } } } -/// A package is searched by what the user sees (its display name) and by what -/// is on disk (its file name), whichever matches better. fn query_score(p: &PackageEntry, query: &SearchQuery) -> Option { query.best_score([p.name.as_str(), p.file_name.as_str()]) } -/// Rows to show for the current toolbar state. The search is scoped to the -/// active tab: a package has to belong to the tab *and* match the query, so -/// typing in a category never pulls in packages from another one. `Tab::All` -/// admits everything, which keeps a search from there a search over the whole -/// cluster. +/// Search is scoped to the active tab a package must match the tab *and* the query +/// `Tab::All` searches the whole cluster fn visible_packages( items: &[PackageEntry], tab: Option<&Tab>, @@ -314,9 +295,7 @@ fn visible_packages( .collect() } -/// Reorders search results so the closest matches come first. Stable, so the -/// sort the user picked in the toolbar survives as the tie-break between rows -/// that matched equally well. +/// Stable so the toolbar's sort survives as the tie-break between equally good matches fn rank_by_query(rows: &mut [PackageEntry], query: &SearchQuery) { rows.sort_by_key(|p| std::cmp::Reverse(query_score(p, query))); } @@ -332,8 +311,7 @@ pub fn bundle_categories(bundles: &[BundleWithUpdateStatus]) -> Vec { cats } -/// `hidden` is applied here too, so a category whose only members are hidden -/// bundle dependencies does not offer a tab that leads to an empty list. +/// `hidden` applies here too so a category of only hidden dependencies offers no tab fn build_tabs(categories: &[String], items: &[PackageEntry], hidden: HiddenFilter) -> Vec { let mut cats: Vec = categories.to_vec(); for item in items.iter().filter(|p| hidden.keep(p)) { @@ -344,7 +322,7 @@ fn build_tabs(categories: &[String], items: &[PackageEntry], hidden: HiddenFilte } } - // Category tabs are hidden when empty; All + Browser + Local are always shown. + // Category tabs are hidden when empty All + Browser + Local are always shown let mut tabs: Vec = vec![Tab::All]; tabs.extend( cats.into_iter() @@ -398,9 +376,7 @@ impl Component for PackageManager { let cluster_id = self.cluster_id; let content_type = self.content_type; - // Minecraft reads its content once at startup, so a toggle made now is - // recorded but cannot reach the session already playing. Say so rather - // than letting the switch look like it did nothing. + // Minecraft reads its content once at startup so a toggle now cannot reach the running session let session_live = use_game_snapshot().is_active(cluster_id); let active = use_state(|| 0usize); @@ -427,16 +403,12 @@ impl Component for PackageManager { let mut filtered = visible_packages(&items, tab_filter, &query, show, hidden); sort_mode.sort(&mut filtered); - // Relevance has to win while searching: with fuzzy matching, a name that - // only matched after forgiving a typo would otherwise sort above the one - // the user actually typed. + // Relevance has to win while searching a fuzzy typo match would otherwise outrank the exact one if !query.is_empty() { rank_by_query(&mut filtered, &query); } - // Coming up empty during a search is about the query, not about the tab - // having nothing in it, so it gets its own empty state naming the tab - // that was searched. + // Coming up empty during a search is about the query not the tab so it gets its own empty state let content_kind = if filtered.is_empty() && !query.is_empty() { ContentKind::NoMatches { scope: match tab_filter { diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs index e8f220b1..056c9fe2 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs @@ -101,8 +101,7 @@ impl EnabledFilter { } } -/// Whether a bundle's hidden files get a row. They are dependencies the bundle -/// manages on the user's behalf, so the list leaves them out until asked. +/// Hidden files are dependencies the bundle manages so the list leaves them out until asked #[derive(Clone, Copy, PartialEq)] pub(super) enum HiddenFilter { Hide, @@ -214,9 +213,7 @@ pub(super) fn toolbar_bar( .into_element() } -/// Shown while this cluster is being played. Enabling or disabling still takes -/// effect — it is stored, and the folder is brought in line at the next launch -/// — but the session already running cannot pick it up. +/// Enabling still stores and applies at the next launch but the running session cannot pick it up pub(super) fn running_notice(noun_plural: &'static str) -> Element { rect() .horizontal() @@ -263,8 +260,7 @@ impl Component for FilterButton { let enabled_filter = self.enabled_filter; let hidden_filter = self.hidden_filter; - // Hiding hidden packages is the default, so it does not count as the - // filters being touched. + // Hiding hidden packages is the default so it does not count as the filters being touched let is_open = open(); let active = current_sort != SortMode::NameAsc || *enabled_filter.read() != EnabledFilter::All @@ -524,7 +520,6 @@ fn add_from_file_button( .text("Add from file") } -/// Opens the content browser scoped to this cluster and package type. fn open_browser(cluster_id: i64, package_type: &'static str) { let _ = RouterContext::get().push(Route::Browser { cluster_id, @@ -546,8 +541,7 @@ pub(super) enum ContentKind { Browser, Local, Other, - /// A search returned nothing. `scope` names the tab that was searched when - /// it is narrower than "All". + /// `scope` names the tab that was searched when it is narrower than "All" NoMatches { scope: Option, }, @@ -638,8 +632,7 @@ impl Component for ContentBox { let header = (count > 0) .then(|| match kind { - // The toolbar above already carries a permanent "Add Content" - // button, so this list needs no browse action of its own. + // The toolbar already carries a permanent "Add Content" button ContentKind::Browser => None, ContentKind::Local => Some( action_header(add_from_file_button( @@ -656,8 +649,7 @@ impl Component for ContentBox { let mut bottom_corners = CornerRadius::new_all(0.); bottom_corners.fill_bottom(12.); - // File drops are handled window-wide by the app shell, which prompts for a - // destination, so this box does not claim them. + // File drops are handled window-wide by the app shell so this box does not claim them rect() .vertical() .width(Size::fill()) @@ -772,8 +764,7 @@ fn empty_state(noun_plural: &'static str) -> impl IntoElement { .into_element() } -/// The search only ever looks inside the active tab, so say which one came up -/// empty and point at the tab that does cover everything. +/// The search only looks inside the active tab so name it and point at the tab that covers everything fn search_empty(noun_plural: &'static str, scope: Option<&str>) -> impl IntoElement { let (title, hint) = match scope { Some(tab) => ( diff --git a/packages/oneclient_app/src/view/app/clusters/page.rs b/packages/oneclient_app/src/view/app/clusters/page.rs index b8880b98..babc9767 100644 --- a/packages/oneclient_app/src/view/app/clusters/page.rs +++ b/packages/oneclient_app/src/view/app/clusters/page.rs @@ -38,7 +38,7 @@ impl Component for Clusters { let clusters = settled_or_loading(&clusters_query).unwrap_or_default(); let groups = group_clusters_by_release(&clusters); - // Newest version first. + // Newest version first let lines: Vec = groups.keys().rev().copied().collect(); if lines.is_empty() { @@ -309,8 +309,7 @@ fn tags_row(tags: &[String]) -> Option { ) } -/// Only shown when the line holds more than one version; a single-version line -/// already names it on the card and in the heading. +/// A single-version line already names it on the card and in the heading fn version_rows( line: ReleaseLine, keys: &[VersionKey], diff --git a/packages/oneclient_app/src/view/app/debug.rs b/packages/oneclient_app/src/view/app/debug.rs index 9158bd55..030d1063 100644 --- a/packages/oneclient_app/src/view/app/debug.rs +++ b/packages/oneclient_app/src/view/app/debug.rs @@ -288,9 +288,7 @@ impl Component for ClusterUpdateSimulator { type ClusterUpdatePreset = fn() -> Vec; -/// Each preset pins down one variation the changes modal has to handle: the -/// singular/plural copy, an empty category tab, the single- vs multi-cluster -/// footer, per-tab cluster grouping, and header overflow. +/// Each preset pins one variation plural copy empty category tab single- vs multi-cluster footer grouping header overflow const CLUSTER_UPDATE_PRESETS: [(&str, IconType, ClusterUpdatePreset); 6] = [ ( "1 cluster · all categories", @@ -407,8 +405,7 @@ fn cluster_update_items(names: &[impl AsRef]) -> Vec { .collect() } -/// Mirrors the copy the bridge builds for real syncs so the simulated -/// notification is indistinguishable from the real one. +/// Mirrors the copy the bridge builds so the simulated notification matches the real one fn send_cluster_update(dispatch: &crate::Actions, summaries: Vec) { if summaries.is_empty() { return; @@ -480,9 +477,7 @@ impl Component for LauncherUpdateSimulator { } } -/// Pins `oneclient_net::status` so the connectivity banner can be looked at -/// without unplugging anything. Each preset is one banner the bar knows how to -/// draw; the bar itself picks the first active issue. +/// Pins `oneclient_net::status` so the connectivity banner can be seen without unplugging anything #[derive(PartialEq)] struct StatusBarSimulator; @@ -594,9 +589,7 @@ struct AuthGuidancePreview; impl Component for AuthGuidancePreview { fn render(&self) -> impl IntoElement { let samples = preview_samples(); - // Index 0 is the no-error state; the rest line up with `samples`, so a - // dropdown holds all ~16 cases in one row instead of six rows of - // full-width buttons. + // Index 0 is the no-error state the rest line up with `samples` let mut choice = use_state(|| 0usize); let mut open = use_state(|| false); @@ -661,14 +654,7 @@ impl Component for AuthGuidancePreview { } } -/// Breaks an installation on purpose, so the repair paths can be driven without -/// waiting for a genuinely bad connection. -/// -/// The presets are grouped by *which layer is supposed to catch them*, because -/// that is the thing worth testing: same-length corruption is invisible to the -/// launch-time size check and only the hashing pass finds it, truncation is -/// caught cheaply on the next launch, and a deleted assets tree is the case that -/// used to surface as a raw "No such file or directory". +/// Presets are grouped by which layer should catch them same-length corruption only the hashing pass finds truncation the launch-time size check #[derive(PartialEq)] struct CorruptionSimulator; @@ -846,11 +832,7 @@ impl Component for CorruptionSimulator { } } -/// Raises the genuine "some files could not be downloaded" prompt. -/// -/// Calls the same function the download path calls, rather than rebuilding the -/// copy here — a simulated modal that has drifted from the real one is worse -/// than no simulator at all. +/// Calls the same function the download path calls so the simulated modal cannot drift from the real one fn prompt_button( text: &'static str, icon: IconType, @@ -877,13 +859,8 @@ fn prompt_button( }) } -/// Raises the post-crash repair offer, as if the game had just died on a -/// `java.util.zip.ZipException`. -/// -/// Accepting really does run the verify pass, so this exercises the whole path -/// and not just the copy. To test the detection itself rather than the offer, -/// corrupt some libraries above and launch the game — the JVM will raise the -/// real exception and the log watcher will read it out of the output. +/// Accepting really runs the verify pass +/// To test detection itself corrupt libraries above and launch the game fn crash_repair_button( text: &'static str, jar: Option<&'static str>, @@ -1123,8 +1100,6 @@ fn split_csv(raw: &str) -> Vec { .collect() } -/// The debug page's rule: the shared one, but inset and rounded to sit -/// between its loose stacks of rows rather than butt up against them. fn divider() -> impl IntoElement { crate::ui::divider() .margin(Gaps::new_symmetric(8., 0.)) @@ -1148,36 +1123,6 @@ fn section(title: &'static str, rows: Vec) -> impl IntoElement { .into_element() } -// fn field_row(label_text: &'static str, value: &'static str) -> impl IntoElement { -// rect() -// .horizontal() -// .width(Size::fill()) -// .cross_align(Alignment::Center) -// .spacing(12.) -// .child( -// label() -// .text(label_text) -// .width(Size::px(80.)) -// .font_size(13.) -// .color(colors::fg_secondary()), -// ) -// .child( -// rect() -// .width(Size::flex(1.0)) -// .padding(Gaps::new_symmetric(7., 12.)) -// .corner_radius(CornerRadius::new_all(8.)) -// .background(colors::component_bg()) -// .border(border_all_color(1., colors::component_border())) -// .child( -// label() -// .text(value) -// .font_size(12.) -// .color(colors::fg_primary()), -// ), -// ) -// .into_element() -// } - fn toggle_row(title: &'static str, on: State) -> Element { rect() .horizontal() diff --git a/packages/oneclient_app/src/view/app/mod.rs b/packages/oneclient_app/src/view/app/mod.rs index 07c73d80..2f95edd3 100644 --- a/packages/oneclient_app/src/view/app/mod.rs +++ b/packages/oneclient_app/src/view/app/mod.rs @@ -22,12 +22,11 @@ pub fn launch_button_state( Some(LaunchStage::Downloading) => ("Downloading", false), Some(LaunchStage::Launching) => ("Launching", false), Some(LaunchStage::Running) => ("Running", false), - // The click's own flag, raised before core has said anything and held - // for a beat after a failure so a burst of clicks starts one game. + // The click's own flag held for a beat after a failure so a burst of clicks starts one game _ if game.is_launch_pending(cluster_id) => ("Launching", false), _ => ("Launch", true), }; - // Block launching while the startup bundle download is still running. + // Block launching while the startup bundle download is still running if syncing && state.1 { return (state.0, false); } diff --git a/packages/oneclient_app/src/view/app/settings/accounts.rs b/packages/oneclient_app/src/view/app/settings/accounts.rs index c7acfc6c..6dcb253a 100644 --- a/packages/oneclient_app/src/view/app/settings/accounts.rs +++ b/packages/oneclient_app/src/view/app/settings/accounts.rs @@ -18,18 +18,7 @@ use crate::hooks::{ use crate::theme::colors; use crate::ui::border_all_color; -/// The model is framed by the height of its canvas, so the hero pins both sides -/// instead of letting it grow with the page. -/// -/// The SkSL renderer raymarches twelve boxes per fragment, so its cost is linear -/// in `WIDTH * HEIGHT`. At 160x264 that is ~42k fragments — about 2.2x the old -/// 116x168 frame, and still under a tenth of the window, so the larger preview is -/// free in practice. -/// -/// The shader scales the model off `res.y` alone and only crops horizontally, so -/// the height is what actually makes the player bigger. The width is kept just -/// wide enough to clear the arms (~26 model units visible vs ~17 needed) and -/// otherwise left to the text column. +/// The shader scales the model off `res.y` alone so height makes the player bigger width only needs to clear the arms const MODEL_WIDTH_PX: f32 = 160.; const MODEL_HEIGHT_PX: f32 = 264.; @@ -138,7 +127,6 @@ impl Component for SettingsAccounts { } } -/// The active account, its model, and the two ways to add another one. fn hero( account: Option, has_microsoft: bool, @@ -203,10 +191,7 @@ fn hero( .width(Size::fill()) .spacing(8.) .child( - // The bigger model leaves ~245px beside it at the - // 800px minimum, which the two buttons overflow, so - // they wrap onto a second line there and sit side by - // side again once there is room. + // The two buttons overflow the ~245px beside the model at the 800px minimum so they wrap rect() .horizontal() .width(Size::fill()) @@ -388,8 +373,7 @@ fn field_label(text: &str) -> impl IntoElement { .into_element() } -/// The hint sits in whatever is left of the hero beside the model, so the text -/// has to be free to wrap instead of pushing the row wider. +/// Sits in whatever is left of the hero beside the model so the text must wrap fn hint_line(icon: IconType, text: String, color: Color) -> impl IntoElement { rect() .horizontal() @@ -526,9 +510,7 @@ impl Component for AccountRow { .maybe_child(is_default.then(default_badge)) .maybe_child(expired.then(expired_badge)), ) - // The kind rides along with the id on the second line: a - // third badge and a full uuid do not both fit beside the - // settings sidebar. + // The kind rides with the id a third badge and a full uuid do not both fit .child( label() .text(format!("{} · {id}", kind_label(self.kind))) diff --git a/packages/oneclient_app/src/view/app/settings/changelog.rs b/packages/oneclient_app/src/view/app/settings/changelog.rs index 302c4af2..40e59626 100644 --- a/packages/oneclient_app/src/view/app/settings/changelog.rs +++ b/packages/oneclient_app/src/view/app/settings/changelog.rs @@ -22,9 +22,7 @@ impl Component for SettingsChangelog { let mut marked = use_state(|| None::); let installed_version = env!("CARGO_PKG_VERSION").to_string(); - // Opening this page counts as reading the changelog, so clear the sidebar - // dot once the newest entry is known. `marked` keeps us from re-sending the - // command while the settings snapshot catches up. + // Opening this page counts as reading it `marked` avoids re-sending while the settings snapshot catches up if let Some(latest) = latest_changelog_version(&query) { let already_marked = marked.peek().as_deref() == Some(latest.as_str()); let already_seen = settings.seen_changelog_version.as_deref() == Some(latest.as_str()); @@ -74,8 +72,7 @@ impl Component for SettingsChangelog { } } -/// Chevron angle for a collapsed card: `ChevronDown` swung a quarter turn -/// anticlockwise points right. +/// `ChevronDown` swung a quarter turn anticlockwise points right const CHEVRON_CLOSED_DEG: f32 = -90.; #[derive(PartialEq)] @@ -91,9 +88,7 @@ impl Component for ReleaseCard { let mut open = use_state(|| self.initially_open); let is_open = *open.read(); - // One chevron that swings between pointing right (collapsed) and down - // (expanded), so the arrow tracks the section instead of cutting to a - // different glyph that did not follow the state. + // One chevron that swings so the arrow tracks the section instead of cutting to a different glyph let swing = use_animation_with_dependencies(&is_open, |conf, open| { conf.on_creation(OnCreation::Run); conf.on_change(OnChange::Rerun); diff --git a/packages/oneclient_app/src/view/app/settings/minecraft.rs b/packages/oneclient_app/src/view/app/settings/minecraft.rs index f5b7e975..da5a19db 100644 --- a/packages/oneclient_app/src/view/app/settings/minecraft.rs +++ b/packages/oneclient_app/src/view/app/settings/minecraft.rs @@ -178,9 +178,7 @@ fn build_update( } } -/// Dispatched on its own rather than folded into [`build_update`]: that effect -/// mirrors text fields on every keystroke, and a dropdown has no intermediate -/// states to debounce. +/// Dispatched separately from [`build_update`] which debounces keystrokes a dropdown has no intermediate states fn update_mode_field( selected: PackageUpdateMode, dispatch: crate::Actions, diff --git a/packages/oneclient_app/src/view/app/settings/mod.rs b/packages/oneclient_app/src/view/app/settings/mod.rs index 1c43d18b..44173202 100644 --- a/packages/oneclient_app/src/view/app/settings/mod.rs +++ b/packages/oneclient_app/src/view/app/settings/mod.rs @@ -4,7 +4,6 @@ mod appearance; mod apis; mod changelog; mod developer; -// mod feedback; mod java; mod language; mod launcher; @@ -17,7 +16,6 @@ pub use accounts::SettingsAccounts; pub use apis::SettingsApis; pub use changelog::SettingsChangelog; pub use developer::SettingsDeveloper; -// pub use feedback::SettingsFeedback; pub use appearance::SettingsAppearance; pub use java::SettingsJava; pub use language::SettingsLanguage; diff --git a/packages/oneclient_app/src/view/app/settings/storage.rs b/packages/oneclient_app/src/view/app/settings/storage.rs index d47663e7..e3883dc2 100644 --- a/packages/oneclient_app/src/view/app/settings/storage.rs +++ b/packages/oneclient_app/src/view/app/settings/storage.rs @@ -8,7 +8,7 @@ use crate::hooks::{ }; use crate::theme::colors; -/// Rows narrower than this would render as a sliver, so they get a floor. +/// Rows narrower than this would render as a sliver so they get a floor const MIN_BAR_FRACTION: f32 = 0.015; #[derive(PartialEq)] @@ -16,10 +16,7 @@ pub struct SettingsStorage; impl Component for SettingsStorage { fn render(&self) -> impl IntoElement { - // Every hook this component owns, before anything can return early. The - // report is absent on the first render while the scan runs, and a hook - // called only on the later render would change the hook order between - // renders. + // Every hook before any early return the report is absent on the first render and a later-only hook would change the hook order let report_query = use_storage_report(); let Some(report) = try_storage_report(&report_query) else { @@ -88,7 +85,6 @@ impl Component for SettingsStorage { } } -/// The total, sized to be read first. fn hero(report: &StorageReport, refresh: Element) -> impl IntoElement { let reclaimable = report.unreferenced_cache.bytes + report.legacy_cluster_content.bytes; @@ -149,12 +145,7 @@ fn hero_placeholder() -> impl IntoElement { .into_element() } -/// A reclaimable line with its own cleanup button. -/// -/// A component rather than a plain function because it owns a hook. Each -/// instance gets its own hook scope, so the button tracks only its own progress -/// — cleaning the cache does not grey out the leftovers button — and the rows -/// can be built without constraining where the parent calls them. +/// A component rather than a function because it owns a hook each instance gets its own scope so a button tracks only its own progress #[derive(PartialEq)] struct ReclaimRow { icon: IconType, @@ -214,8 +205,6 @@ impl Component for ReclaimRow { } } -/// One breakdown line: name, a bar showing its share of the largest item, size, -/// and a way to open it. fn usage_row(entry: &StorageEntry, largest: u64, bar: Color) -> impl IntoElement { let fraction = if largest == 0 { 0.0 diff --git a/packages/oneclient_app/src/view/console.rs b/packages/oneclient_app/src/view/console.rs index 5f117758..85bfcd41 100644 --- a/packages/oneclient_app/src/view/console.rs +++ b/packages/oneclient_app/src/view/console.rs @@ -1,9 +1,4 @@ -//! The live launcher log console, in a window of its own. -//! -//! Freya gives every window its own component tree, so this one shares nothing -//! with the main window — no radio station, no query storage. All it needs is -//! the process-wide channel in [`oneclient_core::logger::console`], and that -//! channel only carries anything while this window is up. +//! Its own Freya window sharing no radio station or query storage with the main one all it needs is the process-wide channel in [`oneclient_core::logger::console`] use std::collections::VecDeque; use std::sync::{Arc, Mutex}; @@ -20,18 +15,10 @@ use crate::theme::colors; const WINDOW_TITLE: &str = "OneClient — Log Console"; -/// How many lines the console keeps. -/// -/// Old lines are dropped from the front rather than kept forever: this window -/// can be left open for an entire session, and a launcher that leaks memory -/// while you watch it for leaks is not much use. +/// Old lines are dropped from the front this window can be left open for a whole session const MAX_LINES: usize = 5_000; -/// Where the console window is in its lifecycle. -/// -/// `Opening` is not set by the task that asks for the window but cleared by the -/// window itself, so a task cancelled mid-launch — the settings page being -/// navigated away from, say — cannot strand the state. +/// `Opening` is cleared by the window itself not the task that asks for it so a cancelled launch cannot strand the state enum ConsoleWindow { Closed, Opening, @@ -40,7 +27,6 @@ enum ConsoleWindow { static CONSOLE_WINDOW: Mutex = Mutex::new(ConsoleWindow::Closed); -/// Opens the log console, or focuses it if it is already up. pub fn open_log_console() { { let mut state = CONSOLE_WINDOW.lock().expect("console window state"); @@ -54,8 +40,7 @@ pub fn open_log_console() { } } - // Taken here, in the caller's scope: the future below runs detached, and - // `Platform::get()` needs a component scope to resolve against. + // Taken in the caller's scope the future below runs detached and `Platform::get()` needs a component scope let platform = Platform::get(); spawn(async move { platform.launch_window(window_config()).await; @@ -72,8 +57,7 @@ fn window_config() -> WindowConfig { .with_size(1000., 640.) .with_min_size(520., 320.) .with_background(colors::page()) - // Native decorations, unlike the main window's custom titlebar. This is - // a developer tool, and a second bespoke titlebar is not worth the code. + // Native decorations a developer tool is not worth a second bespoke titlebar .with_decorations(true) .with_window_handle(|window| { *CONSOLE_WINDOW.lock().expect("console window state") = @@ -93,11 +77,7 @@ impl App for LogConsoleApp { } } -/// The lowest severity the console shows. -/// -/// A floor rather than an exact match, unlike the cluster logs page: when you -/// are watching a stream go past, "warnings and worse" is the question you -/// actually have. +/// A floor rather than an exact match unlike the cluster logs page #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Severity { All, @@ -142,9 +122,7 @@ impl Component for LogConsole { use_hook(move || { spawn(async move { - // The subscription lives in this task and nowhere else, so when - // the window closes and its tree is dropped the receiver goes - // with it and capture switches itself off. + // The subscription lives only in this task so closing the window drops the receiver and switches capture off let mut subscription = console::subscribe(); tracing::info!("live log console attached"); @@ -208,8 +186,7 @@ fn push(entries: &mut State>, line: ConsoleLine) { entries.push_back(line); } -/// What of the captured lines to show. Purely local: nothing here changes what -/// the launcher records. +/// Purely local nothing here changes what the launcher records fn view_row( search: State, severity: State, @@ -268,11 +245,7 @@ fn view_row( .into_element() } -/// The `EnvFilter` directives the whole launcher is running under. -/// -/// This is the half that matters: the view filter above can only narrow what -/// was already captured, whereas applying here reloads the subscriber's filter, -/// so a target the launcher was not recording at all starts arriving. +/// Applying reloads the subscriber's filter so a target the launcher was not recording at all starts arriving fn filter_row(directives: State, mut error: State>) -> impl IntoElement { let apply = move |_| match logger::set_filter(&directives.read()) { Ok(()) => error.set(None), diff --git a/packages/oneclient_app/src/view/onboarding/bundles/mod.rs b/packages/oneclient_app/src/view/onboarding/bundles/mod.rs index 8b47e306..c6619d61 100644 --- a/packages/oneclient_app/src/view/onboarding/bundles/mod.rs +++ b/packages/oneclient_app/src/view/onboarding/bundles/mod.rs @@ -149,7 +149,7 @@ struct OptionalMod { provider: ProviderId, fallback_name: String, size: u64, - /// `(cluster_id, bundle_name)` pairs this mod is available from. + /// `(cluster_id, bundle_name)` pairs this mod is available from locations: Vec<(i64, String)>, } @@ -255,8 +255,7 @@ impl Component for OnboardingBundles { let opt_ins = opt_in_bundles(&clusters); let extras = optional_mods(&clusters); - // These are hooks, so they run every render regardless of whether there - // are any extras to show yet. + // Hooks so they run every render regardless of whether there are extras yet let (mr_ids, cf_ids) = collect_ids(&extras); let mr_meta = package_meta_batch(&use_package_meta_batch(ProviderId::Modrinth, mr_ids)); let cf_meta = package_meta_batch(&use_package_meta_batch(ProviderId::CurseForge, cf_ids)); diff --git a/packages/oneclient_app/src/view/onboarding/downloading/backdrop.rs b/packages/oneclient_app/src/view/onboarding/downloading/backdrop.rs index 06625b10..b9030aed 100644 --- a/packages/oneclient_app/src/view/onboarding/downloading/backdrop.rs +++ b/packages/oneclient_app/src/view/onboarding/downloading/backdrop.rs @@ -102,10 +102,7 @@ impl Component for LoadingBackdrop { let pan_x = cx * sw * PARALLAX_STRENGTH; let pan_y = cy * sh * PARALLAX_STRENGTH; - // Left at the default edge on purpose: this is the same full-screen art - // the home background shows next, so asking for the same variant means - // the onboarding download is the one home renders instead of it having - // to fetch the same picture again at a different size. + // Default edge on purpose home shows the same art next so the same variant avoids a second fetch let art = match clusters.get(current) { Some(cluster) => DynamicArt::for_cluster(cluster), None => DynamicArt::fallback(), diff --git a/packages/oneclient_app/src/view/onboarding/downloading/mod.rs b/packages/oneclient_app/src/view/onboarding/downloading/mod.rs index 1ecfe877..4232460e 100644 --- a/packages/oneclient_app/src/view/onboarding/downloading/mod.rs +++ b/packages/oneclient_app/src/view/onboarding/downloading/mod.rs @@ -42,11 +42,9 @@ const MAX_TASK_ROWS: usize = 6; struct ClusterPlan { cluster_id: i64, mc_version: String, - /// `(bundle_name, package_id, override)` for every file whose fate differs - /// from the bundle manifest's default. + /// `(bundle_name, package_id, override)` for every file whose fate differs from the manifest default overrides: Vec<(String, String, OverrideType)>, - /// The manifest's `predownload` flag for this version. Versions without it - /// only ever record their package choices here and fetch on first launch. + /// Manifest `predownload` flag versions without it fetch on first launch predownload: bool, } @@ -65,7 +63,7 @@ struct TaskLine { category: TaskCategory, } -/// Display order for the task rows, mirroring the notification centre. +/// Display order for the task rows mirroring the notification centre const TASK_LANES: [TaskCategory; 7] = [ TaskCategory::Java, TaskCategory::Client, @@ -99,13 +97,7 @@ impl GroupedAgg { } } - /// One lane per category, taken round-robin. - /// - /// Assets run 32-at-a-time and sort before everything alphabetically, so a - /// flat sorted list showed nothing but assets for the whole game download; - /// libraries, natives and the client were downloading at the same time and - /// never got a row. Round-robin gives every active category a slot before - /// any category gets a second one. + /// Round-robin assets run 32-at-a-time and sort first so a flat sorted list showed nothing but assets for the whole download fn task_list(&self) -> Vec { let mut lanes: Vec> = TASK_LANES .iter() @@ -527,9 +519,7 @@ fn build_plans( .collect() } -/// Whether the manifest asks for this cluster's version to be fetched up front. -/// Clusters with no manifest row (e.g. seeded from the bundle catalog alone) -/// are not predownloaded. +/// Clusters with no manifest row (e.g. seeded from the bundle catalog alone) are not predownloaded fn cluster_predownloads(versions: &[VersionMetadata], cluster: &Cluster) -> bool { let loader = cluster.mc_loader.to_string(); versions.iter().any(|m| { @@ -541,9 +531,7 @@ fn cluster_predownloads(versions: &[VersionMetadata], cluster: &Cluster) -> bool }) } -/// Whether a bundle keeps its hidden files: true once anything visible in it was -/// taken, including a single opted-in extra that would otherwise lose its -/// dependencies. +/// True once anything visible in the bundle was taken including one opted-in extra that would otherwise lose its dependencies fn bundle_taken( cluster_id: i64, archive: &BundleArchive, @@ -559,14 +547,7 @@ fn bundle_taken( }) } -/// Every package id this cluster is keeping, across all of its bundles. -/// -/// Bundles overlap: the library one pack needs is often the library another -/// needs too, shipped hidden by both. Deciding each archive on its own meant a -/// declined bundle recorded its hidden files as removed even when an accepted -/// bundle right beside it was about to install the very same package — leaving a -/// `removed` row for something the cluster demonstrably has, which then answered -/// for it in anything that looks a package up by id. +/// Bundles overlap deciding each archive alone recorded a removal for a package an accepted bundle was about to install fn kept_package_ids( cluster_id: i64, archives: &[BundleArchive], @@ -603,8 +584,7 @@ fn archive_overrides( selected.contains(&pkg_key(cluster_id, bundle_name, &file.kind.package_id())) }; - // Hidden files are dependencies the user never sees, so they follow the - // bundle: kept if anything from it was taken, dropped if not. + // Hidden files follow the bundle kept if anything from it was taken dropped if not let takes_hidden = bundle_taken(cluster_id, archive, selected); let mut overrides = Vec::new(); @@ -616,13 +596,11 @@ fn archive_overrides( }; let override_type = match (wanted, file.enabled) { - // Matches the manifest default; nothing to record. + // Matches the manifest default nothing to record (true, true) | (false, false) => continue, - // Opting into a mod the bundle ships turned off. + // Opting into a mod the bundle ships turned off (true, false) => OverrideType::Enabled, - // Declined — but only really declined if no bundle the user did take - // ships the same package. Recording a removal for something that is - // being installed anyway states the opposite of what happens. + // Only really declined if no bundle the user took ships the same package (false, true) => { if kept_elsewhere.contains(&file.kind.package_id()) { continue; @@ -649,11 +627,7 @@ fn rough_download_estimate( if !cluster_predownloads(versions, &cb.cluster) { continue; } - // What the cluster is keeping is already the answer to "what gets - // downloaded", so the estimate reads it directly rather than - // reconstructing it from the overrides. Counted once per package: two - // bundles shipping the same library is one download, which is what the - // install path does with it too. + // Counted once per package two bundles shipping the same library is one download let kept = kept_package_ids(cb.cluster.id, &cb.archives, selected); let mut counted: std::collections::HashSet = std::collections::HashSet::new(); for archive in &cb.archives { @@ -722,7 +696,6 @@ fn run_install_batch(plans: Vec, predownload: bool, handles: Instal if let oneclient_events::Event::Progress(ProgressEvent::Grouped(event)) = first { apply_grouped(&mut local, event); } - // Drain everything already queued in one pass. while let Ok(notification) = notif_rx.try_recv() { if let oneclient_events::Event::Progress(ProgressEvent::Grouped(event)) = notification { apply_grouped(&mut local, event); @@ -790,10 +763,7 @@ fn run_install_batch(plans: Vec, predownload: bool, handles: Instal let fetch_now = predownload && plan.predownload; if let Err(err) = install_one(&plan, fetch_now, &events, &ui_tx).await { - // Environmental / expected failures (out of disk, offline, no Java - // build for this version, ...) are surfaced to the user but aren't - // crashes, so log them at warn! (breadcrumb) instead of error! - // (a reported Sentry event). See LauncherError::is_sentry_excluded. + // Expected failures (out of disk offline no Java build) are not crashes warn! is a breadcrumb error! reports to Sentry if err.is_sentry_excluded() { tracing::warn!( cluster_id = plan.cluster_id, @@ -1002,7 +972,7 @@ mod tests { .collect() } - /// Manifest row matching `test_support::cluster` (1.21.11 fabric). + /// Manifest row matching `test_support::cluster` (1.21.11 fabric) fn version_row(predownload: bool) -> VersionMetadata { VersionMetadata { major_version: 21, @@ -1027,7 +997,7 @@ mod tests { let plans = build_plans(&items(vec![sb.clone()]), &HashSet::new(), &versions()); let ov = &plans[0].overrides; - // The hidden dependency must be dropped too, or it installs anyway. + // The hidden dependency must be dropped too or it installs anyway assert!(ov.contains(&( sb.manifest.name.clone(), "sb-dep".to_string(), @@ -1148,7 +1118,7 @@ mod tests { let all = items(vec![skyblock()]); assert!(build_plans(&all, &HashSet::new(), &versions())[0].predownload); assert!(!build_plans(&all, &HashSet::new(), &[version_row(false)])[0].predownload); - // A cluster with no manifest row at all is never predownloaded. + // A cluster with no manifest row at all is never predownloaded assert!(!build_plans(&all, &HashSet::new(), &[])[0].predownload); } diff --git a/packages/oneclient_app/src/view/onboarding/downloading/progress.rs b/packages/oneclient_app/src/view/onboarding/downloading/progress.rs index 8a196d2e..c3becf38 100644 --- a/packages/oneclient_app/src/view/onboarding/downloading/progress.rs +++ b/packages/oneclient_app/src/view/onboarding/downloading/progress.rs @@ -94,7 +94,7 @@ pub(super) struct ProgressView<'a> { pub(super) stage: &'a DownloadStage, pub(super) agg: &'a GroupedAgg, pub(super) activity: &'a str, - /// `None` until the meter has two samples to work from. + /// `None` until the meter has two samples to work from pub(super) transfer: Option, pub(super) total_estimate: u64, pub(super) elapsed_secs: Option, @@ -249,8 +249,7 @@ fn build_stats(view: StatsView) -> Option { if finished { parts.push("Complete".to_string()); } else { - // Both readings come off the same smoothed rate, so the countdown and - // the throughput beside it always agree. + // Both readings come off the same smoothed rate so they always agree match transfer.and_then(|t| t.eta_secs) { Some(remaining) => parts.push(format!( "~{} left", diff --git a/packages/oneclient_app/src/view/onboarding/downloading/tips.rs b/packages/oneclient_app/src/view/onboarding/downloading/tips.rs index a0d5e49a..66485104 100644 --- a/packages/oneclient_app/src/view/onboarding/downloading/tips.rs +++ b/packages/oneclient_app/src/view/onboarding/downloading/tips.rs @@ -35,8 +35,7 @@ fn parse_funfacts(text: &str) -> Vec { .collect() } -/// Loads onboarding tips (remote with backup fallback) and returns a single -/// stable random tip for the lifetime of the component. +/// One stable random tip for the lifetime of the component pub(super) fn use_onboarding_tip() -> String { let mut tips = use_state(|| { ONBOARDING_TIPS_BACKUP diff --git a/packages/oneclient_app/src/view/onboarding/migration.rs b/packages/oneclient_app/src/view/onboarding/migration.rs index 3a0e8dd9..a5c577a9 100644 --- a/packages/oneclient_app/src/view/onboarding/migration.rs +++ b/packages/oneclient_app/src/view/onboarding/migration.rs @@ -26,8 +26,7 @@ fn migrated_categories(source: &[SourceInstance]) -> Vec { categories } -/// Whether a new cluster exists matching this source instance's version+loader -/// (needed for the "dedicated dir" import option to have a target). +/// Needed for the "dedicated dir" import option to have a target pub(crate) fn matching_new_cluster_id( instance: &SourceInstance, new: &[ClusterBundles], @@ -47,12 +46,11 @@ impl Component for OnboardingMigration { let bundles_query = use_onboarding_bundles(); let selection = use_onboarding_selection(); let mut categories = selection.migrated_categories; - // Shared across the flow; the file import is dispatched later, on Setup. + // Shared across the flow the file import is dispatched later on Setup let mut import_folder = selection.import_folder; let import_dedicated = selection.import_dedicated; - // No detection yet -> render nothing meaningful; the shell only routes - // here when a source was detected, so this is a transient loading state. + // The shell only routes here when a source was detected so this is transient let Some(detection) = migration_detection(&migration_query) else { return rect() .width(Size::fill()) @@ -92,7 +90,7 @@ impl Component for OnboardingMigration { .width(Size::fill()) .spacing(16.) .child(heading) - // "Don't import files" is the default choice, pinned above the list. + // "Don't import files" is the default choice pinned above the list .maybe_child(any_importable.then(|| { import_choice_card( "Don't import files", @@ -226,10 +224,7 @@ fn import_body( .into_element() } -/// A detected source instance rendered as a card. Shows version + categories -/// (always-reselected info); when the instance has a game dir it also acts as -/// the file-import selector and, when selected, exposes the shared/dedicated -/// target choice inline. +/// When the instance has a game dir it also acts as the file-import selector and exposes the shared/dedicated target choice fn version_card( instance: &SourceInstance, selected: bool, @@ -283,7 +278,7 @@ fn version_card( colors::fg_secondary() })), ) - // Selection affordance only when the instance actually has files. + // Selection affordance only when the instance actually has files .maybe_child(importable.then(|| { if selected { Icon::new(IconType::Check).size(18.).into_element() @@ -314,7 +309,6 @@ fn version_card( .on_press(move |_| on_select(())); } - // When this version is the chosen import, offer where its files should go. if selected { let mut targets = rect() .vertical() @@ -348,7 +342,7 @@ fn version_card( card.into_element() } -/// A standalone import choice with no associated version (e.g. "Don't import"). +/// A standalone import choice with no associated version (e.g. "Don't import") fn import_choice_card( title: &str, subtitle: &str, @@ -408,8 +402,7 @@ fn import_choice_card( .into_element() } -/// Custom nav: like `onboarding_nav` but the Next button runs `on_next` (which -/// applies the migration + navigates) instead of a plain route replace. +/// Like `onboarding_nav` but Next runs `on_next` (apply migration + navigate) instead of a plain route replace fn migration_nav(next_enabled: bool, on_next: impl FnMut() + 'static) -> impl IntoElement { let mut on_next = on_next; rect() diff --git a/packages/oneclient_app/src/view/onboarding/mod.rs b/packages/oneclient_app/src/view/onboarding/mod.rs index 9692c921..811bad42 100644 --- a/packages/oneclient_app/src/view/onboarding/mod.rs +++ b/packages/oneclient_app/src/view/onboarding/mod.rs @@ -37,8 +37,7 @@ use crate::hooks::{has_migration_data, use_migration, use_onboarding_selection}; use crate::theme::colors; use crate::ui::{border_all_color, entrance_motion_layer}; -/// Number of onboarding steps. The v1-migration step only exists (and is only -/// counted) when old launcher data was detected. +/// The v1-migration step only exists and is only counted when old launcher data was detected pub fn onboarding_total(has_migration: bool) -> usize { if has_migration { 8 } else { 7 } } diff --git a/packages/oneclient_app/src/view/startup.rs b/packages/oneclient_app/src/view/startup.rs index 68b30dd6..28c11da8 100644 --- a/packages/oneclient_app/src/view/startup.rs +++ b/packages/oneclient_app/src/view/startup.rs @@ -58,17 +58,13 @@ impl Component for Startup { Route::Home {} }; - // Onboarding needs the bundle catalog before it can render its steps, - // so gate it on the fetch. Returning users go straight to the app and - // let the bundle download finish in the background. + // Onboarding needs the bundle catalog before it can render its steps so gate it on the fetch let heading_to_onboarding = matches!( destination, Route::OnboardingWelcome { .. } | Route::OnboardingTerms { .. } ); if !heading_to_onboarding || !launcher.fetching { - // Heading into Home: raise the splash curtain so the hard route - // swap is hidden until Home's clusters/art have settled, then it - // fades out. Onboarding animates itself, so skip the curtain. + // Raise the splash curtain so the hard route swap is hidden until Home settles onboarding animates itself if !heading_to_onboarding { let mut active = splash.active; let mut home_ready = splash.home_ready; diff --git a/packages/oneclient_auth/src/diagnostics.rs b/packages/oneclient_auth/src/diagnostics.rs index 46cf5367..c919656f 100644 --- a/packages/oneclient_auth/src/diagnostics.rs +++ b/packages/oneclient_auth/src/diagnostics.rs @@ -302,13 +302,8 @@ fn xerr(code: u64) -> MinecraftAuthError { } } -/// Which layer of the connection failed. -/// -/// reqwest reports every one of these as a connect error, so the text of the -/// source chain is the only thing that separates "your antivirus is -/// intercepting TLS" from "your DNS is broken" — and they need different -/// instructions. A wrong guess here costs the user a wasted support round, so -/// anything unrecognised falls back to the generic advice. +/// reqwest reports all of these as a connect error so the source chain text is +/// the only thing separating them anything unrecognised falls back to Generic #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum NetworkFailure { Certificate, @@ -320,8 +315,8 @@ enum NetworkFailure { fn classify_network_failure(err: &reqwest::Error) -> NetworkFailure { let chain = oneclient_net::error_chain(err).to_ascii_lowercase(); - // Certificate before handshake: a rejected certificate also aborts the - // handshake, and the certificate wording is the more specific of the two. + // Certificate before handshake a rejected certificate also aborts the + // handshake and the certificate wording is the more specific of the two const CERTIFICATE: &[&str] = &[ "invalid peer certificate", "unknownissuer", diff --git a/packages/oneclient_auth/src/error.rs b/packages/oneclient_auth/src/error.rs index 7639de0e..2a7242c6 100644 --- a/packages/oneclient_auth/src/error.rs +++ b/packages/oneclient_auth/src/error.rs @@ -33,9 +33,8 @@ pub enum MinecraftAuthError { source: serde_json::Error, status_code: reqwest::StatusCode, }, - // `source` is rendered through `error_chain` rather than with `{source}`: - // reqwest's `Display` names only the URL, and the cause we need to tell - // apart (certificate, handshake, DNS, refused) is under it. + // Rendered via `error_chain` reqwest's `Display` names only the URL and the + // cause we need (certificate handshake DNS refused) is under it #[error("failed to request using HTTP during MSA step {step:?}: {}", oneclient_net::error_chain(source))] RequestError { step: MinecraftAuthStep, @@ -106,12 +105,8 @@ pub enum AuthError { #[error(transparent)] Request(#[from] RequestError), - /// The user backed out of a Microsoft sign-in that was still running. - /// - /// An outcome rather than a failure: it exists so the flow has something to - /// return when it is torn down mid-handshake, and so callers can tell that - /// apart from a sign-in that actually went wrong. Nothing was written by the - /// time this is produced. + /// An outcome rather than a failure nothing was written by the time this is + /// produced #[error("sign-in was cancelled")] LoginCancelled, diff --git a/packages/oneclient_auth/src/lib.rs b/packages/oneclient_auth/src/lib.rs index 704e3751..ce401a6b 100644 --- a/packages/oneclient_auth/src/lib.rs +++ b/packages/oneclient_auth/src/lib.rs @@ -1,9 +1,6 @@ -//! Microsoft/Xbox/Minecraft authentication and the launcher's credentials store. -//! -//! The whole surface is [`AuthService`], which owns the credentials store, the -//! in-flight browser logins and the per-account refresh guards. Construct one -//! in the composition layer and pass it down; nothing in here reaches for a -//! global or a database. Accounts are persisted to `auth.json`. +//! Construct [`AuthService`] in the composition layer and pass it down nothing +//! in here reaches for a global or a database +//! Accounts persist to `auth.json` mod data; mod diagnostics; diff --git a/packages/oneclient_auth/src/msa.rs b/packages/oneclient_auth/src/msa.rs index 328ba822..110e6b69 100644 --- a/packages/oneclient_auth/src/msa.rs +++ b/packages/oneclient_auth/src/msa.rs @@ -234,7 +234,6 @@ fn redirect_page_html(page: RedirectPage) -> String { let (accent, glyph, heading, detail) = match page { RedirectPage::Waiting => ( "#2567d8", - // Spinning ring. r#"
"#, "Signing you in", "Finish signing in with Microsoft in this window.", diff --git a/packages/oneclient_auth/src/service.rs b/packages/oneclient_auth/src/service.rs index ca6aacd3..136571a7 100644 --- a/packages/oneclient_auth/src/service.rs +++ b/packages/oneclient_auth/src/service.rs @@ -12,59 +12,34 @@ use crate::error::{AuthError, AuthResult, MinecraftAuthError}; use crate::msa::{self, PendingBrowserLogin}; use crate::store::{self, CredentialsStore}; -/// Stable id for the Microsoft login's progress, so a front-end can recognise -/// it among everything else on the bus. -/// -/// Naming it here is the core stating an identity, not a display decision. It -/// says nothing about whether this is a toast or a modal, which is the -/// front-end's call. It exists because the login is driven by a mutation with -/// no place to own a private bus. +/// Stable id for the Microsoft login's progress so a front-end can recognise +/// it among everything else on the bus pub const MICROSOFT_LOGIN_PROGRESS: Uuid = Uuid::from_u128(0x4D53_4141_5554_4800_0000_0000_0000_0001); -/// A Microsoft login that has been started but has not finished. -/// -/// The two halves have different owners once the flow is under way, which is -/// the whole reason this is a struct: `finish_microsoft_login` takes the -/// listener and runs with it, while the token stays here so a cancel arriving -/// from the UI has something to flip. +/// `finish_microsoft_login` takes the listener and runs with it while the +/// token stays here so a cancel from the UI has something to flip struct PendingLogin { - /// The loopback listener, until the flow claims it. - /// - /// `None` once someone owns it, or once a cancel freed the port before any - /// flow got that far. + /// `None` once a flow owns the listener or once a cancel freed the port + /// before any flow got that far browser: Option, cancel: CancellationToken, } -/// Owns everything the authentication flows need. -/// -/// Constructed once by the composition layer and passed down, so nothing here -/// reaches for a global. No database; accounts live in `auth.json` beside the -/// launcher data. pub struct AuthService { store: Mutex, - /// Browser logins waiting on their redirect, keyed by CSRF state token. + /// Keyed by CSRF state token pending_logins: Mutex>, - /// Serialises token renewal per account. - /// - /// Microsoft rotates the refresh token on every use, so two concurrent - /// renewals of one account would race: the loser spends an already-consumed - /// refresh token and the account gets signed out. Clusters can launch in - /// parallel, so this is reachable. The guard makes the second caller wait and - /// reuse the first caller's result instead of starting its own chain. - /// - /// A `std::sync::Mutex` because its critical section is one map lookup; the - /// `tokio::Mutex` inside is what is held across the handshake. + /// Serialises token renewal per account Microsoft rotates the refresh token + /// on every use so concurrent renewals would sign the account out refresh_guards: StdMutex>>>, net: RequestClient, events: EventBus, } impl AuthService { - /// Loads the credentials store from disk. A missing or unreadable file - /// yields an empty store rather than failing: a corrupt `auth.json` should - /// mean "sign in again", not "the launcher will not start". + /// A missing or unreadable `auth.json` yields an empty store rather than + /// failing so a corrupt file means "sign in again" not "will not start" pub async fn load(net: RequestClient, events: EventBus) -> AuthResult { Ok(Self::with_store(CredentialsStore::load().await?, net, events)) } @@ -80,8 +55,6 @@ impl AuthService { } } - // --- Microsoft login --------------------------------------------------- - #[tracing::instrument(skip_all)] pub async fn begin_microsoft_login(&self) -> AuthResult { tracing::info!("beginning Microsoft login"); @@ -92,7 +65,7 @@ impl AuthService { let mut logins = self.pending_logins.lock().await; // A login cancelled before its flow ever started has nobody left to remove - // it, so the next login sweeps it up. + // it so the next login sweeps it up logins.retain(|_, login| !login.cancel.is_cancelled()); logins.insert( browser.state.clone(), @@ -105,16 +78,15 @@ impl AuthService { Ok(MicrosoftLoginSession { browser, device }) } - /// Takes the loopback listener for `state_token`'s flow, plus the token that - /// cancels it. Only one caller can ever claim a login. + /// Only one caller can ever claim a login async fn claim_pending_login( &self, state_token: &str, ) -> AuthResult<(PendingBrowserLogin, CancellationToken)> { let mut logins = self.pending_logins.lock().await; - // Cancelled between `begin` and here: the listener is already gone, and - // there is nothing to run. + // Cancelled between `begin` and here the listener is already gone and + // there is nothing to run if logins .get(state_token) .is_some_and(|login| login.cancel.is_cancelled()) @@ -153,30 +125,24 @@ impl AuthService { }, ); - // Racing the token is what makes a cancel prompt: losing the race drops - // `flow`, and with it the device-code poll, the loopback listener and - // whichever request was mid-flight. There is nothing left to check - // between polls, and nothing keeps running behind the closed dialog. + // Losing the race drops `flow` and with it the device-code poll the + // loopback listener and whichever request was mid-flight let result = tokio::select! { biased; () = cancel.cancelled() => Err(AuthError::LoginCancelled), res = flow => res.map_err(AuthError::from), }; - // Whoever runs the flow owns the cleanup; a cancel only flips the token, - // so that an in-flight flow is the one to tear its own state down. + // Whoever runs the flow owns the cleanup a cancel only flips the token self.pending_logins.lock().await.remove(&session.browser.state); - // Drives the card to 100%, which is how a front-end knows the flow is - // over and can clear whatever it was rendering. A cancelled flow needs - // this just as much as a successful one, or the modal's progress line - // would outlive the sign-in it belongs to. + // Drives the card to 100% so the front-end can clear it a cancelled flow + // needs this as much as a successful one self.events .progress(MICROSOFT_LOGIN_PROGRESS, "Signed in", 1, 1); - // Re-read rather than trusting `result`: the last handshake step can - // complete in the same breath as a cancel, and an account the user has - // already walked away from must not reach the store or the toast. + // Re-read rather than trusting `result` the last handshake step can + // complete in the same breath as a cancel if cancel.is_cancelled() { tracing::info!("Microsoft login cancelled by the user"); return Err(AuthError::LoginCancelled); @@ -201,11 +167,8 @@ impl AuthService { .await } - /// Stops an in-flight Microsoft login and leaves nothing of it behind. - /// - /// The entry is deliberately left in the map: `finish_microsoft_login` may - /// not have claimed it yet, and it needs to find the cancelled token rather - /// than a hole it would report as "this sign-in is no longer active". + /// The entry is deliberately left in the map `finish_microsoft_login` may + /// not have claimed it yet and must find the cancelled token not a hole #[tracing::instrument(level = "debug", skip_all)] pub async fn cancel_microsoft_login(&self, state_token: &str) { let mut logins = self.pending_logins.lock().await; @@ -214,13 +177,11 @@ impl AuthService { }; login.cancel.cancel(); - // Frees the port now if no flow ever took the listener; once one has, the - // flow drops it as it unwinds. + // Frees the port now if no flow ever took the listener once one has the + // flow drops it as it unwinds login.browser = None; } - // --- account management ------------------------------------------------ - #[tracing::instrument(skip(self), fields(username = %username))] pub async fn add_offline_account(&self, username: String) -> AuthResult { self.store @@ -258,8 +219,6 @@ impl AuthService { self.store.lock().await.has_microsoft_account() } - // --- token renewal ----------------------------------------------------- - fn refresh_guard(&self, id: Uuid) -> Arc> { Arc::clone( self.refresh_guards @@ -270,8 +229,6 @@ impl AuthService { ) } - /// Clones an account out of the store, so no caller holds the store lock - /// past this call. async fn account_snapshot(&self, id: Uuid) -> AuthResult { self.store .lock() @@ -281,11 +238,8 @@ impl AuthService { .ok_or(AuthError::AccountNotFound(id)) } - /// Renews `id`'s access token if it has lapsed, returning a usable account. - /// - /// The store lock is not held across the Microsoft handshake, only around the - /// reads and the final write. Holding it across those six sequential round - /// trips stalls every other account read behind a multi-second network call. + /// The store lock is never held across the Microsoft handshake only around + /// the reads and the final write #[tracing::instrument(level = "debug", skip(self), fields(%id))] async fn renew_token(&self, id: Uuid, force: bool) -> AuthResult { let existing = self.account_snapshot(id).await?; @@ -296,7 +250,7 @@ impl AuthService { let guard = self.refresh_guard(id); let _serialised = guard.lock().await; - // Re-read under the guard: whoever held it may have just refreshed. + // Re-read under the guard whoever held it may have just refreshed let existing = self.account_snapshot(id).await?; if !existing.is_microsoft() || (!force && !existing.is_expired()) { return Ok(existing); @@ -341,7 +295,6 @@ impl AuthService { Ok(refreshed) } - /// An account with a token good enough to launch with, renewing if needed. #[tracing::instrument(level = "debug", skip(self), fields(%id))] pub async fn account_for_launch(&self, id: Uuid) -> AuthResult { let account = self.renew_token(id, false).await?; @@ -374,9 +327,8 @@ mod tests { AuthService::with_store(CredentialsStore::default(), net, events) } - /// Registers a login the way `begin_microsoft_login` does, minus the device - /// code request: the browser half is the one that binds a port, and it is - /// the only half that needs no network to set up. + /// Registers a login the way `begin_microsoft_login` does minus the device + /// code request which is the only half that needs the network async fn register_login(service: &AuthService) -> MicrosoftLoginSession { let (browser, pending) = msa::begin_browser_login().await.expect("loopback bind"); @@ -403,9 +355,7 @@ mod tests { #[tokio::test] async fn a_cancel_that_beats_the_flow_is_still_a_cancel() { - // The flow starts on its own task, so a quick cancel can land before it - // claims the login. Answering that with "this sign-in is no longer - // active" would show the user an error for working the dialog correctly. + // A quick cancel can land before the flow claims the login let (events, _rx) = EventBus::channel(); let service = service(events); let session = register_login(&service).await; @@ -439,8 +389,7 @@ mod tests { #[tokio::test] async fn a_cancel_reaches_a_flow_that_has_already_claimed_the_login() { - // Once claimed, the entry is the only channel a cancel has to the flow - // that is running, so it has to survive the claim. + // Once claimed the entry is the only channel a cancel has to the flow let (events, _rx) = EventBus::channel(); let service = service(events); let session = register_login(&service).await; diff --git a/packages/oneclient_auth/src/store.rs b/packages/oneclient_auth/src/store.rs index 74775de2..b9d45990 100644 --- a/packages/oneclient_auth/src/store.rs +++ b/packages/oneclient_auth/src/store.rs @@ -168,8 +168,8 @@ impl CredentialsStore { Ok(Some(id)) } - /// The current account as stored. Never touches the network: callers that - /// need a usable token go through [`crate::AuthService::account_for_launch`]. + /// Never touches the network callers that need a usable token go through + /// [`crate::AuthService::account_for_launch`] #[tracing::instrument(level = "debug", skip_all)] pub async fn default_account(&mut self) -> AuthResult> { let Some(id) = self.resolve_default_id().await? else { @@ -187,10 +187,8 @@ impl CredentialsStore { } } -/// Whether a renewal failure was the network rather than a rejected token. -/// -/// A transient failure must keep the existing (possibly stale) token: throwing -/// it away because Wi-Fi dropped would sign the user out of a working account. +/// A transient failure must keep the existing token discarding it because +/// Wi-Fi dropped would sign the user out of a working account pub(crate) fn is_transient_auth_error(err: &AuthError) -> bool { matches!( err, diff --git a/packages/oneclient_auth/tests/offline.rs b/packages/oneclient_auth/tests/offline.rs index 191c469b..21325eec 100644 --- a/packages/oneclient_auth/tests/offline.rs +++ b/packages/oneclient_auth/tests/offline.rs @@ -2,7 +2,6 @@ use chrono::{TimeDelta, Utc}; use oneclient_auth::{AccountKind, CredentialsStore, offline_account, offline_uuid}; use uuid::Uuid; -// fake launcher dir because yes fn isolate_launcher_dir() { oneclient_common::paths::set_launcher_dir( std::env::temp_dir().join(format!("oneclient-auth-test-{}", Uuid::new_v4())), diff --git a/packages/oneclient_cluster/src/lib.rs b/packages/oneclient_cluster/src/lib.rs index 404a1430..825353fb 100644 --- a/packages/oneclient_cluster/src/lib.rs +++ b/packages/oneclient_cluster/src/lib.rs @@ -1,14 +1,6 @@ -//! Cluster records and everything that belongs to one: the settings profile it -//! launches with, and the logs and screenshots it writes. -//! -//! Not the whole of cluster management, though. Preparing a cluster for launch, -//! provisioning from the remote catalog and applying version migrations all need -//! Java, Minecraft metadata, the package store and the versions manifest; they -//! compose this crate rather than living in it, and stay in `oneclient_core`. -//! -//! It carries no event bus: nothing here reports progress or asks questions. It -//! does take a network client, for the one thing that needs it: uploading a log -//! to mclo.gs. +//! Launch prep catalog provisioning and version migrations stay in +//! `oneclient_core` they need Java MC metadata and the package store +//! No event bus here the network client exists only for mclo.gs uploads mod cluster; mod error; diff --git a/packages/oneclient_cluster/src/logs/manage.rs b/packages/oneclient_cluster/src/logs/manage.rs index 51bb5f84..3f15250b 100644 --- a/packages/oneclient_cluster/src/logs/manage.rs +++ b/packages/oneclient_cluster/src/logs/manage.rs @@ -9,12 +9,8 @@ use oneclient_common::paths; use super::parse::parse_level; use super::{LogFileInfo, LogKind, LogLevel, LogLine, LogsError, ReadOptions}; -/// Everything reachable from here lives under a cluster. -/// -/// The launcher's own log directory used to be allowed too, back when this -/// module listed launcher logs alongside the game's. It no longer does — those -/// belong to the live console — so reading, deleting or uploading them is not -/// something a cluster page should be able to reach. +/// Restricted to cluster dirs the launcher's own logs belong to the live +/// console and must not be readable deletable or uploadable from a cluster page pub(super) fn ensure_allowed(path: &Path) -> ClusterResult { polyio::ensure_under(path, [paths::clusters_dir()?])? .ok_or_else(|| LogsError::InvalidName(path.display().to_string()).into()) @@ -62,8 +58,6 @@ fn collect_dir( } } -/// Everything the game wrote for this cluster: its own logs, its crash reports, -/// and the launcher's capture of the process' stdout. #[tracing::instrument(level = "debug", skip(cluster), fields(cluster_id = cluster.id))] pub fn list_cluster_logs(cluster: &Cluster) -> ClusterResult> { let mut out = Vec::new(); diff --git a/packages/oneclient_cluster/src/logs/mod.rs b/packages/oneclient_cluster/src/logs/mod.rs index 278c9f15..2d4789f0 100644 --- a/packages/oneclient_cluster/src/logs/mod.rs +++ b/packages/oneclient_cluster/src/logs/mod.rs @@ -12,10 +12,7 @@ pub use manage::{cluster_output_log, delete_log_at, list_cluster_logs, read_log_ pub use mclogs::upload_log_at; pub use parse::parse_level; -/// What a listed file is. -/// -/// There is no launcher variant: the launcher's own logs are not a cluster's -/// business, and live in the log console instead. +/// No launcher variant on purpose the launcher's own logs live in the log console #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum LogKind { Game { cluster_id: i64 }, @@ -25,8 +22,6 @@ pub enum LogKind { } impl LogKind { - /// Whether this is a crash report, which the logs page lists separately - /// from the run-of-the-mill output. pub fn is_crash_report(self) -> bool { matches!(self, Self::CrashReport) } diff --git a/packages/oneclient_cluster/src/manager.rs b/packages/oneclient_cluster/src/manager.rs index 429ec8a9..9b77379d 100644 --- a/packages/oneclient_cluster/src/manager.rs +++ b/packages/oneclient_cluster/src/manager.rs @@ -18,16 +18,10 @@ use crate::error::ClusterError; use crate::options::{ClusterUpdate, CreateClusterOptions}; use crate::stage::ClusterStage; -/// Cluster records: create, read, update, delete, and the settings profile each -/// one points at. -/// -/// An instance rather than a bag of associated functions, so the creation lock -/// lives with the thing it protects instead of on the launcher state. Matches -/// `BundlesManager`, `VersionsManager`, `JavaService` and `AuthService`. pub struct ClusterManager { db: DbPool, /// Serialises creation so two concurrent creates cannot resolve to the same - /// folder name and race each other onto disk. + /// folder name and race each other onto disk provisioning: Mutex<()>, } @@ -40,12 +34,8 @@ impl ClusterManager { } } - /// Holds the creation lock for the caller. - /// - /// Startup recovery scans the clusters directory and adopts orphan folders; - /// it must hold this across the whole scan, or a concurrent create (whose - /// folder lands on disk before its row commits) gets adopted into a duplicate - /// row. + /// Orphan-folder recovery must hold this across its whole scan or a + /// concurrent create's folder is adopted into a duplicate row pub async fn provisioning_guard(&self) -> tokio::sync::MutexGuard<'_, ()> { self.provisioning.lock().await } @@ -81,7 +71,7 @@ impl ClusterManager { self.create_core(global, options).await } - /// Create a cluster for a version/loader that doesn't exist yet, and provision it. + /// Returns `None` if a cluster for this version/loader already exists #[tracing::instrument(level = "debug", skip(self))] pub async fn create_provisioned( &self, @@ -102,8 +92,7 @@ impl ClusterManager { self.create_core(global, options).await.map(Some) } - /// Folder-resolve + insert core shared by `create` and `create_provisioned`. - /// Callers MUST hold `self.provisioning`; this does not lock. + /// Callers MUST hold `self.provisioning` this does not lock #[tracing::instrument(level = "debug", skip(self, global))] async fn create_core( &self, @@ -198,8 +187,8 @@ impl ClusterManager { } #[tracing::instrument(level = "debug", skip(self))] - /// `is_running` is passed in rather than looked up: whether a game process - /// is alive belongs to the game lifecycle, not to cluster records. + /// `is_running` is passed in because process liveness belongs to the game + /// lifecycle not to cluster records pub async fn set_dedicated_dir( &self, cluster_id: ClusterId, diff --git a/packages/oneclient_cluster/src/profile.rs b/packages/oneclient_cluster/src/profile.rs index d476f201..a23c69e4 100644 --- a/packages/oneclient_cluster/src/profile.rs +++ b/packages/oneclient_cluster/src/profile.rs @@ -17,13 +17,12 @@ pub struct GameSettingsProfile { pub hook_wrapper: Option, pub hook_post: Option, pub os_extra: Option, - /// What to do when a package installed from the browser has a newer - /// version. Bundle content is never touched by this. + /// Never applies to bundle content pub browser_update_mode: Option, } -// `Resolution` moved to oneclient_common: the launch-argument builder needs it -// too, and that crate cannot depend on the launcher settings. +// `Resolution` lives in oneclient_common the launch-argument builder needs it +// and cannot depend on the launcher settings pub use oneclient_common::Resolution; pub use oneclient_common::domain::PackageUpdateMode; @@ -129,8 +128,8 @@ impl GameSettingsProfile { .os_extra .map(|json| serde_json::from_str(&json)) .transpose()?, - // An unrecognised value inherits rather than errors: a profile - // written by a newer build must not make the cluster unloadable. + // Unrecognised values inherit rather than error so a profile from a + // newer build cannot make the cluster unloadable browser_update_mode: row .browser_update_mode .as_deref() diff --git a/packages/oneclient_cluster/src/profiles.rs b/packages/oneclient_cluster/src/profiles.rs index 24ec19ad..2a684fbf 100644 --- a/packages/oneclient_cluster/src/profiles.rs +++ b/packages/oneclient_cluster/src/profiles.rs @@ -1,13 +1,6 @@ -//! Named settings profiles. -//! -//! A profile is a sparse override on the launcher's global baseline: every -//! `Option` field left `None` inherits the global value, which is why -//! [`ProfileUpdate`] carries `Patch::Clear`. Clearing a field means "go back to -//! inheriting" rather than "set to nothing". -//! -//! These take the global baseline as a `&GameSettingsProfile` parameter rather -//! than reading the launcher settings, so this crate does not depend on the -//! launcher config that embeds it. +//! A profile is a sparse override on the global baseline a `None` field +//! inherits so `Patch::Clear` means "inherit again" not "set to nothing" +//! The baseline is passed in so this crate needn't depend on launcher config use oneclient_common::patch::Patch; use oneclient_db::DbPool; @@ -33,12 +26,8 @@ pub async fn get_profile_or_default( } #[tracing::instrument(level = "debug", skip(pool, global))] -/// Resolves the profile a cluster should launch with. -/// -/// A named profile is a sparse override on the global baseline: any field -/// left `None` inherits the global value. Falling back to the baseline whole -/// when the name is missing or unknown keeps a deleted profile from bricking a -/// cluster. +/// Falls back to the whole baseline when the name is missing or unknown so a +/// deleted profile cannot brick a cluster pub async fn resolve_cluster_profile( pool: &DbPool, global: &GameSettingsProfile, diff --git a/packages/oneclient_cluster/src/screenshots/mod.rs b/packages/oneclient_cluster/src/screenshots/mod.rs index 33ca08be..027184ae 100644 --- a/packages/oneclient_cluster/src/screenshots/mod.rs +++ b/packages/oneclient_cluster/src/screenshots/mod.rs @@ -113,7 +113,7 @@ fn thumbnail(raw: &[u8], max_edge: u32) -> Option { pub fn delete_screenshot(path: &Path) -> ClusterResult<()> { let path = ensure_in_clusters(path)?; - // TODO: maybe async? + // TODO maybe async? match trash::delete(&path) { Ok(()) => Ok(()), Err(err) => Err(ScreenshotsError::Trash(err.to_string()).into()), diff --git a/packages/oneclient_common/src/domain.rs b/packages/oneclient_common/src/domain.rs index 402967ea..045fe085 100644 --- a/packages/oneclient_common/src/domain.rs +++ b/packages/oneclient_common/src/domain.rs @@ -125,15 +125,8 @@ pub enum HashAlgorithm { Sha1, } -/// What the launcher does when a package the user installed from the browser -/// has a newer version at launch. -/// -/// Bundle-provided content is not covered by this: bundles carry their own -/// update flow, and the two must not fight over the same file. -/// -/// The check itself always runs; this only decides what happens with the -/// result, so the "out of date" markers in the package manager are populated -/// no matter which variant is selected. +/// Applies only to browser-installed packages bundles have their own update flow +/// The check always runs regardless of variant so "out of date" markers stay populated #[derive( Debug, Clone, @@ -149,17 +142,14 @@ pub enum HashAlgorithm { )] #[serde(rename_all = "snake_case")] pub enum PackageUpdateMode { - /// Apply every update as soon as it is found, with no modal. Automatic, - /// Never apply and never ask. Skip, - /// Ask, listing each package so the user decides one at a time. #[default] Prompt, } impl PackageUpdateMode { - /// Display order, defaulting variant first. + /// Display order defaulting variant first pub const ALL: &[Self] = &[Self::Prompt, Self::Automatic, Self::Skip]; #[must_use] @@ -171,7 +161,7 @@ impl PackageUpdateMode { } } - /// The value stored in the `setting_profiles.browser_update_mode` column. + /// The value stored in the `setting_profiles.browser_update_mode` column #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -287,11 +277,8 @@ impl Display for GameLoader { } } -/// A game window size. -/// -/// Lives here rather than with the settings because both sides need it: the -/// settings profile stores it, and the launch-argument builder turns it into -/// `--width`/`--height`. +/// Shared by the settings profile (which stores it) and the launch-argument +/// builder (which turns it into `--width`/`--height`) #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct Resolution { pub width: u32, diff --git a/packages/oneclient_common/src/error.rs b/packages/oneclient_common/src/error.rs index 5b5fd1be..86b3b938 100644 --- a/packages/oneclient_common/src/error.rs +++ b/packages/oneclient_common/src/error.rs @@ -1,9 +1,6 @@ pub type PathsResult = Result; -/// The only way path resolution can fail: the platform did not give us a data -/// directory to work in. It stays narrow because callers that hit this cannot -/// do anything except surface it, and keeping it separate from the -/// launcher-wide error keeps this crate a leaf. +/// Kept separate from the launcher-wide error so this crate stays a leaf #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum PathsError { #[error("Could not resolve launcher data directory")] diff --git a/packages/oneclient_common/src/lib.rs b/packages/oneclient_common/src/lib.rs index 1fea44f0..bd0372b7 100644 --- a/packages/oneclient_common/src/lib.rs +++ b/packages/oneclient_common/src/lib.rs @@ -1,10 +1,5 @@ -//! The vocabulary every other OneClient crate is allowed to depend on. -//! -//! This crate is a leaf: it knows about the filesystem layout, the target -//! platform, Minecraft version strings and the content-type taxonomy, and -//! nothing else. It must never gain a dependency on the database, the network -//! stack, or any launcher subsystem. Everything above it depends on it, so a -//! back-edge here would recreate the monolith the split is undoing. +//! Leaf crate must never depend on the database network stack or any +//! launcher subsystem or the dependency graph cycles back into a monolith pub mod constants; pub mod domain; diff --git a/packages/oneclient_common/src/paths.rs b/packages/oneclient_common/src/paths.rs index e821897b..143b81f6 100644 --- a/packages/oneclient_common/src/paths.rs +++ b/packages/oneclient_common/src/paths.rs @@ -10,8 +10,7 @@ const ORGANIZATION: &str = "Polyfrost"; #[cfg(not(debug_assertions))] const APPLICATION: &str = "OneClient"; -// Dev builds use a separate launcher dir (e.g. `oneclient-dev`) so a running -// dev environment never touches prod data. +// Separate dir so a running dev environment never touches prod data #[cfg(debug_assertions)] const APPLICATION: &str = "OneClient-dev"; @@ -70,8 +69,7 @@ pub fn shared_minecraft_dir() -> PathsResult { Ok(launcher_dir()?.join(".minecraft")) } -/// Marks a cluster folder as being its own game directory rather than playing -/// out of the shared `.minecraft`. +/// Presence marks a cluster folder as its own game dir instead of the shared `.minecraft` pub const DEDICATED_MARKER: &str = ".dedicated_directory"; pub fn cluster_dir(folder_name: &str) -> PathsResult { @@ -82,11 +80,8 @@ pub fn cluster_uses_dedicated_dir(folder_name: &str) -> bool { cluster_dir(folder_name).is_ok_and(|dir| dir.join(DEDICATED_MARKER).exists()) } -/// Where a cluster's game actually runs. -/// -/// Lives here rather than on `Cluster` because the content layer has to resolve -/// it from a bare `ClusterRow` too, and a second copy of the marker-file rule is -/// exactly the kind of drift that leaves files behind. +/// Lives here not on `Cluster` so the content layer can resolve it from a bare +/// `ClusterRow` without duplicating the marker-file rule pub fn cluster_game_dir(folder_name: &str) -> PathsResult { if cluster_uses_dedicated_dir(folder_name) { cluster_dir(folder_name) diff --git a/packages/oneclient_common/src/search.rs b/packages/oneclient_common/src/search.rs index cae65155..0c496e5e 100644 --- a/packages/oneclient_common/src/search.rs +++ b/packages/oneclient_common/src/search.rs @@ -1,19 +1,8 @@ -//! Query normalisation and typo-tolerant matching for the launcher's search -//! boxes. -//! -//! Every search surface — the package browser, a cluster's package list, the -//! settings index — funnels its raw text box through [`SearchQuery`] so they all -//! agree on what a query *is*. Surfaces that match locally then use the scoring -//! here; surfaces that hand the query to a remote provider only take the -//! normalised text, because ranking a provider's results is the provider's job. +//! Local surfaces use the scoring here surfaces backed by a remote provider +//! take only the normalised text since ranking their results is their job -/// Collapses a text box's raw contents into the canonical form every search -/// agrees on: no leading or trailing whitespace, internal runs of whitespace -/// reduced to a single space. -/// -/// Without this, `"sodium"`, `" sodium"` and `"sodium extra"` are three -/// different searches — different provider requests, different cache keys, and -/// for a plain `contains` filter, different results. +/// Trims and collapses internal whitespace runs so spacing variants share one +/// provider request cache key and filter result #[must_use] pub fn normalize_query(raw: &str) -> String { let mut out = String::with_capacity(raw.len()); @@ -26,24 +15,19 @@ pub fn normalize_query(raw: &str) -> String { out } -/// How well a candidate matched. Ordered worst to best, so a list of candidates -/// sorts into relevance order with `sort_by_key(|c| Reverse(score))`. -/// -/// The tier dominates the comparison: a prefix match always beats a substring -/// match, which always beats a subsequence match, which always beats a -/// typo-tolerant one. The remainder only orders candidates *within* a tier, so -/// the ordering never surprises — a worse kind of match cannot climb above a -/// better one by scoring well. +/// Ordered worst to best +/// The tier dominates the within-tier bonus can never lift a weaker kind of +/// match above a stronger one #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct MatchScore(u32); impl MatchScore { - /// What an empty query awards every candidate: nothing is more relevant than - /// anything else, so whatever order the caller already had survives. + /// Awarded to every candidate by an empty query preserving the caller's order pub const NEUTRAL: Self = Self(0); } -/// Tier floors. The gap between them is wider than any bonus below can reach. +/// Tier floors +/// The gap between them is wider than any bonus below can reach const TIER_EXACT: u32 = 5_000; const TIER_PREFIX: u32 = 4_000; const TIER_WORD_PREFIX: u32 = 3_000; @@ -51,22 +35,17 @@ const TIER_SUBSTRING: u32 = 2_000; const TIER_SUBSEQUENCE: u32 = 1_000; const TIER_TYPO: u32 = 100; -/// Ceiling for the within-tier bonus, comfortably below the 1000-wide tier gap. +/// Ceiling for the within-tier bonus comfortably below the 1000-wide tier gap const BONUS_MAX: u32 = 900; -/// Below this many characters a typo budget would match almost anything, so -/// short terms are held to the exact tiers. +/// Below this many characters a typo budget would match almost anything so +/// short terms are held to the exact tiers const MIN_TYPO_LEN: usize = 4; -/// A search box's contents, normalised once so matching a list of candidates -/// against it doesn't redo the work per row. +/// Normalised once so matching a list of candidates doesn't redo the work per row #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SearchQuery { - /// Normalised and lowercased; this is what gets matched and what a remote - /// provider should be sent. text: String, - /// `text` split on spaces. Multi-word queries fall back to matching each - /// word independently, so "sodum extra" can still reach "Sodium Extra". terms: Vec, } @@ -82,8 +61,7 @@ impl SearchQuery { Self { text, terms } } - /// The normalised query text. Safe to use as a cache key or to send to a - /// provider — two raw inputs differing only in spacing produce the same one. + /// Safe as a cache key inputs differing only in spacing produce the same text #[must_use] pub fn as_str(&self) -> &str { &self.text @@ -94,9 +72,8 @@ impl SearchQuery { self.text.is_empty() } - /// Scores `haystack`, or `None` if it doesn't match at all. - /// - /// An empty query matches everything at [`MatchScore::NEUTRAL`]. + /// `None` if it doesn't match + /// An empty query matches everything at [`MatchScore::NEUTRAL`] #[must_use] pub fn score(&self, haystack: &str) -> Option { if self.is_empty() { @@ -112,12 +89,8 @@ impl SearchQuery { return Some(score); } - // The query as one string didn't land, so try it as independent words - // against the candidate's: every term has to hit one, and the weakest - // hit sets the tier. This is what lets word order differ ("extra - // sodium"), what keeps a typo in one word from sinking the whole query, - // and what gives the typo budget a word to work against instead of a - // whole name it could never be close enough to. + // Per-word fallback lets word order differ keeps one typo from sinking + // the query and gives the typo budget a word rather than a whole name let mut worst = u32::MAX; for term in &self.terms { let best = hay @@ -127,13 +100,10 @@ impl SearchQuery { .or_else(|| score_whole(&hay, term))?; worst = worst.min(best.0); } - // A per-word match is a weaker claim than a whole-query one, so it can - // never outrank the tier it matched at. + // A per-word match must never outrank the tier it matched at Some(MatchScore(worst.saturating_sub(1))) } - /// The best score across several fields of the same candidate — a package's - /// display name and its file name, say. `None` if no field matched. #[must_use] pub fn best_score<'a>(&self, fields: impl IntoIterator) -> Option { fields.into_iter().filter_map(|f| self.score(f)).max() @@ -145,7 +115,7 @@ impl SearchQuery { } } -/// Scores one already-normalised, already-lowercased pair. +/// Both inputs must already be normalised and lowercased fn score_whole(hay: &str, needle: &str) -> Option { if hay == needle { return Some(MatchScore(TIER_EXACT)); @@ -154,8 +124,7 @@ fn score_whole(hay: &str, needle: &str) -> Option { return Some(MatchScore(TIER_PREFIX + coverage(hay, needle))); } if let Some(at) = hay.find(needle) { - // Landing on a word boundary ("extra" in "Sodium Extra") reads as a real - // hit; landing mid-word ("ode" in "Sodium") is closer to a coincidence. + // A word-boundary hit reads as intentional mid-word is closer to coincidence let word_start = hay[..at].ends_with(' '); let tier = if word_start { TIER_WORD_PREFIX @@ -165,8 +134,7 @@ fn score_whole(hay: &str, needle: &str) -> Option { return Some(MatchScore(tier + coverage(hay, needle))); } if let Some(span) = subsequence_span(hay, needle) { - // A subsequence spread over the whole name is a weaker match than one - // packed together, so tightness — not just length — sets the bonus. + // Tightness not just length sets the bonus a scattered subsequence is weaker let tightness = scale(needle.chars().count(), span); return Some(MatchScore(TIER_SUBSEQUENCE + tightness)); } @@ -177,14 +145,12 @@ fn score_whole(hay: &str, needle: &str) -> Option { } let budget = typo_budget(needle_len); let distance = osa_distance(hay, needle, budget)?; - // Fewer typos to forgive is a better match; an exact-length hit with one - // wrong letter should beat one with two. + // Fewer typos forgiven ranks higher let bonus = BONUS_MAX.saturating_sub((distance as u32) * (BONUS_MAX / (budget as u32 + 1))); Some(MatchScore(TIER_TYPO + bonus)) } -/// How much of the candidate the query accounts for, as a within-tier bonus. -/// "Sodium" hit by "sodiu" outranks "Sodium Extra Extras" hit by the same. +/// Within-tier bonus for how much of the candidate the query accounts for fn coverage(hay: &str, needle: &str) -> u32 { scale(needle.chars().count(), hay.chars().count()) } @@ -197,15 +163,15 @@ fn scale(part: usize, whole: usize) -> u32 { ratio as u32 } -/// Letters a term of this length may get wrong. Deliberately stingy: a generous -/// budget turns every search into a match for everything. +/// Letters a term of this length may get wrong +/// Deliberately stingy a generous budget makes every query match everything fn typo_budget(len: usize) -> usize { if len >= 8 { 2 } else { 1 } } -/// The width of the tightest window of `hay` containing `needle`'s characters in -/// order, or `None` if it isn't a subsequence at all. Covers the typo class a -/// dropped or doubled letter produces ("sodum", "sodiium"). +/// Width of the tightest window of `hay` holding `needle`'s chars in order or +/// `None` if not a subsequence +/// Covers dropped/doubled-letter typos fn subsequence_span(hay: &str, needle: &str) -> Option { let hay: Vec = hay.chars().collect(); let needle: Vec = needle.chars().collect(); @@ -213,9 +179,7 @@ fn subsequence_span(hay: &str, needle: &str) -> Option { return None; } - // Walk forward for the end of the earliest match, then walk back from there - // for the latest start that still matches — that pair is the tightest window - // around this match. + // Earliest end then latest start walking back from it the tightest window let mut n = 0; let mut end = None; for (i, c) in hay.iter().enumerate() { @@ -243,9 +207,8 @@ fn subsequence_span(hay: &str, needle: &str) -> Option { Some(end - start + 1) } -/// Optimal string alignment distance, giving up as soon as it exceeds `max`. -/// Unlike a plain Levenshtein it counts a transposition ("soduim") as one edit, -/// which is the typo people actually make when typing quickly. +/// Optimal string alignment distance giving up once it exceeds `max` +/// Unlike Levenshtein a transposition costs one edit fn osa_distance(a: &str, b: &str, max: usize) -> Option { let a: Vec = a.chars().collect(); let b: Vec = b.chars().collect(); @@ -253,8 +216,8 @@ fn osa_distance(a: &str, b: &str, max: usize) -> Option { return None; } - // Three rows rotate through these buffers; `two_ago` is only ever read from - // row two onwards, by which point it holds real values. + // Three rows rotate through these buffers `two_ago` is only read from row + // two onwards by which point it holds real values let mut two_ago: Vec = vec![0; b.len() + 1]; let mut prev: Vec = (0..=b.len()).collect(); let mut cur: Vec = vec![0; b.len() + 1]; @@ -271,8 +234,7 @@ fn osa_distance(a: &str, b: &str, max: usize) -> Option { cur[j] = best; row_min = row_min.min(best); } - // Every later row is at least this large, so there is no way back under - // the budget from here. + // Every later row is at least this large so the budget is unrecoverable if row_min > max { return None; } @@ -322,7 +284,6 @@ mod tests { #[test] fn common_typos_still_find_the_package() { - // Dropped letter, transposition, wrong letter, doubled letter. for typo in ["sodum", "soduim", "sodiun", "sodiium"] { assert!(SearchQuery::new(typo).matches("Sodium"), "{typo}"); } @@ -330,7 +291,6 @@ mod tests { #[test] fn short_terms_are_not_fuzzed() { - // Two edits away from half the alphabet; a budget here is noise. assert!(!SearchQuery::new("abc").matches("Sodium")); } @@ -353,8 +313,6 @@ mod tests { ] ), [ - // exact, then prefix (shorter first), then word-boundary - // substring, then the typo-tolerant tail. "Sodium", "Sodium Extra", "Indium (Sodium addon)", diff --git a/packages/oneclient_content/src/bundles/install.rs b/packages/oneclient_content/src/bundles/install.rs index 53bee7bf..8fac85da 100644 --- a/packages/oneclient_content/src/bundles/install.rs +++ b/packages/oneclient_content/src/bundles/install.rs @@ -39,22 +39,11 @@ fn find_override( .and_then(|o| OverrideType::parse(&o.override_type)) } -/// The user's standing objection to a package, wherever it happens to be -/// recorded. -/// -/// Overrides are stored per bundle, but a package does not stay in one: -/// `track_bundle_artifact` rewrites `bundle_name` on every install, and the -/// update flow deliberately re-resolves a package to whichever loaded bundle now -/// ships it. Asking only the bundle an artifact currently sits under therefore -/// misses a choice the user made while it sat somewhere else — and reading that -/// silence as consent is what switches a mod they turned off back on. -/// -/// `Removed` outranks `Disabled` where a package is spoken for twice. An -/// `Enabled` opt-in is not an objection and never suppresses. -/// -/// Deliberately wider than [`find_override`], which answers a different -/// question: which manifest default one bundle's file should take, where the -/// bundle is exactly what identifies the file. +/// Searches all bundles +/// a package's `bundle_name` is rewritten on every install so a choice filed +/// under its old bundle still counts +/// `Removed` outranks `Disabled` +/// `Enabled` never suppresses pub(crate) fn find_user_suppression( overrides: &[oneclient_db::models::ClusterBundleOverrideRow], package_id: &str, @@ -72,13 +61,8 @@ pub(crate) fn find_user_suppression( found } -/// Drops the user's objections to a package everywhere they were recorded. -/// -/// The inverse of [`find_user_suppression`], and it has to be: an objection that -/// counts no matter which bundle wrote it has to be clearable no matter which -/// bundle wrote it. Clearing only the current bundle's row would leave one -/// behind that keeps answering "off" to every later question, which is the -/// stuck state this whole area exists to prevent. +/// Clears across all bundles +/// a row left behind under another bundle would keep answering "off" forever async fn clear_suppressing_overrides( cluster_id: i64, package_id: &str, @@ -208,22 +192,18 @@ pub async fn install_bundle( install_enabled_bundle_files(&archive, cluster_id, skip_compatibility, None, ctx).await } -/// Whether a bundle file that is switched off was switched off on purpose. -/// -/// `suppression` comes from [`find_user_suppression`], so a choice filed under a -/// bundle the file has since left still counts. +/// `suppression` comes from [`find_user_suppression`] so a choice filed under a +/// bundle the file has since left still counts pub(crate) fn disable_was_deliberate(hidden: bool, suppression: Option) -> bool { match suppression { Some(OverrideType::Removed) => true, - // Hidden files are dependencies the bundle needs and the user was never - // shown, so they follow the bundle rather than standing on their own. - // Only an outright removal keeps one off. + // Hidden files are never-shown dependencies + // they follow the bundle so only an outright removal keeps one off Some(OverrideType::Disabled) => !hidden, Some(OverrideType::Enabled) | None => false, } } -/// Switches bundle content back on where it was left off without saving the choice #[tracing::instrument(level = "debug", skip(archives, ctx))] pub async fn heal_bundle_activity( cluster_id: i64, @@ -259,9 +239,9 @@ pub async fn heal_bundle_activity( continue; }; - // Across bundles, not just this one. A package that moved bundles keeps - // its old override row, and reading only the bundle it sits under today - // would take that silence for consent and switch it back on. + // Across bundles + // a package that moved keeps its old override row and reading only its + // current bundle would switch it back on let suppression = find_user_suppression(&overrides, package_id); if disable_was_deliberate(is_hidden, suppression) { continue; @@ -276,8 +256,7 @@ pub async fn heal_bundle_activity( "re-enabling bundle content that was switched off with nothing recording the choice" ); - // One unrepairable row is not worth failing an install over; the rest of - // the cluster still wants fixing. + // One unrepairable row must not fail the whole install if let Err(err) = PackageStore::set_artifact_enabled_to(cluster_id, &row.hash, true, ctx).await { @@ -285,10 +264,8 @@ pub async fn heal_bundle_activity( continue; } - // A hidden file reaching here with a `disabled` row is the one case where - // the override is the wrong half. Drop it — everywhere, or the copy left - // under some other bundle keeps answering "off" and the two records never - // settle. + // Drop the stale override everywhere or a copy under another bundle + // keeps answering "off" and the two records never settle if suppression == Some(OverrideType::Disabled) { clear_suppressing_overrides(cluster_id, package_id, ctx).await?; } @@ -315,9 +292,9 @@ pub async fn install_enabled_bundle_files( let linked = PackageStore::list_linked_artifacts(cluster_id, ctx).await?; let mut linked_projects: std::collections::HashSet<&str> = std::collections::HashSet::new(); let mut linked_hashes: std::collections::HashSet<&str> = std::collections::HashSet::new(); - // "Already installed" is what the database says, not what is on disk right - // now: content lives in the cache between sessions, so probing the folder - // would report every package as missing and reinstall the whole bundle. + // "Already installed" means the database not disk + // content lives in the cache between sessions so probing the folder would + // reinstall everything for info in &linked { if let Some(pid) = &info.project_id { linked_projects.insert(pid.as_str()); @@ -401,8 +378,7 @@ pub async fn install_enabled_bundle_files( Ok(installed) } -/// How many bundle packages are fetched at once. Kept modest because each one -/// also costs a provider API call, and those are rate limited per-minute. +/// Kept modest each fetch also costs a provider API call rate limited per-minute pub(crate) const BUNDLE_INSTALL_CONCURRENCY: usize = 6; #[tracing::instrument(level = "debug", skip(bundles, ctx))] @@ -466,11 +442,8 @@ pub async fn set_bundle_package_override( ctx: &ContentCtx, ) -> ContentResult<()> { match override_type { - // Saying "on" has to mean on. The package manager shows one row per - // package however many bundles ship it, so a switch flipped there is a - // statement about the package — and leaving an objection standing under - // some other bundle would let the next bundle pass read it and undo the - // switch the user just flipped. + // The UI shows one row per package across bundles so an objection left + // under another bundle would let the next pass undo this switch Some(ty @ OverrideType::Enabled) => { clear_suppressing_overrides(cluster_id, package_id, ctx).await?; bundle_dao::save_override(&ctx.db, cluster_id, bundle_name, package_id, ty).await?; @@ -486,19 +459,10 @@ pub async fn set_bundle_package_override( Ok(()) } -/// The package manager's switch, for a bundle file the cluster has not installed -/// (an installed one goes through [`toggle_artifact_enabled`] instead). -/// -/// Matching the manifest default is "no opinion" and clears the override; -/// contradicting it pins the choice. Either way, switching a package *on* also -/// drops any objection filed against it under another bundle — the package -/// manager shows one row per package however many bundles ship it, so the switch -/// is a statement about the package, and a row left standing elsewhere would be -/// read by the next bundle pass and quietly undo it. -/// -/// Which is why the mapping lives here rather than at the call site: `None` -/// alone cannot tell "on, and the default agrees" from "off, and the default -/// agrees", and only the first of those should clear anything. +/// For bundle files the cluster has not installed +/// installed ones go through [`toggle_artifact_enabled`] +/// Matching the manifest default clears the override +/// switching *on* also drops objections filed under other bundles #[tracing::instrument(level = "debug", skip(ctx))] pub async fn set_bundle_package_enabled( cluster_id: i64, @@ -608,11 +572,6 @@ pub async fn on_user_remove_artifact( } #[tracing::instrument(level = "debug", skip(ctx))] -/// Flips an artifact's enabled state and updates the bundle override records. -/// -/// The composition point between the two layers: `PackageStore` does the -/// storage half and knows nothing about bundles; this adds the bookkeeping that -/// only makes sense once bundles exist. #[tracing::instrument(level = "debug", skip(ctx))] pub async fn toggle_artifact_enabled( cluster_id: i64, @@ -630,15 +589,10 @@ pub async fn toggle_artifact_enabled( Ok(enabled) } -/// Puts an artifact into a known enabled state and updates the bundle override -/// records to match. -/// -/// The set counterpart to [`toggle_artifact_enabled`], and the one to reach for -/// whenever the caller already knows the state it wants. Flipping only lands on -/// the right answer when the current value is known to be the opposite, which is -/// true of a user pressing a switch and false of everything else: an artifact -/// that is relinked keeps whatever `enabled` it already had, so a flip applied -/// to a row that was already correct puts it wrong. +/// Prefer this over [`toggle_artifact_enabled`] unless the current value is +/// known to be the opposite +/// a relinked artifact keeps its old `enabled` so a flip on an already-correct +/// row puts it wrong #[tracing::instrument(level = "debug", skip(ctx))] pub async fn set_artifact_enabled_to( cluster_id: i64, @@ -655,14 +609,9 @@ pub async fn set_artifact_enabled_to( } } -/// [`crate::packages::reconcile_duplicate_activity`] with the bundle half added. -/// -/// Same split as [`toggle_artifact_enabled`]: `packages` owns the flag and knows -/// nothing about bundles, and the override that has to move with it is written -/// here. A bundle copy that loses to a newer one used to be switched off with -/// nothing recording why, which read as a state nobody chose — and once this -/// module started repairing exactly that state, the two passes would have spent -/// every launch undoing each other. +/// Writes the override alongside the flag +/// without it the losing bundle copy looks disabled-by-nobody and +/// heal_bundle_activity re-enables it every launch #[tracing::instrument(level = "debug", skip(ctx))] pub async fn reconcile_duplicate_activity( cluster_id: i64, @@ -733,9 +682,8 @@ pub async fn remove_artifact_from_cluster( ) -> ContentResult<()> { let bundle_data = bundle_dao::get_bundle_tracked(&ctx.db, cluster_id, hash).await?; - // Everything needed for the eager folder cleanup is looked up first, but - // none of it is allowed to block the removal itself. A package whose - // artifact row has gone missing still has to be removable. + // Looked up first but never allowed to block removal + // a package whose artifact row has gone missing must still be removable let cluster = PackageStore::get_cluster(cluster_id, ctx).await?; let target = artifact_dao::get_artifact_by_hash(&ctx.db, hash) .await? @@ -746,22 +694,19 @@ pub async fn remove_artifact_from_cluster( .into_iter() .find(|l| l.hash == hash); - // The database first, and unconditionally. Removal used to delete the file - // before dropping the row, so a jar held open by a running game — on Windows - // that blocks deleting every hard link to it — failed the whole operation - // and left the package installed. Dropping the row always succeeds, and the - // game folder is rebuilt from the database at the next launch. + // Database first unconditionally + // on Windows a jar held open by a running game blocks deleting every hard + // link which used to fail the whole removal + // The folder is rebuilt from the database at the next launch artifact_dao::unlink_cluster_artifact(&ctx.db, cluster_id, hash).await?; - // Then a best-effort pass at the folder so it matches straight away when - // nothing is holding the file. Failure here is not an error. + // Best-effort folder cleanup failure here is not an error if let (Some(content_type), Some(link)) = (target, link) { try_unlink_materialized(&cluster, content_type, &link.cluster_file_name).await; } - // The cache is where the package actually lives, so it goes too once the - // last cluster stops asking for it. Still installed elsewhere means still - // needed, which `evict_if_unused` checks for us. + // The package actually lives in the cache + // `evict_if_unused` drops it only once no other cluster still needs it if let Err(err) = evict_if_unused(hash, ctx).await { tracing::warn!(hash, error = %err, "failed to evict unused artifact from the cache"); } @@ -940,7 +885,6 @@ mod tests { find_override(&rows, "Bundle B", "pkg-2"), Some(OverrideType::Disabled) ); - // Same package id under a different bundle must not collide. assert_eq!(find_override(&rows, "Bundle B", "pkg-1"), None); assert_eq!(find_override(&rows, "Bundle A", "pkg-3"), None); } diff --git a/packages/oneclient_content/src/bundles/manager.rs b/packages/oneclient_content/src/bundles/manager.rs index 5e1894cd..358a4ad6 100644 --- a/packages/oneclient_content/src/bundles/manager.rs +++ b/packages/oneclient_content/src/bundles/manager.rs @@ -127,10 +127,6 @@ impl BundlesManager { Ok(true) } - /// Seeds the in-memory manifest cache for an archive already on disk. - /// - /// For callers that put a bundle there themselves (the dev helpers) and would - /// otherwise pay a re-read on first use. pub async fn cache_archive_manifest( &self, path: std::path::PathBuf, @@ -239,12 +235,10 @@ impl BundlesManager { Ok(()) } - /// Fetches the bundle catalog, deferring the ETag commit. - /// - /// The catalog is only half the payload: each entry then has to download. - /// Committing the ETag here would make the next sync answer 304 and skip - /// bundles that never actually arrived, so [`BundlesManager::sync`] writes - /// it only once every bundle has landed. + /// Defers the ETag commit + /// bundles download after the catalog so an early ETag would 304 past + /// bundles that never arrived + /// `sync` commits it once all land #[tracing::instrument(level = "debug", skip(ctx))] async fn fetch_manifest( ctx: &ContentCtx, diff --git a/packages/oneclient_content/src/bundles/overrides.rs b/packages/oneclient_content/src/bundles/overrides.rs index 797d7e03..9afd2980 100644 --- a/packages/oneclient_content/src/bundles/overrides.rs +++ b/packages/oneclient_content/src/bundles/overrides.rs @@ -105,18 +105,15 @@ async fn sync_bundle_overrides_at( } match disk_sha1 { - // Nothing on disk -> copy from bundle None => { if write_override(&dest, &bytes, rel).await { report.written.push(rel.to_string()); next.insert(rel.to_string(), new_sha1); } } - // Already matches the bundle -> so just record it in the lock Some(disk) if disk == new_sha1 => { next.insert(rel.to_string(), new_sha1); } - // Untouched -< safe to update Some(disk) if base.as_deref() == Some(disk.as_str()) => { if write_override(&dest, &bytes, rel).await { report.written.push(rel.to_string()); @@ -125,7 +122,6 @@ async fn sync_bundle_overrides_at( next.insert(rel.to_string(), disk); } } - // Differs from both the bundle and the base -> conflict Some(_) => { if let Some(b) = base { if b != new_sha1 { @@ -133,12 +129,10 @@ async fn sync_bundle_overrides_at( } next.insert(rel.to_string(), b); } - // untracked and untouched } } } - // delete files the bundle no longer ships, but only if they were unmodified locally for (rel, base_sha1) in &previous { if next.contains_key(rel) { continue; @@ -467,7 +461,6 @@ mod tests { .await; sync(&v1, root.path()).await; - // User modified kept.toml; gone.toml untouched. polyio::write(root.join("config/kept.toml"), b"edited") .await .unwrap(); diff --git a/packages/oneclient_content/src/bundles/runtime.rs b/packages/oneclient_content/src/bundles/runtime.rs index d2fb633c..652fba10 100644 --- a/packages/oneclient_content/src/bundles/runtime.rs +++ b/packages/oneclient_content/src/bundles/runtime.rs @@ -13,9 +13,7 @@ pub fn is_bundle_syncing() -> bool { BUNDLE_SYNCING.load(Ordering::Relaxed) } -/// Syncs every cluster's bundle content. When `session` is provided, all cluster -/// downloads report into that one grouped session so they appear as a single -/// notification the caller can later convert to its finished state. +/// `session` when given groups every cluster's downloads into one notification #[tracing::instrument(skip_all)] pub async fn sync_all_cluster_bundles( bundles: &BundlesManager, diff --git a/packages/oneclient_content/src/bundles/types.rs b/packages/oneclient_content/src/bundles/types.rs index 8bf76cb9..5c11a4f3 100644 --- a/packages/oneclient_content/src/bundles/types.rs +++ b/packages/oneclient_content/src/bundles/types.rs @@ -109,13 +109,11 @@ pub struct BundlePackageRemoval { pub hash: String, pub package_id: String, pub bundle_name: String, - /// Provider of the installed artifact being removed, resolved from the - /// linked-artifact row at check time. `None` for external/local files. + /// `None` for external/local files pub provider: Option, - /// Remote project id of the installed artifact, for meta-cache lookups. pub project_id: Option, - /// Best-effort human name (display name / file name) captured at check - /// time, used as fallback when meta cache has no entry. + /// Captured at check time + /// fallback when the meta cache has no entry pub display_name: Option, } diff --git a/packages/oneclient_content/src/bundles/updates.rs b/packages/oneclient_content/src/bundles/updates.rs index f17e3e7f..944fd547 100644 --- a/packages/oneclient_content/src/bundles/updates.rs +++ b/packages/oneclient_content/src/bundles/updates.rs @@ -360,7 +360,6 @@ pub async fn apply_bundle_updates( bundles: &BundlesManager, ctx: &ContentCtx, ) -> ContentResult { - // Manual, single-cluster path: its own short-lived progress notification. let session = oneclient_events::GroupedProgressSession::start( &ctx.events, "Updating bundle content", @@ -370,10 +369,8 @@ pub async fn apply_bundle_updates( result } -/// Applies pending bundle updates for one cluster. When `session` is provided, -/// download progress is reported into that (possibly shared) grouped session, so -/// a whole batch of clusters can surface as a single notification. The caller -/// owns the session's lifetime. +/// The caller owns `session`'s lifetime +/// sharing one across clusters makes a batch surface as a single notification #[tracing::instrument(skip(bundles, ctx, session))] pub async fn apply_bundle_updates_with( cluster_id: i64, @@ -384,10 +381,9 @@ pub async fn apply_bundle_updates_with( let lock = cluster_lock(cluster_id); let _guard = lock.lock().await; - // Before anything is decided. The check below works from the override table - // and the manifest defaults and never consults `enabled`, so a cluster - // holding content that was switched off with nothing recording the choice - // would be read as healthy and left exactly as it is. + // Must run first + // the check below never consults `enabled` so unrecorded disables would + // read as healthy and be left untouched { let cluster = PackageStore::get_cluster(cluster_id, ctx).await?; let loader = GameLoader::from_repr(cluster.mc_loader as u8).unwrap_or(GameLoader::Fabric); @@ -431,13 +427,9 @@ pub async fn apply_bundle_updates_with( s.expect(oneclient_events::TaskCategory::Packages, count, bytes); } - // Fetch concurrently, reconcile in order. The install path already fans out - // over its files; applying updates one at a time made the same work take - // several times longer here than it does during a fresh install. - // - // Splitting fetch from reconcile is what makes the fan-out safe: no artifact - // is unlinked while another package is still downloading, so an update that - // supersedes a hash another update is installing can't race it. + // Fetch concurrently reconcile in order + // splitting the two keeps the fan-out safe since no artifact is unlinked + // while another download still needs it let fetched_updates = futures_util::stream::iter(check.updates_available.into_iter().map( |update| async move { let child = session.map(|s| { @@ -558,21 +550,9 @@ pub async fn apply_bundle_updates_with( Ok(result) } -/// Local bookkeeping for an update whose new file is already downloaded and -/// linked: apply the user's enable/disable choice, then drop the artifact the -/// update supersedes. -/// -/// The choice is *set*, not flipped. Linking an artifact leaves an existing row's -/// `enabled` alone, so re-applying an update to a row that already read correctly -/// used to turn it into its opposite. -/// -/// It is also read across bundles. `update.bundle_name` is the bundle that ships -/// the file *now* — [`check_bundle_updates_inner`] re-resolves a package to -/// whichever loaded bundle carries it — and a file only reaches here at all -/// because it passed `effective_enabled` under that bundle, so asking that -/// bundle alone whether the user objects can only ever answer no. Consulting -/// only it would forcibly enable every package whose objection was filed while -/// it lived somewhere else. +/// `enabled` is set not flipped and overrides are read across all bundles +/// packages get re-resolved between bundles so a per-bundle lookup misses +/// objections filed while the file lived elsewhere #[tracing::instrument(level = "debug", skip_all, fields(cluster_id = update.cluster_id, bundle = %update.bundle_name, new_version = %update.new_version_id))] async fn reconcile_update( update: &BundlePackageUpdate, @@ -792,19 +772,9 @@ fn bundle_package_key( } } -/// Bundles that may still take on files the catalog has newly added. -/// -/// Subscription is deliberately looser than this: a bundle whose mods are all -/// installed-but-disabled still wants version updates and still owns its files. -/// New content is different. Removing or disabling everything a bundle ships is -/// how a user opts out of that bundle, and an absent per-file override is not -/// consent for a file that did not exist when they opted out — so gating -/// additions on subscription alone let one catalog addition quietly reinstate a -/// bundle the user had emptied. -/// -/// A bundle qualifies while any of it is still live here: a tracked artifact -/// that is enabled, an enabled artifact that can only have come from this bundle -/// (the untracked/migrated case), or an explicit opt-in override. +/// Stricter than subscription +/// emptying a bundle is how a user opts out and an absent override is not +/// consent for a file that did not exist back then #[tracing::instrument(level = "debug", skip_all)] async fn addition_eligible_bundles( ctx: &ContentCtx, diff --git a/packages/oneclient_content/src/ctx.rs b/packages/oneclient_content/src/ctx.rs index abece5d1..f3bf88d4 100644 --- a/packages/oneclient_content/src/ctx.rs +++ b/packages/oneclient_content/src/ctx.rs @@ -4,18 +4,9 @@ use oneclient_net::RequestClient; use crate::packages::provider::PackageProviderRegistry; -/// What the content subsystems need to do their work. -/// -/// All four fields are used here, unlike the Java and Minecraft crates which -/// each need a strict subset. -/// -/// The database handle is taken directly rather than behind a port. The tables -/// here (`artifacts`, `clusters`, `cluster_bundles`) are shared relational -/// state with cross-entity operations (`link_cluster_artifact`, -/// `is_cluster_linked`, `track_bundle_artifact`) used from four subsystems. A -/// trait over that would be the SQL schema with extra steps, and every schema -/// change would touch it. `java_versions` gets a port because it has one -/// owner and no joins; these do not. +/// `db` is taken directly not behind a port +/// these tables are shared relational state with cross-entity joins used from +/// four subsystems #[derive(Clone)] pub struct ContentCtx { pub db: DbPool, diff --git a/packages/oneclient_content/src/error.rs b/packages/oneclient_content/src/error.rs index ebb12fd8..a696c2a7 100644 --- a/packages/oneclient_content/src/error.rs +++ b/packages/oneclient_content/src/error.rs @@ -3,12 +3,6 @@ use crate::packages::PackageError; pub type ContentResult = Result; -/// The crate's error, funnelling the two subsystem errors plus plumbing. -/// -/// `PackageError` and `BundleError` stay distinct because they describe -/// different failures (a provider rejecting a request versus a bundle manifest -/// not matching what is on disk) and callers inside each subsystem still return -/// their own. #[derive(Debug, thiserror::Error)] pub enum ContentError { #[error(transparent)] @@ -44,15 +38,11 @@ pub enum ContentError { #[error("Invalid URL: {0}")] Url(#[from] url::ParseError), - /// A value that came out of the database or a manifest did not match the - /// shape the code expects: an unknown content type, a loader id with no - /// matching variant. #[error("invalid content data: {reason}")] InvalidData { reason: String }, } impl ContentError { - /// Whether this is the user's environment rather than a launcher bug. #[must_use] pub fn is_transient(&self) -> bool { match self { diff --git a/packages/oneclient_content/src/lib.rs b/packages/oneclient_content/src/lib.rs index bcab5995..962c04c0 100644 --- a/packages/oneclient_content/src/lib.rs +++ b/packages/oneclient_content/src/lib.rs @@ -1,12 +1,3 @@ -//! Everything the user installs into a cluster: mods and resource packs from -//! Modrinth and CurseForge, the content-addressed artifact store that holds -//! them, and the Polyfrost bundles that curate sets of them. -//! -//! `packages` and `bundles` ship together because they are one concern viewed -//! from two sides: the store holds files, bundles decide which files a cluster -//! should have. The dependency between them runs one way only, though; -//! `bundles` builds on `packages`, never the reverse. - pub mod bundles; pub mod packages; diff --git a/packages/oneclient_content/src/packages/activity.rs b/packages/oneclient_content/src/packages/activity.rs index 0a8abf82..6e0862d2 100644 --- a/packages/oneclient_content/src/packages/activity.rs +++ b/packages/oneclient_content/src/packages/activity.rs @@ -1,17 +1,6 @@ -//! Which copy wins when a cluster holds several versions of one package. -//! -//! Holding more than one is a defect the launcher does not aim for, but nothing -//! in the schema forbids it: `cluster_artifacts` is unique on `(cluster_id, -//! hash)`, so two versions are two perfectly legal rows. Until that changes, a -//! cluster can reach a state where every copy is enabled and the game is handed -//! three jars of the same mod — which for mods is a classloader conflict, not a -//! preference. -//! -//! The rule here is that the newest copy is the live one and the rest are -//! switched off. Nothing is unlinked: the older versions stay installed, stay -//! visible in the package manager, and can be switched back on one at a time. -//! `enabled` is also what materialization already reads, so enforcing it here -//! needs no special case at launch. +//! `cluster_artifacts` is unique on `(cluster_id, hash)` so duplicate versions +//! of one package are legal rows +//! the newest stays enabled the rest are switched off rather than unlinked use std::collections::HashMap; @@ -22,17 +11,12 @@ use crate::error::ContentResult; use crate::packages::store::PackageStore; use crate::packages::types::LinkedArtifactInfo; -/// One copy of a package, reduced to what picking a winner needs. struct Copy { hash: String, enabled: bool, published_at: Option, } -/// Leaves each project in the cluster with the newest copy. -/// -/// A no-op on a cluster with no duplicates, which is every healthy one, so it is -/// cheap enough to run before a launch and after an install. #[tracing::instrument(level = "debug", skip(ctx))] pub async fn reconcile_duplicate_activity( cluster_id: i64, @@ -78,12 +62,9 @@ fn duplicates_to_disable(linked: &[LinkedArtifactInfo]) -> Vec { out } -/// Groups the cluster's content by project, keeping only the projects it has -/// more than one copy of. -/// -/// Local files and a bundle's external files have no project to group on and -/// are left out; two of those are two different packages as far as anything -/// here can tell. +/// Local files and a bundle's external files have no project to group on so +/// they are left out +/// two of those are two packages not two copies fn group_duplicates( linked: &[LinkedArtifactInfo], ) -> Vec<((ProviderId, String), Vec)> { @@ -108,13 +89,8 @@ fn group_duplicates( by_project.into_iter().collect() } -/// The hash of the newest copy. -/// -/// `published_at` is written as RFC 3339 by the download path, so the strings -/// order the same way the timestamps do. A copy the provider gave no date for -/// loses to any copy that has one — it is usually an older row written before -/// the field was recorded — and an all-undated group falls back to the first, -/// which at least makes the choice stable across runs rather than arbitrary. +/// `published_at` is RFC 3339 so string order matches time order +/// undated copies lose to dated ones and an all-undated group still picks one fn newest<'a>(copies: impl IntoIterator) -> Option { copies .into_iter() diff --git a/packages/oneclient_content/src/packages/dependencies.rs b/packages/oneclient_content/src/packages/dependencies.rs index 27f30b3a..3d359c6a 100644 --- a/packages/oneclient_content/src/packages/dependencies.rs +++ b/packages/oneclient_content/src/packages/dependencies.rs @@ -11,12 +11,9 @@ use crate::packages::types::{ DependencyKind, ProjectDetail, ReleaseType, VersionDependency, VersionDetail, VersionSummary, }; -/// How far the transitive walk goes. Real graphs are two or three deep; the cap -/// is only here so a provider serving a cycle can't spin forever. +/// Caps the transitive walk so a provider serving a cycle can't spin forever const MAX_DEPTH: usize = 4; -/// How many of a project's versions to consider when picking one. The providers -/// already filter by game version and loader, so the answer is near the top. const VERSION_WINDOW: usize = 50; #[derive(Debug, Clone)] @@ -27,11 +24,9 @@ pub struct ResolvedDependency { #[derive(Debug, Clone, Default)] pub struct DependencyResolution { - /// Dependencies that need installing, ordered breadth-first from the root. - /// Already-present ones are filtered out. + /// Ordered breadth-first from the root + /// already-present ones are filtered out pub install: Vec, - /// Required dependencies that couldn't be resolved, by whatever id the - /// provider gave for them. The caller decides how loud to be about these. pub unresolved: Vec, } @@ -41,8 +36,6 @@ impl DependencyResolution { } } -/// Collects the required dependencies of `root` that `cluster_id` doesn't -/// already have, transitively. #[tracing::instrument(level = "debug", skip(root, ctx), fields(project_id = %root.project_id, version_id = %root.version_id))] pub async fn resolve_required( provider_id: ProviderId, @@ -58,8 +51,7 @@ pub async fn resolve_required( let provider = ctx.providers.get(provider_id)?; let cluster = PackageStore::get_cluster(cluster_id, ctx).await?; - // Seeded with what the cluster already has so an installed library is never - // fetched twice, plus the root itself against self-referencing graphs. + // Seeded with installed projects and the root against refetches and self-referencing graphs let mut seen = installed_project_ids(provider_id, cluster_id, ctx).await?; seen.insert(root.project_id.clone()); @@ -91,8 +83,7 @@ pub async fn resolve_required( } }; - // A version-pinned dependency only reveals its project once fetched, so - // the second dedupe check has to happen here rather than up front. + // A version-pinned dependency only reveals its project once fetched so dedupe again here if !seen.insert(resolved.version.project_id.clone()) { continue; } @@ -126,9 +117,8 @@ async fn resolve_one( cluster: &ClusterRow, ctx: &ContentCtx, ) -> ContentResult> { - // A pinned version wins outright: the author named that exact file, so it is - // used even when a newer one exists. Both providers ignore the project id - // argument when the version id is known. + // A pinned version wins even when newer exists + // providers ignore the project id once the version id is known let version = match &dep.version_id { Some(version_id) => { provider @@ -154,11 +144,7 @@ async fn resolve_one( Ok(Some(ResolvedDependency { project, version })) } -/// Picks the newest release of `project_id` that fits the cluster, falling back -/// to the newest prerelease when that's all there is. -/// -/// Shared with the browser update check, which asks the same question of an -/// already-installed package: "what would we install for this cluster today?". +/// Falls back to the newest prerelease when no release fits the cluster pub(crate) async fn pick_version( provider: &dyn PackageProvider, project_id: &str, @@ -180,8 +166,7 @@ pub(crate) async fn pick_version( .await? .items; - // Resource packs, shaders and datapacks declare no loader, so a - // loader-filtered query comes back empty even though they fit the cluster. + // Resource packs shaders and datapacks declare no loader so a loader-filtered query is empty if candidates.is_empty() && loader_filter.is_some() { candidates = provider .list_versions(project_id, Some(&cluster.mc_version), None, 0, VERSION_WINDOW, ctx) @@ -198,7 +183,7 @@ fn choose_version( loader: GameLoader, ) -> Option { candidates.retain(|version| fits_cluster(version, cluster, loader)); - // Neither provider promises an order, so sort rather than trust the page. + // Neither provider promises an order so sort rather than trust the page candidates.sort_by_key(|version| std::cmp::Reverse(version.published)); candidates @@ -208,8 +193,8 @@ fn choose_version( .cloned() } -/// Mirrors the compatibility check the store applies at install time, so a -/// version that would be rejected there is never picked here. +/// Mirrors the store's install-time compatibility check +/// must stay in sync with it fn fits_cluster(version: &VersionSummary, cluster: &ClusterRow, loader: GameLoader) -> bool { if !version.game_versions.is_empty() && !version @@ -236,8 +221,7 @@ async fn installed_project_ids( .collect()) } -/// Dependencies only make sense for content the loader reads out of the cluster; -/// a modpack brings its own file list. +/// False for modpacks they bring their own file list pub fn resolves_dependencies(content_type: ContentType) -> bool { !matches!(content_type, ContentType::Modpack) } diff --git a/packages/oneclient_content/src/packages/metadata_cache.rs b/packages/oneclient_content/src/packages/metadata_cache.rs index ac248b42..e9a5c88f 100644 --- a/packages/oneclient_content/src/packages/metadata_cache.rs +++ b/packages/oneclient_content/src/packages/metadata_cache.rs @@ -159,8 +159,7 @@ async fn cached_version_detail( size: artifact.size_bytes.unwrap_or(0) as u64, fingerprint: None, }], - // Not persisted with the release row; callers that need the dependency - // graph go to the provider. + // Not persisted with the release row callers needing the graph go to the provider dependencies: Vec::new(), }) } diff --git a/packages/oneclient_content/src/packages/mod.rs b/packages/oneclient_content/src/packages/mod.rs index 508930f6..74ee1cb6 100644 --- a/packages/oneclient_content/src/packages/mod.rs +++ b/packages/oneclient_content/src/packages/mod.rs @@ -10,8 +10,7 @@ pub mod updates; mod file_identity; -// Re-exported so `oneclient_core::packages::ContentType` keeps working; the -// types themselves now live in the leaf vocabulary crate. +// Re-exported so `oneclient_core::packages::ContentType` keeps working pub use oneclient_common::domain::{ContentType, GameLoader, HashAlgorithm, ProviderId}; pub use metadata_cache::{ CachedPackageMeta, cached_project_detail, fetch_package_meta, get_version_cached, diff --git a/packages/oneclient_content/src/packages/provider/curseforge.rs b/packages/oneclient_content/src/packages/provider/curseforge.rs index 8e358313..ffc24aea 100644 --- a/packages/oneclient_content/src/packages/provider/curseforge.rs +++ b/packages/oneclient_content/src/packages/provider/curseforge.rs @@ -478,8 +478,7 @@ impl From for VersionDependency { Self { project_id: Some(d.mod_id.to_string()), version_id: None, - // 1 embedded library, 2 optional, 3 required, 4 tool, 5 incompatible, - // 6 include. + // CF relationType 1 embedded 2 optional 3 required 4 tool 5 incompatible 6 include kind: match d.relation_type { 3 => DependencyKind::Required, 5 => DependencyKind::Incompatible, @@ -733,8 +732,8 @@ fn cf_loader_type(loader: GameLoader) -> Option { mod tests { use super::*; - /// Guards the camelCase contract; the relation type is a bare number, so a - /// mismap here quietly turns required libraries into optional ones. + /// relationType is a bare number + /// a mismap quietly turns required libraries into optional ones #[test] fn file_carries_its_dependencies() { let raw = serde_json::json!({ diff --git a/packages/oneclient_content/src/packages/provider/mod.rs b/packages/oneclient_content/src/packages/provider/mod.rs index 98fa6564..725e5bdd 100644 --- a/packages/oneclient_content/src/packages/provider/mod.rs +++ b/packages/oneclient_content/src/packages/provider/mod.rs @@ -32,10 +32,9 @@ pub trait PackageProvider: Send + Sync { ctx: &ContentCtx, ) -> ContentResult; - /// Same as [`PackageProvider::get_project`], but guarantees [`ProjectDetail::body`] - /// is populated when the provider has one. Only worth calling when the body is - /// actually rendered: providers that serve descriptions separately pay an extra - /// request here, so install paths should stay on `get_project`. + /// Populates [`ProjectDetail::body`] + /// Costs an extra request on providers that serve descriptions separately + /// so install paths should stay on `get_project` async fn get_project_with_body( &self, project_id: &str, diff --git a/packages/oneclient_content/src/packages/provider/modrinth.rs b/packages/oneclient_content/src/packages/provider/modrinth.rs index 675b25ef..e0cc3e0f 100644 --- a/packages/oneclient_content/src/packages/provider/modrinth.rs +++ b/packages/oneclient_content/src/packages/provider/modrinth.rs @@ -424,9 +424,8 @@ struct ModrinthProject { #[derive(Deserialize)] struct ModrinthGalleryItem { url: String, - /// The image as it was uploaded. `url` is Modrinth's own re-encoded copy, - /// small enough to look soft once it is blown up to fill a gallery tile, so - /// this is preferred where the API hands it out. + /// Original upload + /// `url` is Modrinth's re-encoded copy and looks soft when scaled up #[serde(default)] raw_url: Option, #[serde(default)] @@ -691,8 +690,7 @@ fn parse_release_type(s: &str) -> ReleaseType { mod tests { use super::*; - /// Guards the field names against the shape Modrinth actually serves; a - /// rename here silently empties the dependency list. + /// A field rename here silently empties the dependency list #[test] fn version_carries_its_dependencies() { let raw = serde_json::json!({ diff --git a/packages/oneclient_content/src/packages/store/download.rs b/packages/oneclient_content/src/packages/store/download.rs index 9b87e7d9..6c12290a 100644 --- a/packages/oneclient_content/src/packages/store/download.rs +++ b/packages/oneclient_content/src/packages/store/download.rs @@ -22,7 +22,7 @@ pub async fn ensure_artifact_file( } tracing::debug!("downloading artifact file"); - // Modrinth and CurseForge both index by SHA-1. + // Modrinth and CurseForge both index by SHA-1 let expected = polyio::Checksum::sha1(hash); oneclient_net::download_verified( &ctx.net, diff --git a/packages/oneclient_content/src/packages/store/gc.rs b/packages/oneclient_content/src/packages/store/gc.rs index c766b4b4..5a7b78a6 100644 --- a/packages/oneclient_content/src/packages/store/gc.rs +++ b/packages/oneclient_content/src/packages/store/gc.rs @@ -10,12 +10,7 @@ use oneclient_common::paths; use super::paths::{artifact_absolute_path, relative_cache_path}; -/// How recently a cache file must have been touched to be spared. -/// -/// A download lands in the cache *before* its row is inserted, so an in-flight -/// fetch looks exactly like an orphan. Orphans are permanent and cost nothing to -/// collect later, whereas deleting a half-written download costs the user the -/// transfer — so the sweep stays well clear of anything recent. +/// A download lands in the cache before its row is inserted so an in-flight fetch looks exactly like an orphan const RECENT_GRACE: Duration = Duration::from_secs(60 * 60); #[derive(Debug, Default, Clone, Copy)] @@ -25,13 +20,9 @@ pub struct GcReport { pub bytes_freed: u64, } -/// Drops an artifact and its cached file if no cluster refers to it. -/// -/// Returns whether it went. A package still installed somewhere else is left -/// alone, which is what makes this safe to call on every removal. #[tracing::instrument(level = "debug", skip(ctx))] pub async fn evict_if_unused(hash: &str, ctx: &ContentCtx) -> ContentResult { - // Read the row first: once it is deleted there is no way back to the file. + // Read the row first once it is deleted there is no way back to the file let row = artifact_dao::get_artifact_by_hash(&ctx.db, hash).await?; if !artifact_dao::delete_artifact_if_unused(&ctx.db, hash).await? { @@ -47,12 +38,6 @@ pub async fn evict_if_unused(hash: &str, ctx: &ContentCtx) -> ContentResult ContentResult { let mut report = GcReport::default(); @@ -60,9 +45,8 @@ pub async fn collect_unused_artifacts(ctx: &ContentCtx) -> ContentResult ContentResult ContentResult> { let cache_root = paths::packages_cache_dir()?; if !polyio::try_exists(&cache_root).await.unwrap_or(false) { @@ -106,10 +86,8 @@ pub async fn find_unreferenced_files(ctx: &ContentCtx) -> ContentResult ContentResult ContentResult { let cache_root = paths::packages_cache_dir()?; @@ -170,8 +144,6 @@ async fn remove_cached_file(path: &Path) { } } -/// Walks back up `////` removing the -/// directories a deleted file has left empty, stopping at the cache root. async fn prune_empty_parents(file: &Path, root: &Path) { let mut dir = file.parent().map(Path::to_path_buf); @@ -180,8 +152,7 @@ async fn prune_empty_parents(file: &Path, root: &Path) { return; } - // `remove_dir` refuses a non-empty directory, which is exactly the stop - // condition — no need to read the entries first. + // `remove_dir` refuses a non-empty directory which is exactly the stop condition if polyio::remove_dir(¤t).await.is_err() { return; } @@ -192,7 +163,7 @@ async fn prune_empty_parents(file: &Path, root: &Path) { async fn is_recent(path: &Path) -> bool { let Ok(meta) = polyio::stat(path).await else { - // Cannot tell how old it is, so leave it be. + // Cannot tell how old it is so leave it be return true; }; @@ -275,8 +246,6 @@ mod tests { std::fs::remove_dir_all(root.path()).ok(); } - /// A download writes its file before inserting the row, so a fresh - /// unreferenced file is very likely one in flight. #[tokio::test] async fn fresh_files_are_spared() { let root = polyio::testing::ScratchDir::new("gc_recent"); diff --git a/packages/oneclient_content/src/packages/store/link.rs b/packages/oneclient_content/src/packages/store/link.rs index 9af6c89a..045d3595 100644 --- a/packages/oneclient_content/src/packages/store/link.rs +++ b/packages/oneclient_content/src/packages/store/link.rs @@ -24,11 +24,7 @@ pub async fn link_or_copy(src: &Path, dest: &Path) -> ContentResult<()> { Ok(()) } -/// Deletes `path` if anything is there, symlink or not. -/// -/// `Path::exists` resolves symlinks, so a link whose artifact was evicted from -/// the cache reads as absent and the dead link survives every unlink; the -/// package then keeps showing up in the folder it was removed from. +/// `Path::exists` resolves symlinks so a link to an evicted artifact reads as absent and survives every unlink pub async fn remove_entry(path: &Path) -> ContentResult<()> { if polyio::symlink_metadata(path).await.is_err() { return Ok(()); @@ -38,14 +34,8 @@ pub async fn remove_entry(path: &Path) -> ContentResult<()> { Ok(()) } -/// Drops a package out of the game folder right now, if that is possible. -/// -/// Purely an optimisation for what the user sees: removal is a database -/// operation, and the folder is brought in line at the next launch regardless. -/// A running game holds its jars open — on Windows that blocks deleting *any* -/// hard link to them — so failure here is expected and is not an error. -/// -/// Returns whether anything was actually deleted. +/// Best-effort only the folder is reconciled at the next launch regardless +/// A running game holds its jars open which on Windows blocks deletion so failure here is expected #[tracing::instrument(level = "debug", skip(cluster), fields(cluster_id = cluster.id))] pub async fn try_unlink_materialized( cluster: &ClusterRow, @@ -60,9 +50,9 @@ pub async fn try_unlink_materialized( return false; }; - // The shared game dir belongs to whichever cluster played last. Touching a - // file we did not put there would delete another cluster's content, or a - // file the user placed by hand. + // The shared game dir belongs to whichever cluster played last + // touching a file we did not put there would delete another cluster's or + // the user's content let relative = manifest::entry_path(content_type.folder_name(), file_name); if !loaded.owns(cluster.id, &relative) { return false; @@ -88,9 +78,6 @@ pub async fn try_unlink_materialized( mod tests { use super::*; - /// A dead link — one whose target was deleted underneath it — has to go. - /// `Path::exists` follows the link and reports it absent, which is what let - /// these survive every unlink. #[tokio::test] async fn remove_entry_clears_a_dangling_link() { let root = polyio::testing::ScratchDir::new("dangling_link"); diff --git a/packages/oneclient_content/src/packages/store/manifest.rs b/packages/oneclient_content/src/packages/store/manifest.rs index 67fdd490..34d5ee59 100644 --- a/packages/oneclient_content/src/packages/store/manifest.rs +++ b/packages/oneclient_content/src/packages/store/manifest.rs @@ -9,7 +9,7 @@ const MANIFEST_VERSION: u32 = 1; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ManifestEntry { - /// Slash-separated, relative to the game directory, e.g. `mods/sodium.jar`. + /// Slash-separated relative to the game directory e.g. `mods/sodium.jar` pub path: String, pub hash: String, } @@ -17,9 +17,7 @@ pub struct ManifestEntry { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MaterializedManifest { pub version: u32, - /// Which cluster last materialized into this directory. The shared - /// `.minecraft` is used by every non-dedicated cluster in turn, so a - /// manifest found there often belongs to somebody else. + /// The shared `.minecraft` is used by every non-dedicated cluster in turn so this often is not us pub cluster_id: i64, pub entries: Vec, } @@ -44,10 +42,7 @@ impl MaterializedManifest { self.entries.iter().any(|e| e.path == relative_path) } - /// Whether this manifest lists `relative_path` as belonging to `cluster_id`. - /// - /// The cluster check is what makes it safe to delete out of the shared game - /// directory: another cluster's live content is never listed under ours. + /// The cluster check is what makes deleting out of the shared game directory safe #[must_use] pub fn owns(&self, cluster_id: i64, relative_path: &str) -> bool { self.cluster_id == cluster_id && self.contains(relative_path) @@ -59,11 +54,8 @@ pub fn manifest_path(game_dir: &Path) -> PathBuf { game_dir.join(MANIFEST_NAME) } -/// Reads the manifest, or `None` when there is none or it cannot be understood. -/// -/// A manifest we cannot parse is treated as absent rather than as an error: the -/// worst case is that one session's links get stashed as user content, which is -/// recoverable, whereas refusing to launch over it is not. +/// An unparseable manifest reads as `None` not an error +/// stashing links as user content is recoverable refusing to launch is not pub async fn load(game_dir: &Path) -> Option { let raw = polyio::read_to_string(manifest_path(game_dir)).await.ok()?; @@ -101,7 +93,6 @@ pub async fn clear(game_dir: &Path) { polyio::remove_file(manifest_path(game_dir)).await.ok(); } -/// Builds the relative path a content file is materialized at. #[must_use] pub fn entry_path(content_folder: &str, file_name: &str) -> String { format!("{content_folder}/{file_name}") @@ -126,8 +117,6 @@ mod tests { let manifest = manifest(); assert!(manifest.owns(7, "mods/sodium.jar")); - // The shared game dir is reused; another cluster's launch must not treat - // this entry as its own to delete. assert!(!manifest.owns(8, "mods/sodium.jar")); assert!(!manifest.owns(7, "mods/iris.jar")); } diff --git a/packages/oneclient_content/src/packages/store/mod.rs b/packages/oneclient_content/src/packages/store/mod.rs index f32081b6..5362f5b7 100644 --- a/packages/oneclient_content/src/packages/store/mod.rs +++ b/packages/oneclient_content/src/packages/store/mod.rs @@ -106,13 +106,9 @@ impl PackageStore { Ok(artifact) } - /// Records that a cluster wants this artifact. - /// - /// Nothing is written to the cluster or game folder here. The artifact stays - /// in the cache and is materialized into the game directory at launch, which - /// is the only moment the launcher can be sure no game is holding the files - /// open. See [`crate::packages::store::manifest`] for why the folder is not - /// kept in sync eagerly. + /// Writes nothing to disk + /// the artifact is materialized into the game directory at launch the only + /// moment no game is holding the files open #[tracing::instrument(level = "debug", skip(artifact, cluster, ctx))] pub async fn link_artifact( artifact: &ArtifactRow, @@ -183,19 +179,11 @@ impl PackageStore { } #[tracing::instrument(level = "debug", skip(ctx))] - /// Flips an artifact's enabled flag, returning the new state. - /// - /// The flag is all that is recorded. Minecraft reads its mods once at - /// startup and holds the jars open for the rest of the session, so there is - /// no useful moment to rewrite the folder other than the next launch — and - /// on Windows a running game makes it impossible anyway. Disabling makes a - /// best-effort attempt to drop the file now purely so the folder matches - /// what the user just did; whether it lands changes nothing. - /// - /// Storage only: bundle override bookkeeping is composed on top by - /// [`crate::bundles::toggle_artifact_enabled`], because that is the layer that - /// knows about bundles. Calling up into `bundles` from here would make the two - /// modules mutually recursive. + /// Only the flag is recorded + /// the folder is rewritten at the next launch since a running game holds + /// its jars open + /// Storage only bundle override bookkeeping lives in + /// [`crate::bundles::toggle_artifact_enabled`] to avoid mutual recursion pub async fn set_artifact_enabled( cluster_id: i64, hash: &str, @@ -204,10 +192,7 @@ impl PackageStore { Self::write_artifact_enabled(cluster_id, hash, None, ctx).await } - /// Puts an artifact into a known enabled state rather than flipping it. - /// - /// Returns the state it ended in, which is the requested one unless the - /// link was already there. + /// Returns the state it ended in which is the requested one unless the link was already there #[tracing::instrument(level = "debug", skip(ctx))] pub async fn set_artifact_enabled_to( cluster_id: i64, @@ -218,7 +203,7 @@ impl PackageStore { Self::write_artifact_enabled(cluster_id, hash, Some(enabled), ctx).await } - /// `target` of `None` means "the opposite of whatever it is now". + /// `target` of `None` means "the opposite of whatever it is now" async fn write_artifact_enabled( cluster_id: i64, hash: &str, diff --git a/packages/oneclient_content/src/packages/types.rs b/packages/oneclient_content/src/packages/types.rs index 96600674..168427fb 100644 --- a/packages/oneclient_content/src/packages/types.rs +++ b/packages/oneclient_content/src/packages/types.rs @@ -21,11 +21,7 @@ pub struct SearchFilters { } impl SearchFilters { - /// The query as it should go on the wire: no surrounding whitespace, and - /// internal runs collapsed to a single space. Providers score the raw string - /// they are given, so " sodium " and "sodium" come back ranked differently — - /// normalising at the boundary is what stops a stray keystroke from changing - /// the results. Empty when there is nothing to search for. + /// Providers rank the raw string so " sodium " and "sodium" score differently #[must_use] pub fn normalized_query(&self) -> String { self.query.as_deref().map(normalize_query).unwrap_or_default() @@ -155,8 +151,7 @@ pub struct VersionDetail { pub published: DateTime, pub downloads: u64, pub files: Vec, - /// What the provider says this version needs alongside it. Only - /// [`DependencyKind::Required`] entries are pulled in automatically. + /// Only [`DependencyKind::Required`] entries are pulled in automatically #[serde(default)] pub dependencies: Vec, } @@ -170,9 +165,7 @@ pub enum DependencyKind { Embedded, } -/// A provider-declared relation to another project. At least one of the two ids -/// is set: Modrinth pins either a project, a specific version, or both, while -/// CurseForge only ever names the project. +/// At least one id is set Modrinth may pin a version CurseForge only names the project #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct VersionDependency { pub project_id: Option, @@ -240,7 +233,6 @@ pub struct LinkedArtifactInfo { pub display_name: Option, pub display_version: Option, pub provider: Option, - /// When the provider published this version, RFC 3339. Ordering two copies - /// of one project by recency is the only thing that reads it. + /// Provider publish time RFC 3339 pub published_at: Option, } diff --git a/packages/oneclient_content/src/packages/updates.rs b/packages/oneclient_content/src/packages/updates.rs index d13f2960..c70d2cc6 100644 --- a/packages/oneclient_content/src/packages/updates.rs +++ b/packages/oneclient_content/src/packages/updates.rs @@ -1,21 +1,9 @@ -//! Update checking for packages the user installed from the browser. +//! Bundle-tracked artifacts are skipped +//! [`crate::bundles::updates`] owns those files and the two flows would fight +//! over the same artifact //! -//! "Browser installed" means a remote provider release that no bundle claims. -//! Bundles have their own update flow in [`crate::bundles::updates`] and own the -//! files they ship; touching one from here would have the two fight over the -//! same artifact, so anything with a bundle tracking row is skipped outright. -//! -//! The check answers the same question the installer asks — "what would we -//! install for this cluster today?" — by reusing -//! [`crate::packages::dependencies::pick_version`], and calls the package stale -//! when the answer is not the version that is already there. -//! -//! Results are cached in `browser_package_updates` so the package manager can -//! mark a row out of date without a round trip, and still knows after a -//! restart. The cache is only ever *narrowed* by a check that actually reached -//! the provider: a package whose lookup failed keeps whatever the last -//! successful run recorded, which is what makes an offline launch a no-op -//! rather than a mass "everything is up to date". +//! The `browser_package_updates` cache is only narrowed by a check that reached +//! the provider so an offline run is a no-op rather than "all up to date" use std::collections::HashSet; @@ -34,15 +22,14 @@ use crate::packages::dependencies::pick_version; use crate::packages::store::{PackageStore, evict_if_unused, try_unlink_materialized}; use crate::packages::types::LinkedArtifactInfo; -/// How many projects to ask about at once. Matches the bundle installer's -/// fan-out; the providers are the bottleneck, not us. +/// Matches the bundle installer's fan-out +/// the providers are the bottleneck const UPDATE_CHECK_CONCURRENCY: usize = 6; -/// A browser-installed package with a newer version available. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BrowserPackageUpdate { pub cluster_id: i64, - /// The artifact currently linked into the cluster. + /// The artifact currently linked into the cluster pub hash: String, pub provider: ProviderId, pub project_id: String, @@ -50,9 +37,9 @@ pub struct BrowserPackageUpdate { pub installed_version_name: String, pub latest_version_id: String, pub latest_version_name: String, - /// Best display name known locally, for a UI that has no meta cache entry. + /// Local fallback for a UI with no meta cache entry pub display_name: String, - /// The user has already declined this exact newer version. + /// The user has already declined this exact newer version pub skipped: bool, } @@ -73,25 +60,22 @@ impl BrowserPackageUpdate { } } -/// One pass of the check over a cluster. #[derive(Debug, Clone, Default)] pub struct BrowserUpdateCheck { pub cluster_id: i64, pub updates: Vec, - /// Artifacts the providers could not be asked about this run. Their cached - /// rows are preserved rather than treated as "no longer stale". + /// Unreachable this run + /// their cached rows are preserved not cleared pub unchecked: Vec, } impl BrowserUpdateCheck { - /// Updates the user has not already declined — what the modal shows. #[must_use] pub fn pending(&self) -> Vec { self.updates.iter().filter(|u| !u.skipped).cloned().collect() } } -/// A browser-installed package, i.e. one candidate for this check. struct Candidate { hash: String, provider: ProviderId, @@ -101,12 +85,7 @@ struct Candidate { display_version: String, } -/// Every artifact in the cluster that came from a remote provider and that no -/// bundle claims. -/// -/// A missing bundle tracking row is the data-layer marker for "the user added -/// this themselves"; the package manager's `External` tab is the same rule -/// spelled out in the UI. +/// A missing bundle tracking row is the marker for "the user added this themselves" fn browser_installed( linked: &[LinkedArtifactInfo], bundle_hashes: &HashSet, @@ -134,12 +113,8 @@ fn browser_installed( .collect() } -/// Asks every provider what the newest compatible version of each -/// browser-installed package is. -/// -/// Never fails because one project could not be reached: that project lands in -/// [`BrowserUpdateCheck::unchecked`] and the run continues. The whole call only -/// errors when the cluster itself cannot be read. +/// An unreachable project lands in [`BrowserUpdateCheck::unchecked`] +/// only an unreadable cluster errors #[tracing::instrument(level = "debug", skip(ctx))] pub async fn check_browser_package_updates( cluster_id: i64, @@ -202,12 +177,9 @@ pub async fn check_browser_package_updates( skipped: skipped.contains(&key), }); } - // Up to date, or the provider has nothing that fits this cluster any - // more. Either way there is nothing to offer. Ok(None) => {} Err(err) => { - // Offline is the common case and says nothing about the - // launcher, so it stays at debug; anything else is worth seeing. + // Offline is the common case and says nothing about the launcher if err.is_transient() { tracing::debug!( project_id = %candidate.project_id, @@ -229,8 +201,7 @@ pub async fn check_browser_package_updates( Ok(check) } -/// The newest compatible version id and name, or `None` when the installed one -/// is already it. +/// `None` when the installed version is already the newest compatible one async fn latest_for( candidate: &Candidate, cluster: &ClusterRow, @@ -254,11 +225,8 @@ async fn latest_for( Ok(Some((latest.version_id, name))) } -/// Runs the check and writes what it found into the cache table. -/// -/// Rows the check did not report are dropped, so a package that was updated or -/// removed stops being marked stale — except for the ones it could not reach, -/// which keep their last known state. +/// Unreported rows are dropped except unreachable ones which keep their last +/// known state #[tracing::instrument(level = "debug", skip(ctx))] pub async fn refresh_browser_package_updates( cluster_id: i64, @@ -284,8 +252,7 @@ pub async fn refresh_browser_package_updates( ) .await?; keep.push(row.hash.clone()); - // The row is the authority on `skipped`: it knows whether the user has - // already answered for this exact version. + // The row is the authority on `skipped` for this exact version stored.push(BrowserPackageUpdate::from_row(row)); } @@ -298,7 +265,7 @@ pub async fn refresh_browser_package_updates( }) } -/// What the last check recorded for one cluster. Never touches the network. +/// Never touches the network #[tracing::instrument(level = "debug", skip(ctx))] pub async fn cached_browser_package_updates( cluster_id: i64, @@ -311,11 +278,9 @@ pub async fn cached_browser_package_updates( .collect()) } -/// Records that the user does not want this particular newer version. -/// -/// The row stays: the package is still out of date and the package manager -/// still says so. Only the modal stops asking, and only until a version newer -/// than the declined one appears. +/// The row stays and the package still reads as out of date +/// only the modal stops asking until a newer version than the declined one +/// appears #[tracing::instrument(level = "debug", skip(ctx))] pub async fn skip_browser_package_update( cluster_id: i64, @@ -326,18 +291,14 @@ pub async fn skip_browser_package_update( Ok(()) } -/// Installs the newer version and drops the one it supersedes. -/// -/// Download first, unlink second: a failed download leaves the cluster exactly -/// as it was rather than with nothing installed. +/// Download first unlink second so a failed download leaves the cluster as it was #[tracing::instrument(level = "debug", skip(update, child, ctx), fields(cluster_id = update.cluster_id, project_id = %update.project_id))] pub async fn apply_browser_package_update( update: &BrowserPackageUpdate, child: Option<&GroupedProgressChild>, ctx: &ContentCtx, ) -> ContentResult { - // Refuse to touch anything a bundle owns, however this was called. The - // check already filters these out; this is the invariant, not a retry. + // Never touch anything a bundle owns however this was called if bundle_dao::get_bundle_tracked(&ctx.db, update.cluster_id, &update.hash) .await? .is_some() @@ -356,9 +317,9 @@ pub async fn apply_browser_package_update( .get_version(&update.project_id, &update.latest_version_id, ctx) .await?; - // The check already established this version fits the cluster, and a - // provider that disagrees at install time would strand the user on a build - // they cannot move off. Compatibility is not re-litigated here. + // Compatibility is not re-checked + // a provider disagreeing at install time would strand the user on a build + // they cannot move off let installed = PackageStore::install_to_cluster( update.provider, &project, @@ -375,11 +336,8 @@ pub async fn apply_browser_package_update( .await? .map(|link| link.enabled != 0); - // Everything else of this project goes, not just the artifact the check - // happened to record. A cluster that already held two versions would - // otherwise come out of an update holding two again, and the recorded hash - // can itself be stale if the user installed a version by hand since the - // check ran. + // Everything else of this project goes not just the recorded hash which can + // be stale if the user installed a version by hand since the check ran unlink_other_versions( update.cluster_id, update.provider, @@ -389,16 +347,15 @@ pub async fn apply_browser_package_update( ) .await?; - // Nothing moved: the provider handed back the file that is already linked. - // Leave the link, and its enabled flag, exactly as they were. + // Provider handed back the already-linked file + // leave the link untouched if installed.hash == update.hash { update_dao::delete(&ctx.db, update.cluster_id, &update.hash).await?; return Ok(installed.hash); } - // A package the user had switched off stays off across an update; being out - // of date is not a reason to turn it back on. The fresh link starts enabled, - // so this is a toggle rather than a set. + // A package switched off stays off + // the fresh link starts enabled so this is a toggle rather than a set if enabled_before == Some(false) { PackageStore::set_artifact_enabled(update.cluster_id, &installed.hash, ctx).await?; } @@ -408,12 +365,8 @@ pub async fn apply_browser_package_update( Ok(installed.hash) } -/// Drops every other version of `project_id` the cluster holds. -/// -/// An update is a replacement, so what it leaves behind is exactly one artifact -/// per project. Bundle-owned copies are stepped over rather than removed: this -/// flow refuses to touch them at all, and unlinking one here would have the next -/// bundle sync put it straight back. +/// Bundle-owned copies are stepped over +/// unlinking one here would have the next bundle sync put it straight back #[tracing::instrument(level = "debug", skip(ctx))] async fn unlink_other_versions( cluster_id: i64, @@ -455,11 +408,9 @@ async fn unlink_other_versions( Ok(()) } -/// Drops the artifact an applied update replaced. -/// -/// Deliberately not `bundles::remove_artifact_from_cluster`: that one also -/// reconciles bundle overrides, which cannot apply to a package no bundle -/// provides, and `packages` must not depend on `bundles`. +/// Not `bundles::remove_artifact_from_cluster` +/// `packages` must not depend on `bundles` and its override reconciliation +/// does not apply here #[tracing::instrument(level = "debug", skip(ctx))] async fn unlink_superseded(cluster_id: i64, hash: &str, ctx: &ContentCtx) -> ContentResult<()> { let cluster = PackageStore::get_cluster(cluster_id, ctx).await?; diff --git a/packages/oneclient_core/src/clusters/mod.rs b/packages/oneclient_core/src/clusters/mod.rs index 5b15cfbf..8c484704 100644 --- a/packages/oneclient_core/src/clusters/mod.rs +++ b/packages/oneclient_core/src/clusters/mod.rs @@ -1,11 +1,3 @@ -//! Cluster orchestration: preparing a cluster for launch, provisioning from the -//! remote catalog, and applying version migrations. -//! -//! The records themselves, plus settings profiles, logs and screenshots, live -//! in `oneclient_cluster`. These three modules are what compose that crate with -//! Java, Minecraft metadata, the package store and the versions manifest, which -//! is why they stay here. - mod migrate; pub(crate) mod prepare; mod provision; @@ -16,8 +8,6 @@ pub use prepare::{estimate_cluster_download, prepare_cluster, prepare_cluster_lo pub use provision::{ensure_from_bundles, ensure_from_versions}; pub use unlink_legacy::{SweepReport, unlink_legacy_cluster_content}; -// Re-exported so the rest of the launcher keeps addressing these through -// `clusters::`, as it did before the split. pub use oneclient_cluster::{ Cluster, ClusterError, ClusterLinkTarget, ClusterManager, ClusterStage, ClusterUpdate, CreateClusterOptions, diff --git a/packages/oneclient_core/src/clusters/prepare.rs b/packages/oneclient_core/src/clusters/prepare.rs index 50339e7c..7077b03f 100644 --- a/packages/oneclient_core/src/clusters/prepare.rs +++ b/packages/oneclient_core/src/clusters/prepare.rs @@ -18,11 +18,6 @@ use oneclient_common::paths; use crate::state::{LauncherServices, LauncherState}; use crate::{GameError, LauncherResult}; -/// Locks the metadata store, then prepares the cluster. -/// -/// Was `ClusterManager::prepare`, which only existed to take that lock before -/// delegating here. It lives with `prepare` rather than with cluster records, -/// because the metadata store is a download concern. #[tracing::instrument(skip(state, shared_progress))] pub async fn prepare_cluster_locked( state: &Arc, @@ -107,9 +102,9 @@ pub async fn prepare_cluster( const JRE_ESTIMATE_BYTES: u64 = 45_000_000; -/// Bytes the game install still needs. Falls back to the manifest's full size -/// when the asset index isn't cached yet, which is also the case where nothing -/// is on disk, so the full size is the right answer anyway. +/// Bytes the game install still needs +/// Falls back to the manifest's full size +/// when the asset index isn't cached which is also when nothing is on disk async fn game_download_bytes(services: &LauncherServices, info: &VersionInfo) -> u64 { let Some(assets_index) = cached_assets_index(info).await else { let client = info @@ -127,8 +122,8 @@ async fn game_download_bytes(services: &LauncherServices, info: &VersionInfo) -> }; let _ = services; - // The estimate runs before a Java runtime is resolved, so rules are checked - // against the host architecture. That only shifts natives-related edge cases. + // Runs before a Java runtime is resolved so rules are checked against the + // host architecture only natives-related edge cases shift match game::plan_downloads(info, assets_index, std::env::consts::ARCH, false, false).await { Ok(plan) => plan.total_bytes(), Err(err) => { diff --git a/packages/oneclient_core/src/clusters/unlink_legacy.rs b/packages/oneclient_core/src/clusters/unlink_legacy.rs index 47bfb6a0..297b1462 100644 --- a/packages/oneclient_core/src/clusters/unlink_legacy.rs +++ b/packages/oneclient_core/src/clusters/unlink_legacy.rs @@ -24,18 +24,8 @@ pub struct SweepReport { pub skipped: usize, } -/// Clears out the content links older launchers wrote into cluster folders. -/// -/// Deliberately **not** automatic. The rule it applies — "this file's hash -/// matches a cached artifact, so it is one of ours" — is true during the -/// transition and false afterwards, once the cluster folder becomes the stash -/// for the user's own content. Running it on a schedule would need a flag to -/// stop it running twice, and that flag would be one more thing to get out of -/// step with the disk it describes. Instead the Storage settings page reports -/// what is there and the user asks for it. -/// -/// Nothing depends on this having run: [`crate::game::materialize_content`] -/// ignores leftovers either way. This only reclaims the space. +/// User-triggered only the "hash matches a cached artifact so it is ours" rule +/// is true during the transition and false once cluster folders hold user content #[tracing::instrument(skip(state))] pub async fn unlink_legacy_cluster_content(state: &LauncherState) -> LauncherResult { let mut report = SweepReport::default(); @@ -65,7 +55,6 @@ pub async fn unlink_legacy_cluster_content(state: &LauncherState) -> LauncherRes Ok(report) } -/// Deletes launcher-owned files out of a shared cluster's content folders. async fn sweep_shared(state: &LauncherState, cluster_root: &Path, report: &mut SweepReport) { for content_type in SWEPT_TYPES { let dir = cluster_root.join(content_type.folder_name()); @@ -79,8 +68,7 @@ async fn sweep_shared(state: &LauncherState, cluster_root: &Path, report: &mut S continue; }; - // A symlink here is unambiguously one of ours: this is where the old - // scheme put them, and nothing else writes links into these folders. + // A symlink here is unambiguously ours nothing else writes links here if file_type.is_symlink() { polyio::remove_file(&path).await.ok(); report.removed += 1; @@ -97,9 +85,8 @@ async fn sweep_shared(state: &LauncherState, cluster_root: &Path, report: &mut S report.removed += 1; } } - // The cache row points at this very file — an artifact adopted by - // an older recovery pass, which stored content in place instead of - // in the cache. Deleting it would destroy the only copy. + // Cache row points at this very file (older recovery pass stored it + // in place) deleting it would destroy the only copy Some(CacheMatch::IsTheArtifact) => { tracing::debug!( path = %path.display(), @@ -113,8 +100,7 @@ async fn sweep_shared(state: &LauncherState, cluster_root: &Path, report: &mut S } } -/// Gives a dedicated cluster's already-materialized content a manifest, so later -/// launches can tell it apart from the user's own files. +/// Writes a manifest so later launches can tell adopted content from user files async fn adopt_dedicated( state: &LauncherState, cluster: &crate::clusters::Cluster, @@ -164,9 +150,9 @@ async fn adopt_dedicated( } enum CacheMatch { - /// The cache holds this content at its own path; the cluster copy is a link. + /// Cache holds this content at its own path the cluster copy is a link Elsewhere, - /// The artifact row points at this exact file. + /// The artifact row points at this exact file IsTheArtifact, } diff --git a/packages/oneclient_core/src/dev.rs b/packages/oneclient_core/src/dev.rs index 87d5f196..e2cf62e3 100644 --- a/packages/oneclient_core/src/dev.rs +++ b/packages/oneclient_core/src/dev.rs @@ -177,9 +177,8 @@ fn spawn_notification_handler(mut rx: oneclient_events::EventReceiver) { Event::Game(GameEvent::Failed { cluster_id, message }) => { mp.suspend(|| tracing::error!(cluster_id, "launch failed: {message}")); } - // No TTY dialog here: answer `None` so the waiting task sees a - // dismissal and fails cleanly, rather than hanging on a prompt - // nobody can answer. + // No TTY dialog here answer `None` so the waiting task fails + // cleanly instead of hanging on a prompt nobody can answer Event::Notification(Notification::Prompt(request)) => { let choices: Vec<&str> = request.choices.iter().map(|c| c.id).collect(); mp.suspend(|| { diff --git a/packages/oneclient_core/src/error.rs b/packages/oneclient_core/src/error.rs index b6598e59..213bf101 100644 --- a/packages/oneclient_core/src/error.rs +++ b/packages/oneclient_core/src/error.rs @@ -83,11 +83,7 @@ impl LauncherError { } } - /// Whether this is the user backing out of their own sign-in. - /// - /// Worth telling apart because it is not something to report: by the time it - /// lands the dialog is already gone, and putting a red line under the button - /// would be complaining about the button the user just pressed. + /// Not something to report the dialog is already gone by the time it lands #[must_use] pub fn is_login_cancelled(&self) -> bool { matches!( @@ -96,27 +92,17 @@ impl LauncherError { ) } - /// Whether this failure reads as "the install is missing pieces", and so is - /// worth repairing rather than just reporting. + /// Whether the install is missing pieces and so is worth repairing /// - /// Something like - /// `An error occurred whilst accessing path '.../metadata/assets': No such - /// file or directory (os error 2)` is a dead end as a message: it names a - /// path the user did not create and cannot fix, when the actual answer is - /// that a file the install needs is not there and should be downloaded - /// again. - /// - /// Deliberately narrow. Only a genuine not-found reaches here: a permission - /// error, a full disk, or an unreachable network share would all survive a - /// repair unchanged, and re-downloading the game to rediscover that would - /// waste a lot of the user's time. + /// Deliberately narrow only a genuine not-found qualifies + /// Permission errors full disks and unreachable shares all survive a + /// repair unchanged #[must_use] pub fn indicates_missing_files(&self) -> bool { let mut source: Option<&(dyn std::error::Error + 'static)> = Some(self); - // Walk the chain rather than matching the top variant: by the time this - // surfaces it has usually been wrapped two or three layers deep, and the - // `io::Error` that actually carries the kind is at the bottom. + // Walk the chain the `io::Error` carrying the kind is usually wrapped + // two or three layers deep by the time this surfaces while let Some(err) = source { if let Some(io) = err.downcast_ref::() && io.kind() == std::io::ErrorKind::NotFound @@ -131,8 +117,6 @@ impl LauncherError { } pub trait SentryExclusion { - /// Whether this error is expected/environmental noise that should be kept out - /// of Sentry rather than reported as a crash. fn is_sentry_excluded(&self) -> bool { false } @@ -154,9 +138,8 @@ impl SentryExclusion for std::io::Error { fn is_sentry_excluded(&self) -> bool { use std::io::ErrorKind; - // Out of disk space. `ErrorKind::StorageFull` is still unstable, so match - // the raw OS codes instead. Codes are platform-gated so a Unix errno can't - // collide with a Windows code (112 is the unrelated EHOSTDOWN on Linux). + // `ErrorKind::StorageFull` is still unstable so match raw OS codes + // Platform-gated 112 is the unrelated EHOSTDOWN on Linux if let Some(code) = self.raw_os_error() { #[cfg(unix)] if code == 28 { @@ -171,7 +154,6 @@ impl SentryExclusion for std::io::Error { let _ = code; } - // Lost/refused network connections during downloads. matches!( self.kind(), ErrorKind::ConnectionRefused @@ -185,8 +167,7 @@ impl SentryExclusion for std::io::Error { impl SentryExclusion for reqwest::Error { fn is_sentry_excluded(&self) -> bool { - // Connectivity problems (offline, connection refused, timed out) are the - // user's network, not a launcher bug. + // Connectivity problems are the user's network not a launcher bug self.is_timeout() || self.is_connect() } } @@ -207,9 +188,8 @@ pub type LauncherResult = Result; mod tests { use super::*; - /// The OS-level "no such file or directory" the filesystem actually - /// returns, rather than a synthesized `ErrorKind`: they map to the same - /// kind, but only this one renders the `(os error 2)` text users report. + /// Raw OS error rather than a synthesized `ErrorKind` same kind but only + /// this one renders the `(os error 2)` text users report fn not_found(path: &str) -> LauncherError { LauncherError::IoError(polyio::IOError::PathIOError { source: std::io::Error::from_raw_os_error(2), @@ -219,8 +199,6 @@ mod tests { #[test] fn a_missing_asset_directory_asks_for_a_repair() { - // The exact shape users report: a path deep in the launcher's own - // metadata that they never created and cannot fix by hand. let path = "/Users/someone/Library/Application Support/org.Polyfrost.OneClient-dev/metadata/assets"; let err = not_found(path); @@ -231,8 +209,6 @@ mod tests { #[test] fn a_not_found_nested_several_layers_deep_is_still_found() { - // By the time this surfaces it has usually been wrapped on its way up, - // so matching only the outermost variant would miss it. let err = LauncherError::McError(oneclient_mc::McError::Io(polyio::IOError::PathIOError { source: std::io::Error::from(std::io::ErrorKind::NotFound), path: "metadata/libraries/foo.jar".to_string(), @@ -243,9 +219,6 @@ mod tests { #[test] fn failures_a_repair_could_not_fix_are_left_alone() { - // Re-downloading the entire game to rediscover that the disk is full, - // or that the folder is not writable, would waste a lot of the user's - // time and end at the same error. for kind in [ std::io::ErrorKind::PermissionDenied, std::io::ErrorKind::ConnectionRefused, diff --git a/packages/oneclient_core/src/game/diagnosis.rs b/packages/oneclient_core/src/game/diagnosis.rs index 7a8af08e..5298637c 100644 --- a/packages/oneclient_core/src/game/diagnosis.rs +++ b/packages/oneclient_core/src/game/diagnosis.rs @@ -1,27 +1,20 @@ -//! Reading a crash out of the game's own output. -//! -//! Minecraft's exit code says only that it died. The log says why, and some of -//! the reasons are ones the launcher can fix — so it is worth reading rather -//! than handing the user a stack trace and an exit status. +//! Minecraft's exit code says only that it died the log says why and some +//! reasons are ones the launcher can fix use std::sync::{Arc, Mutex}; -/// A crash cause the launcher recognises and can act on. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CrashDiagnosis { - /// The JVM could not read a jar: truncated, wrong bytes, or not a zip at - /// all. On the classpath this is nearly always a damaged library or mod, - /// which is exactly what verification repairs. + /// A jar the JVM could not read on the classpath this is nearly always a + /// damaged library or mod which verification repairs CorruptArchive { - /// The jar named in the message, when the JVM included one. Several of - /// these messages carry no path at all, so this is a bonus rather than - /// something to depend on. + /// Many of these messages carry no path so this is a bonus rather than + /// something to depend on file: Option, }, } impl CrashDiagnosis { - /// What to tell the user, phrased as the problem rather than the exception. #[must_use] pub fn body(&self) -> String { match self { @@ -37,11 +30,8 @@ impl CrashDiagnosis { } } -/// Signatures that all mean "a jar could not be read". -/// -/// `ZipException` is the JVM's own; the other two are what the launcher and the -/// `java -jar` front-end print for the same underlying damage. They share a -/// remedy, so they share a diagnosis. +/// Different sources printing the same underlying damage they share a remedy +/// so they share a diagnosis const CORRUPT_ARCHIVE_MARKERS: [&str; 4] = [ "java.util.zip.ZipException", "java.util.zip.ZipError", @@ -49,10 +39,8 @@ const CORRUPT_ARCHIVE_MARKERS: [&str; 4] = [ "zip END header not found", ]; -/// Reads one log line, returning a diagnosis when it recognises the failure. -/// -/// Runs against every line the game prints, so it stays a handful of substring -/// scans rather than anything that has to parse the line. +/// Runs against every line the game prints so it stays substring scans rather +/// than anything that parses the line #[must_use] pub fn diagnose(line: &str) -> Option { if !CORRUPT_ARCHIVE_MARKERS @@ -67,11 +55,8 @@ pub fn diagnose(line: &str) -> Option { }) } -/// Pulls a jar path out of a line, when the message happens to carry one. -/// -/// `ZipFile` reports "error in opening zip file" with no path on some JVMs and -/// with a full path on others, so this is opportunistic: naming the file is a -/// better message, but not naming it still leads to the same repair. +/// Opportunistic some JVMs omit the path entirely and a missing name still +/// leads to the same repair fn jar_in(line: &str) -> Option { let token = line .split(|c: char| c.is_whitespace() || c == '(' || c == ')') @@ -79,8 +64,8 @@ fn jar_in(line: &str) -> Option { let token = token.trim_end_matches(['.', ',', ':']); - // The full path is noise in a notification; the file name is what the user - // recognises and what they would go looking for. + // The full path is noise in a notification the file name is what the user + // recognises let name = token .rsplit(['/', '\\']) .next() @@ -90,11 +75,8 @@ fn jar_in(line: &str) -> Option { (!name.is_empty()).then_some(name) } -/// Remembers the first recognised crash cause seen in a session's output. -/// -/// The first rather than the last: a corrupt jar tends to cascade into pages of -/// unrelated failures, and the earliest recognised line is the closest thing to -/// a root cause the log offers. +/// Keeps the first recognised cause not the last a corrupt jar cascades into +/// unrelated failures so the earliest line is closest to the root cause #[derive(Clone, Default)] pub(crate) struct CrashWatch { found: Arc>>, @@ -106,8 +88,7 @@ impl CrashWatch { } pub(crate) fn observe(&self, line: &str) { - // Cheap rejection first: almost every line is ordinary game output, and - // this runs on all of them. + // Cheap rejection first this runs on every line the game prints if self.found.lock().is_ok_and(|found| found.is_some()) { return; } @@ -132,7 +113,6 @@ mod tests { #[test] fn a_zip_exception_is_recognised() { - // What the JVM prints when a jar on the classpath is damaged. let diagnosis = diagnose("java.util.zip.ZipException: error in opening zip file"); assert_eq!( diagnosis, @@ -173,8 +153,7 @@ mod tests { #[test] fn ordinary_game_output_is_not_a_crash() { - // This runs against every line the game prints, so a false positive - // would nag the user into re-downloading the game for nothing. + // A false positive would nag the user into re-downloading for nothing for line in [ "[Render thread/INFO]: Setting user: Dev", "[main/INFO]: Loading 42 mods", @@ -188,8 +167,6 @@ mod tests { #[test] fn the_watch_keeps_the_first_cause_not_the_last() { - // A corrupt jar cascades, so the earliest recognised line is the one - // closest to the root cause. let watch = CrashWatch::new(); watch.observe("[main/INFO]: Loading mods"); diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs index b3a6458f..0d42e1fd 100644 --- a/packages/oneclient_core/src/game/launch.rs +++ b/packages/oneclient_core/src/game/launch.rs @@ -62,10 +62,10 @@ pub async fn launch_cluster( let game_dir = existing.game_dir()?; let dedicated = existing.uses_dedicated_dir(); - // Every cluster without a dedicated folder plays out of the same shared - // directory, so a second game there would materialize its content over the - // running one's. `dir_in_use_by` waves a cluster past its own session, which - // parallel launches make reachable, so the directory is checked outright. + // Non-dedicated clusters share one directory so a second game there would + // materialize over the running one's + // `dir_in_use_by` waves a cluster past + // its own session which parallel launches make reachable if !dedicated && let Some(other) = state.games.dir_in_use(&game_dir) { tracing::warn!(cluster_id, other, "shared game dir busy; refusing launch"); let name = running_cluster_name(state, other).await; @@ -223,19 +223,15 @@ pub async fn launch_cluster( tracing::warn!(cluster_id, error = %err, "failed to write allowed_symlinks.txt"); } - // The one moment nothing is holding the content open: put what the database - // says into the game dir, and clear out what it no longer says. This is - // where a package removed or disabled mid-session actually leaves the - // folder, for dedicated and shared clusters alike. + // The one moment nothing holds the content open so this is where a package + // removed or disabled mid-session actually leaves the folder if let Err(err) = crate::game::materialize_content(&state.services, &cluster, &cwd).await { tracing::warn!(cluster_id, error = %err, "failed to materialize cluster content"); } if !dedicated { - // Redirect the shared dir's `logs`/`crash-reports` into this cluster's - // own folder so its output is attributable while it plays; unlinked on - // exit. Keeps the shared `.minecraft` (and the launcher's own logs dir) - // free of another cluster's leftovers. + // Redirects the shared dir's `logs`/`crash-reports` into this cluster's + // folder so output is attributable unlinked on exit crate::game::link_cluster_logs(&cluster, &cwd).await; } @@ -311,15 +307,10 @@ pub async fn launch_cluster( polyio::create_dir_all(parent).await.ok(); } - // The game's output goes straight to the log file rather than through pipes - // held by the launcher. A pipe would tie the game's lifetime to ours: once - // the launcher exits its read end closes, and the game's next write to a - // broken stdout takes it down with us. Writing to a file keeps the two - // independent, and the launcher tails that file for the live console. - // A cloned handle shares the file offset, so stdout and stderr interleave - // into one stream instead of overwriting each other. - // `Stdio` needs owned std handles, so the tokio files are unwrapped only - // once the async work of opening and cloning them is done. + // A file not a pipe a pipe would die with the launcher and take the game's + // next stdout write down with it + // The cloned handle shares the file offset + // so stdout and stderr interleave instead of overwriting let handles = match tokio::fs::File::create(&log_path).await { Ok(out) => match out.try_clone().await { Ok(err) => Ok((out.into_std().await, err.into_std().await)), @@ -356,8 +347,8 @@ pub async fn launch_cluster( let recorder = SessionRecorder::start(state, cluster_id, profile.mem_max.unwrap_or(2048), &java).await; - // Pin the process to the session row so that if the launcher exits before - // the game does, the next start can tell whether it is still playing. + // Pinned to the session row so that if the launcher exits first the next + // start can tell whether the game is still playing if let (Some(recorder), Some(pid)) = (recorder.as_ref(), pid) { recorder .record_process(pid, crate::game::process_start_time(pid)) @@ -424,8 +415,6 @@ pub async fn launch_cluster( Ok(LaunchedGame { cluster_id, pid }) } -/// Names the cluster in the user's way for a refusal they have to act on. -/// Falls back to the id, which is still better than no answer at all. async fn running_cluster_name(state: &Arc, cluster_id: i64) -> String { state .clusters @@ -434,9 +423,8 @@ async fn running_cluster_name(state: &Arc, cluster_id: i64) -> St .map_or_else(|_| format!("Cluster {cluster_id}"), |cluster| cluster.name) } -/// Cut the game loose from the launcher's process group / console, so signals -/// aimed at the launcher (a terminal Ctrl-C, a console window closing) don't -/// reach the game as collateral. +/// Cuts the game loose from the launcher's process group/console so signals +/// aimed at the launcher (Ctrl-C console close) don't reach the game fn detach(command: &mut Command) { #[cfg(unix)] { @@ -455,18 +443,15 @@ fn detach(command: &mut Command) { } } -/// How a session ended, as far as anyone could tell. pub(crate) enum Exit { - /// The launcher was there and saw the process exit. Observed { code: Option, success: bool, display: String, }, - /// Waiting on the process itself failed. Failed(String), - /// The game exited while the launcher was closed; the time was recovered - /// from its log, and the exit code is gone for good. + /// Game exited while the launcher was closed time recovered from the log + /// exit code unrecoverable Inferred, } @@ -474,22 +459,17 @@ pub(crate) struct SessionEnd { pub started_at: DateTime, pub ended_at: DateTime, pub outcome: Exit, - /// Whether this session is the one currently holding the cluster's running - /// slot. A stale session recovered from the database may share its cluster - /// with a game that is playing right now. Booking the old session's - /// playtime is right, but clearing that slot would report the live game as - /// exited. + /// False for a stale session recovered from the database whose cluster has a + /// live game book its playtime but clearing the slot would report the live + /// game as exited pub owns_slot: bool, - /// What the game's own output said about why it died, when the launcher - /// recognised the cause. `None` for a clean exit, an unrecognised crash, or - /// a session recovered after the fact with no log being watched. + /// `None` for a clean exit an unrecognised crash or a session recovered + /// after the fact with no log watched pub diagnosis: Option, } -/// Everything that has to happen once a game is gone: clear its running state, -/// bank the playtime, close the session row, run the post hook and unwind the -/// shared-directory plumbing. Shared by the live exit path and by recovery of -/// sessions that outlived the launcher. +/// Shared by the live exit path and by recovery of sessions that outlived the +/// launcher pub(crate) async fn finalize_session( state: &Arc, cluster: &Cluster, @@ -529,10 +509,8 @@ pub(crate) async fn finalize_session( run_hook(post_hook, cwd).await; if dedicated { - // The folder stays materialized between sessions so it remains a real - // Minecraft directory for external tools; the next launch reconciles it. - // Adopting drop-ins now just means the UI is right the moment the game - // closes rather than at the next launch. + // The folder stays materialized so it remains a real Minecraft directory + // for external tools adopting drop-ins now keeps the UI right on close crate::game::import_manual_content(&state.services, cluster, cwd).await; } else { if let Err(err) = crate::game::dematerialize_content(&state.services, cluster, cwd).await { @@ -557,25 +535,20 @@ pub(crate) async fn finalize_session( .services .events .notify("Game error").body(format!("{name}: {err}")).error().send(), - // Nothing was watching, so there is no crash to report and no news the - // user wants a popup about, so the session is just booked and closed. + // Nothing was watching so there is no crash to report Exit::Inferred => {} } - // Only after the crash notice, and only when the game actually died: a - // `ZipException` the game recovered from is not worth interrupting a - // finished session over. + // Only when the game actually died a `ZipException` it recovered from is not + // worth interrupting a finished session over if crashed && let Some(diagnosis) = end.diagnosis { offer_repair(state, cluster_id, &diagnosis).await; } } -/// Offers to verify and repair after a crash the launcher recognised. -/// -/// Asks rather than acting: verification reads every installed file and can -/// re-download a good part of the game, which is not something to start behind -/// the user's back the moment they close a crashed session. Declining is a real -/// answer — the same offer comes back on the next crash. +/// Asks rather than acting verification can re-download much of the game too +/// much to start unprompted +/// The offer returns on the next crash #[tracing::instrument(skip(state, diagnosis), level = "debug")] pub async fn offer_repair( state: &Arc, @@ -601,8 +574,7 @@ pub async fn offer_repair( match answer { Ok(Some(_)) => {} - // Dismissed, or nobody was there to ask. Neither is consent to spend - // several minutes re-downloading the game. + // Dismissed or nobody there to ask neither is consent to re-download Ok(None) => { tracing::info!(cluster_id, "user declined the post-crash repair"); return; diff --git a/packages/oneclient_core/src/game/log_replay.rs b/packages/oneclient_core/src/game/log_replay.rs index 283a9f62..46a04d2f 100644 --- a/packages/oneclient_core/src/game/log_replay.rs +++ b/packages/oneclient_core/src/game/log_replay.rs @@ -1,24 +1,17 @@ -//! Reconstructing a game session from its log. -//! -//! When the launcher is closed while the game keeps playing, nothing observes -//! the exit. The log is the only witness left, so on the next start we replay -//! it to recover when the session ended and which servers it visited. -//! -//! Minecraft logs a bare wall-clock time (`[12:29:00]`) in the machine's local -//! zone: no date, no offset. Absolute timestamps therefore have to be rebuilt -//! by anchoring at the session start and walking forward. +//! Minecraft logs a bare local wall-clock time (`[12:29:00]`) no date no +//! offset Absolute timestamps must be rebuilt by anchoring at the session +//! start and walking forward use chrono::{DateTime, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; use super::session::{ServerJoin, parse_server_join}; /// A backwards jump larger than this means the clock wrapped past midnight -/// rather than lines merely arriving out of order between threads. +/// rather than lines merely arriving out of order between threads const ROLLOVER_SLACK_SECS: i64 = 12 * 60 * 60; -/// Lines that mean "no longer connected to the server we were tracking". -/// Absent any of these the span is closed by the next join, or by the session -/// end. A missing marker costs accuracy, never correctness. +/// Absent any of these the span is closed by the next join or the session end +/// so a missing marker costs accuracy never correctness const LEAVE_MARKERS: &[&str] = &[ "Stopping!", "Stopping worker threads", @@ -28,19 +21,19 @@ const LEAVE_MARKERS: &[&str] = &[ "Disconnected from server", ]; -/// The clean-shutdown marker. Its timestamp is the truest exit time available. +/// Clean-shutdown marker its timestamp is the truest exit time available const STOP_MARKER: &str = "Stopping!"; pub(crate) fn parse_log_time(line: &str) -> Option { let stamp = line.strip_prefix('[')?.split(']').next()?.trim(); - // `%.f` also matches an empty fraction, so this covers `[12:29:00]` and - // `[12:29:00.123]` alike. + // `%.f` also matches an empty fraction covering both `[12:29:00]` and + // `[12:29:00.123]` if let Ok(time) = NaiveTime::parse_from_str(stamp, "%H:%M:%S%.f") { return Some(time); } - // Some pack-shipped log4j configs prepend the date. + // Some pack-shipped log4j configs prepend the date for fmt in ["%Y-%m-%d %H:%M:%S%.f", "%d.%m.%Y %H:%M:%S%.f"] { if let Ok(dt) = NaiveDateTime::parse_from_str(stamp, fmt) { return Some(dt.time()); @@ -58,12 +51,10 @@ fn local_to_utc(date: NaiveDate, time: NaiveTime) -> DateTime { let naive = NaiveDateTime::new(date, time); match Local.from_local_datetime(&naive) { LocalResult::Single(dt) => dt.with_timezone(&Utc), - // DST end: the same wall-clock time happens twice. The first is the - // better guess for a forward-walking log. + // DST end the time happens twice the first fits a forward-walking log LocalResult::Ambiguous(first, _) => first.with_timezone(&Utc), - // DST start: this wall-clock time never existed, so step over the gap - // rather than reading the local time as if it were UTC, which would be wrong - // by the zone's whole offset. + // DST start this time never existed so step over the gap rather than + // reading it as UTC which would be off by the zone's whole offset LocalResult::None => Local .from_local_datetime(&(naive + chrono::Duration::hours(1))) .earliest() @@ -72,12 +63,11 @@ fn local_to_utc(date: NaiveDate, time: NaiveTime) -> DateTime { } } -/// Turns the log's bare times into absolute instants by walking forward from an -/// anchor and advancing the date whenever the clock wraps. +/// Turns bare times into absolute instants advancing the date on wrap pub(crate) struct LogClock { date: NaiveDate, - /// High-water mark, not the previous line: threads interleave, so a line - /// may legitimately sit a second behind the one before it. + /// High-water mark not the previous line threads interleave so a line + /// may legitimately sit behind the one before it peak: NaiveTime, } @@ -112,19 +102,17 @@ pub(crate) struct ServerSpan { #[derive(Debug, Default, Clone)] pub(crate) struct LogReplay { - /// Timestamp of the last line carrying one, a lower bound on the exit. + /// Timestamp of the last line carrying one a lower bound on the exit pub last_activity: Option>, - /// Set when the game logged a clean shutdown. pub stopped_at: Option>, pub servers: Vec, } -/// Walk a whole session log, recovering server spans and the last sign of life. pub(crate) fn replay(content: &str, started_at: DateTime) -> LogReplay { let mut clock = LogClock::new(started_at); let mut out = LogReplay::default(); - // Timestamps only appear on the first line of a multi-line entry (stack - // traces continue underneath), so carry the last one forward. + // Timestamps appear only on the first line of a multi-line entry so carry + // the last one forward let mut now = started_at; for line in content.lines() { @@ -231,7 +219,7 @@ mod tests { fn clock_tolerates_out_of_order_threads() { let mut clock = LogClock::new(anchor()); clock.absolute(NaiveTime::from_hms_opt(12, 29, 8).unwrap()); - // A second thread's line lands a second late; this must not add a day. + // A second thread's line lands a second late this must not add a day let at = clock.absolute(NaiveTime::from_hms_opt(12, 29, 7).unwrap()); assert_eq!(at, local_at(12, 29, 7)); } diff --git a/packages/oneclient_core/src/game/mod.rs b/packages/oneclient_core/src/game/mod.rs index 41a02b55..39bd11a7 100644 --- a/packages/oneclient_core/src/game/mod.rs +++ b/packages/oneclient_core/src/game/mod.rs @@ -12,8 +12,6 @@ pub use analytics::{ Analytics, DayPlaytime, Persona, PlaytimeStats, ServerStat, WEEKDAY_LABELS, aggregate_servers, cluster_analytics, global_analytics, }; -// Minecraft content lives in `oneclient_mc` now; re-exported so the rest of the -// launcher keeps addressing it through `game::`. pub use oneclient_mc::{ append_profile_game_arguments, classpaths, download_to_path, fetch_bytes_verified, get_classpath_library, get_library, java_arguments, main_class, minecraft_arguments, diff --git a/packages/oneclient_core/src/game/process.rs b/packages/oneclient_core/src/game/process.rs index 335fc674..211e1935 100644 --- a/packages/oneclient_core/src/game/process.rs +++ b/packages/oneclient_core/src/game/process.rs @@ -17,29 +17,28 @@ fn probe(pid: u32) -> Option<(System, Pid)> { Some((sys, pid)) } -/// Process start time in unix seconds. Pids get recycled, this pins identity. +/// Unix seconds Pids get recycled so this is what pins process identity pub fn process_start_time(pid: u32) -> Option { let (sys, pid) = probe(pid)?; Some(sys.process(pid)?.start_time()) } -/// Whether `pid` is still the process we launched. A matching pid whose start -/// time differs is a recycled pid belonging to someone else, never ours. +/// A matching pid whose start time differs is a recycled pid belonging to +/// someone else never ours pub fn is_process_alive(pid: u32, started_at: Option) -> bool { let Some(actual) = process_start_time(pid) else { return false; }; match started_at { Some(expected) => actual == expected, - // Pre-migration sessions have no recorded start time. Liveness alone is - // a weaker signal, but treating the game as running is the safer error: - // worst case the exit time is recovered on a later start. + // Pre-migration sessions have no recorded start time assuming running + // is the safer error as the exit time is recovered on a later start None => true, } } -/// Ask a process to exit. Used for games re-adopted after a launcher restart, -/// where no `Child` handle survives to kill through. +/// For games re-adopted after a launcher restart where no `Child` handle +/// survives to kill through pub fn kill_process(pid: u32) -> bool { let Some((sys, pid)) = probe(pid) else { return false; @@ -47,7 +46,7 @@ pub fn kill_process(pid: u32) -> bool { let Some(process) = sys.process(pid) else { return false; }; - // Prefer a graceful terminate so the game can save and shut down cleanly. + // Prefer a graceful terminate so the game can save before shutting down process .kill_with(Signal::Term) .unwrap_or_else(|| process.kill()) @@ -118,8 +117,8 @@ impl GameProcessManager { self.dirs.lock().unwrap().insert(cluster_id, dir); } - /// Like [`Self::dir_in_use_by`], but counts a cluster's own session too. The - /// shared game directory holds one game at a time, whoever it belongs to. + /// Like [`Self::dir_in_use_by`] but counts a cluster's own session too the + /// shared game directory holds one game at a time whoever it belongs to pub fn dir_in_use(&self, dir: &Path) -> Option { self.dirs .lock() diff --git a/packages/oneclient_core/src/game/reattach.rs b/packages/oneclient_core/src/game/reattach.rs index c83bc3e4..738fa103 100644 --- a/packages/oneclient_core/src/game/reattach.rs +++ b/packages/oneclient_core/src/game/reattach.rs @@ -1,13 +1,5 @@ -//! Picking up game sessions that outlived the launcher. -//! -//! Games are spawned detached, so closing the launcher no longer takes the game -//! down with it. That leaves two loose ends to tidy on the next start, both of -//! them sessions whose row still has no `ended_at`: -//! -//! * the game is still running, so re-adopt it and the UI shows it as playing -//! with its exit recorded properly when it finally happens; -//! * the game already exited unobserved, and its log is the only witness, so -//! replay it to recover when it ended and which servers it visited. +//! Games are spawned detached so sessions can outlive the launcher they are +//! settled on the next start (rows with no `ended_at`) use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -27,12 +19,10 @@ use crate::game::tail::spawn_log_tail; use oneclient_events::LaunchStage; use crate::state::LauncherState; -/// How often to check whether a re-adopted game is still alive. It is only a -/// fallback for a process we don't own, so a slow poll is plenty. +/// Only a fallback for a process we don't own so a slow poll is plenty const LIVENESS_POLL: Duration = Duration::from_secs(3); -/// Settle every session left open by a previous launcher run. Safe to call once -/// at startup, before anything can launch a game. +/// Call once at startup before anything can launch a game #[tracing::instrument(skip(state), level = "debug")] pub async fn recover_sessions(state: &Arc) { let sessions = match session_dao::unfinished_sessions(&state.services.db).await { @@ -63,9 +53,8 @@ pub async fn recover_sessions(state: &Arc) { } } - // Re-adopt the live games first. Reconciling an exited session tears down - // its shared game directory, which a still-running cluster may be sharing. - // That check reads the live registrations, so they have to go in first. + // Live games first reconciling an exited session tears down a shared game + // dir and that check reads the live registrations for (cluster, session, started_at, pid) in live { readopt(state, cluster, session, started_at, pid).await; } @@ -75,15 +64,13 @@ pub async fn recover_sessions(state: &Arc) { } } -/// Resolve a session to its cluster and decide whether its process is alive. -/// `None` means the session was closed out here and needs no further work. +/// `None` means the session was closed out here and needs no further work async fn classify( state: &Arc, session: UnfinishedSession, ) -> Option<(Cluster, UnfinishedSession, DateTime, Option)> { let cluster_id = session.cluster_id; - // A session this launcher already owns needs no recovery. if state.games.is_active(cluster_id) { return None; } @@ -97,8 +84,8 @@ async fn classify( } }; - // The cluster is gone, so there is no log to consult and no playtime worth - // attributing. Just close the row so it stops being reconsidered forever. + // No cluster means no log and no playtime worth attributing close the row + // so it stops being reconsidered forever let Ok(cluster) = state.clusters.get(cluster_id).await else { tracing::warn!(cluster_id, "cluster missing for unfinished session; closing it"); close_untraceable(state, &session).await; @@ -113,8 +100,7 @@ async fn classify( Some((cluster, session, started_at, alive)) } -/// Close a session we can say nothing about. Ending it at its start time books -/// no playtime, which beats inventing some. +/// Ends at the start time which books no playtime rather than inventing some async fn close_untraceable(state: &Arc, session: &UnfinishedSession) { if let Err(err) = session_dao::finish_session_at( &state.services.db, @@ -128,7 +114,6 @@ async fn close_untraceable(state: &Arc, session: &UnfinishedSessi } } -/// The game is still playing. Wire it back up as though we had launched it. #[tracing::instrument(skip(state, cluster, session), fields(cluster_id = cluster.id, pid), level = "debug")] async fn readopt( state: &Arc, @@ -163,9 +148,8 @@ async fn readopt( let Ok(log_path) = oneclient_cluster::logs::cluster_output_log(&cluster) else { return; }; - // A re-adopted game is still writing to its log, so its crashes are still - // worth reading even though the exit code went with the launcher that - // spawned it. + // A re-adopted game still writes its log so crashes stay readable even + // though the exit code went with the launcher that spawned it let crash_watch = crate::game::diagnosis::CrashWatch::new(); let tail = spawn_log_tail( cluster_id, @@ -178,8 +162,7 @@ async fn readopt( let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::<()>(); state.games.register_kill(cluster_id, kill_tx); - // The post hook belongs to whichever settings apply now; resolve it up front - // so the watcher task doesn't have to reach back into the state. + // Resolved up front so the watcher task doesn't reach back into the state let global = state.settings.read().global_game_settings.clone(); let post_hook = state.clusters.resolve_settings(&global, &cluster) .await @@ -209,14 +192,10 @@ async fn readopt( Some(recorder), SessionEnd { started_at, - // We watched this one die, so the time is real even though the - // exit code went with the launcher that spawned it. ended_at: Utc::now(), outcome: Exit::Inferred, - // Only tear down the cluster's running state if it is still - // ours. With parallel clusters enabled the user can start a - // fresh game on this cluster while the re-adopted one plays on, - // and that newer game now owns the slot. + // With parallel clusters a newer game may already own this + // cluster's slot only tear down if it is still ours owns_slot: state.games.pid(cluster_id) == Some(pid), diagnosis: crash_watch.take(), }, @@ -231,7 +210,6 @@ async fn wait_for_exit(pid: u32, pid_started_at: Option) { } } -/// The game exited while nothing was watching. Rebuild what we can from the log. #[tracing::instrument(skip(state, cluster, session), fields(cluster_id = cluster.id), level = "debug")] async fn reconcile( state: &Arc, @@ -247,9 +225,8 @@ async fn reconcile( .map(|log| log_replay::replay(&log.content, started_at)) .unwrap_or_default(); - // Prefer the log's own testimony over the file's mtime: a clean shutdown - // says exactly when it happened, and the last timestamped line is the last - // proof of life. mtime only helps when nothing in the log carries a time. + // Prefer the log's own timestamps over mtime mtime only helps when nothing + // in the log carries a time let ended_at = replay .stopped_at .or(replay.last_activity) @@ -278,9 +255,8 @@ async fn reconcile( return; }; - // Unwinding the shared directory is only safe if nobody is playing in it. - // A cluster re-adopted moments ago may share this exact directory, and - // clearing its content mid-session would pull the game apart underneath it. + // A cluster re-adopted moments ago may share this directory clearing it + // mid-session would pull that game apart underneath it let shared_dir_busy = state.games.dir_in_use_by(&cwd, cluster_id).is_some(); if shared_dir_busy { tracing::debug!(cluster_id, "shared game dir still in use; skipping exit cleanup"); @@ -291,29 +267,25 @@ async fn reconcile( &cluster, &cwd, cluster.uses_dedicated_dir() || shared_dir_busy, - // The post hook is skipped on purpose. It is an *on exit* hook, and the exit - // already happened, possibly days ago. Firing it now, during startup, would - // surprise anyone whose hook does something real. + // Post hook skipped on purpose the exit already happened possibly days + // ago so firing it during startup would surprise the user None, Some(recorder), SessionEnd { started_at, ended_at, outcome: Exit::Inferred, - // This session is over, but the cluster may have a newer one playing right - // now, so only claim the slot if nothing else holds it. + // The cluster may have a newer session playing right now so only + // claim the slot if nothing else holds it owns_slot: !state.games.is_active(cluster_id), - // Nothing was watching this session's log while it ran, so there is - // no crash to have recognised. + // Nothing watched this log while it ran so no crash was recognised diagnosis: None, }, ) .await; } -/// Rewrite a session's server spans from the log replay, which unlike the live -/// rows covers the whole session including the part we missed. Returns the -/// `joined_at` of a span still left open, for the caller to close. +/// Returns the `joined_at` of a span still left open for the caller to close async fn apply_spans( state: &Arc, session_id: &str, @@ -324,9 +296,8 @@ async fn apply_spans( .await .unwrap_or_default(); - // A log that yields fewer spans than we already recorded has been rotated or - // truncated. Live rows are then the better record; keep them and just report - // whichever is still open so it gets closed at the session's end. + // Fewer replayed spans than recorded means the log was rotated or truncated + // so the live rows are the better record keep them if spans.len() < existing.len() { tracing::debug!( session = %session_id, @@ -390,9 +361,8 @@ struct SessionLog { modified: Option>, } -/// Read whichever log covers the session. `cluster-output.log` is preferred: the -/// launcher truncates it at every launch, so it belongs to exactly one session, -/// whereas `latest.log` is the game's own and may already have been rotated. +/// `cluster-output.log` is preferred it is truncated at every launch so it +/// belongs to one session whereas `latest.log` may already have rotated async fn read_session_log(cluster: &Cluster) -> Option { let mut candidates: Vec = Vec::new(); if let Ok(path) = oneclient_cluster::logs::cluster_output_log(cluster) { diff --git a/packages/oneclient_core/src/game/session.rs b/packages/oneclient_core/src/game/session.rs index 75e85faf..57c0d27b 100644 --- a/packages/oneclient_core/src/game/session.rs +++ b/packages/oneclient_core/src/game/session.rs @@ -15,7 +15,7 @@ pub(crate) struct ServerJoin { pub port: Option, } -/// The session row's identity: its `started_at`, which is the primary key. +/// The session row's `started_at` which is its primary key pub(crate) type SessionId = String; pub(crate) fn parse_server_join(line: &str) -> Option { @@ -88,8 +88,8 @@ impl SessionRecorder { }) } - /// Re-attach to a session row left open by a previous launcher run, so a - /// game that outlived the launcher keeps recording into the same session. + /// Re-attaches to a row left open by a previous launcher run so a game that + /// outlived the launcher keeps recording into the same session pub(crate) fn resume(db: DbPool, session_started_at: SessionId, open_server: Option) -> Self { Self { session_started_at, @@ -98,16 +98,16 @@ impl SessionRecorder { } } - /// When the session row says it began. Playtime is measured against this so - /// it agrees with the session span analytics reads back out of the row. + /// Playtime is measured against this so it agrees with the session span + /// analytics reads back out of the row pub(crate) fn started_at(&self) -> Option> { chrono::DateTime::parse_from_rfc3339(&self.session_started_at) .ok() .map(|at| at.with_timezone(&chrono::Utc)) } - /// Remember which OS process backs this session. Without it, a launcher - /// restart cannot tell a live game from one that exited unobserved. + /// Without this a launcher restart cannot tell a live game from one that + /// exited unobserved #[tracing::instrument(skip(self), level = "debug")] pub(crate) async fn record_process(&self, pid: u32, pid_started_at: Option) { if let Err(err) = session_dao::set_session_process( @@ -160,8 +160,8 @@ impl SessionRecorder { } } - /// Close the session. The time is explicit because an exit is not always - /// observed as it happens; one recovered from a log ended in the past. + /// The time is explicit because an exit is not always observed as it happens + /// one recovered from a log ended in the past #[tracing::instrument(skip(self), fields(exit_code), level = "debug")] pub(crate) async fn finish_at(self, ended_at: &str, exit_code: Option) { if let Some(open) = self.open_server.lock().await.take() diff --git a/packages/oneclient_core/src/game/shared_dir.rs b/packages/oneclient_core/src/game/shared_dir.rs index 569b24d0..eb837337 100644 --- a/packages/oneclient_core/src/game/shared_dir.rs +++ b/packages/oneclient_core/src/game/shared_dir.rs @@ -24,12 +24,11 @@ const SWAP_TYPES: [ContentType; 3] = [ const FABRIC_DEP_OVERRIDES: &str = "config/fabric_loader_dependencies.json"; -/// One file the database wants in the game directory. struct Desired { content_type: ContentType, file_name: String, hash: String, - /// Where it lives in the artifact cache. + /// Where it lives in the artifact cache src: std::path::PathBuf, } @@ -39,10 +38,8 @@ impl Desired { } } -/// Brings the game directory in line with the database, ready for a launch. -/// -/// Safe to run over a directory left behind by a crashed session, by a -/// different cluster, or by a launcher version that predates the manifest. +/// Safe to run over a directory left by a crashed session another cluster or +/// a launcher version predating the manifest #[tracing::instrument(skip(services, cluster), fields(cluster_id = cluster.id, game_dir = %game_dir.display()), level = "debug")] pub async fn materialize_content( services: &LauncherServices, @@ -52,34 +49,29 @@ pub async fn materialize_content( let dedicated = cluster.uses_dedicated_dir(); polyio::create_dir_all(game_dir).await.ok(); - // What we put here last time. In the shared directory this often belongs to - // another cluster, which is why every use of it checks the id. + // In the shared directory this often belongs to another cluster so every + // use of it checks the id let previous = manifest::load(game_dir).await; import_manual_content(services, cluster, game_dir).await; - // Before the folder is built, not after: a cluster holding several versions - // of one package would otherwise hand the game every enabled copy, and for - // mods that is a classloader conflict rather than a choice. Resolving to the - // newest here means the rest are simply not desired. + // Before the folder is built not after handing the game several enabled + // versions of one mod is a classloader conflict if let Err(err) = oneclient_content::bundles::reconcile_duplicate_activity( cluster.id, &services.content(), ) .await { - // A launch is not worth blocking over this. The duplicates were already - // there and the game still gets a folder, just an untidy one. + // Not worth blocking a launch the duplicates were already there tracing::warn!(cluster_id = cluster.id, %err, "failed to resolve duplicate package versions"); } let desired = desired_content(services, cluster).await?; let desired_paths: HashSet = desired.iter().map(Desired::relative_path).collect(); - // Anything we materialized before and no longer want goes now, while the - // game is still closed. This is what lands a package the user removed (or - // disabled) during a session, and what clears another cluster's content out - // of the shared directory. + // While the game is still closed this is what lands a package removed + // mid-session and clears another cluster's content from the shared dir prune_previous(game_dir, previous.as_ref(), &desired_paths).await; if !dedicated { @@ -92,8 +84,8 @@ pub async fn materialize_content( let stash = cluster.dir()?.join(content_type.folder_name()); polyio::create_dir_all(&dir).await.ok(); - // Whatever is still here belongs to whoever played last (or crashed - // last), so take it into this cluster rather than deleting it. + // Whatever is still here belongs to whoever played (or crashed) + // last so take it into this cluster rather than deleting it let ours = ours_in_folder(content_type, &linked, previous.as_ref()); stash_content_files(&dir, &stash, &ours).await; ensure_note(&dir, content_type).await; @@ -109,18 +101,16 @@ pub async fn materialize_content( Ok(()) } -/// Empties the shared game directory after a session. -/// -/// Only for shared directories: a dedicated cluster's content stays put between -/// sessions, and [`materialize_content`] reconciles it at the next launch. +/// Only for shared directories a dedicated cluster's content stays put between +/// sessions and [`materialize_content`] reconciles it at the next launch #[tracing::instrument(skip(services, cluster), fields(cluster_id = cluster.id), level = "debug")] pub async fn dematerialize_content( services: &LauncherServices, cluster: &Cluster, game_dir: &Path, ) -> LauncherResult<()> { - // Runs first so anything the user dropped in during the session is a tracked - // artifact by now, and gets dropped rather than stashed as a loose file. + // Runs first so anything dropped in during the session is a tracked artifact + // by now and gets dropped rather than stashed as a loose file import_manual_content(services, cluster, game_dir).await; let current = manifest::load(game_dir).await; @@ -142,7 +132,6 @@ pub async fn dematerialize_content( Ok(()) } -/// The enabled, cached content the database wants materialized. async fn desired_content( services: &LauncherServices, cluster: &Cluster, @@ -177,10 +166,8 @@ async fn desired_content( Ok(desired) } -/// Links the wanted content in, returning what actually landed. -/// -/// Only files that made it are recorded, so a link that failed is never later -/// mistaken for ours and deleted out from under the user. +/// Only files that made it are recorded so a failed link is never later +/// mistaken for ours and deleted out from under the user async fn link_desired(game_dir: &Path, desired: &[Desired]) -> Vec { let mut entries = Vec::with_capacity(desired.len()); @@ -205,10 +192,8 @@ async fn link_desired(game_dir: &Path, desired: &[Desired]) -> Vec, @@ -234,12 +219,9 @@ async fn prune_previous( } } -/// The names in one content folder that are the launcher's rather than the -/// user's: what we materialized last, plus everything the database currently -/// tracks for this cluster. -/// -/// The database half covers files just adopted by [`import_manual_content`], -/// which are the user's in origin but the launcher's to manage from now on. +/// Names in one folder that are the launcher's not the user's what we +/// materialized last plus what the database tracks (covering files just +/// adopted by [`import_manual_content`]) fn ours_in_folder( content_type: ContentType, linked: &[oneclient_content::packages::LinkedArtifactInfo], @@ -312,18 +294,15 @@ pub async fn import_manual_content( continue; } - // Ours, from a previous launch. Either it is still wanted and gets - // relinked, or it was removed and `prune_previous` clears it. Taking - // it as a user drop-in here would reinstall the package the user - // just deleted. + // Ours from a previous launch `prune_previous` handles it + // Taking it as a user drop-in would reinstall a just-deleted package let relative = manifest::entry_path(content_type.folder_name(), name); if manifest.as_ref().is_some_and(|m| m.contains(&relative)) { continue; } - // No manifest to consult — a directory written by a launcher version - // that predates it. Fall back to asking whether the cache already - // holds this exact file, which means the launcher put it here. + // No manifest a directory from a launcher version predating it + // If the cache holds this exact file the launcher put it here if manifest.is_none() && is_cached_artifact(services, &path).await { tracing::debug!(file = name, "discarding stale launcher content in game dir"); if let Err(err) = polyio::remove_file(&path).await { @@ -346,9 +325,8 @@ pub async fn import_manual_content( } } -/// Whether `path` is a copy of something already in the launcher's artifact -/// cache, i.e. content the launcher itself put in the game dir rather than -/// something the user dropped there. +/// Already in the artifact cache i.e. the launcher put it in the game dir +/// rather than the user dropping it there async fn is_cached_artifact(services: &LauncherServices, path: &Path) -> bool { let Ok(hash) = polyio::sha1_file(path).await else { return false; @@ -386,15 +364,9 @@ pub async fn write_allowed_symlinks(game_dir: &Path) -> LauncherResult<()> { const EMPTY_NOTE_NAME: &str = "WHY_NOTHING_HERE.txt"; -/// Empty a shared content folder without losing anything the game wrote into -/// it. -/// -/// Content the launcher owns is dropped, since the cache already holds it. -/// Everything else (a shaderpack's settings sidecar, an unzipped pack, a stray -/// config) is *moved* into the cluster's own copy of the folder, and -/// [`restore_stashed`] links it back on the next launch. That is what keeps -/// shader configs attached to the cluster they were tuned in instead of being -/// wiped by the next launch of any cluster. +/// Launcher-owned content is dropped (the cache has it) everything else +/// (sidecars unzipped packs stray configs) is *moved* into the cluster folder +/// so it stays attached to that cluster and [`restore_stashed`] links it back async fn stash_content_files(dir: &Path, stash: &Path, ours: &HashSet) { let Ok(mut entries) = polyio::read_dir(dir).await else { return; @@ -413,9 +385,8 @@ async fn stash_content_files(dir: &Path, stash: &Path, ours: &HashSet) { continue; }; - // Ours: either a link we made this launch, or a name the manifest or the - // database accounts for (on Windows `symlink_file` hard-links, so ours - // is not always a symlink). Either way the cache has it covered. + // Ours and the cache has it covered + // On Windows `symlink_file` hard-links so ours is not always a symlink if file_type.is_symlink() || ours.contains(&name) { remove_dir_or_file(&path, file_type).await; continue; @@ -432,10 +403,8 @@ async fn stash_content_files(dir: &Path, stash: &Path, ours: &HashSet) { } } -/// Link the cluster's stashed leftovers back into the shared folder so the -/// game finds them where it left them. The game writes straight through the -/// link, so edits land in the cluster folder even if we never get to run on -/// exit. +/// Links stashed leftovers back so the game writes straight through into the +/// cluster folder even if we never get to run on exit async fn restore_stashed( stash: &Path, dir: &Path, @@ -458,22 +427,16 @@ async fn restore_stashed( let Ok(file_type) = entry.file_type().await else { continue; }; - // A link into the artifact cache. Managed content is materialized from - // the database further down, not from here. + // A link into the artifact cache managed content is materialized from + // the database not from here if file_type.is_symlink() { continue; } - // A bare jar or pack in the stash is not something the stash should ever - // hold: the launcher's own content comes from the cache, and a file the - // user drops into the game folder is adopted into the cache before it - // could land here. What is left is a link written by a launcher version - // that put content in cluster folders — often one whose package has since - // been removed. Restoring it would put a deleted mod back in the game on - // every launch, which is the exact bug this scheme exists to end. - // - // Left on disk rather than deleted; the Storage settings page reports - // them and can clear them out. + // A bare jar in the stash is a leftover from the old scheme often with + // its package already removed restoring it would put a deleted mod back + // every launch + // Left on disk for the Storage settings page to clear if file_type.is_file() && has_content_extension(content_type, &name) { tracing::debug!( file = %name, @@ -503,7 +466,7 @@ async fn remove_dir_or_file(path: &Path, file_type: FileType) { if file_type.is_dir() { polyio::remove_dir_all(path).await.ok(); } else if polyio::remove_file(path).await.is_err() { - // A Windows junction has to go through `remove_dir`. + // A Windows junction has to go through `remove_dir` polyio::remove_symlink_dir(path).await.ok(); } } @@ -513,8 +476,8 @@ async fn move_entry(src: &Path, dest: &Path) -> LauncherResult<()> { polyio::create_dir_all(parent).await.ok(); } - // A stash left from an earlier session is always older than what the game - // just wrote, so it loses. + // A stash from an earlier session is older than what the game just wrote + // so it loses if let Ok(meta) = polyio::symlink_metadata(dest).await { remove_dir_or_file(dest, meta.file_type()).await; } @@ -523,7 +486,7 @@ async fn move_entry(src: &Path, dest: &Path) -> LauncherResult<()> { return Ok(()); } - // `rename` cannot cross devices; fall back to a copy for plain files. + // `rename` cannot cross devices fall back to a copy for plain files polyio::copy(src, dest).await?; polyio::remove_file(src).await.ok(); Ok(()) @@ -581,20 +544,17 @@ async fn redirect_dir(shared: &Path, target: &Path) -> LauncherResult<()> { polyio::create_dir_all(target).await.ok(); match polyio::symlink_metadata(shared).await { - // check if a symlink from a previous launch Ok(meta) if meta.file_type().is_symlink() => { polyio::remove_symlink_dir(shared).await?; } - // real directory (most likely either done by the user or a tool) - // so instead of deleting it we first move the contents of it into the designated - // cluster folder + // A real directory likely the user's so salvage its contents into the + // cluster folder instead of deleting it Ok(meta) if meta.is_dir() => { move_dir_contents(shared, target).await; polyio::remove_dir_all(shared).await.ok(); } - // some file, so remove it so the link can take its place. Ok(_) => { polyio::remove_file(shared).await.ok(); } @@ -692,8 +652,8 @@ mod tests { ) } - /// A shaderpack's settings sidecar has to survive a launch/exit cycle, and - /// end up in the cluster rather than the shared dir. + /// A shaderpack's settings sidecar has to survive a launch/exit cycle and + /// end up in the cluster rather than the shared dir #[tokio::test] async fn shader_settings_survive_a_session() { let root = polyio::testing::ScratchDir::new("shader_settings"); @@ -703,7 +663,6 @@ mod tests { let ours = names(&["bsl.zip"]); - // Play: the pack is linked in, the game writes its settings next to it. polyio::write(shared.join("bsl.zip"), b"pack".as_slice()) .await .unwrap(); @@ -711,7 +670,6 @@ mod tests { .await .unwrap(); - // Exit. stash_content_files(&shared, &stash, &ours).await; assert!(!shared.join("bsl.zip").exists(), "managed pack left behind"); assert!(!shared.join("bsl.zip.txt").exists(), "sidecar left behind"); @@ -720,14 +678,12 @@ mod tests { "BLOOM=off" ); - // Next launch. restore_stashed(&stash, &shared, ContentType::Shader, &ours).await; assert_eq!( polyio::read_to_string(shared.join("bsl.zip.txt")).await.unwrap(), "BLOOM=off" ); - // The game edits its settings through the link we restored. polyio::write(stash.join("bsl.zip.txt"), b"BLOOM=on".as_slice()) .await .unwrap(); @@ -739,7 +695,6 @@ mod tests { std::fs::remove_dir_all(root.path()).ok(); } - /// An unzipped pack is a directory; it moves out and comes back as a link. #[tokio::test] async fn unpacked_dirs_move_into_the_cluster() { let root = polyio::testing::ScratchDir::new("unpacked"); @@ -757,7 +712,7 @@ mod tests { restore_stashed(&stash, &shared, ContentType::Shader, &HashSet::new()).await; assert!(shared.join("Loose/shaders/final.fsh").exists()); - // Only the link goes, never the stashed original. + // Only the link goes never the stashed original stash_content_files(&shared, &stash, &HashSet::new()).await; assert!(!shared.join("Loose").exists()); assert!(stash.join("Loose/shaders/final.fsh").exists()); @@ -765,7 +720,6 @@ mod tests { std::fs::remove_dir_all(root.path()).ok(); } - /// The note explaining the empty folder is ours, and stays put. #[tokio::test] async fn note_is_never_stashed() { let root = polyio::testing::ScratchDir::new("note"); @@ -783,10 +737,10 @@ mod tests { std::fs::remove_dir_all(root.path()).ok(); } - /// The reported bug. A package removed while the game held it open stays in - /// the folder; the next launch has to clear it. `remove_entry` is used - /// rather than a link so the file is an ordinary one — which is what a - /// managed file looks like on Windows, where `symlink_file` hard-links. + /// The reported bug a package removed while the game held it open stays in + /// the folder so the next launch must clear it + /// An ordinary file is used + /// because that is what a managed file looks like on Windows #[tokio::test] async fn removed_package_is_pruned_at_the_next_launch() { let root = polyio::testing::ScratchDir::new("prune_removed"); @@ -809,8 +763,8 @@ mod tests { std::fs::remove_dir_all(root.path()).ok(); } - /// The shared game dir is reused by every cluster. Whatever the previous - /// cluster materialized is ours to clear, not the user's to keep. + /// Whatever a previous cluster materialized in the shared dir is ours to + /// clear not the user's to keep #[tokio::test] async fn another_clusters_content_is_cleared_not_stashed() { let root = polyio::testing::ScratchDir::new("cross_cluster"); @@ -820,8 +774,6 @@ mod tests { let jar = game_dir.join("mods").join("theirs.jar"); polyio::write(&jar, b"jar".as_slice()).await.unwrap(); - // Cluster 2 launches; cluster 1's manifest is what is on disk, and none - // of it is wanted now. prune_previous(game_dir, Some(&manifest_of(1, &["mods/theirs.jar"])), &HashSet::new()).await; assert!(polyio::symlink_metadata(&jar).await.is_err()); @@ -829,8 +781,8 @@ mod tests { std::fs::remove_dir_all(root.path()).ok(); } - /// A file the user dropped in by hand is not in any manifest, so it is - /// stashed into the cluster rather than deleted. + /// A hand-dropped file is in no manifest so it is stashed into the cluster + /// rather than deleted #[tokio::test] async fn user_files_are_never_pruned() { let root = polyio::testing::ScratchDir::new("user_file"); @@ -847,10 +799,8 @@ mod tests { std::fs::remove_dir_all(root.path()).ok(); } - /// The reported bug, in its stuck form. A jar left in a cluster folder by an - /// older launcher, whose package has since been removed, must not be linked - /// back into the game. It is left on disk — the Storage page clears it — but - /// it never reaches the session again. + /// A jar left in a cluster folder by an older launcher whose package has + /// since been removed must not be linked back into the game #[tokio::test] async fn stray_content_in_the_stash_is_not_restored() { let root = polyio::testing::ScratchDir::new("stray_content"); @@ -859,12 +809,11 @@ mod tests { polyio::create_dir_all(&shared).await.unwrap(); polyio::create_dir_all(&stash).await.unwrap(); - // A leftover from the old scheme: an ordinary file (which is what a - // Windows hard link looks like) with no database row to explain it. + // A leftover from the old scheme an ordinary file (which is what a + // Windows hard link looks like) with no database row to explain it polyio::write(stash.join("removed.jar"), b"jar".as_slice()) .await .unwrap(); - // A genuine sidecar, which still has to come back. polyio::write(stash.join("options.txt"), b"k=v".as_slice()) .await .unwrap(); @@ -884,13 +833,9 @@ mod tests { std::fs::remove_dir_all(root.path()).ok(); } - /// The cluster folder is the user's. A full launch/exit cycle may *add* to - /// it, never take from it. - /// - /// Pinned because the two halves are easy to get backwards: `stash` reads - /// the game dir and writes the cluster, `restore` reads the cluster and - /// writes the game dir. A swapped argument in either would quietly delete a - /// user's files, and both take two same-typed `&Path`s. + /// Pinned because the halves are easy to swap `stash` reads the game dir + /// and writes the cluster `restore` the reverse and both take two + /// same-typed `&Path`s a swap would quietly delete a user's files #[tokio::test] async fn a_session_never_removes_anything_from_the_cluster_folder() { let root = polyio::testing::ScratchDir::new("stash_is_sacred"); @@ -899,7 +844,6 @@ mod tests { polyio::create_dir_all(&shared).await.unwrap(); polyio::create_dir_all(&stash).await.unwrap(); - // Everything a cluster folder might legitimately be holding. polyio::write(stash.join("options.txt"), b"k=v".as_slice()) .await .unwrap(); @@ -911,7 +855,6 @@ mod tests { .await .unwrap(); - // And something in the game dir that is ours to clear. polyio::write(shared.join("managed.jar"), b"jar".as_slice()) .await .unwrap(); @@ -920,7 +863,6 @@ mod tests { let before = dir_entries(&stash).await; - // Launch, then exit, then launch again. stash_content_files(&shared, &stash, &ours).await; restore_stashed(&stash, &shared, ContentType::Mod, &ours).await; stash_content_files(&shared, &stash, &ours).await; @@ -960,8 +902,8 @@ mod tests { let ours = ours_in_folder(ContentType::Mod, &linked, Some(&manifest)); assert!(ours.contains("from_manifest.jar")); - // Scoped to the folder: a shaderpack entry must not make a mod of the - // same name look managed. + // Scoped to the folder a shaderpack entry must not make a mod of the + // same name look managed assert!(!ours.contains("bsl.zip")); } } diff --git a/packages/oneclient_core/src/game/tail.rs b/packages/oneclient_core/src/game/tail.rs index f7a8023d..5e5394f5 100644 --- a/packages/oneclient_core/src/game/tail.rs +++ b/packages/oneclient_core/src/game/tail.rs @@ -17,9 +17,8 @@ pub(crate) struct LogTail { } impl LogTail { - /// Stop tailing, but only after one last pass: the lines explaining *why* - /// the game exited are written moments before it does, and would be lost if - /// the tail stopped the instant the process did. + /// Stops only after one last pass the lines explaining why the game + /// exited are written moments before it does pub(crate) async fn stop(self) { self.stop.store(true, Ordering::Relaxed); let _ = self.handle.await; @@ -53,8 +52,8 @@ async fn tail( crash_watch: &CrashWatch, stop: &AtomicBool, ) -> std::io::Result<()> { - // The game may not have created the file yet on a re-adopt, and on launch it - // exists but is empty. Either way, wait for it rather than giving up. + // The game may not have created the file yet on a re-adopt so wait for it + // rather than giving up let file = loop { match tokio::fs::File::open(path).await { Ok(file) => break file, @@ -74,13 +73,13 @@ async fn tail( let read = reader.read_line(&mut line).await?; if read == 0 { - // A final pass has already run by the time the flag is seen here, - // since the read above returned EOF on a fully drained file. + // The read above returned EOF on a drained file so the final pass + // has already run by the time the flag is seen here if stop.load(Ordering::Relaxed) { return Ok(()); } - // The log is truncated when a cluster relaunches; if the file - // shrank, our offset is past the end and we would never read again. + // A relaunch truncates the log if the file shrank our offset is + // past the end and we would never read again let len = polyio::stat(path).await.map(|m| m.len()).unwrap_or(offset); if len < offset { reader.seek(std::io::SeekFrom::Start(0)).await?; @@ -92,12 +91,11 @@ async fn tail( offset += read as u64; - // A partial line means the game is mid-write; wait for the newline - // rather than emitting half a line and re-emitting the rest. + // A partial line means the game is mid-write wait for the newline + // rather than emitting half a line and re-emitting the rest later if !line.ends_with('\n') { - // Unless the game is already gone. A process killed mid-write - // leaves a newline that will never arrive, and waiting for it would - // hang the caller of `stop()` forever. + // Unless the game is gone a process killed mid-write leaves a + // newline that never arrives hanging the caller of `stop()` if stop.load(Ordering::Relaxed) { emit(cluster_id, &line, events, recorder, crash_watch).await; return Ok(()); @@ -119,14 +117,13 @@ async fn emit( recorder: Option<&SessionRecorder>, crash_watch: &CrashWatch, ) { - // Blank lines are kept: the game prints them, and the console should read - // the way the log does. + // Blank lines are kept so the console reads the way the log does let text = line.trim_end_matches(['\n', '\r']).to_string(); if let Some(recorder) = recorder { recorder.observe(&text).await; } - // Read on the way past rather than by re-opening the log afterwards: the - // line is already here, and a crash can take the log with it. + // Read on the way past rather than re-opening the log afterwards a crash + // can take the log with it crash_watch.observe(&text); events.game_log(cluster_id, text); } @@ -144,7 +141,6 @@ mod tests { } impl Harness { - /// The temp dir removes itself on drop, so nothing here needs cleaning up. async fn new() -> Self { let (tx, rx) = mpsc::unbounded_channel(); let dir = polyio::tempdir().await.expect("temp dir"); @@ -159,7 +155,6 @@ mod tests { self.dir.dir_path().join("cluster-output.log") } - /// Drain whatever the tail has emitted so far. fn drain(&mut self) -> Vec { let mut out = Vec::new(); while let Ok(oneclient_events::Event::Game(oneclient_events::GameEvent::Log { line, .. })) = @@ -171,7 +166,6 @@ mod tests { } } - /// Give the tail a few poll cycles to notice what we wrote. async fn settle() { tokio::time::sleep(POLL_INTERVAL * 4).await; } @@ -220,7 +214,7 @@ mod tests { let tail = spawn_log_tail(7, path.clone(), h.events.clone(), None, CrashWatch::new()); settle().await; - // A line mid-write must not be emitted, or it would arrive twice. + // A line mid-write must not be emitted or it would arrive twice assert!(h.drain().is_empty()); let mut file = tokio::fs::OpenOptions::new() @@ -246,8 +240,8 @@ mod tests { settle().await; assert_eq!(h.drain(), vec!["[12:00:00] old session line"]); - // A relaunch truncates the log; the tail would otherwise sit forever at - // an offset past the new end and go silent. + // A relaunch truncates the log the tail would otherwise sit forever at + // an offset past the new end polyio::write(&path, "[13:00:00] new\n").await.unwrap(); settle().await; tail.stop().await; @@ -259,13 +253,13 @@ mod tests { async fn stops_even_when_the_last_line_has_no_newline() { let mut h = Harness::new().await; let path = h.path(); - // A process killed mid-write leaves a newline that never arrives. + // A process killed mid-write leaves a newline that never arrives polyio::write(&path, "[12:00:00] cut off mid-writ").await.unwrap(); let tail = spawn_log_tail(7, path.clone(), h.events.clone(), None, CrashWatch::new()); settle().await; - // Waiting for that newline would hang the exit path forever. + // Waiting for that newline would hang the exit path forever let stopped = tokio::time::timeout(Duration::from_secs(5), tail.stop()).await; assert!(stopped.is_ok(), "tail.stop() hung on an unterminated final line"); assert_eq!(h.drain(), vec!["[12:00:00] cut off mid-writ"]); @@ -277,8 +271,8 @@ mod tests { let path = h.path(); polyio::write(&path, "[12:00:00] Stopping!\n").await.unwrap(); - // Stopping immediately, as the exit path does, must still flush the - // reason the game exited rather than race past it. + // Stopping immediately as the exit path does must still flush the + // reason the game exited let tail = spawn_log_tail(7, path.clone(), h.events.clone(), None, CrashWatch::new()); tail.stop().await; diff --git a/packages/oneclient_core/src/java_store.rs b/packages/oneclient_core/src/java_store.rs index 78ea52df..656e3306 100644 --- a/packages/oneclient_core/src/java_store.rs +++ b/packages/oneclient_core/src/java_store.rs @@ -1,9 +1,5 @@ -//! The database side of [`oneclient_java::JavaStore`]. -//! -//! `oneclient_java` defines what it needs; this maps it onto the launcher's -//! `java_versions` table. Keeping the adapter here rather than the trait there -//! lets the Java crate build without sqlx, and lets its tests run against -//! `MemoryJavaStore` instead of a real database file. +//! The adapter lives here rather than in `oneclient_java` so that crate builds +//! without sqlx and can test against `MemoryJavaStore` use std::str::FromStr; @@ -21,8 +17,6 @@ impl SqlJavaStore { } } -/// The one place a stored row becomes a domain value. Keeping it here spares -/// `JavaRuntime` from knowing the database's row type. fn row_to_runtime(row: JavaVersionRow) -> JavaRuntime { JavaRuntime { absolute_path: row.absolute_path, diff --git a/packages/oneclient_core/src/lib.rs b/packages/oneclient_core/src/lib.rs index bcc073eb..2cc161d2 100644 --- a/packages/oneclient_core/src/lib.rs +++ b/packages/oneclient_core/src/lib.rs @@ -46,7 +46,6 @@ pub use oneclient_cluster::logs::{ LogFileInfo, LogKind, LogLevel, LogLine, LogsError, MclogsUploadResponse, ReadOptions, delete_log_at, list_cluster_logs, read_log_at, upload_log_at, }; -// Minecraft metadata moved to `oneclient_mc`; kept re-exported for the app. pub use oneclient_mc::{McError as MetadataError, MetadataStore}; pub use migration::{ detect as detect_migration, import_game_dir as import_migration_game_dir, ImportTarget, diff --git a/packages/oneclient_core/src/logger.rs b/packages/oneclient_core/src/logger.rs index 584702e6..b704f0fd 100644 --- a/packages/oneclient_core/src/logger.rs +++ b/packages/oneclient_core/src/logger.rs @@ -11,9 +11,8 @@ use crate::LauncherResult; pub mod console; -/// The launcher's own crates. These are the ones anyone reading a log actually -/// wants to hear from, so they get an explicit directive instead of falling -/// back to the base level. +/// The launcher's own crates given an explicit directive rather than falling +/// back to the base level const APP_TARGETS: &[&str] = &[ "oneclient_app", "oneclient_auth", @@ -31,12 +30,8 @@ const APP_TARGETS: &[&str] = &[ "polyio", ]; -/// Dependencies that talk on every request, query, redraw or DNS lookup. -/// -/// The release base level already covers them, but the debug filter raises the -/// base to `info` so that an unfamiliar crate misbehaving still shows up — and -/// that would let this lot back in. A log drowned in `hyper` connection -/// bookkeeping helps nobody, so they are pinned regardless of the base. +/// Pinned regardless of the base level because the debug filter raises the base +/// to `info` and that would otherwise let this lot flood the log const NOISY_TARGETS: &[&str] = &[ "calloop", "freya_core", @@ -52,8 +47,7 @@ const NOISY_TARGETS: &[&str] = &[ "reqwest", "rustls", "smithay_client_toolkit", - // Only `sqlx::query`: `sqlx` proper reports pool and migration problems we - // do want, it is the statement-per-line log that is unreadable. + // Only `sqlx::query` `sqlx` proper reports pool and migration problems we want "sqlx::query", "tokio_util", "tower", @@ -62,14 +56,12 @@ const NOISY_TARGETS: &[&str] = &[ "zbus", ]; -/// Directives for a normal run: our crates at `info`, everything else silent -/// unless it is a warning. pub fn default_directives() -> String { directives("info", "warn") } -/// Directives for "Log Debug Info": our crates at `debug`, and a raised base so -/// an unlisted dependency's `info` still reaches the log. +/// Raises the base level too so an unlisted dependency's `info` still reaches +/// the log pub fn debug_directives() -> String { directives("debug", "info") } @@ -85,8 +77,8 @@ fn directives(app_level: &str, base_level: &str) -> String { out } -/// The filter layer sits directly on the [`Registry`], so that is the subscriber -/// type the reload handle is parameterised over. +/// The filter layer sits directly on the [`Registry`] so that is the subscriber +/// type the reload handle is parameterised over type FilterHandle = reload::Handle; static FILTER: OnceLock = OnceLock::new(); @@ -101,17 +93,13 @@ pub enum FilterError { NotInitialized, } -/// The directives currently in force, for anything that wants to show them. pub fn active_directives() -> String { ACTIVE_DIRECTIVES.lock().clone() } -/// Swap the active filter without restarting the launcher. -/// -/// This is what makes the live console worth having: a developer can widen the -/// filter, watch the noise, and narrow it again in the same session. A string -/// that fails to parse leaves the previous filter in place rather than blanking -/// the log. +/// Swaps the active filter without restarting +/// Unparseable directives leave the previous filter in place rather than +/// blanking the log pub fn set_filter(directives: &str) -> Result<(), FilterError> { let filter = EnvFilter::try_new(directives).map_err(|err| FilterError::Invalid(err.to_string()))?; @@ -137,9 +125,8 @@ pub fn init() -> LauncherResult<()> { } pub fn init_filtered(filter: impl FnOnce() -> String) -> LauncherResult<()> { - // `RUST_LOG` wins outright rather than being merged in: someone who set it - // asked for exactly those directives, and appending ours would quietly - // override the targets they care about. + // `RUST_LOG` wins outright rather than merging appending ours would quietly + // override the targets the user asked for let directives = std::env::var(EnvFilter::DEFAULT_ENV) .ok() .filter(|value| !value.trim().is_empty()) diff --git a/packages/oneclient_core/src/logger/console.rs b/packages/oneclient_core/src/logger/console.rs index 72b6c75e..1435f737 100644 --- a/packages/oneclient_core/src/logger/console.rs +++ b/packages/oneclient_core/src/logger/console.rs @@ -1,10 +1,5 @@ -//! The live launcher log console. -//! -//! A [`Layer`] that hands formatted lines to whoever is watching them, and -//! nothing at all when nobody is. The gate is the broadcast channel's own -//! receiver count rather than a flag we maintain: a receiver that goes away -//! because its window closed decrements it on drop, so there is no way to leave -//! capture switched on by accident. +//! Capture is gated on the broadcast channel's receiver count rather than a flag +//! we maintain so a dropped receiver can never leave capture switched on use std::sync::{Arc, OnceLock}; @@ -14,25 +9,21 @@ use tracing::{Event, Level, Subscriber}; use tracing_subscriber::Layer; use tracing_subscriber::layer::Context; -/// How many lines the channel holds for a console that is slow to drain. -/// -/// A console that falls further behind than this loses the oldest lines and is -/// told so, which beats blocking the thread that emitted the log. +/// A console falling further behind than this loses its oldest lines which +/// beats blocking the thread that emitted the log const CAPACITY: usize = 4096; static CHANNEL: OnceLock> = OnceLock::new(); -/// One captured event. #[derive(Clone, Debug)] pub struct ConsoleLine { pub level: Level, - /// Pre-formatted as `[HH:MM:SS.mmm] [target/LEVEL]: message`, which is the - /// shape the log viewer already parses for Minecraft logs, so launcher - /// lines get the same colouring for free. + /// Pre-formatted as `[HH:MM:SS.mmm] [target/LEVEL]: message` the shape the + /// log viewer already parses for Minecraft logs pub text: Arc, } -/// A live console. Capture runs for exactly as long as this is alive. +/// Capture runs for exactly as long as this is alive pub struct ConsoleSubscription(broadcast::Receiver); impl ConsoleSubscription { @@ -41,20 +32,11 @@ impl ConsoleSubscription { } } -/// Start watching the launcher's logs. -/// -/// Nothing is buffered before this call, so the console opens empty and fills -/// from the moment it is shown. +/// Nothing is buffered before this call so the console opens empty pub fn subscribe() -> ConsoleSubscription { ConsoleSubscription(channel().subscribe()) } -/// Whether anything is listening. -/// -/// Two atomic loads and a branch is the entire cost of the console layer while -/// no console window is open: nothing is formatted, nothing is allocated, and -/// nothing is buffered. Before a console has ever been opened the channel does -/// not even exist, so it is one load. pub fn is_capturing() -> bool { CHANNEL.get().is_some_and(|tx| tx.receiver_count() > 0) } @@ -71,9 +53,8 @@ struct ConsoleLayer; impl Layer for ConsoleLayer { fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { - // Gating here rather than in `Layer::enabled`: that one is ANDed into - // the whole subscriber's decision, so returning `false` from it would - // silence stdout and the log file too whenever the console is shut. + // Not `Layer::enabled` that is ANDed into the whole subscriber's + // decision so returning false there would silence stdout and the file too if !is_capturing() { return; } @@ -92,9 +73,8 @@ impl Layer for ConsoleLayer { let level = *meta.level(); let tx = channel(); - // A message with embedded newlines — a panic backtrace, say — is sent - // line by line so the viewer can scroll and select it like any other. - // Only the first carries the prefix; the rest read as continuations. + // Embedded newlines (a panic backtrace say) are sent line by line so the + // viewer can select them only the first carries the prefix let mut body = visitor.message; body.push_str(&visitor.fields); @@ -114,8 +94,7 @@ impl Layer for ConsoleLayer { } } -/// Pulls the `message` field out of an event and renders the rest as -/// `key=value`, matching what the `fmt` layer writes to stdout. +/// Renders non-message fields as `key=value` matching what `fmt` writes to stdout #[derive(Default)] struct LineVisitor { message: String, diff --git a/packages/oneclient_core/src/migration/mod.rs b/packages/oneclient_core/src/migration/mod.rs index 61090dd7..f8d1f770 100644 --- a/packages/oneclient_core/src/migration/mod.rs +++ b/packages/oneclient_core/src/migration/mod.rs @@ -39,15 +39,15 @@ impl MigrationSource { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SourceInstance { - /// Source-local identifier (v1: the cluster row id). Purely informational. + /// Source-local identifier (v1 the cluster row id) + /// Purely informational pub instance_id: i64, pub folder_name: String, pub mc_version: String, - /// Used for any "migrated" clusters (e.g. 26.1 fabric -> 26.1.2 fabric) + /// Set for "migrated" clusters (e.g. 26.1 fabric -> 26.1.2 fabric) pub target_mc_version: Option, pub mc_loader: GameLoader, - /// Bundle categories the user had installed on this instance, e.g. - /// `["HUD", "Performance"]`. Empty when the source has no category concept. + /// Empty when the source has no category concept pub categories: Vec, pub has_game_dir: bool, } @@ -66,13 +66,12 @@ impl SourceInstance { } } -/// Where an imported game directory should land in *this* launcher. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ImportTarget { - /// Copy into the shared `/.minecraft`. + /// Copy into the shared `/.minecraft` Shared, - /// Copy into a specific new cluster's own folder and mark it dedicated so - /// `Cluster::game_dir()` resolves there instead of the shared dir. + /// Copy into the new cluster's own folder and mark it dedicated so + /// `Cluster::game_dir()` resolves there instead of the shared dir Dedicated { new_cluster_id: i64 }, } diff --git a/packages/oneclient_core/src/migration/oneclient_v1.rs b/packages/oneclient_core/src/migration/oneclient_v1.rs index a48d86f0..4da642d6 100644 --- a/packages/oneclient_core/src/migration/oneclient_v1.rs +++ b/packages/oneclient_core/src/migration/oneclient_v1.rs @@ -159,7 +159,7 @@ pub async fn import_game_dir( let cluster = state.clusters.get(*new_cluster_id).await?; let dir = cluster.dir()?; polyio::create_dir_all(&dir).await?; - // Mark dedicated so game_dir() resolves to this cluster's own dir. + // Mark dedicated so game_dir() resolves to this cluster's own dir polyio::write(cluster.dedicated_marker()?, Vec::new()).await?; dir } diff --git a/packages/oneclient_core/src/migration/vanilla.rs b/packages/oneclient_core/src/migration/vanilla.rs index 0772744e..68804855 100644 --- a/packages/oneclient_core/src/migration/vanilla.rs +++ b/packages/oneclient_core/src/migration/vanilla.rs @@ -44,18 +44,17 @@ const IMPORT_EXCLUDE_TOP: &[&str] = &[ pub fn old_root() -> Option { let base = directories::BaseDirs::new().map(|d| { - // Windows: `%APPDATA%\.minecraft` (data_dir is Roaming). + // Windows `%APPDATA%\.minecraft` (data_dir is Roaming) #[cfg(target_os = "windows")] { d.data_dir().join(".minecraft") } - // macOS: `~/Library/Application Support/minecraft`, no leading dot. + // macOS `~/Library/Application Support/minecraft` no leading dot #[cfg(target_os = "macos")] { d.data_dir().join("minecraft") } - // Linux and friends: `~/.minecraft`. Not data_dir(), which is - // `~/.local/share`. + // Linux `~/.minecraft` not data_dir() (`~/.local/share`) #[cfg(not(any(target_os = "windows", target_os = "macos")))] { d.home_dir().join(".minecraft") diff --git a/packages/oneclient_core/src/recovery.rs b/packages/oneclient_core/src/recovery.rs index c4db85a3..60ae7ee0 100644 --- a/packages/oneclient_core/src/recovery.rs +++ b/packages/oneclient_core/src/recovery.rs @@ -41,9 +41,8 @@ impl RecoveryReport { pub async fn reconstruct_from_disk(state: &LauncherState) -> LauncherResult { let mut report = RecoveryReport::default(); - // Hold the provisioning lock across the whole scan+adopt so a concurrent - // cluster create (folder-on-disk written before its DB row commits) can't be - // misclassified as an orphan and adopted into a duplicate row. + // Held across the whole scan+adopt so a concurrent cluster create (folder + // written before its DB row commits) isn't misclassified as an orphan let _guard = state.clusters.provisioning_guard().await; let clusters_dir = paths::clusters_dir()?; @@ -235,12 +234,8 @@ async fn adopt_cluster( Ok(()) } -/// Adopts the content sitting in a rediscovered cluster's folders. -/// -/// Files are taken *into the artifact cache* rather than indexed where they lie. -/// An artifact's `path` has to resolve to the cache, because that is the only -/// place the launcher materializes content from; a row pointing back into a -/// cluster folder would break the moment that folder was tidied. +/// Files are taken into the artifact cache not indexed in place the cache is +/// the only place content is materialized from so in-place rows break on tidy-up #[tracing::instrument(level = "debug", skip(state, report))] async fn relink_cluster_files( state: &LauncherState, @@ -282,8 +277,8 @@ async fn relink_cluster_files( } }; - // `import_local_file` links under the artifact's own name, which is - // the `.disabled` spelling for a file the user had switched off. + // `import_local_file` links under the artifact's own name which is + // the `.disabled` spelling for a file the user had switched off if !enabled { artifact_dao::update_cluster_artifact(pool, cluster_id, &row.hash, &base_name, 0) .await?; diff --git a/packages/oneclient_core/src/reporting.rs b/packages/oneclient_core/src/reporting.rs index d65f5abb..f60bb0ee 100644 --- a/packages/oneclient_core/src/reporting.rs +++ b/packages/oneclient_core/src/reporting.rs @@ -16,8 +16,7 @@ const ENVIRONMENT: &str = if cfg!(debug_assertions) { "production" }; -/// `before_send` hook: drop any event a log call explicitly opted out of by -/// setting the `sentry` field to `false`, e.g. +/// `before_send` hook drops events whose log call set `sentry = false` /// /// ```ignore /// tracing::error!(sentry = false, "handled MSA failure, don't report as a crash"); @@ -94,8 +93,7 @@ mod tests { assert!(init(false).is_none()); } - /// Build an event the way `sentry-tracing` would, given the value of a - /// `sentry` field on the log call (or `None` for no such field). + /// Builds an event the way `sentry-tracing` would fn event_with_sentry_field(value: Option) -> Event<'static> { let mut event = Event::default(); if let Some(value) = value { diff --git a/packages/oneclient_core/src/settings/mod.rs b/packages/oneclient_core/src/settings/mod.rs index 417ca8d4..c6383136 100644 --- a/packages/oneclient_core/src/settings/mod.rs +++ b/packages/oneclient_core/src/settings/mod.rs @@ -1,7 +1,6 @@ mod launcher; pub use launcher::{LauncherSettings, ViewLayout, ViewState}; -// Profiles moved to `oneclient_cluster`; re-exported for existing paths. pub use oneclient_cluster::{ GameSettingsProfile, PackageUpdateMode, ProfileUpdate, SettingsOsExtra, }; @@ -9,13 +8,8 @@ pub use oneclient_common::Resolution; pub mod store; -/// The endpoint/credential subset of the settings, in the shape the transport -/// wants. -/// -/// This is the seam that replaced `api_config`: rather than the HTTP client -/// reaching back into global settings (and therefore into the state object that -/// owns the client), the composition layer pushes these down whenever settings -/// change. See [`crate::settings::store::save_settings`]. +/// Pushed down by the composition layer on every settings change so the HTTP +/// client never reaches back into global settings (and thus into its own owner) #[must_use] pub fn net_config(settings: &LauncherSettings) -> oneclient_net::NetConfig { oneclient_net::NetConfig::default().with_overrides( diff --git a/packages/oneclient_core/src/settings/store.rs b/packages/oneclient_core/src/settings/store.rs index 42ebaa1d..4ca91f32 100644 --- a/packages/oneclient_core/src/settings/store.rs +++ b/packages/oneclient_core/src/settings/store.rs @@ -35,12 +35,8 @@ pub async fn load_settings(notify: Option<&EventBus>) -> LauncherSettings { } } -/// Persists settings to disk. -/// -/// Prefer [`save_settings_and_apply`] where the services are in reach: this one -/// leaves the HTTP client on whatever endpoints and API keys it already had, so -/// a changed CurseForge key or custom endpoint would not take effect until the -/// next launch. +/// Prefer [`save_settings_and_apply`] this leaves the HTTP client on its old +/// endpoints/keys so those changes only take effect on the next launch #[tracing::instrument(level = "debug", skip(settings))] pub async fn save_settings(settings: &LauncherSettings) -> LauncherResult<()> { let path = paths::settings_file()?; @@ -50,8 +46,6 @@ pub async fn save_settings(settings: &LauncherSettings) -> LauncherResult<()> { Ok(()) } -/// Persists settings and pushes the endpoint/credential subset into the -/// transport, so an API key or endpoint change applies immediately. #[tracing::instrument(level = "debug", skip(services, settings))] pub async fn save_settings_and_apply( services: &crate::LauncherServices, diff --git a/packages/oneclient_core/src/simulate.rs b/packages/oneclient_core/src/simulate.rs index ee913954..11ff2ffd 100644 --- a/packages/oneclient_core/src/simulate.rs +++ b/packages/oneclient_core/src/simulate.rs @@ -1,19 +1,6 @@ -//! Deliberately damaging an installation, so the repair paths can be exercised -//! without waiting for a real bad connection. -//! -//! Every function here breaks files in a *specific* way, because the launcher -//! catches different damage at different layers and the distinction is the -//! whole point of testing it: -//! -//! - **Same length, wrong bytes** slips past the launch-time size check by -//! design, and is only caught by the hashing pass behind "Verify Files". -//! - **Truncated** is what an interrupted download used to leave behind, and is -//! caught cheaply on the next launch. -//! - **Absent** is the case that used to surface as a raw "No such file or -//! directory" naming a path inside the launcher's own metadata folder. -//! -//! Destructive by nature. Everything is repairable by the launcher itself, -//! which is what makes it safe to point at a real installation. +//! Deliberately damaging an installation so repair paths can be exercised +//! Each mode breaks files differently because the launcher catches different +//! damage at different layers use std::path::{Path, PathBuf}; @@ -25,13 +12,11 @@ use oneclient_db::dao::artifact as artifact_dao; use crate::state::LauncherState; use crate::LauncherResult; -/// What a simulation actually did, so the UI can say so rather than claiming -/// success over an installation that had nothing to break. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct SimulationReport { pub affected: usize, - /// A few of the paths touched, for a notification body. Not the full list: - /// corrupting 50 assets should not produce a 50-line toast. + /// Capped sample not the full list 50 damaged assets must not become a + /// 50-line toast pub samples: Vec, } @@ -64,15 +49,12 @@ impl SimulationReport { } } -/// How a file should be damaged. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Damage { - /// Flip bytes in the middle, keeping the length. Passes a size check and - /// fails a hash check — the case "Verify Files" exists for. + /// Same length wrong bytes passes a size check fails a hash check Corrupt, - /// Cut the file to half its length, as an interrupted write would. + /// Half length as an interrupted write would leave it Truncate, - /// Remove it outright. Delete, } @@ -99,10 +81,8 @@ impl Damage { if bytes.is_empty() { return Ok(()); } - // Flipping every bit of a few bytes in the middle guarantees a - // different hash without changing the length, and lands past - // any header a reader might validate first — so the file stays - // superficially plausible, exactly like real bit rot. + // Mid-file so the damage lands past any header a reader + // validates first keeping the file superficially plausible let start = bytes.len() / 2; let end = (start + 8).min(bytes.len()); for byte in &mut bytes[start..end] { @@ -114,11 +94,8 @@ impl Damage { } } -/// Damages `count` asset objects, spread across the sharded subdirectories. -/// -/// Spread rather than taken in bulk from one shard because that is how real -/// damage arrives — a handful of failed requests scattered through a download — -/// and because it proves the verify pass walks the whole tree. +/// Spread across shards rather than taken from one so the verify pass has to +/// walk the whole tree #[tracing::instrument(level = "debug")] pub async fn damage_assets(count: usize, damage: Damage) -> LauncherResult { let dir = paths::assets_object_dir()?; @@ -129,8 +106,8 @@ pub async fn damage_assets(count: usize, damage: Damage) -> LauncherResult = shards .flatten() .map(|entry| entry.path()) @@ -168,8 +145,7 @@ pub async fn damage_assets(count: usize, damage: Damage) -> LauncherResult LauncherResult LauncherResult { let dir = paths::libraries_dir()?; @@ -237,11 +212,8 @@ pub async fn damage_libraries(count: usize, damage: Damage) -> LauncherResult LauncherResult { let dir = paths::assets_dir()?; @@ -261,11 +233,8 @@ pub async fn delete_assets_tree() -> LauncherResult { }) } -/// Damages the installed content of one cluster. -/// -/// Content is cached content-addressed, so a mod that no longer hashes to its -/// own key cannot be found again — the repair has to go back to the provider -/// for it, which is a different path from the game files. +/// Content is cached content-addressed so a mod that no longer hashes to its +/// key must be refetched from the provider a different path from game files #[tracing::instrument(level = "debug", skip(state))] pub async fn damage_cluster_content( state: &std::sync::Arc, @@ -330,8 +299,7 @@ mod tests { #[test] fn corrupting_keeps_the_length_and_changes_the_bytes() { - // The whole point of this mode: it has to survive a size check so that - // only the hashing pass can catch it. + // Must survive a size check so only the hashing pass can catch it let dir = scratch("corrupt"); let file = dir.join("object.bin"); let original: Vec = (0..=255u8).collect(); @@ -390,8 +358,6 @@ mod tests { report.push(Path::new("/tmp/assets/objects/03/dddd")); assert_eq!(report.affected, 4); - // Only a few names are kept, so a large run cannot produce a toast the - // size of the asset index. assert_eq!(report.samples.len(), 3); assert_eq!(report.summary("Corrupted"), "Corrupted 4 file(s): aaaa and 3 more"); } diff --git a/packages/oneclient_core/src/state.rs b/packages/oneclient_core/src/state.rs index ee34e9f9..7bda2d29 100644 --- a/packages/oneclient_core/src/state.rs +++ b/packages/oneclient_core/src/state.rs @@ -41,10 +41,6 @@ pub struct LauncherState { } impl LauncherServices { - /// What the content crate needs: db, client, bus and the provider registry. - /// - /// Unlike [`LauncherServices::mc`] this is the whole of `LauncherServices`; the - /// packages and bundles code uses all four fields. #[must_use] pub fn content(&self) -> oneclient_content::ContentCtx { oneclient_content::ContentCtx::new( @@ -55,10 +51,7 @@ impl LauncherServices { ) } - /// The subset the Minecraft content crate needs: a client and a bus. - /// - /// Both are refcounted handles, so this is cheap enough to build per call - /// rather than store alongside the fields it is made of. + /// Both fields are refcounted handles so building this per call is cheap #[must_use] pub fn mc(&self) -> oneclient_mc::McCtx { oneclient_mc::McCtx::new(self.requester.clone(), self.events.clone()) @@ -66,20 +59,15 @@ impl LauncherServices { } impl LauncherState { - /// Builds the launcher's services and returns the handle. - /// - /// Construction has no side effects beyond opening the database and reading - /// settings: the background startup work is [`run_startup_tasks`], which the - /// caller kicks off when it is ready. That split is why the state can be - /// constructed twice, or in a test only once. + /// No side effects beyond opening the database and reading settings the + /// background startup work is [`run_startup_tasks`] which the caller runs #[tracing::instrument(skip(events))] pub async fn new(events: EventBus) -> LauncherResult> { let services = LauncherServices { events, db: oneclient_db::connect(paths::database_file()?).await?, - // Built with defaults because loading settings needs the event bus, - // which is already inside `services`. The user's endpoints and keys - // are pushed in immediately below, before anything makes a request. + // Defaults because loading settings needs the event bus already inside + // `services` Real config is pushed in below before any request requester: RequestClient::new(oneclient_net::NetConfig::default())?, packages: PackageProviderRegistry::new(), }; @@ -118,11 +106,6 @@ impl LauncherState { } } -/// Reconstructs anything missing from disk, adopts orphaned game sessions, then -/// syncs the remote catalogs. -/// -/// Split out so constructing a `LauncherState` is just construction. The caller -/// decides when, and whether, this runs. pub fn run_startup_tasks(state: &Arc) { let background = Arc::clone(state); tokio::spawn(async move { @@ -164,15 +147,9 @@ pub fn run_startup_tasks(state: &Arc) { background.services.events.signal(oneclient_events::Signal::ClustersChanged); } - // Last, so it runs against a database that recovery, bundle tracking - // and provisioning have all finished settling. Judging an artifact - // unused before the rows that use it have been restored would evict - // content that is very much still wanted. - // - // Only the row-driven half runs unattended. Deciding from the - // filesystem which cached files nothing points at is the half that can - // misread a half-reconstructed install, so it lives behind the Storage - // settings page where the user sees what it proposes to delete. + // Must run last judging an artifact unused before recovery bundle + // tracking and provisioning have restored their rows would evict + // content still in use Only the row-driven half runs unattended if let Err(err) = oneclient_content::packages::store::collect_unused_artifacts(&content).await { diff --git a/packages/oneclient_core/src/storage.rs b/packages/oneclient_core/src/storage.rs index 323561de..9e5e48be 100644 --- a/packages/oneclient_core/src/storage.rs +++ b/packages/oneclient_core/src/storage.rs @@ -1,10 +1,3 @@ -//! What the launcher is using the disk for, and what of it can be reclaimed. -//! -//! Backs the Storage settings page. Everything here is read-only except the two -//! cleanup entry points at the bottom, which exist so the destructive work is -//! something the user asks for after seeing a number, rather than something that -//! happens quietly at startup and has to be gated against running twice. - use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; @@ -17,7 +10,6 @@ use oneclient_content::packages::store::{ find_unreferenced_files, remove_unreferenced_files, }; -/// Content folders an older launcher would have written links into. const LEGACY_TYPES: [ContentType; 4] = [ ContentType::Mod, ContentType::ResourcePack, @@ -25,17 +17,14 @@ const LEGACY_TYPES: [ContentType; 4] = [ ContentType::DataPack, ]; -/// One line on the Storage page. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StorageEntry { pub label: String, pub bytes: u64, pub path: PathBuf, - /// How many files it covers, where a count is more meaningful than a size. pub files: Option, } -/// Something the user can reclaim, with what it would cost them to do so. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ReclaimableEntry { pub bytes: u64, @@ -52,13 +41,9 @@ impl ReclaimableEntry { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StorageReport { pub total_bytes: u64, - /// Minecraft versions, libraries, assets, Java runtimes, logs, image cache. pub categories: Vec, - /// Per-cluster usage, largest first. pub clusters: Vec, - /// Cached package files no artifact row points at. pub unreferenced_cache: ReclaimableEntry, - /// Content links an older launcher wrote into cluster folders. pub legacy_cluster_content: ReclaimableEntry, } @@ -109,18 +94,13 @@ pub async fn storage_report(state: &LauncherState) -> LauncherResult LauncherResult { let mut found = ReclaimableEntry::default(); for cluster in state.clusters.list().await? { - // A dedicated cluster's folder *is* its game directory, so content there - // is exactly where it belongs. + // A dedicated cluster's folder *is* its game directory so content there belongs if cluster.uses_dedicated_dir() { continue; } @@ -181,11 +161,8 @@ async fn entry(label: &str, path: PathBuf) -> StorageEntry { } } -/// Total size of everything under `root`, following nothing. -/// -/// Links are counted as the few bytes the link itself takes rather than the size -/// of what they point at, so a materialized game directory does not appear to -/// double the size of the package cache. +/// Links are not followed so a materialized game directory does not appear to +/// double the size of the package cache pub async fn dir_size(root: impl AsRef) -> u64 { let mut total = 0; let mut stack = vec![root.as_ref().to_path_buf()]; @@ -215,11 +192,8 @@ pub async fn dir_size(root: impl AsRef) -> u64 { total } -/// Whether the page is showing made-up numbers. -/// -/// The cleanup actions refuse to run while it is: the buttons would be acting -/// on a report that has nothing to do with what is actually on disk, and the -/// only thing they could delete is the user's real data. +/// Cleanup refuses to run while fixture numbers are shown the report bears no +/// relation to disk so the only thing it could delete is the user's real data fn showing_fixture() -> bool { #[cfg(debug_assertions)] { @@ -230,7 +204,6 @@ fn showing_fixture() -> bool { false } -/// Deletes cached package files that no artifact refers to. pub async fn clean_unreferenced_cache(state: &LauncherState) -> LauncherResult { if showing_fixture() { tracing::info!("fixture storage report is active; skipping cache cleanup"); @@ -241,7 +214,6 @@ pub async fn clean_unreferenced_cache(state: &LauncherState) -> LauncherResult LauncherResult { if showing_fixture() { tracing::info!("fixture storage report is active; skipping leftover cleanup"); @@ -252,16 +224,9 @@ pub async fn clean_legacy_cluster_content(state: &LauncherState) -> LauncherResu Ok(report.removed) } -/// Stand-in reports for working on the Storage page. -/// -/// Set `ONECLIENT_FAKE_STORAGE` to one of `empty`, `clean` or `full` to make -/// [`storage_report`] return a fixture instead of scanning the disk. Debug -/// builds only, and read on every refresh so the scenario can be changed -/// without a rebuild. -/// -/// The three cover the states worth looking at: a fresh install with nothing -/// anywhere, a well-used install with nothing to reclaim, and one with plenty -/// to reclaim so the cleanup buttons are live. +/// Set `ONECLIENT_FAKE_STORAGE` to `empty` `clean` or `full` to return a fixture +/// instead of scanning disk +/// Read on every refresh so no rebuild is needed #[cfg(debug_assertions)] fn fixture_report() -> Option { let scenario = std::env::var("ONECLIENT_FAKE_STORAGE").ok()?; @@ -343,7 +308,6 @@ fn fixture_report() -> Option { Some(report) } -/// Renders a byte count the way the settings page shows it. #[must_use] pub fn format_bytes(bytes: u64) -> String { const UNITS: [&str; 4] = ["KB", "MB", "GB", "TB"]; @@ -385,7 +349,6 @@ mod tests { assert!(is_content_file(ContentType::Mod, "sodium.jar")); assert!(is_content_file(ContentType::Mod, "sodium.jar.disabled")); assert!(is_content_file(ContentType::Shader, "BSL.zip")); - // A shaderpack's settings sidecar is the user's, not a leftover link. assert!(!is_content_file(ContentType::Shader, "BSL.zip.txt")); assert!(!is_content_file(ContentType::Mod, "options.txt")); } @@ -403,7 +366,6 @@ mod tests { assert_eq!(dir_size(dir).await, 1500); - // A link to a big file must not be counted as that file again. polyio::symlink_file(dir.join("a.bin"), dir.join("link.bin")) .await .unwrap(); diff --git a/packages/oneclient_core/src/verify.rs b/packages/oneclient_core/src/verify.rs index d6b6b8dd..69ffe303 100644 --- a/packages/oneclient_core/src/verify.rs +++ b/packages/oneclient_core/src/verify.rs @@ -15,25 +15,16 @@ use crate::game::{ use crate::state::LauncherState; use crate::LauncherResult; -/// What a verify pass found and what it managed to do about it. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct ClusterVerifyReport { - /// Files read and hashed, across game files and installed content. pub checked: usize, - /// Files whose contents did not match and were removed. + /// Mismatched files also deleted from disk pub corrupt: usize, - /// Files a manifest expected that were not on disk at all. pub missing: usize, - /// Files that were successfully fetched again. pub repaired: usize, - /// Content that was corrupt but has no source to re-download from — - /// locally imported files, or a version the provider no longer serves. - /// Named so the user can replace them by hand. + /// Corrupt with no source to re-fetch from named so the user can replace them pub unrepairable: Vec, - /// The game files were re-downloaded without being counted, because the - /// assets index they would have been checked against was itself gone. - /// Reported separately so a wholesale reinstall is never described as a - /// clean sweep of the handful of files that could still be counted. + /// Game files re-downloaded uncounted because the assets index was gone too pub reinstalled_game_files: bool, } @@ -46,7 +37,6 @@ impl ClusterVerifyReport { && !self.reinstalled_game_files } - /// A one-line summary for a notification. #[must_use] pub fn summary(&self) -> String { if self.is_clean() { @@ -75,18 +65,8 @@ impl ClusterVerifyReport { } } -/// Verifies every file a cluster installed, and re-downloads what is broken. -/// -/// Runs in three passes. Game files are hashed against the Mojang manifest and -/// the bad ones deleted; installed content is hashed against the hash it is -/// indexed under and the bad ones deleted and re-fetched from the provider that -/// served them; then the ordinary prepare path runs, which re-downloads exactly -/// what is now missing using the same verified, retrying download every other -/// install goes through. -/// -/// Deleting and re-preparing rather than overwriting in place is deliberate: it -/// reuses one well-tested download path instead of adding a second one that -/// would have to be kept in step with it. +/// Deletes bad files and re-prepares rather than overwriting in place to reuse +/// the one well-tested download path instead of maintaining a second #[tracing::instrument(skip(state), level = "debug")] pub async fn verify_cluster_files( state: &Arc, @@ -125,8 +105,7 @@ async fn run_verify( let cluster = state.clusters.get(cluster_id).await?; let mc_version = oneclient_common::version::normalize_mc_version_input(&cluster.mc_version); - // Resolving the version needs the metadata lock, which prepare also takes. - // Scope it so it is released before the repair pass below. + // Scoped so the metadata lock is released before the repair pass which takes it too let (version_info, minecraft_updated) = { let mut metadata = state.metadata.lock().await; let (version, _index, minecraft_updated) = @@ -157,15 +136,12 @@ async fn run_verify( let mut report = ClusterVerifyReport::default(); - // The index is needed to know which assets this version expects. Reading - // the cached copy avoids a network round trip; if it is absent or itself - // corrupt there is nothing to verify assets against, so skip that part - // rather than fail — prepare will fetch it and the assets below. + // Absent or corrupt index means nothing to verify assets against so skip + // rather than fail prepare fetches it and the assets below let assets_index = cached_assets_index(&version_info).await; - // Library rules are evaluated per architecture, so checking against the - // wrong one would verify a different set of libraries than the launch path - // installs. Use the runtime this cluster actually launches with. + // Library rules are per-architecture use the runtime this cluster launches + // with or a different library set than the launch path installs is verified let java_arch = { let global = state.settings.read().global_game_settings.clone(); let profile = state.clusters.resolve_settings(&global, &cluster).await?; @@ -178,16 +154,12 @@ async fn run_verify( .map_or_else(|| std::env::consts::ARCH.to_string(), |rt| rt.os_arch) }; - // Counted apart from content: these are repaired by the prepare pass below, - // whereas content repairs itself as it goes. Adding both into one bucket - // would count the content ones twice. + // Counted apart from content which repairs itself as it goes one bucket + // would count the content ones twice let mut game_broken = 0; - // Without the index there is nothing to check the assets against — and the - // index lives inside the assets directory, so its absence usually means - // that whole directory is gone. That is precisely the case this repair - // exists for, so hand the work to prepare rather than reporting a clean - // sweep of the nothing that could be checked. + // The index lives in the assets directory so its absence usually means that + // whole directory is gone hand the work to prepare let reinstalled_game_files = assets_index.is_none(); if let Some(assets_index) = assets_index { @@ -210,15 +182,8 @@ async fn run_verify( verify_cluster_content(state, cluster_id, progress, &mut report).await?; - // Everything corrupt is gone, so a normal prepare sees it as missing and - // fetches it back through the verified, retrying download path. A file it - // cannot get surfaces as an error rather than a silent gap, so reaching the - // line after this means the game files are whole again. - // - // Note this runs `prepare` directly rather than going through the launch - // path, which skips preparing a cluster already marked `Ready` — the very - // reason a cluster with deleted files keeps failing to launch instead of - // healing itself. + // Runs `prepare` directly not the launch path which skips clusters already + // marked `Ready` and so never heals one with deleted files if game_broken > 0 || reinstalled_game_files { prepare_cluster_locked(state, cluster_id, false, true, true, Some(progress)).await?; report.repaired += game_broken; @@ -229,12 +194,8 @@ async fn run_verify( Ok(report) } -/// Hashes the cluster's installed content against the hashes it is indexed -/// under, and re-fetches whatever no longer matches. -/// -/// Content is cached content-addressed, so a file that does not hash to its own -/// key is unusable by definition: nothing can find it again, and materializing -/// it into the game folder would put a corrupt mod in front of the game. +/// The content cache is content-addressed so a file that does not hash to its +/// own key is unusable nothing can find it again async fn verify_cluster_content( state: &Arc, cluster_id: i64, @@ -305,12 +266,8 @@ async fn verify_cluster_content( Ok(()) } -/// Re-downloads one piece of content from the provider that served it. -/// -/// Failures are recorded rather than propagated: one mod that a provider has -/// since delisted should not abandon verification of everything else. A locally -/// imported file has no provider at all, and the user is the only one who can -/// replace it — so it is named in the report instead of silently vanishing. +/// Failures are recorded not propagated one delisted mod should not abandon +/// verification of everything else async fn repair_content( state: &Arc, link: &oneclient_content::packages::LinkedArtifactInfo, @@ -332,9 +289,8 @@ async fn repair_content( let provider = content.providers.get(provider_id)?; let version = provider.get_version(project_id, version_id, &content).await?; - // The cluster is pinned to one exact file, identified by the hash it is - // indexed under. Re-downloading a different file from the same version - // would silently change what is installed. + // The cluster is pinned to one exact file by hash another file from the + // same version would silently change what is installed let file = version .files .iter() @@ -413,10 +369,6 @@ mod tests { #[test] fn a_wholesale_reinstall_is_not_reported_as_a_clean_sweep() { - // The `metadata/assets` case: the index the assets would have been - // checked against was gone too, so almost nothing could be counted. - // Saying "All 2 files verified" after re-downloading the game would be - // actively misleading. let report = ClusterVerifyReport { checked: 2, reinstalled_game_files: true, @@ -432,9 +384,6 @@ mod tests { #[test] fn content_that_cannot_be_replaced_is_still_reported() { - // A locally imported mod has no provider to fetch it from again, so the - // user is the only one who can put it back. Saying nothing would leave - // them with a cluster that is quietly missing a file. let report = ClusterVerifyReport { checked: 12, corrupt: 1, diff --git a/packages/oneclient_core/src/versions/manager.rs b/packages/oneclient_core/src/versions/manager.rs index 06738f00..48754a74 100644 --- a/packages/oneclient_core/src/versions/manager.rs +++ b/packages/oneclient_core/src/versions/manager.rs @@ -19,13 +19,8 @@ impl VersionsManager { } } - /// Seeds the manifest from the last synced copy on disk. - /// - /// Without this the manifest is empty until the network answers, and - /// everything read off it — a version's art, name, description — waits on a - /// request that, for a returning user, ends in a 304 and the very same file. - /// The UI would rather render slightly stale metadata now and pick up the - /// sync when it lands. + /// Seeds from the last synced copy on disk so the UI renders stale metadata + /// immediately instead of blocking on a request that usually 304s #[must_use] pub async fn from_cache() -> Self { let manifest = Self::cached_manifest() diff --git a/packages/oneclient_core/src/versions/manifest.rs b/packages/oneclient_core/src/versions/manifest.rs index 8413fdd6..913f91eb 100644 --- a/packages/oneclient_core/src/versions/manifest.rs +++ b/packages/oneclient_core/src/versions/manifest.rs @@ -24,7 +24,7 @@ pub struct RemoteCluster { pub long_description: Option, #[serde(default)] pub tags: Vec, - /// Default for every entry in this cluster; an entry's own key wins. + /// Default for every entry in this cluster an entry's own key wins #[serde(default)] pub predownload: Option, #[serde(default)] @@ -68,15 +68,9 @@ pub struct MigrationTarget { pub mc_version: String, } -/// Follow the remote migration chain forward from `mc_version` for the given -/// loader, returning the final target version after every applicable rule has -/// been applied. Returns the input unchanged when no rule matches. -/// -/// This mirrors what [`crate::clusters::apply_remote_migrations`] does to -/// existing cluster rows, so callers that only know an *old* version (e.g. the -/// OneClient v1 importer) can find the cluster the migrations have already moved -/// that version to. Multi-hop chains (`a -> b -> c`) are followed; the loop is -/// bounded by the rule count so a cyclic/self rule can't spin forever. +/// Returns the input unchanged when no rule matches +/// Multi-hop chains are +/// followed the loop is bounded by rule count so cyclic rules can't spin forever #[must_use] pub fn resolve_migration_chain( mc_version: &str, @@ -113,8 +107,7 @@ pub struct VersionMetadata { pub art_url: Option, pub long_description: Option, pub tags: Vec, - /// Whether this version is fetched up front during onboarding, rather than - /// on first launch. Cluster rows carry the cluster's own default. + /// Fetched up front during onboarding rather than on first launch pub predownload: bool, } @@ -124,8 +117,7 @@ impl VersionMetadata { Some((self.minor_version?, self.patch_version)) } - /// The `mc_version` string this row maps to, or `None` for the synthetic - /// cluster-level row (which has no minor version of its own). + /// `None` for the synthetic cluster-level row which has no minor version #[must_use] pub fn mc_version(&self) -> Option { Some(format_mc_version( @@ -247,13 +239,10 @@ mod tests { .predownload }; - // Cluster default flows down to entries that stay silent. assert!(flag(26, None)); assert!(flag(26, Some(1))); - // An entry's own key wins over the cluster default, in both directions. assert!(!flag(26, Some(2))); assert!(flag(21, Some(11))); - // Absent everywhere means off. assert!(!flag(21, None)); assert!(!flag(21, Some(1))); } @@ -336,7 +325,6 @@ mod tests { rule("a", "26.1", "fabric", "26.2"), rule("b", "26.2", "fabric", "26.1"), ]; - // Must not loop forever; lands on one of the two, bounded by rule count. let out = resolve_migration_chain("26.1", GameLoader::Fabric, &cycle); assert!(out == "26.1" || out == "26.2"); } diff --git a/packages/oneclient_core/tests/bundle_updates.rs b/packages/oneclient_core/tests/bundle_updates.rs index b4dc3d49..f8427fb9 100644 --- a/packages/oneclient_core/tests/bundle_updates.rs +++ b/packages/oneclient_core/tests/bundle_updates.rs @@ -26,8 +26,7 @@ fn managed_file(enabled: bool) -> BundleFile { } } -/// A file the catalog added after the cluster was last synced: nothing on the -/// cluster tracks it, so it has no override either. +/// Added by the catalog after the last sync untracked so it has no override fn newly_shipped_file() -> BundleFile { BundleFile { enabled: true, @@ -129,7 +128,6 @@ async fn mod_still_in_manifest_is_not_removed() { assert!(check.updates_available.is_empty()); } -/// behaviour the disabled-mod case must not be allowed to imitate. #[tokio::test] async fn mod_dropped_from_manifest_is_removed() { let state = oneclient_core::dev::ephemeral_state().await.unwrap(); @@ -251,9 +249,9 @@ async fn emptied_bundle_does_not_take_on_new_catalog_files() { .unwrap(); let cluster_id = cluster_with_tracked_mod(&state).await; - // The user disabled everything this bundle shipped, which is how you opt out - // of it. The newcomer has no override of its own precisely because it did not - // exist yet, and that absence must not read as consent. + // Disabling everything is how you opt out + // The newcomer lacks an override only + // because it did not exist yet that absence must not read as consent artifact_dao::update_cluster_artifact(&state.services.db, cluster_id, HASH, "sodium.jar", 0) .await .unwrap(); diff --git a/packages/oneclient_core/tests/clusters.rs b/packages/oneclient_core/tests/clusters.rs index e8ba2c66..3ab6afd1 100644 --- a/packages/oneclient_core/tests/clusters.rs +++ b/packages/oneclient_core/tests/clusters.rs @@ -13,9 +13,8 @@ async fn cluster_lifecycle_with_settings_profile() { let global = state.settings.read().global_game_settings.clone(); - // Deliberately not the global default of 4096, so the assertions below - // distinguish "the cluster's own profile was used" from "we fell back to - // global and happened to match". + // Deliberately not the global default of 4096 so the assertions distinguish + // "the cluster's own profile was used" from "fell back to global and matched" let cluster = state.clusters.create( &global, CreateClusterOptions::new("Test Cluster", "1.21.1", GameLoader::Fabric).mem_max(2048), @@ -83,7 +82,7 @@ async fn cleared_profile_fields_inherit_from_global() { settings.global_game_settings.mem_max = Some(8192); } - // Read the baseline *after* the write above, so the new cluster inherits it. + // Read the baseline *after* the write above so the new cluster inherits it let global = state.settings.read().global_game_settings.clone(); let cluster = state.clusters.create( &global, diff --git a/packages/oneclient_core/tests/download_verify.rs b/packages/oneclient_core/tests/download_verify.rs index c859e44e..6d762017 100644 --- a/packages/oneclient_core/tests/download_verify.rs +++ b/packages/oneclient_core/tests/download_verify.rs @@ -4,8 +4,8 @@ use oneclient_events::{GroupedProgressSession, TaskCategory}; use polyio::testing::ScratchDir as Scratch; use polyio::{Sha1Stream, sha1_bytes, sha1_file}; -/// A Minecraft asset object. Objects are content-addressed and immutable, so -/// this URL/hash pair stays valid. +/// Minecraft asset objects are content-addressed and immutable so this +/// URL/hash pair stays valid const ASSET_SHA1: &str = "af96f55a90eaf11b327f1b5f8834a051027dc506"; const ASSET_URL: &str = "https://resources.download.minecraft.net/af/af96f55a90eaf11b327f1b5f8834a051027dc506"; @@ -33,7 +33,7 @@ async fn sha1_file_matches_sha1_bytes() { let dir = Scratch::new("sha1-file"); let path = dir.join("payload.bin"); - // Larger than the hashing buffer, so the multi-read path is covered too. + // Larger than the hashing buffer so the multi-read path is covered too let data: Vec = (0..600_000u32).map(|i| (i % 253) as u8).collect(); std::fs::write(&path, &data).unwrap(); @@ -49,8 +49,8 @@ async fn sha1_file_handles_empty_file() { assert_eq!(sha1_file(&path).await.unwrap(), sha1_bytes(&[])); } -/// Serves `body` with a 200, declaring `content_length` however long `body` is — -/// pass a larger one to imitate a mirror holding the wrong file. +/// Serves `body` with a 200 declaring `content_length` however long `body` is +/// pass a larger one to imitate a mirror holding the wrong file async fn serve(body: Vec, content_length: usize) -> String { use tokio::io::AsyncWriteExt; @@ -78,8 +78,9 @@ async fn serve(body: Vec, content_length: usize) -> String { format!("http://127.0.0.1:{port}/artifact.jar") } -/// Answers the first `failures` requests with `status`, then serves `ok_body`. -/// Returns the URL and a request counter. `usize::MAX` never recovers. +/// Answers the first `failures` requests with `status` then serves `ok_body` +/// Returns the URL and a request counter +/// `usize::MAX` never recovers async fn serve_flaky( status: u16, reason: &'static str, @@ -106,7 +107,7 @@ async fn serve_flaky( let served = Arc::clone(&served); tokio::spawn(async move { - // Draining the request first, or the client sees a reset. + // Draining the request first or the client sees a reset let mut request = [0u8; 1024]; let _ = tokio::io::AsyncReadExt::read(&mut stream, &mut request).await; @@ -139,7 +140,7 @@ async fn download_to_path_never_writes_an_error_response_to_disk() { let dir = Scratch::new("download-404"); let dest = dir.join("artifact.jar"); - // What a CDN answers for a missing object: well-formed, but XML not a jar. + // What a CDN answers for a missing object well-formed but XML not a jar let (url, served) = serve_flaky( 404, "Not Found", @@ -166,7 +167,7 @@ async fn download_to_path_never_writes_an_error_response_to_disk() { assert!(err.to_string().contains("404"), "{err}"); assert!(err.to_string().contains("NoSuchKey"), "{err}"); assert!(!dest.exists(), "an error body must never reach disk"); - // A 404 answers the same way every time; retrying it only delays the error. + // A 404 answers the same way every time retrying it only delays the error assert_eq!(served.load(std::sync::atomic::Ordering::SeqCst), 1); } @@ -177,7 +178,7 @@ async fn download_to_path_retries_a_server_error_and_recovers() { let dir = Scratch::new("download-500"); let dest = dir.join("artifact.jar"); - // Unlike a 404, a 500 can succeed on the next attempt. + // Unlike a 404 a 500 can succeed on the next attempt let (url, served) = serve_flaky( 500, "Internal Server Error", @@ -212,7 +213,6 @@ async fn download_to_path_rejects_a_body_shorter_than_the_manifest_size() { let dir = Scratch::new("download-short"); let dest = dir.join("artifact.jar"); - // Valid HTTP, just not the file the manifest described. let url = serve(vec![7u8; 64], 64).await; let err = download_to_path( @@ -268,7 +268,7 @@ async fn download_to_path_keeps_an_unhashed_body_of_unknown_length() { let url = serve(vec![7u8; 128], 128).await; - // Nothing declared a size, so there is nothing to check against. + // Nothing declared a size so there is nothing to check against download_to_path( &services.requester, &services.events, diff --git a/packages/oneclient_db/src/console.rs b/packages/oneclient_db/src/console.rs index 5fb23824..678b86d7 100644 --- a/packages/oneclient_db/src/console.rs +++ b/packages/oneclient_db/src/console.rs @@ -6,9 +6,8 @@ use sqlx::{Column, Row, ValueRef}; use crate::{DbError, DbPool}; -/// sqlx runs SQLite on a worker thread and requires the SQL string to be -/// `'static`. Intern each distinct console query so repeated runs of the same -/// text don't leak, while still accepting arbitrary runtime strings. +/// sqlx requires the SQL string to be `'static` interning stops repeated runs +/// of the same text from leaking fn intern(sql: &str) -> &'static str { static CACHE: Mutex>> = Mutex::new(None); @@ -24,24 +23,16 @@ fn intern(sql: &str) -> &'static str { leaked } -/// Outcome of a console query, stringified for display. #[derive(Debug, Clone, Default)] pub struct ConsoleQueryResult { - /// Column headers (empty when the statement returned no rows). pub columns: Vec, - /// Row cells, already converted to display strings. pub rows: Vec>, - /// Rows affected by a non-select statement. pub rows_affected: u64, - /// Whether the statement returned a result set. pub is_select: bool, } -/// Execute an arbitrary SQL statement. -/// -/// Statements that start with a row-returning keyword (`SELECT`, `WITH`, -/// `PRAGMA`, `EXPLAIN`) are fetched into a table; everything else is executed -/// and reports `rows_affected`. +/// Statements starting with `SELECT` `WITH` `PRAGMA` or `EXPLAIN` are fetched +/// into a table everything else is executed and reports `rows_affected` pub async fn run_console_query(pool: &DbPool, sql: &str) -> Result { let sql = intern(sql.trim().trim_end_matches(';').trim()); @@ -97,7 +88,7 @@ fn rows_to_result(fetched: &[SqliteRow]) -> ConsoleQueryResult { } } -/// SQLite is dynamically typed, so probe the raw value and decode accordingly. +/// SQLite is dynamically typed so probe the raw value and decode accordingly fn cell_to_string(row: &SqliteRow, idx: usize) -> String { match row.try_get_raw(idx) { Ok(raw) if raw.is_null() => return "NULL".to_string(), diff --git a/packages/oneclient_db/src/dao/artifact.rs b/packages/oneclient_db/src/dao/artifact.rs index c707a507..f8160670 100644 --- a/packages/oneclient_db/src/dao/artifact.rs +++ b/packages/oneclient_db/src/dao/artifact.rs @@ -49,10 +49,8 @@ pub async fn insert_artifact( .await } -/// Drops an artifact's row when no cluster refers to it any more. -/// -/// `provider_releases` cascades, so the metadata goes with it. The cached file -/// is the caller's to delete — this layer does not touch the disk. +/// `provider_releases` cascades the cached file is the caller's to delete +/// this layer does not touch the disk pub async fn delete_artifact_if_unused(pool: &SqlitePool, hash: &str) -> Result { let linked: (i64,) = sqlx::query_as( "SELECT COUNT(*) FROM cluster_artifacts WHERE hash = ?", @@ -72,11 +70,6 @@ pub async fn delete_artifact_if_unused(pool: &SqlitePool, hash: &str) -> Result< Ok(true) } -/// Every artifact no cluster refers to any more. -/// -/// Clusters are deleted with an `ON DELETE CASCADE` on `cluster_artifacts`, and -/// bundle updates swap one version for another, so artifacts are orphaned in -/// bulk and in places too far from the store to evict them one at a time. pub async fn list_unused_artifacts(pool: &SqlitePool) -> Result, sqlx::Error> { sqlx::query_as::<_, ArtifactRow>( r#" @@ -89,8 +82,6 @@ pub async fn list_unused_artifacts(pool: &SqlitePool) -> Result .await } -/// The stored path of every artifact, used to spot cached files that no row -/// accounts for. pub async fn list_artifact_paths(pool: &SqlitePool) -> Result, sqlx::Error> { let rows: Vec<(String,)> = sqlx::query_as("SELECT path FROM artifacts") .fetch_all(pool) @@ -211,12 +202,8 @@ pub async fn link_cluster_artifact( .await } -/// Every artifact the cluster has from the same project as `exclude_hash`, -/// excluding that one. -/// -/// `provider_releases` is keyed by version, so one artifact can join several -/// rows of it. Without `DISTINCT` a package would be reported once per release -/// row and a caller unlinking the results would work from an inflated list. +/// `DISTINCT` is required `provider_releases` is keyed by version so one +/// artifact joins several rows and would otherwise be reported repeatedly pub async fn list_cluster_artifacts_for_project( pool: &SqlitePool, cluster_id: i64, diff --git a/packages/oneclient_db/src/dao/browser_package_update.rs b/packages/oneclient_db/src/dao/browser_package_update.rs index 62b4dff8..e8341b7b 100644 --- a/packages/oneclient_db/src/dao/browser_package_update.rs +++ b/packages/oneclient_db/src/dao/browser_package_update.rs @@ -42,12 +42,8 @@ pub async fn list_all( .await } -/// Records a pending update. -/// -/// A row that already names the same `latest_version_id` keeps its `skipped` -/// flag: the user has answered for that version and re-running the check is not -/// a reason to ask again. A different `latest_version_id` is a new question, so -/// the flag resets. +/// Re-upserting the same `latest_version_id` keeps `skipped` a different one +/// resets it since it is a new question for the user #[allow(clippy::too_many_arguments)] pub async fn upsert( pool: &SqlitePool, @@ -143,11 +139,8 @@ pub async fn delete(pool: &SqlitePool, cluster_id: i64, hash: &str) -> Result<() Ok(()) } -/// Drops rows for hashes the latest check did not report. -/// -/// A package that was updated, removed, or turned out to be current again must -/// stop being marked out of date, and the check only tells us what *is* stale. -/// Passing an empty list clears the cluster. +/// Drops rows for hashes the latest check did not report an empty list clears +/// the cluster pub async fn retain_hashes( pool: &SqlitePool, cluster_id: i64, @@ -187,8 +180,7 @@ mod tests { .await .expect("in-memory sqlite"); sqlx::migrate!().run(&pool).await.expect("migrations run"); - // Runtime query, not the macro: the offline cache is generated for the - // library only, and a fixture is not worth a `--all-targets` prepare. + // Runtime query not the macro the offline cache is library-only sqlx::query( r#" INSERT INTO clusters (id, name, folder_name, mc_version) diff --git a/packages/oneclient_db/src/dao/cluster_bundle.rs b/packages/oneclient_db/src/dao/cluster_bundle.rs index abcde1ec..96b44f45 100644 --- a/packages/oneclient_db/src/dao/cluster_bundle.rs +++ b/packages/oneclient_db/src/dao/cluster_bundle.rs @@ -189,10 +189,7 @@ pub async fn remove_override( Ok(()) } -/// Drops every objection the cluster holds against a package, under any bundle. -/// -/// Opt-in (`enabled`) rows are left alone: they say the opposite thing, and -/// clearing an objection is not a reason to forget one. +/// Opt-in (`enabled`) rows are left alone they say the opposite thing pub async fn clear_suppressing_overrides( pool: &SqlitePool, cluster_id: i64, diff --git a/packages/oneclient_db/src/dao/game_session.rs b/packages/oneclient_db/src/dao/game_session.rs index 7496e40a..55557b20 100644 --- a/packages/oneclient_db/src/dao/game_session.rs +++ b/packages/oneclient_db/src/dao/game_session.rs @@ -40,8 +40,7 @@ pub async fn finish_session( finish_session_at(pool, started_at, &Utc::now().to_rfc3339(), exit_code).await } -/// Close a session at an explicit time. Used when the exit time is inferred -/// from the game's log rather than observed live. +/// For exit times inferred from the game's log rather than observed live pub async fn finish_session_at( pool: &SqlitePool, started_at: &str, @@ -107,7 +106,6 @@ pub async fn insert_server_join( insert_server_join_at(pool, session_started_at, address, port, &Utc::now().to_rfc3339()).await } -/// Record a join at an explicit time, for spans replayed out of a game log. pub async fn insert_server_join_at( pool: &SqlitePool, session_started_at: &str, @@ -134,7 +132,6 @@ pub async fn finish_server(pool: &SqlitePool, joined_at: &str) -> Result<(), sql finish_server_at(pool, joined_at, &Utc::now().to_rfc3339()).await } -/// Close a server span at an explicit time, for spans replayed out of a log. pub async fn finish_server_at( pool: &SqlitePool, joined_at: &str, @@ -192,7 +189,7 @@ pub async fn list_session_servers( .await } -/// Drop every server span of a session, so a log replay can rewrite them from scratch. +/// Lets a log replay rewrite a session's server spans from scratch pub async fn delete_session_servers( pool: &SqlitePool, session_started_at: &str, diff --git a/packages/oneclient_db/src/models/browser_package_update.rs b/packages/oneclient_db/src/models/browser_package_update.rs index a9116f24..24ca147f 100644 --- a/packages/oneclient_db/src/models/browser_package_update.rs +++ b/packages/oneclient_db/src/models/browser_package_update.rs @@ -1,10 +1,7 @@ use sqlx::FromRow; -/// A browser-installed package the last update check found a newer version for. -/// -/// `hash` is the artifact currently linked into the cluster, which is also the -/// primary key the package manager renders by, so a row can never be attached -/// to a file the cluster no longer has. +/// `hash` is the artifact currently linked into the cluster so a row can never +/// be attached to a file the cluster no longer has #[derive(Debug, Clone, FromRow)] pub struct BrowserPackageUpdateRow { pub cluster_id: i64, diff --git a/packages/oneclient_db/src/models/cluster_bundle.rs b/packages/oneclient_db/src/models/cluster_bundle.rs index fdabe3f7..3cb80206 100644 --- a/packages/oneclient_db/src/models/cluster_bundle.rs +++ b/packages/oneclient_db/src/models/cluster_bundle.rs @@ -4,7 +4,7 @@ use sqlx::FromRow; pub enum OverrideType { Removed, Disabled, - /// Opt-in: install a file the bundle manifest ships with `enabled: false`. + /// Opt-in install a file the bundle manifest ships with `enabled: false` Enabled, } diff --git a/packages/oneclient_db/src/models/setting_profile.rs b/packages/oneclient_db/src/models/setting_profile.rs index 4d764127..f236bb04 100644 --- a/packages/oneclient_db/src/models/setting_profile.rs +++ b/packages/oneclient_db/src/models/setting_profile.rs @@ -13,6 +13,6 @@ pub struct SettingProfileRow { pub hook_wrapper: Option, pub hook_post: Option, pub os_extra: Option, - /// `PackageUpdateMode::as_str`; NULL inherits the global setting. + /// `PackageUpdateMode::as_str` NULL inherits the global setting pub browser_update_mode: Option, } diff --git a/packages/oneclient_discord/src/lib.rs b/packages/oneclient_discord/src/lib.rs index f6357528..1ed17a42 100644 --- a/packages/oneclient_discord/src/lib.rs +++ b/packages/oneclient_discord/src/lib.rs @@ -1,7 +1,3 @@ -//! Discord rich presence. -//! -//! A leaf: it takes a presence to show and nothing else. - use std::sync::mpsc::{self, RecvTimeoutError, Sender}; use std::time::{Duration, Instant}; diff --git a/packages/oneclient_events/src/bus.rs b/packages/oneclient_events/src/bus.rs index 41d048cd..15645bb1 100644 --- a/packages/oneclient_events/src/bus.rs +++ b/packages/oneclient_events/src/bus.rs @@ -5,11 +5,8 @@ use crate::error::{EventError, EventResult}; use crate::event::{Event, GameEvent, LaunchStage, Level, Message, Notification, ProgressEvent, Signal}; use crate::prompt::{Answer, Chosen, Prompt, PromptRequest}; -/// The sending half of the event bus. -/// -/// Cheap to clone, since `UnboundedSender` is refcounted internally and every -/// clone feeds the same channel. Hand clones to subsystems rather than wrapping -/// this in an `Arc`. +/// Cheap to clone every clone feeds the same channel +/// Hand clones to subsystems rather than wrapping this in an `Arc` #[derive(Clone, Debug)] pub struct EventBus { tx: mpsc::UnboundedSender, @@ -23,35 +20,25 @@ impl EventBus { Self { tx } } - /// Creates a bus and its receiver. The owner of the receiver is the UI. #[must_use] pub fn channel() -> (Self, EventReceiver) { let (tx, rx) = mpsc::unbounded_channel(); (Self::new(tx), rx) } - /// The one primitive every other method goes through. - /// - /// A closed bus is logged rather than returned: emitting is called from - /// deep inside download loops where there is nothing useful to do about a - /// UI that has already shut down. Use [`EventBus::ask`] when you need to - /// know whether the other end is alive. + /// A closed bus is logged rather than returned use [`EventBus::ask`] when + /// you need to know whether the other end is alive pub fn emit(&self, event: impl Into) { if let Err(err) = self.tx.send(event.into()) { tracing::debug!("dropping event, bus is closed: {err}"); } } - /// Whether the receiving end is still up. #[must_use] pub fn is_open(&self) -> bool { !self.tx.is_closed() } - // --- notifications ----------------------------------------------------- - - /// Starts building a notification. Nothing is emitted until `.send()`. - /// /// ```ignore /// events.notify("Install failed").body(err.to_string()).error().send(); /// ``` @@ -66,14 +53,7 @@ impl EventBus { } } - // --- progress ---------------------------------------------------------- - - /// Reports progress for a single task. Repeated calls with the same `id` - /// replace one another. - /// - /// Not a builder: this runs tens of thousands of times during a download, - /// and the argument list is short enough that a builder would only add - /// ceremony to a hot path. + /// Repeated calls with the same `id` replace one another pub fn progress(&self, id: Uuid, label: impl Into, current: u64, total: u64) { self.emit(ProgressEvent::Update { id, @@ -83,7 +63,6 @@ impl EventBus { }); } - /// Converts the in-flight progress entry `id` into a finished message. pub fn finish_progress(&self, id: Uuid, title: impl Into, body: impl Into) { self.emit(ProgressEvent::Complete { id, @@ -92,8 +71,6 @@ impl EventBus { }); } - // --- game -------------------------------------------------------------- - pub fn game_stage(&self, cluster_id: i64, stage: LaunchStage) { self.emit(GameEvent::Stage { cluster_id, stage }); } @@ -112,19 +89,11 @@ impl EventBus { }); } - // --- signals ----------------------------------------------------------- - pub fn signal(&self, signal: Signal) { self.emit(signal); } - // --- prompts ----------------------------------------------------------- - - /// Asks the user a question and waits for the answer. - /// - /// Returns `Ok(None)` when the prompt was dismissed. The caller's own value - /// for the chosen option comes back in [`Chosen::value`]; the untyped choice - /// id never escapes this function. + /// Returns `Ok(None)` when the prompt was dismissed #[tracing::instrument(level = "debug", skip_all, fields(title = %prompt.title))] pub async fn ask(&self, prompt: Prompt) -> EventResult>> { let Prompt { @@ -147,8 +116,7 @@ impl EventBus { }))) .map_err(|_| EventError::BusClosed)?; - // A dropped sender means the UI went away without answering, which is - // indistinguishable from a dead bus as far as the caller is concerned. + // A dropped sender means the UI went away without answering let Some(answer) = reply_rx.await.map_err(|_| EventError::BusClosed)? else { return Ok(None); }; @@ -222,8 +190,6 @@ mod tests { .dismiss("Cancel") } - /// The whole point of the redesign: the caller gets its own enum back, not - /// a string it has to re-match. #[tokio::test] async fn ask_maps_the_answer_back_to_the_callers_value() { let (bus, mut rx) = EventBus::channel(); @@ -280,8 +246,6 @@ mod tests { assert!(task.await.unwrap().unwrap().is_none()); } - /// A front-end answering with a choice that was never offered is a bug, and - /// must not be silently folded into "the user cancelled". #[tokio::test] async fn unknown_choice_is_an_error_not_a_dismissal() { let (bus, mut rx) = EventBus::channel(); @@ -298,8 +262,6 @@ mod tests { ); } - /// Dropping the UI mid-prompt must unblock the waiting task rather than - /// hanging it forever. #[tokio::test] async fn dropping_the_receiver_unblocks_the_asker() { let (bus, rx) = EventBus::channel(); diff --git a/packages/oneclient_events/src/error.rs b/packages/oneclient_events/src/error.rs index 5a3ef014..d36fc4ec 100644 --- a/packages/oneclient_events/src/error.rs +++ b/packages/oneclient_events/src/error.rs @@ -2,13 +2,11 @@ pub type EventResult = Result; #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum EventError { - /// The receiving end went away: the UI shut down, or was never started. #[error("the event bus is closed")] BusClosed, - /// The UI answered a prompt with a choice that was not offered. Only - /// reachable from a buggy front-end, but it is a wrong answer rather than a - /// missing one, so it must not be silently read as "dismissed". + /// A wrong answer rather than a missing one so it must not be read as + /// "dismissed" #[error("prompt answered with an unknown choice '{0}'")] UnknownChoice(&'static str), } diff --git a/packages/oneclient_events/src/event.rs b/packages/oneclient_events/src/event.rs index 1d9050f2..6c87722d 100644 --- a/packages/oneclient_events/src/event.rs +++ b/packages/oneclient_events/src/event.rs @@ -3,35 +3,19 @@ use uuid::Uuid; use crate::progress::GroupedProgressEvent; use crate::prompt::PromptRequest; -/// Everything the core tells the UI about. -/// -/// One channel carries all of it, so ordering between a progress update and the -/// notification that follows it is preserved. -/// -/// The split between [`Event::Notification`] and [`Event::Progress`] is the -/// difference between an opinion and a fact. A notification is the core saying -/// "tell the user this", so it is user-facing by definition. Progress is the -/// core stating that some work is 40% done; whether that becomes a toast, a bar -/// in a modal, or nothing at all is the front-end's call. -/// -/// That is also why nothing here names a screen or a surface. Progress that -/// should not become a toast simply never reaches the global bus: whoever -/// starts the work hands it a bus they own (see [`crate::EventBus::channel`]), -/// and the core never learns who is listening. +/// One channel carries all of it so ordering between a progress update and the +/// notification that follows it is preserved +/// Nothing here names a screen or a surface rendering is entirely the +/// front-end's call #[derive(Debug)] pub enum Event { - /// Something to put in front of the user: a message, or a question. Notification(Notification), - /// Work happening, with a completion fraction. Carries no display opinion. Progress(ProgressEvent), - /// A running Minecraft instance. Game(GameEvent), - /// State changed elsewhere; whoever caches it should refetch. + /// State changed elsewhere whoever caches it should refetch Signal(Signal), } -/// Something the notification layer owns: a message to show, or a question that -/// blocks the emitting task until answered. #[derive(Debug)] pub enum Notification { Message(Message), @@ -96,8 +80,7 @@ pub enum Level { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProgressEvent { - /// A single task's progress, keyed by `id` so repeated updates replace one - /// another rather than stacking up. + /// Keyed by `id` so repeated updates replace one another rather than stack Update { id: Uuid, label: String, @@ -105,16 +88,14 @@ pub enum ProgressEvent { total: u64, }, - /// Turn the in-flight [`ProgressEvent::Update`] with this `id` into a - /// finished message in place, so a download and its completion notice are - /// one card instead of two. + /// Turns the in-flight [`ProgressEvent::Update`] with this `id` into a + /// finished message in place rather than emitting a second card Complete { id: Uuid, title: String, body: String, }, - /// A tree of related tasks. See [`crate::progress`]. Grouped(GroupedProgressEvent), } @@ -141,12 +122,10 @@ impl LaunchStage { } } -/// Fire-and-forget notices that something changed. Carry no payload beyond -/// what the consumer needs to decide what to invalidate. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Signal { ClustersChanged, JavaChanged, - /// Initial background sync finished. + /// Initial background sync finished SyncComplete, } diff --git a/packages/oneclient_events/src/lib.rs b/packages/oneclient_events/src/lib.rs index 1f5cc583..d6c15444 100644 --- a/packages/oneclient_events/src/lib.rs +++ b/packages/oneclient_events/src/lib.rs @@ -1,13 +1,7 @@ -//! The channel the core talks to the UI over. -//! -//! Every subsystem holds an [`EventBus`] clone and emits [`Event`]s into it; -//! the front-end owns the single receiver and decides what to render. The bus -//! is mostly fire-and-forget. The one exception is [`EventBus::ask`], which -//! blocks the emitting task until the user answers. -//! -//! This crate is a leaf. It must not learn what a Java vendor, a package -//! provider or a cluster is: prompts carry caller-defined values through -//! [`Prompt`], and [`TaskCategory`] holds display labels rather than types. +//! This crate is a leaf +//! It must not learn what a Java vendor a package provider or a cluster is +//! prompts carry caller-defined values through [`Prompt`] and +//! [`TaskCategory`] holds display labels rather than types mod bus; mod error; diff --git a/packages/oneclient_events/src/progress.rs b/packages/oneclient_events/src/progress.rs index 538f4bdc..d7663c9d 100644 --- a/packages/oneclient_events/src/progress.rs +++ b/packages/oneclient_events/src/progress.rs @@ -1,9 +1,6 @@ -//! Progress for a tree of related tasks. One session has many children, so a -//! multi-file download reports as a single aggregate rather than N cards. -//! -//! Both handles are RAII: dropping the last one ends the session or finishes -//! the child, so an early return on an error path cannot leave the UI showing a -//! task that will never complete. +//! Both handles are RAII dropping the last one ends the session or finishes +//! the child so an early return on an error path cannot leave the UI showing a +//! task that will never complete use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; @@ -18,9 +15,8 @@ pub enum GroupedProgressEvent { session_id: Uuid, title: String, }, - /// Pre-announce how many children (and total bytes) a category will have, - /// so the aggregate total is known up-front instead of climbing as each - /// child is added during a `buffer_unordered` fan-out. + /// Pre-announces a category's children so the aggregate total is known + /// up-front instead of climbing during a `buffer_unordered` fan-out Expect { session_id: Uuid, category: TaskCategory, @@ -75,11 +71,7 @@ impl TaskPhase { } } -/// Coarse grouping of a grouped-progress child. Drives the notification body text -/// ("Downloading Minecraft" vs "Downloading Packages") and the per-category task rows. -/// -/// These are display labels, not a dependency on the subsystems they name. The -/// event layer must not learn what a Java vendor or a package provider is. +/// Display labels only not a dependency on the subsystems they name #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum TaskCategory { Client, @@ -106,7 +98,6 @@ impl TaskCategory { } } - /// True for everything that is part of the Minecraft install (not user packages). #[must_use] pub fn is_minecraft(self) -> bool { !matches!(self, Self::Packages) @@ -176,8 +167,7 @@ impl GroupedProgressSession { self.inner.session_id } - /// Reserve the expected work for a category before its children are added. - /// `count` = number of files, `total` = sum of their sizes in bytes. + /// `count` = number of files `total` = sum of their sizes in bytes pub fn expect(&self, category: TaskCategory, count: u64, total: u64) { if count == 0 { return; @@ -247,9 +237,8 @@ impl GroupedProgressSession { self.inner.end(); } - /// Consumes the session without emitting an `End` event, returning its id. - /// Use when another actor (e.g. the UI bridge) will take over the session's - /// notification entry and convert it to a finished state itself. + /// Consumes the session without emitting an `End` event + /// Use when another actor will take over the session's notification entry pub fn detach(self) -> Uuid { self.inner.ended.store(true, Ordering::Relaxed); self.inner.session_id @@ -287,10 +276,8 @@ impl GroupedProgressChild { return; } - // Prefer the largest known total. Streaming reports Content-Length which is - // often missing (1) for compressed/chunked responses; when the child was - // created with a real expected size (from a manifest) we must not let that - // stale header clobber it, otherwise the download bar reads 100%/frozen. + // Prefer the largest known total Content-Length is often missing (1) for + // chunked responses and would clobber a real manifest size freezing the bar let stored = self.inner.total.load(Ordering::Relaxed); let total = total.unwrap_or(0).max(stored).max(1); if total > stored { @@ -325,15 +312,8 @@ impl GroupedProgressChild { } impl Drop for GroupedProgressChild { - /// Only the *last* handle finishes the task. - /// - /// `finished` lives in the shared inner, so finishing on every clone's drop - /// ended the task the moment any temporary copy died, and copies are handed - /// around routinely (into `ResponseOptions`, into stream closures). A - /// download would emit `FinishChild` before its first byte arrived, after - /// which every `set_progress` silently no-opped and the UI removed the row. - /// The safety net for early returns still works: on an error path the last - /// handle goes out of scope too. + /// Only the *last* handle finishes the task clones are handed around + /// routinely and finishing on any clone's drop would end the task early fn drop(&mut self) { if Arc::strong_count(&self.inner) == 1 { self.finish(); @@ -362,10 +342,6 @@ mod tests { .any(|e| matches!(e, GroupedProgressEvent::FinishChild { .. })) } - /// A child handle gets cloned into `ResponseOptions` and into the streaming - /// closure. If dropping one of those copies finished the task, the download - /// would report completion before its first byte and every later progress - /// update would be dropped on the floor. #[test] fn dropping_a_clone_does_not_finish_the_task() { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); diff --git a/packages/oneclient_events/src/prompt.rs b/packages/oneclient_events/src/prompt.rs index d06e4b83..2f54e859 100644 --- a/packages/oneclient_events/src/prompt.rs +++ b/packages/oneclient_events/src/prompt.rs @@ -1,57 +1,39 @@ -//! Asking the user a question and getting a typed answer back. -//! -//! The previous design had one closed `UserChoice` enum listing every answer -//! any prompt could produce, including `Install { vendor: JavaVendor }`, which -//! meant the event layer depended on the Java subsystem that depended on it. -//! -//! Here a prompt carries its own [`Choice`] list. The wire format is untyped -//! (a choice id plus optional collected input), but [`Prompt`] pairs each -//! choice with a caller-defined value and hands that value back, so call sites -//! match on their own enum rather than on strings. +//! A prompt carries its own [`Choice`] list +//! The wire format is untyped (a choice id plus optional collected input) but +//! [`Prompt`] pairs each choice with a caller-defined value keeping this +//! crate free of any dependency on the subsystems that raise prompts use std::path::{Path, PathBuf}; use tokio::sync::oneshot; -/// How prominently the UI should render a choice. Only a hint; the event layer -/// has no business knowing what a button looks like. +/// Only a hint the event layer has no business knowing what a button looks like #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ChoiceStyle { - /// The recommended action. Primary, #[default] Secondary, - /// Destructive; render with a warning treatment. Danger, } -/// Something the UI must collect from the user before the choice can be -/// answered. Without this a prompt could only return "which button", and -/// answers like "…and here is the folder they picked" would have no home. +/// Something the UI must collect before the choice can be answered #[derive(Debug, Clone, PartialEq, Eq)] pub enum ChoiceInput { - /// Open a directory picker. `title` is the dialog's title. Folder { title: String }, - /// Collect a caller-defined selection, e.g. a Java vendor chosen from a - /// list the UI fetches live. `hint` names the kind of thing being picked so - /// the front-end can route to the right picker; its meaning belongs to - /// whoever built the prompt. + /// `hint` names the kind of thing being picked so the front-end can route to + /// the right picker its meaning belongs to whoever built the prompt Selection { hint: String }, } -/// What the UI collected for a [`ChoiceInput`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum InputValue { Folder(PathBuf), Selection(String), } -/// One answer the user can give. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Choice { - /// Stable identifier, matched when the answer comes back. A `&'static str` - /// rather than a `String` because every id in practice is a literal, and it - /// keeps `Choice` cheap to clone onto the wire. + /// Stable identifier matched when the answer comes back pub id: &'static str, pub label: String, pub style: ChoiceStyle, @@ -84,7 +66,6 @@ impl Choice { self } - /// Require a directory before this choice can be answered. #[must_use] pub fn picks_folder(mut self, title: impl Into) -> Self { self.input = Some(ChoiceInput::Folder { @@ -93,7 +74,6 @@ impl Choice { self } - /// Require a caller-defined selection before this choice can be answered. #[must_use] pub fn picks_selection(mut self, hint: impl Into) -> Self { self.input = Some(ChoiceInput::Selection { hint: hint.into() }); @@ -101,8 +81,6 @@ impl Choice { } } -/// A question, its answers, and what each answer means to the caller. -/// /// ```ignore /// enum JavaAnswer { Download, PickFolder } /// @@ -137,22 +115,19 @@ impl Prompt { } } - /// Add a choice and the value returned if the user picks it. pub fn option(mut self, choice: Choice, value: T) -> Self { self.options.push((choice, value)); self } - /// Offer an explicit way out, labelled `label`. Dismissing resolves the - /// prompt to `Ok(None)`; a prompt without this can still be dismissed by - /// closing it, so callers must always handle `None`. + /// A prompt without this can still be dismissed by closing it so callers + /// must always handle `None` pub fn dismiss(mut self, label: impl Into) -> Self { self.dismiss = Some(label.into()); self } } -/// What the user picked: the caller's own value, plus anything the UI collected. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Chosen { pub value: T, @@ -160,7 +135,6 @@ pub struct Chosen { } impl Chosen { - /// The directory picked for a [`ChoiceInput::Folder`] choice. pub fn folder(&self) -> Option<&Path> { match &self.input { Some(InputValue::Folder(path)) => Some(path), @@ -168,7 +142,6 @@ impl Chosen { } } - /// The selection made for a [`ChoiceInput::Selection`] choice. pub fn selection(&self) -> Option<&str> { match &self.input { Some(InputValue::Selection(value)) => Some(value), @@ -177,8 +150,7 @@ impl Chosen { } } -/// The untyped prompt as it crosses the bus. The UI renders `choices` and -/// replies with the id of the one picked; `T` never leaves the caller's crate. +/// The untyped prompt as it crosses the bus `T` never leaves the caller's crate #[derive(Debug)] pub struct PromptRequest { pub title: String, @@ -188,7 +160,7 @@ pub struct PromptRequest { pub reply: oneshot::Sender>, } -/// The UI's reply. `None` on the wire means dismissed. +/// `None` on the wire means dismissed #[derive(Debug, Clone, PartialEq, Eq)] pub struct Answer { pub id: &'static str, diff --git a/packages/oneclient_java/examples/install_provider.rs b/packages/oneclient_java/examples/install_provider.rs index ac82c420..b9ef8b9c 100644 --- a/packages/oneclient_java/examples/install_provider.rs +++ b/packages/oneclient_java/examples/install_provider.rs @@ -1,7 +1,4 @@ -//! Downloads and probes one vendor's JDK, then records it in an in-memory store. -//! -//! Runs against `MemoryJavaStore`, so it needs no database and no ephemeral -//! `LauncherState` just to list packages. +//! Downloads and probes one vendor's JDK then records it in an in-memory store use std::env; @@ -29,8 +26,7 @@ async fn main() -> JavaResult<()> { .expect("major version must be a number"); let net = RequestClient::new(NetConfig::default())?; - // Nothing drains this, so events are dropped; the install path only uses it - // to report progress. + // Nothing drains this so events are dropped let (events, _rx) = EventBus::channel(); let provider: Box = match vendor.as_str() { @@ -62,7 +58,6 @@ async fn main() -> JavaResult<()> { println!(" vendor: {}", info.vendor); println!(" arch: {}", info.os_arch); - // Prove the service records it without a database in sight. let service = JavaService::new(MemoryJavaStore::new(), net, events); service.add_custom_runtime(executable).await?; println!("Registered runtimes: {:?}", service.list_runtimes().await?); diff --git a/packages/oneclient_java/examples/list_providers.rs b/packages/oneclient_java/examples/list_providers.rs index 2f91d866..c85cb345 100644 --- a/packages/oneclient_java/examples/list_providers.rs +++ b/packages/oneclient_java/examples/list_providers.rs @@ -1,8 +1,4 @@ -//! Lists the JDK packages each vendor currently publishes. -//! -//! Needs only a network client, no database or launcher state. That is the -//! point of `JavaStore` being a port: the vendor providers are exercisable on -//! their own. +//! Lists the JDK packages each vendor currently publishes use std::env; diff --git a/packages/oneclient_java/src/checker.rs b/packages/oneclient_java/src/checker.rs index b4d89dd8..d9ba40b4 100644 --- a/packages/oneclient_java/src/checker.rs +++ b/packages/oneclient_java/src/checker.rs @@ -12,11 +12,8 @@ pub struct JavaCheckInfo { pub version: String, pub vendor: String, pub os_arch: String, - /// Whether this is a development kit rather than a bare runtime image. - /// - /// Minecraft starts on either, but mod loaders and the game's own tooling - /// expect the kit, so the locator prefers one when both satisfy the - /// requested major (see [`crate::locate::best_for_major`]). + /// Mod loaders and game tooling expect a JDK so the locator prefers one + /// when both satisfy the requested major pub is_jdk: bool, } @@ -68,9 +65,7 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult JavaResult, key: &str) -> bool { info.get(key) .is_some_and(|value| value.eq_ignore_ascii_case("true")) diff --git a/packages/oneclient_java/src/data.rs b/packages/oneclient_java/src/data.rs index d7364c36..d92a9962 100644 --- a/packages/oneclient_java/src/data.rs +++ b/packages/oneclient_java/src/data.rs @@ -12,9 +12,6 @@ pub struct JavaRuntime { pub version: String, pub vendor: JavaVendor, pub os_arch: String, - /// Whether this is a development kit rather than a bare runtime image, as - /// the probe reported it when the runtime was recorded (see - /// [`crate::JavaCheckInfo::is_jdk`]). pub is_jdk: bool, } @@ -26,26 +23,17 @@ pub struct JavaPackage { pub java_version: Vec, pub vendor: JavaVendor, pub archive: PackageArchive, - /// What the vendor promises the archive hashes to. Every vendor publishes - /// one, in its own algorithm — SHA-256 for Adoptium, Corretto and Zulu, - /// SHA-1 for Liberica — so this carries the algorithm with it. - /// - /// `None` only when a vendor's metadata omitted or malformed it. A runtime - /// that cannot be verified is still worth installing (the alternative is a - /// launcher that cannot start the game at all), so this degrades to an - /// unverified download rather than a hard failure. + /// Algorithm varies per vendor + /// `None` when vendor metadata omitted or malformed it + /// installs unverified rather than failing pub checksum: Option, - /// Archive size in bytes when the vendor publishes it, for progress bars - /// that would otherwise wait on `Content-Length`. + /// Bytes when the vendor publishes it pub size: Option, } impl JavaPackage { - /// Drops a checksum the vendor returned in an unusable shape. - /// - /// A vendor that starts sending an empty string or a placeholder would - /// otherwise make every runtime install fail with what looks to the user - /// like a corrupt download, three retries deep. + /// Drops malformed vendor checksums which would otherwise make every + /// install fail as a corrupt download #[must_use] pub fn with_checksum(mut self, checksum: Option) -> Self { self.checksum = checksum.filter(|sum| { diff --git a/packages/oneclient_java/src/error.rs b/packages/oneclient_java/src/error.rs index 633cc4c7..4a88060c 100644 --- a/packages/oneclient_java/src/error.rs +++ b/packages/oneclient_java/src/error.rs @@ -61,19 +61,13 @@ pub enum JavaError { } impl JavaError { - /// Whether this is an expected outcome or the user's environment, rather - /// than a launcher bug. - /// - /// The crate states the fact; turning it into "keep this out of Sentry" is - /// a policy the composition layer applies, since nothing down here should - /// know that Sentry exists. + /// Whether this is an expected outcome or the user's environment rather + /// than a launcher bug + /// Callers decide what to do with that (e.g. suppress error reporting) #[must_use] pub fn is_transient(&self) -> bool { match self { - // Expected outcomes: no build exists for the requested version, or - // the user cancelled setup. JavaError::PackageNotFound { .. } | JavaError::Cancelled => true, - // Environmental IO (e.g. out of disk while extracting the runtime). JavaError::RuntimeCheckError { source, .. } | JavaError::ArchiveExtract { source, .. } => is_transient_io(source), JavaError::PolyIOError(polyio::IOError::IOError(source)) @@ -92,7 +86,7 @@ fn is_transient_io(error: &std::io::Error) -> bool { if let Some(code) = error.raw_os_error() { #[cfg(unix)] if code == 28 { - // ENOSPC: out of disk part-way through extracting a runtime. + // ENOSPC return true; } #[cfg(windows)] diff --git a/packages/oneclient_java/src/install.rs b/packages/oneclient_java/src/install.rs index fd7f75d5..1e81c076 100644 --- a/packages/oneclient_java/src/install.rs +++ b/packages/oneclient_java/src/install.rs @@ -24,11 +24,7 @@ pub async fn install_package( let major = package.java_version.first().copied().unwrap_or(0); - // When a caller owns a grouped session (onboarding, cluster prepare) the - // runtime is one of that session's tasks. Only a standalone install (from - // settings, say) gets its own notification. - // No size in the package metadata; the child picks up its real total from - // Content-Length as soon as the response headers land. + // 0 means unknown the real total comes from Content-Length once headers land let expected_size = package.size.unwrap_or(0); let child = progress.map(|session| { session.child( @@ -55,12 +51,6 @@ pub async fn install_package( ); } - // A runtime is the largest single file the launcher downloads, so it is the - // most likely to be interrupted — and it used to be the one download with no - // verification at all. Now it is hashed from the bytes in flight against the - // vendor's own checksum, retried on a dropped connection, and only published - // to `archive_path` once it matches, so extraction never sees a partial - // archive. oneclient_net::download_verified( net, events, diff --git a/packages/oneclient_java/src/lib.rs b/packages/oneclient_java/src/lib.rs index 7dfaa3b1..490ff736 100644 --- a/packages/oneclient_java/src/lib.rs +++ b/packages/oneclient_java/src/lib.rs @@ -1,9 +1,5 @@ -//! Locating, probing and installing Java runtimes. -//! -//! [`JavaService`] is the entry point. It takes a [`JavaStore`] rather than a -//! database handle, so this crate has no schema dependency: the launcher wires -//! up a SQLite-backed implementation, while tests and examples use -//! [`MemoryJavaStore`]. +//! [`JavaService`] takes a [`JavaStore`] rather than a database handle so this +//! crate has no schema dependency mod checker; mod data; diff --git a/packages/oneclient_java/src/locate.rs b/packages/oneclient_java/src/locate.rs index 1a50e750..2194b490 100644 --- a/packages/oneclient_java/src/locate.rs +++ b/packages/oneclient_java/src/locate.rs @@ -8,10 +8,9 @@ use crate::data::java_executable_relative_path; use crate::error::JavaResult; use crate::service::parse_major_version; -/// A `java` executable found on this machine and what probing it reported. pub type LocatedJava = (PathBuf, JavaCheckInfo); -/// Every usable installation on this machine, best first. +/// Best first #[tracing::instrument(level = "debug")] pub async fn locate_java() -> JavaResult> { let paths = internal_locate_java(); @@ -36,10 +35,8 @@ pub async fn locate_java() -> JavaResult> { Ok(valid) } -/// The best installation satisfying `major`, if this machine has one. -/// -/// The version requirement is a filter rather than part of the ranking, so a -/// newer JDK can never stand in for the major that was actually asked for. +/// `major` is a filter not a ranking input +/// a newer JDK never stands in for the requested major #[must_use] pub fn best_for_major(candidates: &[LocatedJava], major: u32) -> Option<&LocatedJava> { candidates @@ -48,18 +45,14 @@ pub fn best_for_major(candidates: &[LocatedJava], major: u32) -> Option<&Located .min_by(|(_, a), (_, b)| compare_candidates(a, b)) } -/// Best first: a JDK beats a JRE, and between two of a kind the newer wins. -/// -/// A kit is preferred outright rather than only as a tie-break on version, -/// since a JRE is missing tooling that mod loaders reach for, whereas an older -/// patch level of the same major is merely older. +/// Best first a JDK beats a JRE outright then the newer version wins fn compare_candidates(a: &JavaCheckInfo, b: &JavaCheckInfo) -> Ordering { b.is_jdk .cmp(&a.is_jdk) .then_with(|| version_key(b).cmp(&version_key(a))) } -/// `java.version` as numbers, so `21.0.10` sorts above `21.0.9`. +/// Numeric so `21.0.10` sorts above `21.0.9` fn version_key(info: &JavaCheckInfo) -> (u32, Vec) { let major = parse_major_version(&info.version).unwrap_or(0); let parts = info diff --git a/packages/oneclient_java/src/platform.rs b/packages/oneclient_java/src/platform.rs index 3e5d29eb..af8d3f0c 100644 --- a/packages/oneclient_java/src/platform.rs +++ b/packages/oneclient_java/src/platform.rs @@ -1,13 +1,5 @@ -//! What machine we are running on, decided once. -//! -//! Each of the four vendors had its own `cfg_select!` pair working this out, -//! which is how they drifted: Adoptium says `mac` where the rest say `macos`, -//! Adoptium says `alpine-linux` where Liberica and Zulu say `linux-musl`, and -//! Adoptium alone spells 32-bit x86 `x32`. -//! -//! Detection is shared here. The *naming* stays with each vendor: that part is -//! not duplication, it is what each API actually expects, and flattening it -//! would only hide the differences. +//! Host detection is shared here but the *naming* stays per-vendor +//! each API spells the same platform differently (e.g. Adoptium `mac`/`alpine-linux`/`x32`) use crate::data::PackageArchive; @@ -23,8 +15,7 @@ pub enum HostArch { pub enum HostOs { Windows, MacOs, - /// `musl` distinguishes Alpine-style builds, which several vendors publish - /// under a separate name. + /// Several vendors publish musl builds under a separate name Linux { musl: bool, }, @@ -37,7 +28,6 @@ pub struct HostTarget { } impl HostTarget { - /// The machine this build is running on. pub const CURRENT: Self = Self { arch: cfg_select! { target_arch = "x86" => HostArch::X86, @@ -55,8 +45,7 @@ impl HostTarget { }, }; - /// Pointer width as the vendors spell it. Liberica takes architecture and - /// bitness as separate query parameters rather than one combined token. + /// Liberica takes architecture and bitness as separate query parameters #[must_use] pub const fn bitness(self) -> &'static str { match self.arch { @@ -65,7 +54,6 @@ impl HostTarget { } } - /// Archive format the vendors ship for this platform. #[must_use] pub const fn archive(self) -> PackageArchive { match self.os { diff --git a/packages/oneclient_java/src/service.rs b/packages/oneclient_java/src/service.rs index 9ccf5bad..9f516e41 100644 --- a/packages/oneclient_java/src/service.rs +++ b/packages/oneclient_java/src/service.rs @@ -13,19 +13,15 @@ use crate::resolve::resolve_java_executable; use crate::store::JavaStore; use crate::vendors::{self, JavaVendor}; -/// Choice ids for the "Java required" prompt. Namespaced so a front-end can -/// tell this prompt apart from any other by looking for one of these ids. +/// Choice ids for the "Java required" prompt front-ends match on these pub const JAVA_CHOICE_DOWNLOAD: &str = "java.download"; pub const JAVA_CHOICE_FOLDER: &str = "java.folder"; -/// Names the kind of selection the Java prompt expects back, so the front-end -/// knows to show its vendor list rather than some other picker. +/// Tells the front-end to show its vendor list rather than another picker pub const JAVA_VENDOR_HINT: &str = "java-vendor"; pub const INSTALLABLE_MAJORS: &[u32] = &[8, 11, 16, 17, 18, 19, 20, 21, 22, 23]; -/// What each option on the "Java required" prompt means. Local to this module, -/// since the event bus only ever sees the choice ids. enum JavaPromptAnswer { Download, PickFolder, @@ -37,12 +33,6 @@ pub struct AvailableJava { pub package: JavaPackage, } -/// Everything the Java flows need: somewhere to remember runtimes, a way to -/// fetch them, and a bus to report progress and ask questions on. -/// -/// `JavaManager` was a unit struct whose methods each took a `&DbPool` or a -/// `&LauncherServices`; the state is held here instead, and the store is a port -/// (see [`crate::JavaStore`]) so this crate never depends on the database. pub struct JavaService { store: Arc, net: RequestClient, @@ -65,14 +55,11 @@ impl JavaService { &self.events } - // --- queries ----------------------------------------------------------- - #[tracing::instrument(level = "debug", skip(self))] pub async fn list_runtimes(&self) -> JavaResult> { Ok(self.store.list().await?) } - /// The runtime a settings profile points at, if it is still registered. #[tracing::instrument(level = "debug", skip(self))] pub async fn runtime_for_profile( &self, @@ -109,9 +96,6 @@ impl JavaService { Ok(available) } - // --- registration ------------------------------------------------------ - - /// Re-scans the well-known install locations and records what it finds. #[tracing::instrument(level = "debug", skip(self))] pub async fn rescan(&self) -> JavaResult<()> { self.register_located(&crate::locate::locate_java().await?) @@ -131,13 +115,8 @@ impl JavaService { Ok(()) } - // --- acquisition ------------------------------------------------------- - - /// Finds a usable Java `major`, downloading or asking the user if needed. - /// - /// A JDK is preferred over a JRE of the same major throughout: a recorded - /// runtime that turns out to be a bare JRE is kept only as a fallback, so a - /// system scan gets its chance to turn up a kit first. + /// A recorded JRE is kept only as a fallback so a system scan gets its + /// chance to turn up a JDK of the same major first #[tracing::instrument(level = "debug", skip(self, progress))] pub async fn prepare( &self, @@ -158,8 +137,7 @@ impl JavaService { let located = crate::locate::locate_java().await?; self.register_located(&located).await?; - // Anything located only displaces what is already recorded when it - // is the kit that recording is not. + // A located runtime only displaces a recorded one by being a JDK if let Some((path, info)) = crate::locate::best_for_major(&located, major) && (info.is_jdk || recorded.is_none()) { @@ -205,11 +183,6 @@ impl JavaService { self.register_checked(&executable, Some(major)).await } - /// Ask the user how to get a missing runtime, then act on the answer. - /// - /// The choice ids stay inside this function: `ask` hands back the - /// [`JavaPromptAnswer`] paired with the chosen option, so the vendor never - /// has to be named in the event layer. async fn prompt_and_install( &self, major: u32, @@ -237,8 +210,6 @@ impl JavaService { }; match chosen.value { - // The UI offers a vendor list; picking one installs it directly. - // Without a selection we fall back to trying every vendor in turn. JavaPromptAnswer::Download => match chosen.selection() { Some(vendor) => { let vendor = JavaVendor::from_str(vendor) @@ -257,7 +228,6 @@ impl JavaService { } } - /// Tries every vendor in turn until one yields a working runtime. #[tracing::instrument(level = "debug", skip(self, progress))] async fn download_and_register( &self, @@ -299,9 +269,6 @@ impl JavaService { Err(JavaError::PackageNotFound { major }) } - // --- persistence ------------------------------------------------------- - - /// Probes `executable`, checks it is the version we expected, and records it. #[tracing::instrument(level = "debug", skip(self))] async fn register_checked( &self, @@ -323,8 +290,7 @@ impl JavaService { self.persist(executable, &info).await } - /// Records everything a filesystem scan turned up, skipping anything that - /// fails to probe. A broken JDK on the system should not abort the scan. + /// Skips anything that fails to probe a broken JDK must not abort the scan #[tracing::instrument(level = "debug", skip_all)] async fn register_located(&self, located: &[(PathBuf, JavaCheckInfo)]) -> JavaResult<()> { for (path, info) in located { @@ -360,10 +326,8 @@ fn provider_for_vendor(vendor: &JavaVendor) -> Option Result { if let Some(rest) = version.strip_prefix("1.") { let digit = rest.chars().next().ok_or_else(|| JavaError::ParseVersion { diff --git a/packages/oneclient_java/src/store.rs b/packages/oneclient_java/src/store.rs index 94a4a795..ef582ce6 100644 --- a/packages/oneclient_java/src/store.rs +++ b/packages/oneclient_java/src/store.rs @@ -1,21 +1,11 @@ -//! Where located and installed runtimes are remembered. -//! -//! Defined as a port rather than a concrete database handle so this crate does -//! not depend on the launcher's schema. `java_versions` is a leaf table with a -//! single owner (this crate) and no joins, so the interface stays five methods -//! wide, unlike the artifact and cluster tables where a trait would just be the -//! SQL schema with extra steps. -//! -//! So `oneclient_java` has no database in its dependency tree, and its examples -//! and tests can run against an in-memory store instead of standing up a real -//! SQLite file. +//! A port rather than a concrete database handle so this crate does not depend +//! on the launcher's schema use std::sync::Arc; use crate::data::JavaRuntime; -/// Whatever went wrong in the backing store, kept opaque so this crate never -/// learns what is behind the port. +/// Opaque so this crate never learns what is behind the port #[derive(Debug, thiserror::Error)] #[error("java store: {0}")] pub struct StoreError(#[source] pub Box); @@ -30,12 +20,10 @@ pub type StoreResult = Result; #[async_trait::async_trait] pub trait JavaStore: Send + Sync { - /// Records a runtime, replacing any entry with the same path. async fn upsert(&self, runtime: &JavaRuntime) -> StoreResult; async fn get_by_path(&self, absolute_path: &str) -> StoreResult>; - /// The newest recorded runtime for a major version, if any. async fn latest_by_major(&self, major: u32) -> StoreResult>; async fn delete_by_path(&self, absolute_path: &str) -> StoreResult<()>; @@ -43,8 +31,6 @@ pub trait JavaStore: Send + Sync { async fn list(&self) -> StoreResult>; } -/// An in-memory [`JavaStore`], for tests and for examples that only want to -/// exercise the vendor providers. #[derive(Debug, Default)] pub struct MemoryJavaStore { runtimes: std::sync::Mutex>, diff --git a/packages/oneclient_java/src/vendors/adoptium.rs b/packages/oneclient_java/src/vendors/adoptium.rs index d92f90dc..7149832b 100644 --- a/packages/oneclient_java/src/vendors/adoptium.rs +++ b/packages/oneclient_java/src/vendors/adoptium.rs @@ -12,7 +12,6 @@ use crate::vendors::{JavaRuntimeProvider, JavaVendor}; pub struct AdoptiumRuntimeProvider; -// `/v3/info/available_releases` #[derive(Debug, Deserialize)] struct AdoptiumAvailableReleases { available_releases: Vec, @@ -39,8 +38,7 @@ struct AdoptiumBinary { struct AdoptiumPackage { name: String, link: String, - /// Adoptium's `checksum` is SHA-256; the API has no algorithm field because - /// it has only ever published one. + /// Always SHA-256 the API has no algorithm field #[serde(default)] checksum: Option, #[serde(default)] @@ -59,7 +57,6 @@ impl JavaRuntimeProvider for AdoptiumRuntimeProvider { major: Option, net: &RequestClient, ) -> JavaResult> { - // fetched from `/v3/info/available_releases`. let (low, high) = match major { Some(major) => (major, major), None => { @@ -117,8 +114,7 @@ impl JavaRuntimeProvider for AdoptiumRuntimeProvider { } } -// One request spanning every GA major via a version range (`[low,high]`), -// newest build per major, instead of one request per major. +// A version range spans every GA major in one request instead of one per major fn adoptium_url(low: u32, high: u32) -> JavaResult { let range = format!("%5B{low}%2C{}%5D", high + 1); @@ -129,8 +125,7 @@ fn adoptium_url(low: u32, high: u32) -> JavaResult { url.query_pairs_mut() .append_pair("os", ADOPTIUM_OS) .append_pair("architecture", ADOPTIUM_ARCH) - // Kits only: a JRE image is missing tooling the game and its mod - // loaders expect, so the launcher never installs one. + // Kits only a JRE lacks tooling mod loaders expect .append_pair("image_type", "jdk") .append_pair("jvm_impl", "hotspot") .append_pair("project", "jdk") @@ -144,7 +139,7 @@ fn adoptium_url(low: u32, high: u32) -> JavaResult { Ok(url) } -/// Adoptium alone spells 32-bit x86 `x32`. +/// Adoptium alone spells 32-bit x86 `x32` const ADOPTIUM_ARCH: &str = match HostTarget::CURRENT.arch { HostArch::X86 => "x32", HostArch::X86_64 => "x64", @@ -152,8 +147,8 @@ const ADOPTIUM_ARCH: &str = match HostTarget::CURRENT.arch { HostArch::Aarch64 => "aarch64", }; -/// Adoptium says `mac` where the other vendors say `macos`, and publishes musl -/// builds under `alpine-linux` rather than `linux-musl`. +/// Adoptium says `mac` where the other vendors say `macos` and publishes musl +/// builds under `alpine-linux` rather than `linux-musl` const ADOPTIUM_OS: &str = match HostTarget::CURRENT.os { HostOs::Windows => "windows", HostOs::MacOs => "mac", diff --git a/packages/oneclient_java/src/vendors/corretto.rs b/packages/oneclient_java/src/vendors/corretto.rs index dcc168d5..92b7801b 100644 --- a/packages/oneclient_java/src/vendors/corretto.rs +++ b/packages/oneclient_java/src/vendors/corretto.rs @@ -57,13 +57,8 @@ impl JavaRuntimeProvider for CorrettoRuntimeProvider { continue; }; - // Prefer the exact versioned URL the index names over the `latest` - // redirect. Both resolve to the same build today, but `latest` is - // resolved at download time: if Corretto publishes a new build in - // the window between listing and downloading, the redirect moves and - // the checksum from this index no longer describes those bytes. The - // download would then fail as corrupt, three retries deep, for a - // file that is perfectly intact. + // Prefer the versioned URL the `latest` redirect resolves at + // download time and can move away from this index's checksum let download_url = entry .get("resource") .and_then(Value::as_str) @@ -115,7 +110,7 @@ const CORRETTO_ARCH: &str = match HostTarget::CURRENT.arch { HostArch::Aarch64 => "aarch64", }; -/// Corretto publishes no separate musl build, so Alpine gets the glibc one. +/// Corretto publishes no separate musl build so Alpine gets the glibc one const CORRETTO_OS: &str = match HostTarget::CURRENT.os { HostOs::Windows => "windows", HostOs::MacOs => "macos", diff --git a/packages/oneclient_java/src/vendors/liberica.rs b/packages/oneclient_java/src/vendors/liberica.rs index e8559735..8db786e6 100644 --- a/packages/oneclient_java/src/vendors/liberica.rs +++ b/packages/oneclient_java/src/vendors/liberica.rs @@ -24,7 +24,7 @@ struct LibericaRelease { #[serde(rename = "buildVersion", default)] build_version: u32, version: String, - /// Liberica is the one vendor that publishes SHA-1 rather than SHA-256. + /// Liberica is the one vendor that publishes SHA-1 rather than SHA-256 #[serde(default)] sha1: Option, #[serde(default)] @@ -92,14 +92,14 @@ fn liberica_url(major: Option) -> JavaResult { let mut q = url.query_pairs_mut(); q.append_pair("os", LIBERICA_OS) .append_pair("arch", LIBERICA_ARCH) - // Kits only, never a bare runtime image. + // Kits only never a bare runtime image .append_pair("bundle-type", "jdk") .append_pair("bitness", LIBERICA_BITNESS) .append_pair("package-type", LIBERICA_PACKAGE) .append_pair("installation-type", "archive") .append_pair( "fields", - // `sha1` and `size` are only returned when named here. + // `sha1` and `size` are only returned when named here "downloadUrl,filename,featureVersion,updateVersion,buildVersion,version,sha1,size", ) .append_pair("output", "json"); @@ -110,8 +110,8 @@ fn liberica_url(major: Option) -> JavaResult { Ok(url) } -/// Liberica takes architecture family and bitness as separate parameters, so -/// x86/x86_64 collapse to `x86` and both ARM variants to `arm`. +/// Liberica takes architecture family and bitness as separate parameters so +/// x86/x86_64 collapse to `x86` and both ARM variants to `arm` const LIBERICA_ARCH: &str = match HostTarget::CURRENT.arch { HostArch::X86 | HostArch::X86_64 => "x86", HostArch::Arm | HostArch::Aarch64 => "arm", diff --git a/packages/oneclient_java/src/vendors/zulu.rs b/packages/oneclient_java/src/vendors/zulu.rs index bfb4ea5b..60bf753a 100644 --- a/packages/oneclient_java/src/vendors/zulu.rs +++ b/packages/oneclient_java/src/vendors/zulu.rs @@ -17,8 +17,7 @@ struct ZuluPackage { download_url: String, name: String, java_version: Vec, - /// Only present because `include_fields` asks for it; Azul omits it from - /// the default projection. + /// Only present because `include_fields` asks for it #[serde(default)] sha256_hash: Option, #[serde(default)] @@ -65,15 +64,14 @@ fn zulu_url(major: Option) -> JavaResult { q.append_pair("os", ZULU_OS) .append_pair("arch", ZULU_ARCH) .append_pair("archive_type", "zip") - // Kits only, never a bare runtime image. + // Kits only never a bare runtime image .append_pair("java_package_type", "jdk") .append_pair("javafx_bundled", "false") .append_pair("release_status", "ga") .append_pair("availability_types", "CA") .append_pair("certifications", "tck") .append_pair("latest", "true") - // Azul's default projection omits the hash, so a download could - // only ever be verified by asking for it explicitly. + // Azul's default projection omits the hash .append_pair("include_fields", "sha256_hash,size") .append_pair("page", "1") .append_pair("page_size", "100"); diff --git a/packages/oneclient_java/tests/vendor_checksums.rs b/packages/oneclient_java/tests/vendor_checksums.rs index 54220176..0f33700b 100644 --- a/packages/oneclient_java/tests/vendor_checksums.rs +++ b/packages/oneclient_java/tests/vendor_checksums.rs @@ -2,8 +2,8 @@ use oneclient_java::vendors::runtime_providers; use oneclient_net::{NetConfig, RequestClient}; use polyio::ChecksumAlgorithm; -/// A major every vendor still publishes, so a listing coming back empty means -/// the query is wrong rather than the release being retired. +/// A major every vendor still publishes so a listing coming back empty means +/// the query is wrong rather than the release being retired const MAJOR: u32 = 21; #[tokio::test] @@ -41,8 +41,6 @@ async fn every_vendor_publishes_a_usable_checksum() { continue; } - // A well-formed hash in the wrong algorithm would fail every download, - // so assert the algorithm each vendor actually publishes. for package in &packages { let checksum = package.checksum.as_ref().expect("checked above"); assert!( diff --git a/packages/oneclient_mc/src/arguments.rs b/packages/oneclient_mc/src/arguments.rs index 854d6ff7..0261e28b 100644 --- a/packages/oneclient_mc/src/arguments.rs +++ b/packages/oneclient_mc/src/arguments.rs @@ -122,12 +122,8 @@ fn is_64_bit(java_arch: &str) -> bool { ) } -/// Splits a user-entered JVM argument string into individual arguments. -/// -/// Whitespace-separated, with double quotes for values that contain spaces -/// (`-Dfoo="a b"`), which is what people expect from every other launcher. -/// Pushing the whole string as one argv entry, as this used to, means the JVM -/// sees a single unrecognised option as soon as there is more than one. +/// Whitespace-separated double quotes for values containing spaces +/// Must not be passed as one argv entry or the JVM sees one unrecognised option fn split_custom_args(raw: &str) -> Vec { let mut args = Vec::new(); let mut current = String::new(); diff --git a/packages/oneclient_mc/src/download.rs b/packages/oneclient_mc/src/download.rs index 06f28534..f88bf8ac 100644 --- a/packages/oneclient_mc/src/download.rs +++ b/packages/oneclient_mc/src/download.rs @@ -1,19 +1,10 @@ -//! Minecraft-flavoured wrappers over [`oneclient_net`]'s verified downloads. -//! -//! All that is left here is the part specific to the game install: naming the -//! grouped-progress child and its category. - use oneclient_events::{EventBus, GroupedProgressSession, TaskCategory}; use oneclient_net::RequestClient; use crate::error::McResult; -/// Downloads `url` to `dest` as one child of `progress`, verifying SHA-1 as the -/// bytes stream past. -/// -/// The child is created *before* the request goes out: waiting on response -/// headers is most of a small file's lifetime, and a child that only existed -/// while bytes were moving would never appear in the UI at all. +/// The progress child is created before the request goes out otherwise small +/// files dominated by header wait never appear in the UI #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip(requester, events, progress, label, expected_sha1), fields(%url), level = "debug")] pub async fn download_to_path( @@ -29,8 +20,7 @@ pub async fn download_to_path( ) -> McResult<()> { let child = progress.child(label, expected_size, category); - // Every hash in a Minecraft manifest is SHA-1, so callers here pass a bare - // string and the algorithm is implied. + // Every hash in a Minecraft manifest is SHA-1 so the algorithm is implied let expected = expected_sha1.map(polyio::Checksum::sha1); oneclient_net::download_verified( @@ -47,10 +37,8 @@ pub async fn download_to_path( Ok(()) } -/// Downloads `url` into memory, verifying SHA-1 over the collected bytes. -/// -/// For payloads that get parsed rather than stored, where writing to disk only -/// to read it straight back would be wasted IO. +/// Downloads into memory instead of to disk for payloads that are parsed +/// rather than stored #[tracing::instrument(skip(requester, events, progress, label), fields(%url), level = "debug")] #[allow(clippy::too_many_arguments)] pub async fn fetch_bytes_verified( diff --git a/packages/oneclient_mc/src/error.rs b/packages/oneclient_mc/src/error.rs index 2cb55136..50ce8011 100644 --- a/packages/oneclient_mc/src/error.rs +++ b/packages/oneclient_mc/src/error.rs @@ -5,7 +5,6 @@ pub type McResult = Result; #[derive(Debug, Error)] pub enum McError { - // --- content --- #[error("no client download exists for version {0}")] NoClientDownload(String), @@ -15,7 +14,6 @@ pub enum McError { #[error("invalid game version {0}")] InvalidVersion(String), - // --- manifests --- #[error("failed to fetch metadata")] FetchError, @@ -34,15 +32,13 @@ pub enum McError { #[error("no matching version found")] NoMatchingVersion, - /// Files could not be downloaded and the user chose not to launch anyway. #[error("cancelled: {failed} file(s) could not be downloaded")] IncompleteInstallCancelled { failed: usize }, - /// A Mojang response was well-formed JSON but not what the API documents. + /// A Mojang response was well-formed JSON but not what the API documents #[error("minecraft: {0}")] Minecraft(String), - // --- plumbing --- #[error("failed to parse metadata: {0}")] ParseError(#[from] serde_json::Error), @@ -63,8 +59,7 @@ pub enum McError { } impl McError { - /// Whether this is the user's environment rather than a launcher bug. - /// The composition layer turns this into a Sentry policy. + /// Environment fault rather than a launcher bug drives Sentry policy upstream #[must_use] pub fn is_transient(&self) -> bool { match self { diff --git a/packages/oneclient_mc/src/install.rs b/packages/oneclient_mc/src/install.rs index 9a1e0232..83dec810 100644 --- a/packages/oneclient_mc/src/install.rs +++ b/packages/oneclient_mc/src/install.rs @@ -18,18 +18,14 @@ use oneclient_common::paths; use crate::McCtx; use crate::error::{McError, McResult}; -/// Asset objects are tiny (median ~10 KiB) and latency-bound, so throughput -/// scales with how many are in flight, not with bandwidth. +/// Asset objects are tiny (median ~10 KiB) and latency-bound so throughput +/// scales with how many are in flight not with bandwidth const ASSET_DOWNLOAD_CONCURRENCY: usize = 32; -/// Libraries are larger and fewer; less fan-out is needed to saturate the link. +/// Libraries are larger and fewer less fan-out is needed to saturate the link const LIBRARY_DOWNLOAD_CONCURRENCY: usize = 16; -/// Everything a version still needs, with what is already on disk subtracted. -/// -/// Computed once, up-front, so the size shown before a download starts and the -/// denominators the progress bars fill against come from the same numbers. -/// Without it the estimate assumes a full download while the bars discover -/// their own total as files are added, so neither matches reality. +/// Computed up-front so the pre-download size estimate and the progress-bar +/// denominators come from the same numbers #[derive(Debug, Default)] pub struct DownloadPlan { pub assets: Vec<(String, Asset)>, @@ -55,17 +51,9 @@ fn asset_object_path(dir: &Path, name: &str, hash: &str, legacy: bool) -> PathBu } } -/// Whether `path` is present and as long as the manifest promised. -/// -/// `exists()` and `metadata()` are the same `stat` syscall, so checking the -/// length costs nothing over the presence check it replaces — and it catches the -/// truncated files an interrupted download leaves behind, which is what nearly -/// all download corruption looks like in practice. Comparing content instead -/// would mean reading and hashing every one of ~5000 assets on every launch; -/// that belongs behind an explicit repair action, not in the launch path. -/// -/// An `expected` of 0 means the manifest did not say, so presence is all there -/// is to go on. +/// Size not hash catches interrupted-download truncation without reading +/// ~5000 assets per launch +/// `expected` of 0 means the manifest gave no size fn matches_expected_size(path: &Path, expected: u64) -> bool { match std::fs::metadata(path) { Ok(meta) => expected == 0 || meta.len() == expected, @@ -73,8 +61,8 @@ fn matches_expected_size(path: &Path, expected: u64) -> bool { } } -/// The artifact size a library's manifest entry promises, or 0 when it carries -/// no artifact metadata (maven-coordinate libraries from loader manifests). +/// 0 when the entry carries no artifact metadata (maven-coordinate libraries +/// from loader manifests) fn library_artifact_size(lib: &Library) -> u64 { lib.downloads .as_ref() @@ -98,9 +86,8 @@ fn native_download_size(lib: &Library, java_arch: &str) -> u64 { .map_or(0, |native| u64::from(native.size)) } -/// Stats every candidate file, so it runs on the blocking pool. An asset index -/// is thousands of entries. Doing it here also means the download fan-outs no -/// longer stat each file from inside a concurrency slot. +/// Stats thousands of files so it runs on the blocking pool and keeps the +/// download fan-outs from stat-ing inside a concurrency slot #[tracing::instrument(skip_all, level = "debug")] pub async fn plan_downloads( version: &VersionInfo, @@ -151,7 +138,7 @@ pub async fn plan_downloads( } // A library whose coordinates don't resolve is kept in the plan so the - // download path still reports it as a failure rather than skipping it. + // download path still reports it as a failure rather than skipping it let Ok(artifact_path) = interfrost::utils::get_path_from_artifact(&lib.name) else { plan.libraries.push(lib); continue; @@ -185,39 +172,25 @@ pub async fn plan_downloads( .map_err(Into::into) } -/// What a hash-level sweep of the installed game files found. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct VerifyReport { - /// Files present on disk that were read and hashed. pub checked: usize, - /// Files whose contents did not match the manifest and were removed. + /// Mismatched the manifest and were removed pub corrupt: usize, - /// Files the manifest lists that were not on disk at all. pub missing: usize, } impl VerifyReport { - /// Whether anything needs re-downloading. #[must_use] pub fn needs_repair(&self) -> bool { self.corrupt > 0 || self.missing > 0 } } -/// Reads and hashes every installed game file, deleting the ones that do not -/// match the manifest. -/// -/// This is the expensive counterpart to the size check the launch path uses: -/// it reads every asset and library off disk, which is why it belongs behind an -/// explicit user action rather than in the pre-launch flow. Size catches the -/// truncation an interrupted download leaves; only hashing catches a file whose -/// bytes went bad some other way — a failing disk, a half-synced cloud folder, -/// an antivirus rewrite, or a partial file written before the launcher started -/// publishing downloads by rename. -/// -/// Nothing is re-downloaded here. Corrupt files are removed so that the normal -/// download path, which already knows how to fetch and verify exactly what is -/// missing, repairs them on the next prepare. +/// Expensive reads every asset and library so it belongs behind an explicit +/// user action not the launch path +/// Nothing is re-downloaded here corrupt files are deleted so the next prepare +/// refetches them #[tracing::instrument(skip(progress, version, assets_index), fields(version_id = %version.id), level = "debug")] pub async fn verify_game_files( progress: &GroupedProgressSession, @@ -234,8 +207,6 @@ pub async fn verify_game_files( }; let lib_dir = paths::libraries_dir()?; - // Assets are content-addressed by their own hash; libraries and the client - // carry theirs in the manifest. let mut targets: Vec<(PathBuf, String)> = Vec::new(); for (name, asset) in assets_index.objects { @@ -255,8 +226,8 @@ pub async fn verify_game_files( let Ok(artifact_path) = interfrost::utils::get_path_from_artifact(&lib.name) else { continue; }; - // Only libraries whose manifest entry carries a hash can be checked; - // maven-coordinate entries from loader manifests publish none. + // Only libraries whose manifest entry carries a hash can be checked + // maven-coordinate entries from loader manifests publish none if let Some(artifact) = lib .downloads .as_ref() @@ -281,9 +252,8 @@ pub async fn verify_game_files( let started = std::time::Instant::now(); let child_for_progress = child.clone(); - // One blocking task for the whole sweep rather than one per file: hashing - // thousands of small assets is dominated by per-file overhead, and a - // spawn_blocking dispatch each would cost more than the hashing itself. + // One blocking task for the whole sweep a spawn_blocking per small asset + // would cost more than the hashing itself let report = tokio::task::spawn_blocking(move || { let mut report = VerifyReport::default(); @@ -311,16 +281,16 @@ pub async fn verify_game_files( } } Err(err) => { - // Unreadable is as good as corrupt: the game cannot use it - // either. Removing it lets the download path replace it. + // Unreadable is as good as corrupt removing it lets the + // download path replace it tracing::warn!(path = %path.display(), "could not hash: {err}"); report.corrupt += 1; let _ = std::fs::remove_file(path); } } - // Reporting every file would emit thousands of events; the child's - // own sampling is per-call, so throttle here instead. + // The child's sampling is per-call so throttle here or this emits + // thousands of events if index % 64 == 0 { child_for_progress.set_progress(index as u64, Some(total.max(1))); } @@ -365,8 +335,8 @@ pub async fn download_minecraft( "planned minecraft download" ); - // Reserve each category's real total before any child appears, so the bars - // start at the right denominator instead of growing towards it. + // Reserve each category's real total before any child appears so the bars + // start at the right denominator instead of growing towards it progress.expect( TaskCategory::Assets, plan.assets.len() as u64, @@ -411,24 +381,14 @@ pub async fn download_minecraft( Ok(()) } -/// What the user chose to do about files that could not be downloaded. enum IncompleteInstall { LaunchAnyway, } -/// Asks whether to launch with files missing, when some could not be fetched. -/// -/// Individual assets and libraries are downloaded concurrently and their -/// failures counted rather than propagated, so that one unreachable file does -/// not abandon the other four thousand. That count used to be logged and -/// dropped, which meant an install that had visibly failed still went on to -/// launch a game with missing textures, sounds, or a library the loader needs — -/// and the user's only clue was a crash later. -/// -/// Each of these already survived every retry `oneclient_net` makes, so this is -/// not a transient blip. It is still the user's call: a -/// handful of missing assets is often survivable, and refusing to launch would -/// strand someone offline who just wants to play. +/// Download failures are counted not propagated so one unreachable file does +/// not abandon the other four thousand +/// They already survived every retry but it stays the user's call refusing to +/// launch would strand offline players #[tracing::instrument(skip(ctx), level = "debug")] pub async fn confirm_incomplete_install( ctx: &McCtx, @@ -447,9 +407,6 @@ pub async fn confirm_incomplete_install( ); let body = match (failed_assets, failed_libraries) { - // Libraries are on the classpath, so a missing one usually means the - // game will not start at all. Say so rather than implying it is as - // survivable as a missing sound file. (0, libraries) => format!( "{libraries} librar{} could not be downloaded after several attempts. \ The game will most likely fail to start without {}.", @@ -487,9 +444,8 @@ pub async fn confirm_incomplete_install( Ok(()) } }, - // Dismissed, or the prompt could not be delivered. Both mean nobody - // agreed to launch a broken install, so the safe reading is to stop — - // a headless caller with no UI attached should not silently opt in. + // Dismissed or undeliverable nobody agreed to launch a broken install + // and a headless caller must not silently opt in Ok(None) => Err(McError::IncompleteInstallCancelled { failed }), Err(err) => { tracing::warn!("could not ask about the incomplete install: {err}"); @@ -656,8 +612,8 @@ pub async fn download_assets( polyio::create_dir_all(&dir).await?; - // Objects are sharded into 256 fixed subdirectories; creating them once - // here saves a blocking-pool round trip on every one of the ~5000 files. + // Objects are sharded into 256 fixed subdirectories creating them once + // here saves a blocking-pool round trip on every one of the ~5000 files if !legacy { let dir = dir.clone(); tokio::task::spawn_blocking(move || { @@ -737,11 +693,8 @@ pub async fn download_client( .join(&version.id) .join(format!("{}.jar", version.id)); - // A size check, not a hash: this runs on every launch, and re-reading the - // whole ~25 MiB jar to hash it costs more than everything else in the - // pre-launch check combined. The jar is only ever published by a rename - // after its hash matched, so a wrong-but-right-length jar is not a state the - // launcher can produce. + // Size not hash the jar is only published by rename after its hash + // matched so a wrong-but-right-length jar is unreachable let expected_size = u64::from(client.size); if !force && let Ok(meta) = polyio::stat(&path).await @@ -796,14 +749,8 @@ pub fn libraries_missing( Ok(false) } -/// Whether the assets tree this version needs is absent. -/// -/// The index names every asset the version expects and lives inside the assets -/// directory, so checking both catches a deleted index and a deleted assets -/// tree — the second being the case users actually hit. -/// -/// Takes its paths rather than resolving them, so it can be tested without the -/// process-global launcher directory. +/// Takes its paths rather than resolving them so it can be tested without the +/// process-global launcher directory fn assets_tree_missing(index: &Path, objects: &Path) -> bool { if !index.is_file() { tracing::warn!(path = %index.display(), "assets index is missing; will repair"); @@ -818,19 +765,9 @@ fn assets_tree_missing(index: &Path, objects: &Path) -> bool { false } -/// Whether anything the game needs to start is absent, so the launch path can -/// repair before it tries rather than fail part-way through. -/// -/// A cluster marked `Ready` skips preparation entirely on every later launch, -/// so nothing re-checks its files. Delete the assets directory out from under a -/// ready cluster and the launcher walks straight into an error naming a path -/// inside its own metadata folder — which tells the user nothing they can act -/// on. Checking here turns that into an automatic repair. -/// -/// Only the coarse checks belong here: the assets tree as a whole, the client -/// jar, and the classpath libraries. Verifying the ~5000 individual asset -/// objects is the job of `download_minecraft`'s plan once a repair starts, and -/// doing it on every launch would cost far more than it saves. +/// A `Ready` cluster skips preparation so nothing else re-checks its files +/// this turns a deleted assets tree into an automatic repair +/// Coarse checks only per-object verification belongs to `download_minecraft`'s plan #[tracing::instrument(skip(version_info), level = "debug")] pub fn game_files_missing( version_info: &VersionInfo, @@ -899,8 +836,7 @@ pub async fn download_libraries( let progress = progress.clone(); async move { - // Rule evaluation and the on-disk check already happened in - // `plan_downloads`; everything here is known to need fetching. + // Rules and the on-disk check already ran in `plan_downloads` let artifact_path = interfrost::utils::get_path_from_artifact(&lib.name) .map_err(|_| McError::LibraryPath(lib.name.clone()))?; let path = lib_dir.join(&artifact_path); @@ -1181,8 +1117,6 @@ mod tests { dir } - /// Lays out the assets tree a ready cluster is expected to have, returning - /// the index file and objects directory that `game_files_missing` checks. fn install_assets(dir: &Path) -> (PathBuf, PathBuf) { let indexes = dir.join("assets").join("indexes"); let objects = dir.join("assets").join("objects"); @@ -1207,9 +1141,6 @@ mod tests { #[test] fn a_deleted_assets_directory_asks_for_a_repair() { - // The reported failure: a cluster marked Ready whose assets tree was - // deleted underneath it. Nothing re-checked it, so the launch walked - // into "No such file or directory" on the launcher's own metadata path. let dir = scratch("assets-deleted"); let (index, objects) = install_assets(&dir); std::fs::remove_dir_all(dir.join("assets")).unwrap(); @@ -1232,9 +1163,8 @@ mod tests { #[test] fn an_index_that_is_a_directory_is_not_mistaken_for_a_file() { - // `create_dir_all` on a path that should hold a file is a shape the - // launcher has produced before; `is_file` has to be the check, not - // `exists`. + // The launcher has produced this shape before so the check must be + // `is_file` not `exists` let dir = scratch("assets-index-dir"); let (index, objects) = install_assets(&dir); std::fs::remove_file(&index).unwrap(); @@ -1260,8 +1190,6 @@ mod tests { fn size_check_rejects_a_truncated_file() { let dir = scratch("size-short"); let file = dir.join("object.bin"); - // What a dropped connection used to leave behind, and what every later - // launch used to skip over because the path merely existed. std::fs::write(&file, b"0123").unwrap(); assert!(!matches_expected_size(&file, 10)); @@ -1278,8 +1206,8 @@ mod tests { std::fs::remove_dir_all(&dir).unwrap(); } - /// A context whose bus is drained by the test, so `ask` has somebody to - /// talk to. No request is ever sent through the client. + /// The bus is drained by the test so `ask` has somebody to talk to no + /// request is ever sent through the client fn test_ctx() -> (McCtx, tokio::sync::mpsc::UnboundedReceiver) { let (events, rx) = oneclient_events::EventBus::channel(); let net = oneclient_net::RequestClient::new(oneclient_net::NetConfig::default()) @@ -1313,7 +1241,6 @@ mod tests { else { panic!("expected a prompt"); }; - // Assets alone should not claim the game will fail to start. assert!(request.body.contains("3 game assets"), "{}", request.body); request .reply @@ -1335,8 +1262,6 @@ mod tests { else { panic!("expected a prompt"); }; - // Missing libraries are a classpath problem, so the wording should be - // blunter than it is for assets. assert!( request.body.contains("fail to start"), "{}", @@ -1354,8 +1279,6 @@ mod tests { #[tokio::test] async fn a_bus_with_nobody_listening_cancels_rather_than_launching() { let (ctx, rx) = test_ctx(); - // A headless run with no UI attached: nobody can agree to a broken - // install, so it must not proceed by default. drop(rx); let err = confirm_incomplete_install(&ctx, 1, 1) @@ -1373,8 +1296,7 @@ mod tests { let file = dir.join("object.bin"); std::fs::write(&file, b"anything").unwrap(); - // Maven-coordinate libraries carry no artifact metadata, so presence is - // all there is to check. + // Maven-coordinate libraries carry no artifact metadata assert!(matches_expected_size(&file, 0)); assert!(!matches_expected_size(&dir.join("nope.bin"), 0)); diff --git a/packages/oneclient_mc/src/lib.rs b/packages/oneclient_mc/src/lib.rs index 65c3cf4d..69d04563 100644 --- a/packages/oneclient_mc/src/lib.rs +++ b/packages/oneclient_mc/src/lib.rs @@ -1,19 +1,9 @@ -//! Everything that makes a Minecraft install: the client jar, its libraries and -//! natives, the asset index and objects, the launch arguments assembled from -//! them, and the Mojang player profile. -//! -//! This crate has no database. It takes a [`RequestClient`](oneclient_net::RequestClient) -//! and an [`EventBus`](oneclient_events::EventBus) and works against paths, so -//! the download planner is testable on its own. +//! No database here this crate works against paths so the download planner is +//! testable on its own use oneclient_events::EventBus; use oneclient_net::RequestClient; -/// What the install engine needs: somewhere to fetch from, and somewhere to -/// report progress to. -/// -/// Narrower than the full launcher services: no database handle and no -/// package-provider registry, since none of this code touches them. #[derive(Clone)] pub struct McCtx { pub net: RequestClient, diff --git a/packages/oneclient_mc/src/profile.rs b/packages/oneclient_mc/src/profile.rs index c1e0f9c5..96e1361d 100644 --- a/packages/oneclient_mc/src/profile.rs +++ b/packages/oneclient_mc/src/profile.rs @@ -244,11 +244,8 @@ pub async fn fetch_logged_in_profile( Ok(profile) } -/// How long a cached profile is served without revalidating. -/// -/// The alternative is a Mojang round trip before the first avatar can paint on -/// every cold start. Skins change rarely, so this trades a bounded amount of -/// staleness for startup that doesn't wait on the network. +/// Served without revalidating so cold start doesn't wait on a Mojang round +/// trip before the first avatar paints const PROFILE_CACHE_TTL_HOURS: i64 = 6; #[derive(Debug, Serialize, Deserialize)] @@ -276,7 +273,6 @@ async fn fetch_profile_view( Ok(PlayerProfileView::from(summary)) } -/// Resolves a player's public profile preferring on-disk cache #[tracing::instrument(level = "debug", skip(client, access_token))] pub async fn fetch_player_profile_view( client: &RequestClient, diff --git a/packages/oneclient_net/src/cache.rs b/packages/oneclient_net/src/cache.rs index 308a639a..593df3f7 100644 --- a/packages/oneclient_net/src/cache.rs +++ b/packages/oneclient_net/src/cache.rs @@ -1,11 +1,3 @@ -//! ETag-conditional GET backed by an on-disk cache. -//! -//! Four call sites had a byte-identical copy of the same three helpers -//! (`insert_if_none_match`, `etag_from_response`, `read_sidecar_etag`) plus the -//! same arm-for-arm 304 / 2xx / error / other match around them. They differed -//! only in the cache filename, the log noun, and whether a total failure was an -//! error or an empty result. - use std::path::{Path, PathBuf}; use reqwest::{Method, StatusCode, header}; @@ -13,33 +5,26 @@ use reqwest::{Method, StatusCode, header}; use crate::error::RequestError; use crate::service::RequestClient; -/// Whether the ETag sidecar is written as soon as a fresh body arrives. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EtagPolicy { - /// Write it immediately. Right when the body alone is the whole payload. CommitNow, - /// Leave it alone and hand it back on [`Fetched::etag`]. Use when work - /// downstream of the body can still fail: the bundle catalog only commits - /// its ETag once every bundle in it has actually downloaded, so an - /// interrupted sync retries instead of reporting 304 and skipping them. + /// Hands the ETag back on [`Fetched::etag`] + /// Use when work downstream of the body can still fail so an interrupted run + /// retries instead of getting 304 Defer, } -/// The outcome of a conditional fetch. #[derive(Debug, Clone)] pub struct Fetched { pub bytes: Vec, - /// `false` when the server answered 304, or the request failed and the - /// cached copy was served instead. Callers use this to skip work that only - /// matters when the document actually changed. + /// `false` when the server answered 304 or the request failed and the + /// cached copy was served instead pub changed: bool, - /// The server's ETag, present only for a fresh body under - /// [`EtagPolicy::Defer`]. + /// Present only for a fresh body under [`EtagPolicy::Defer`] pub etag: Option, } impl Fetched { - /// Deserialize the body as JSON. pub fn json(&self) -> Result { serde_json::from_slice(&self.bytes).map_err(|source| RequestError::DeserializeError { source, @@ -50,9 +35,7 @@ impl Fetched { }) } - /// Body as UTF-8, lossily. Used for documents that are text by contract - /// (changelogs), where refusing to render because of one bad byte is worse - /// than showing a replacement character. + /// Lossy a bad byte becomes a replacement character rather than an error #[must_use] pub fn text(&self) -> String { String::from_utf8_lossy(&self.bytes).into_owned() @@ -73,19 +56,16 @@ async fn read_sidecar_etag(cache_path: &Path) -> Option { .filter(|etag| !etag.is_empty()) } -/// Writes the ETag sidecar for `cache_path`. Callers using [`EtagPolicy::Defer`] -/// call this once the work depending on the body has succeeded. +/// Callers using [`EtagPolicy::Defer`] call this once the work depending on the +/// body has succeeded pub async fn commit_etag(cache_path: &Path, etag: &str) { if let Err(err) = polyio::write_atomic(etag_path(cache_path), etag.as_bytes()).await { tracing::warn!("failed to write etag sidecar: {err}"); } } -/// `GET url`, using `cache_path` as both the conditional-request cache and the -/// offline fallback. -/// /// Returns `Ok(None)` only when the request failed *and* there is no cached -/// copy. Callers decide whether that is fatal. +/// copy #[tracing::instrument(level = "debug", skip(client), fields(%url))] pub async fn fetch_cached( client: &RequestClient, diff --git a/packages/oneclient_net/src/config.rs b/packages/oneclient_net/src/config.rs index efe3e757..6cd1b94e 100644 --- a/packages/oneclient_net/src/config.rs +++ b/packages/oneclient_net/src/config.rs @@ -1,17 +1,11 @@ use oneclient_common::constants; -/// Endpoint and credential settings the transport needs. -/// -/// The composition layer pushes these down; the transport never looks anything -/// up. Reaching for the CurseForge key through global state would make the HTTP -/// client depend on the object that owns it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct NetConfig { pub curseforge_api_key: String, pub modrinth_api_key: Option, - /// Base for the metadata API (`meta.polyfrost.org`). pub metadata_api_url: String, - /// Base for static assets; never has a trailing slash. + /// Never has a trailing slash pub meta_url_base: String, } @@ -27,9 +21,8 @@ impl Default for NetConfig { } impl NetConfig { - /// Applies user overrides, falling back to the built-in defaults for any - /// that are absent or blank. Blank is treated as absent so clearing a field - /// in the settings UI restores the default rather than sending an empty key. + /// Blank is treated as absent so clearing a field in the settings UI + /// restores the default rather than sending an empty key #[must_use] pub fn with_overrides( mut self, @@ -52,7 +45,6 @@ impl NetConfig { self } - /// Headers Modrinth needs, if a token is configured. #[must_use] pub fn modrinth_headers(&self) -> Vec<(String, String)> { match &self.modrinth_api_key { diff --git a/packages/oneclient_net/src/download.rs b/packages/oneclient_net/src/download.rs index eda8e99c..014b0e05 100644 --- a/packages/oneclient_net/src/download.rs +++ b/packages/oneclient_net/src/download.rs @@ -1,8 +1,3 @@ -//! Downloading with content verification. -//! -//! Streams and hashes in one pass, so nothing has to re-read a finished file -//! just to verify it. - use std::path::Path; use oneclient_events::{EventBus, GroupedProgressChild, TaskPhase}; @@ -13,20 +8,13 @@ use crate::error::RequestError; use crate::response::{ResponseExt, ResponseNotifyOptions, ResponseOptions}; use crate::service::RequestClient; -/// How many times one file is fetched before it counts as failed. -/// -/// [`RequestClient::send`] already retries what fails before the response -/// headers land; this covers what fails after, which on an unstable connection -/// is most of it. Without a retry here a single dropped chunk permanently fails -/// that one file and the install is quietly left incomplete. +/// [`RequestClient::send`] only retries failures before the response headers +/// land this covers failures mid-body which would otherwise silently leave an +/// install incomplete const MAX_DOWNLOAD_ATTEMPTS: u32 = 3; -/// Whether fetching the same URL again has a realistic chance of working. -/// -/// A transport error, a hash mismatch, or a short body means the bytes arrived -/// wrong, which is exactly what a retry fixes. Everything else — a 404, a bad -/// URL, a full disk — will fail the same way every time, and retrying only -/// multiplies the delay before the user sees the real error. +/// Only errors meaning the bytes arrived wrong are retried a 404 bad URL or +/// full disk fails identically every time fn worth_retrying(err: &RequestError) -> bool { matches!( err, @@ -36,17 +24,11 @@ fn worth_retrying(err: &RequestError) -> bool { ) } -/// Grows with each attempt, so a link that is briefly saturated gets a chance to -/// recover instead of being hit again immediately. fn retry_delay(attempt: u32) -> std::time::Duration { std::time::Duration::from_millis(300 * u64::from(attempt)) } -/// Whether a file already on disk matches `expected_sha1`. -/// -/// Hashing an existing file is cheaper than re-downloading it, so every caller -/// wants this check before fetching. Returns `false` when the file is missing or -/// unreadable, in which case the caller should just download it. +/// Returns `false` when the file is missing or unreadable pub async fn matches_on_disk(path: &Path, expected_sha1: &str) -> bool { if !path.is_file() { return false; @@ -61,17 +43,8 @@ pub async fn matches_on_disk(path: &Path, expected_sha1: &str) -> bool { } } -/// Downloads `url` to `dest`, verifying it from the bytes as they stream past. -/// -/// Hashing happens on the bytes in flight, so verification costs no extra disk -/// read and a good file is never hashed twice. `dest` is only created once the -/// hash matches — a failed or corrupt attempt leaves nothing behind for a later -/// `exists()` check to trip over. Transport failures and mismatches are retried -/// up to [`MAX_DOWNLOAD_ATTEMPTS`] times before the error surfaces. -/// -/// `notify` is optional: pass [`ResponseNotifyOptions::grouped`] when the caller -/// is running a grouped session, [`ResponseNotifyOptions::standalone`] for a -/// one-off with its own notification, or `None` to download silently. +/// `dest` is only created once the hash matches so a failed attempt leaves +/// nothing behind for a later `exists()` check to trip over #[tracing::instrument(level = "debug", skip(client, events, notify), fields(%url))] pub async fn download_verified( client: &RequestClient, @@ -82,16 +55,15 @@ pub async fn download_verified( expected_size: u64, notify: Option, ) -> Result<(), RequestError> { - // Callers that fan out over thousands of files (assets) pre-create the tree, - // so this is usually a stat that avoids a blocking-pool round trip per file. + // Callers fanning out over thousands of files pre-create the tree so this + // stat avoids a blocking-pool round trip per file if let Some(parent) = dest.parent() && !parent.is_dir() { polyio::create_dir_all(parent).await?; } - // A standalone notification needs a fixed id up front, or each retry opens a - // new entry in the UI instead of updating the one already there. + // Needs a fixed id up front or each retry opens a new UI entry let notify = notify.map(ResponseNotifyOptions::pinned); let mut attempt = 1; @@ -128,11 +100,8 @@ pub async fn download_verified( Ok(()) } -/// One pass of [`download_verified`]: fetch, hash in flight, publish on match. -/// -/// Split out so a retry re-enters with a fresh request and a fresh hasher. The -/// progress child is shared across attempts and reports absolute byte counts, so -/// an attempt that restarts simply rewinds its bar rather than double-counting. +/// The progress child is shared across attempts and reports absolute byte +/// counts so a restarted attempt rewinds its bar rather than double-counting #[allow(clippy::too_many_arguments)] async fn download_attempt( client: &RequestClient, @@ -151,7 +120,7 @@ async fn download_attempt( let request = reqwest::Request::new(Method::GET, url.parse()?); let response = client.send(request).await?; - // Without this, an error body is written to disk under the requested file's name. + // Without this an error body is written to disk under the requested file's name let status = response.status(); if !status.is_success() { let bytes = response.bytes().await?; @@ -162,7 +131,7 @@ async fn download_attempt( }); } - // Prefer the manifest size; Content-Length is absent for compressed or chunked bodies. + // Prefer the manifest size Content-Length is absent for compressed or chunked bodies let declared = if expected_size > 0 { Some(expected_size) } else { @@ -188,8 +157,7 @@ async fn download_attempt( item }); let stream = std::pin::pin!(stream); - // Writes to a scratch sibling and renames on success, so `dest` is either - // absent or complete — never a truncated file a later run would skip. + // Writes to a scratch sibling and renames so `dest` is never truncated polyio::write_stream(dest, stream, Some(total)).await?; } @@ -200,8 +168,7 @@ async fn download_attempt( let actual = hasher.finish(); if !expected.matches(&actual) { - // The bytes were wrong, so the file that was just published is wrong - // too. Remove it rather than leave it for the next `exists()` check. + // Remove it rather than leave it for the next `exists()` check let _ = polyio::remove_file(dest).await; return Err(RequestError::HashMismatch { source_desc: dest.display().to_string(), @@ -212,7 +179,7 @@ async fn download_attempt( } else if let Some(declared) = declared && received != declared { - // With no hash, the byte count is the only evidence the transfer finished. + // With no hash the byte count is the only evidence the transfer finished let _ = polyio::remove_file(dest).await; return Err(RequestError::IncompleteBody { source_desc: dest.display().to_string(), @@ -224,10 +191,6 @@ async fn download_attempt( Ok(()) } -/// Downloads `url` into memory, verifying SHA-1 over the collected bytes. -/// -/// For payloads that are parsed rather than stored (version manifests), where -/// writing to disk only to read it straight back would be wasted IO. #[tracing::instrument(level = "debug", skip(client, events, progress), fields(%url))] pub async fn fetch_verified( client: &RequestClient, diff --git a/packages/oneclient_net/src/error.rs b/packages/oneclient_net/src/error.rs index 653fc584..ee9b53e1 100644 --- a/packages/oneclient_net/src/error.rs +++ b/packages/oneclient_net/src/error.rs @@ -1,10 +1,5 @@ -/// Renders an error together with every `source()` beneath it. -/// -/// `reqwest::Error`'s own `Display` stops at "error sending request for url -/// (...)". The fact that actually names the failure — a rustls handshake -/// alert, an untrusted certificate, a DNS lookup failure, `ECONNREFUSED` — -/// lives further down the chain, so without this every transport failure in a -/// user report reads identically and is undiagnosable. +/// `reqwest::Error`'s own `Display` stops at "error sending request for url" +/// the fact that names the failure lives further down the `source()` chain #[must_use] pub fn error_chain(err: &dyn std::error::Error) -> String { let mut rendered = err.to_string(); @@ -12,7 +7,7 @@ pub fn error_chain(err: &dyn std::error::Error) -> String { while let Some(cause) = cursor { let text = cause.to_string(); - // hyper and reqwest repeat the same sentence across several layers. + // hyper and reqwest repeat the same sentence across several layers if !rendered.ends_with(&text) { rendered.push_str(": "); rendered.push_str(&text); @@ -53,7 +48,6 @@ pub enum RequestError { #[error("Failed to serialize request body: {0}")] SerializeError(#[source] serde_json::Error), - /// A download's contents did not match the hash the manifest promised. #[error("{source_desc} has SHA-1 {actual}, expected {expected}")] HashMismatch { source_desc: String, @@ -61,8 +55,7 @@ pub enum RequestError { actual: String, }, - /// A download ended early without the transport reporting an error. Only - /// reachable when no hash was supplied; otherwise it fails as a mismatch. + /// Only reachable when no hash was supplied otherwise it fails as a mismatch #[error("{source_desc} ended after {actual} of {expected} bytes")] IncompleteBody { source_desc: String, @@ -81,11 +74,8 @@ pub enum RequestError { } impl RequestError { - /// Whether this is the user's environment rather than a launcher bug: offline, - /// connection refused, timed out, disk full. - /// - /// The transport reports the fact; deciding that such errors stay out of - /// Sentry is a policy the composition layer applies on top. + /// Whether this is the user's environment rather than a launcher bug offline + /// connection refused timed out disk full #[must_use] pub fn is_transient(&self) -> bool { match self { @@ -104,9 +94,8 @@ fn is_transient_io(error: &polyio::IOError) -> bool { use std::io::ErrorKind; - // Out of disk space. `ErrorKind::StorageFull` is still unstable, so match the - // raw OS codes. They are platform-gated so a Unix errno cannot collide with a - // Windows code (112 is the unrelated EHOSTDOWN on Linux). + // `ErrorKind::StorageFull` is unstable so match raw OS codes platform-gated + // because 112 is the unrelated EHOSTDOWN on Linux if let Some(code) = source.raw_os_error() { #[cfg(unix)] if code == 28 { diff --git a/packages/oneclient_net/src/lib.rs b/packages/oneclient_net/src/lib.rs index 1926a046..ce9d6975 100644 --- a/packages/oneclient_net/src/lib.rs +++ b/packages/oneclient_net/src/lib.rs @@ -1,10 +1,5 @@ -//! HTTP transport for OneClient: one client with retries, throttling and a -//! global in-flight cap, plus the two things every caller was reimplementing: -//! ETag-cached fetches and hash-verified downloads. -//! -//! Endpoints and credentials arrive as data ([`NetConfig`]) rather than being -//! looked up, so the transport does not depend on the launcher state that owns -//! the client. +//! Endpoints and credentials arrive as data ([`NetConfig`]) so the transport +//! does not depend on the launcher state that owns the client mod cache; mod config; diff --git a/packages/oneclient_net/src/response.rs b/packages/oneclient_net/src/response.rs index 842165a9..35cbbd1c 100644 --- a/packages/oneclient_net/src/response.rs +++ b/packages/oneclient_net/src/response.rs @@ -4,7 +4,6 @@ use uuid::Uuid; use crate::error::RequestError; use oneclient_events::{GroupedProgressChild, EventBus}; -/// Minimum gap between progress events emitted for a single response body. const PROGRESS_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50); #[derive(Clone, Default)] @@ -49,20 +48,13 @@ impl ResponseNotifyOptions { self } - /// The grouped child these options report against, if any. Lets a caller - /// driving the download set phases and finish the child without having to - /// keep a second copy of it alongside the options. #[must_use] pub fn child(&self) -> Option<&GroupedProgressChild> { self.child.as_ref() } - /// Fixes the standalone notification's id, so a caller that sends the same - /// request more than once keeps updating one notification. - /// - /// Without this a retried download opens a fresh entry in the UI on every - /// attempt, and a flaky connection reads as several stalled installs rather - /// than one that is recovering. + /// Pins the standalone notification id so a retried request keeps updating + /// one notification instead of opening a fresh UI entry per attempt #[must_use] pub fn pinned(mut self) -> Self { if self.standalone_label.is_some() && self.standalone_id.is_none() { @@ -117,10 +109,8 @@ impl ResponseExt for Response { } let events = events.clone(); - // A chunk arrives every few KiB; forwarding one event per chunk buries the - // UI in tens of thousands of updates per download session, all of which - // collapse into the same repainted progress bar. Sample instead, and - // always emit the last one so the bar lands on complete. + // One event per chunk would bury the UI in tens of thousands of updates + // so sample the last one always emits so the bar lands on complete let mut last_emit: Option = None; let stream = futures_lite::StreamExt::map(self.bytes_stream(), move |item| { match item { diff --git a/packages/oneclient_net/src/service.rs b/packages/oneclient_net/src/service.rs index 30aeb9f5..13036ce2 100644 --- a/packages/oneclient_net/src/service.rs +++ b/packages/oneclient_net/src/service.rs @@ -13,11 +13,9 @@ use crate::response::{ResponseExt, ResponseOptions}; const MAX_THROTTLE_RETRIES: u32 = 6; -/// Ceiling on requests waiting for response headers. This is a backstop against -/// a runaway fan-out, not the download throttle. Per-phase concurrency is set -/// by the callers, and this has to stay above their sum or it becomes the -/// bottleneck instead. Permits are released once headers arrive, so streaming -/// bodies don't hold a slot. +/// Backstop against runaway fan-out not the download throttle +/// Must stay above the sum of per-phase caller concurrency or it becomes the +/// bottleneck const MAX_INFLIGHT_REQUESTS: usize = 64; fn retry_after(response: &Response) -> Option { @@ -85,10 +83,9 @@ fn apply_curseforge_auth( pub struct RequestClient { client: reqwest::Client, semaphore: Arc, - /// Shared across every clone, so a settings save is visible to in-flight - /// handles. `ArcSwap` rather than a lock because this is read once per - /// outbound request (with 64 in flight a lock would serialise them) and - /// written only when the user saves settings. + /// Shared across every clone so a settings save is visible to in-flight + /// handles + /// `ArcSwap` rather than a lock which would serialise requests config: Arc>, } @@ -97,13 +94,11 @@ impl RequestClient { &self.client } - /// The endpoint/credential config every request is sent with. #[must_use] pub fn config(&self) -> arc_swap::Guard> { self.config.load() } - /// Replaces the config for this client and every clone of it. pub fn set_config(&self, config: NetConfig) { self.config.store(Arc::new(config)); } @@ -120,16 +115,9 @@ impl RequestClient { env!("CARGO_PKG_HOMEPAGE") )); - // reqwest defaults this to `cfg!(feature = "hickory-dns")`, so enabling - // the feature turns hickory on for *every* platform. Leaving it unset - // on Windows does not opt out — it has to be switched off explicitly. - // - // It has to be off there because hickory reads UDP responses into a - // fixed buffer, and Windows fails an oversized datagram with - // WSAEMSGSIZE (10040) instead of truncating it the way Unix does. A DNS - // reply that Linux and macOS truncate and retry over TCP is an outright - // lookup failure on Windows, so sign-in broke for whichever users had a - // resolver returning large enough answers. + // Hickory must be explicitly off on Windows it reads UDP into a fixed + // buffer and Windows fails oversized datagrams with WSAEMSGSIZE (10040) + // instead of truncating breaking sign-in for large DNS replies #[cfg(target_os = "windows")] { builder = builder.no_hickory_dns(); @@ -241,7 +229,7 @@ impl RequestClient { ) -> Result<(), RequestError> { let res = self.send(request).await?; - // Without this, an error body is written to disk under the requested file's name. + // Without this an error body is written to disk under the requested file's name let status = res.status(); if !status.is_success() { let url = res.url().to_string(); diff --git a/packages/oneclient_net/src/status.rs b/packages/oneclient_net/src/status.rs index 535e6f4f..199943ac 100644 --- a/packages/oneclient_net/src/status.rs +++ b/packages/oneclient_net/src/status.rs @@ -34,8 +34,8 @@ struct Connectivity { rx: watch::Receiver, notify: Notify, started: AtomicBool, - /// Debug-only pin. While set, probe results are recorded but never - /// published, so the forced status survives a recheck. + /// While set probe results are never published so the forced status + /// survives a recheck forced: RwLock>, } @@ -53,9 +53,8 @@ fn connectivity() -> &'static Connectivity { }) } -/// Pins the reported status to `status`, or releases the pin with `None` and -/// re-probes. Exposed for the debug page: the connectivity banner is otherwise -/// only reachable by actually taking a service down. +/// Pins the reported status to `status` or releases the pin with `None` and +/// re-probes pub fn force(status: Option) { let conn = connectivity(); *conn.forced.write().unwrap_or_else(|e| e.into_inner()) = status; @@ -95,8 +94,8 @@ pub fn note_request_result(success: bool) { } } -/// Starts the connectivity prober. Idempotent; later calls are ignored, so the -/// client handed in on the first call is used for the process lifetime. +/// Idempotent later calls are ignored so the client handed in on the first +/// call is used for the process lifetime pub fn start(client: RequestClient) { let conn = connectivity(); if conn.started.swap(true, Ordering::SeqCst) { diff --git a/packages/oneclient_polyplus/src/lib.rs b/packages/oneclient_polyplus/src/lib.rs index a7df942b..743a7aae 100644 --- a/packages/oneclient_polyplus/src/lib.rs +++ b/packages/oneclient_polyplus/src/lib.rs @@ -1,8 +1,3 @@ -//! Poly+ playtime reporting. -//! -//! Opens a websocket to the Poly+ backend for the signed-in account and keeps -//! it alive while the user plays. Depends only on the auth service. - use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; @@ -66,10 +61,7 @@ enum Event { Message(Option), } -/// Starts the Poly+ playtime reporter. -/// -/// Takes the state it needs rather than reaching for a global; idempotent, so -/// the first caller's handle is the one used for the process lifetime. +/// Idempotent the first caller's handle is used for the process lifetime pub fn start(auth: Arc) { if STARTED.swap(true, Ordering::SeqCst) { return; @@ -111,9 +103,8 @@ fn build_client() -> Result { env!("CARGO_PKG_HOMEPAGE") )); - // Same WSAEMSGSIZE problem as the main client; see `oneclient_net::service`. - // The `hickory-dns` feature is enabled workspace-wide, so every builder is - // opted in unless it says otherwise. + // Same WSAEMSGSIZE problem as the main client see `oneclient_net::service` + // `hickory-dns` is enabled workspace-wide so opt out explicitly #[cfg(target_os = "windows")] { builder = builder.no_hickory_dns(); @@ -188,12 +179,7 @@ async fn run_session( .await } -/// Keeps the playtime socket alive, stopping when `should_stop` says the -/// session is no longer valid or the server hangs up. -/// -/// Takes a predicate rather than the launcher state: this is a websocket ping -/// loop, and whether the signed-in account changed is the caller's business. -/// It also means the loop is testable without standing up a launcher. +/// Returns when `should_stop` reports the session invalid or the server hangs up async fn pump( websocket: &mut reqwest_websocket::WebSocket, ping_interval: Duration, diff --git a/packages/polyio/src/archive.rs b/packages/polyio/src/archive.rs index 64a74db5..948b16f7 100644 --- a/packages/polyio/src/archive.rs +++ b/packages/polyio/src/archive.rs @@ -8,7 +8,6 @@ use tokio_util::compat::TokioAsyncWriteCompatExt; use crate::{IOError, PolyIOResult}; -/// Reads a zip archive from a byte array #[tracing::instrument(skip(data, f), level = "debug")] pub async fn read_zip_entries_bytes(data: Vec, mut f: F) -> PolyIOResult<()> where @@ -38,7 +37,6 @@ where Ok(()) } -/// Reads a zip archive from a byte array and returns a stream of entries. #[tracing::instrument(skip(data), level = "debug")] pub fn stream_zip_entries_bytes( data: Vec, @@ -64,7 +62,6 @@ pub fn stream_zip_entries_bytes( } } -/// Unzips a zip archive from a byte array #[tracing::instrument( level = "debug", skip(data, dest_path), @@ -79,7 +76,6 @@ pub async fn unzip_bytes( unzip_bytes_filtered(data, None:: bool>, dest_path).await } -/// Unzips a zip archive from a byte array #[tracing::instrument( level = "debug", skip(data, filter_entries, dest_path), @@ -148,7 +144,6 @@ pub async fn extract_zip( .await } -/// Unzips a zip archive from a file #[tracing::instrument( level = "debug", skip(zip_path, dest_path, filter_entries, modify_entry_name), @@ -206,11 +201,8 @@ pub async fn extract_zip_filtered( Ok(()) } -/// Reads the bytes of every non-directory zip entry whose name passes `filter`. -/// -/// Returns `(entry_name, bytes)` pairs. Each matching entry is read fully into -/// memory, so this is meant for small files (e.g. bundle config overrides), not -/// large archives. +/// Each matching entry is read fully into memory so this is for small files +/// not large archives #[tracing::instrument( level = "debug", skip(zip_path, filter), @@ -245,7 +237,7 @@ pub async fn read_zip_file_entries( Ok(out) } -/// Returns a zip file entry's bytes without reading the entire file into memory. +/// Returns a zip file entry's bytes without reading the entire file into memory #[tracing::instrument( level = "debug", skip(reader) diff --git a/packages/polyio/src/file.rs b/packages/polyio/src/file.rs index d48efeb5..0797a20f 100644 --- a/packages/polyio/src/file.rs +++ b/packages/polyio/src/file.rs @@ -6,22 +6,14 @@ use serde::{Serialize, de::DeserializeOwned}; use crate::{IOError, PolyIOResult}; -/// Smallest write buffer. HTTP chunks already arrive at ~16 KiB, so anything -/// below this is buffering that was already the right size. +/// HTTP chunks already arrive at ~16 KiB const MIN_WRITE_BUFFER: usize = 16 * 1024; -/// Largest write buffer, for multi-megabyte downloads. const MAX_WRITE_BUFFER: usize = 256 * 1024; -/// Used when the response has no length: assume something mid-sized. +/// Used when the response has no length const DEFAULT_WRITE_BUFFER: usize = 64 * 1024; -/// Picks a write buffer that fits the file rather than a fixed large one. -/// -/// This is a memory-footprint choice, not a throughput one: measured against -/// the real asset-size distribution, 64 KiB / 256 KiB / file-sized all write at -/// the same rate (~200 MB/s, ~25x faster than the download feeding them), so -/// the buffer is nowhere near the critical path. What a fixed 256 KiB *does* -/// cost is 8 MiB of buffers across 32 concurrent asset downloads, each holding -/// a ~10 KiB object. +/// Sizing the buffer to the file is a memory-footprint choice not a throughput +/// one a fixed 256 KiB costs ~8 MiB across 32 concurrent small-asset downloads fn write_buffer_size(size_hint: Option) -> usize { match size_hint { Some(0) | None => DEFAULT_WRITE_BUFFER, @@ -29,7 +21,6 @@ fn write_buffer_size(size_hint: Option) -> usize { } } -/// Returns a stream over the entries within a directory. #[tracing::instrument( level = "debug", skip(path), @@ -46,7 +37,6 @@ pub async fn read_dir(path: impl AsRef) -> PolyIOResult) -> PolyIOResult<()> { }) } -/// Recursively creates a directory and all of its parent components if they are missing. #[tracing::instrument( level = "debug", skip(path), @@ -82,7 +71,6 @@ pub async fn create_dir_all(path: impl AsRef) -> PolyIOResult<( }) } -/// Removes a directory at this path, after removing all its contents. Use carefully! #[tracing::instrument( level = "debug", skip(path), @@ -99,10 +87,8 @@ pub async fn remove_dir_all(path: impl AsRef) -> PolyIOResult<( }) } -/// Removes an empty directory, failing if anything is still in it. -/// -/// The refusal is the point: it makes "remove this if it is empty" a single -/// atomic step, with no read-then-delete window for something to appear in. +/// Fails if the directory is non-empty making "remove if empty" atomic with no +/// read-then-delete window #[tracing::instrument( level = "debug", skip(path), @@ -119,7 +105,6 @@ pub async fn remove_dir(path: impl AsRef) -> PolyIOResult<()> { }) } -/// Checks if a path exists #[tracing::instrument( level = "debug", skip(path), @@ -135,7 +120,6 @@ pub async fn try_exists(path: impl AsRef) -> PolyIOResult }) } -/// Creates a future which will open a gzip compressed file for reading and read the entire contents into a string and return said string. #[tracing::instrument( level = "debug", skip(path), @@ -153,7 +137,6 @@ pub async fn read_gz_to_string(path: impl AsRef) -> PolyIOResul Ok(dst) } -/// Creates a future which will open a file for reading and read the entire contents into a string and return said string. #[tracing::instrument( level = "debug", skip(path), @@ -170,7 +153,6 @@ pub async fn read_to_string(path: impl AsRef) -> PolyIOResult) -> PolyIOResult> { }) } -/// Asynchronously read a file as JSON and return the deserialized object #[tracing::instrument( level = "debug", skip(path), @@ -203,7 +184,6 @@ pub async fn read_json( }) } -/// Asynchrously write to a file. #[tracing::instrument( level = "debug", skip(path, data), @@ -223,7 +203,6 @@ pub async fn write( }) } -/// Asynchronously write buffered data to a file, creating it if it does not exist. #[tracing::instrument( level = "debug", skip(path, f), @@ -249,19 +228,11 @@ where Ok(()) } -/// Streams `stream` into `path`, which only ever appears complete. +/// Streams into a scratch sibling renamed over `path` on success so `path` +/// never holds a truncated file and an existing good file survives a failure /// -/// The bytes go to a sibling scratch file that is renamed over `path` once the -/// stream ends cleanly, so a dropped connection cannot leave a truncated file -/// sitting at the real path where every later `exists()` check would mistake it -/// for a finished download. On any error the scratch file is removed and -/// whatever was already at `path` is left untouched — a failed re-download of a -/// good file is a no-op rather than a deletion. -/// -/// Unlike [`write_atomic`] this deliberately does not fsync: that would be the -/// single most expensive operation in a five-thousand-file asset download, and -/// nothing here needs to survive power loss. A crash can still leave a short -/// file behind, which the caller's size check catches on the next run. +/// Unlike [`write_atomic`] this deliberately does not fsync too expensive +/// across thousands of asset downloads and nothing here must survive power loss #[tracing::instrument( level = "debug", skip(path, stream), @@ -311,7 +282,6 @@ where Ok(()) } -/// Asynchronously write json to a file, creating it if it does not exist. #[tracing::instrument( level = "debug", skip(path, data), @@ -329,13 +299,11 @@ pub async fn write_json( ).await } -/// Counter for [`temp_sibling`] names, so two concurrent atomic writes to the -/// same path can't pick the same scratch file. +/// Keeps two concurrent atomic writes to the same path off the same scratch file static ATOMIC_WRITE_COUNTER: AtomicU64 = AtomicU64::new(0); -/// A scratch path next to `path`, so the subsequent rename stays within one -/// filesystem. A temp dir would not: `rename` across mount points fails with -/// `EXDEV`, which is exactly the case on Linux where `/tmp` is often a tmpfs. +/// Sibling rather than temp dir `rename` across mount points fails with +/// `EXDEV` and `/tmp` is often a separate tmpfs on Linux fn temp_sibling(path: &Path) -> PathBuf { let n = ATOMIC_WRITE_COUNTER.fetch_add(1, Ordering::Relaxed); let stem = path @@ -350,13 +318,8 @@ fn temp_sibling(path: &Path) -> PathBuf { } } -/// Writes a file such that a reader only ever sees the old contents or the -/// complete new ones, never a half-written file. -/// -/// Writes to a sibling scratch file, fsyncs it, then renames over `path`. -/// Without the fsync the rename can land before the data does, so a crash -/// leaves a correctly-named zero-length file, worse than the torn write this -/// is meant to prevent. Parent directories are created if missing. +/// Readers see either the old contents or the complete new ones +/// The fsync before the rename stops a crash leaving a correctly-named zero-length file #[tracing::instrument( level = "debug", skip(path, data), @@ -403,7 +366,6 @@ pub async fn write_atomic( Ok(()) } -/// [`write_atomic`] for a serializable value, mirroring [`write_json`]. #[tracing::instrument( level = "debug", skip(path, data), @@ -421,15 +383,10 @@ pub async fn write_json_atomic( write_atomic(path, bytes).await } -/// Canonicalises `path` and returns it only if it lies under one of `roots`. -/// -/// This is the guard for paths that arrive from outside (a log or screenshot -/// the UI asked to open) so that `../` cannot walk out of the launcher's own -/// directories. Returns `Ok(None)` when the path resolves outside every root; -/// `Err` only when `path` itself cannot be canonicalised. -/// -/// Roots that cannot be canonicalised (typically because they don't exist yet) -/// are skipped rather than treated as a match. +/// Traversal guard for externally supplied paths +/// `Ok(None)` when the path resolves outside every root +/// `Err` only when `path` cannot be canonicalised +/// Roots that cannot be canonicalised (e.g. not yet created) are skipped #[tracing::instrument( level = "debug", skip(path, roots), @@ -439,9 +396,8 @@ pub fn ensure_under>( path: impl AsRef, roots: impl IntoIterator, ) -> PolyIOResult> { - // `canonicalize` rather than `std::fs::canonicalize`: on Windows the latter - // returns a `\\?\` UNC path while the roots are plain paths, so `starts_with` - // would never match and every path would look like an escape. + // Not `std::fs::canonicalize` its Windows `\\?\` UNC output would never + // `starts_with` the plain-path roots so everything would look like an escape let canon = crate::canonicalize(path)?; for root in roots { @@ -455,11 +411,9 @@ pub fn ensure_under>( Ok(None) } -/// Recursively copies `src` into `dst`, creating directories as needed. -/// -/// Entries named in `exclude_top` are skipped, but only at the top level; a -/// nested directory of the same name is still copied. Symlinks are followed, -/// copied as their target's contents. +/// `exclude_top` applies only at the top level a nested directory of the same +/// name is still copied +/// Symlinks are followed and copied as their contents #[tracing::instrument(level = "debug", skip(exclude_top))] pub async fn copy_dir(src: &Path, dst: &Path, exclude_top: &[&str]) -> PolyIOResult<()> { let mut stack: Vec<(PathBuf, PathBuf, bool)> = @@ -498,7 +452,6 @@ pub async fn copy_dir(src: &Path, dst: &Path, exclude_top: &[&str]) -> PolyIORes Ok(()) } -/// Whether `dir` is a directory containing at least one entry. pub async fn dir_has_content(dir: &Path) -> bool { if !dir.is_dir() { return false; @@ -510,7 +463,7 @@ pub async fn dir_has_content(dir: &Path) -> bool { } } -/// Renames a file or directory to a new name, replacing the original file if `to` already exists. +/// Replaces `to` if it already exists #[tracing::instrument( level = "debug", skip(from, to), @@ -534,7 +487,7 @@ pub async fn rename( }) } -/// Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. This function will overwrite the contents of to. +/// Also copies permission bits and overwrites `to` #[tracing::instrument( level = "debug", skip(from, to), @@ -558,7 +511,6 @@ pub async fn copy( }) } -/// Removes a file from the filesystem. #[tracing::instrument( level = "debug", skip(path), @@ -574,10 +526,7 @@ pub async fn remove_file(path: impl AsRef) -> PolyIOResult<()> }) } -/// Queries metadata about a path without following symlinks. -/// -/// Unlike [`stat`], if `path` is a symlink this returns metadata about the -/// link itself, not its target. +/// Unlike [`stat`] returns metadata about the link itself not its target #[tracing::instrument( level = "debug", skip(path), @@ -593,11 +542,7 @@ pub async fn symlink_metadata(path: impl AsRef) -> PolyIOResult }) } -/// Links a file `link` to `original`. -/// -/// On Unix this creates a symlink. On Windows it creates a hard link -/// instead (both paths must then live on the same volume). Either way the -/// caller gets a file at `link` that shares the contents of `original`. +/// Symlink on Unix hard link on Windows so both paths must be on one volume #[tracing::instrument( level = "debug", skip(original, link), @@ -624,11 +569,9 @@ pub async fn symlink_file( }) } -/// Links a directory `link` to `original`. -/// -/// On Windows this creates a directory *junction* (a reparse point), which -/// needs no elevated privilege unlike a real directory symlink. On Unix it -/// creates an ordinary directory symlink. Remove it with [`remove_symlink_dir`]. +/// Windows gets a junction which needs no elevated privilege unlike a real +/// directory symlink +/// Remove with [`remove_symlink_dir`] #[tracing::instrument( level = "debug", skip(original, link), @@ -664,10 +607,7 @@ pub async fn symlink_dir( }) } -/// Removes a directory link created by [`symlink_dir`]. -/// -/// On Windows a junction must be removed with `remove_dir` rather than -/// `remove_file`; this handles the platform difference. +/// A Windows junction must be removed with `remove_dir` not `remove_file` #[tracing::instrument( level = "debug", skip(path), @@ -688,19 +628,17 @@ pub async fn remove_symlink_dir(path: impl AsRef) -> PolyIOResu }) } -/// Creates a temporary directory. #[tracing::instrument(level = "debug")] pub async fn tempdir() -> PolyIOResult { Ok(TempDir::new().await?) } -/// Creates a temporary file. #[tracing::instrument(level = "debug")] pub async fn tempfile() -> PolyIOResult { Ok(TempFile::new().await?) } -/// Sanitises every component of a path and normalises separators to `/`. +/// Sanitises every component and normalises separators to `/` #[tracing::instrument( level = "debug", skip(path), @@ -715,7 +653,6 @@ pub fn sanitize_path(path: impl AsRef) -> PathBuf { .collect() } -/// Returns file metadata #[tracing::instrument( level = "debug", skip(path), @@ -730,12 +667,9 @@ mod tests { #[test] fn write_buffer_fits_the_file() { - // A typical Minecraft asset object. assert_eq!(write_buffer_size(Some(10_217)), MIN_WRITE_BUFFER); assert_eq!(write_buffer_size(Some(1)), MIN_WRITE_BUFFER); - // Mid-sized files get exactly what they need. assert_eq!(write_buffer_size(Some(64 * 1024)), 64 * 1024); - // Large downloads are capped rather than matched. assert_eq!(write_buffer_size(Some(25_000_000)), MAX_WRITE_BUFFER); assert_eq!(write_buffer_size(Some(u64::MAX)), MAX_WRITE_BUFFER); } @@ -778,7 +712,6 @@ mod tests { assert_eq!(std::fs::read(&target).unwrap(), b"new-and-longer"); - // The scratch sibling must not survive a successful write. let leftovers: Vec<_> = std::fs::read_dir(&dir) .unwrap() .flatten() @@ -790,8 +723,6 @@ mod tests { std::fs::remove_dir_all(&dir).unwrap(); } - /// Builds a stream that yields `chunks` and then fails, standing in for a - /// connection dropped part-way through a download. fn failing_stream( chunks: Vec<&'static [u8]>, ) -> impl futures_lite::Stream> + Unpin + Send { @@ -822,7 +753,6 @@ mod tests { assert_eq!(std::fs::read(&target).unwrap(), b"hello world"); - // A successful write must not leave its scratch sibling behind either. let leftovers: Vec<_> = std::fs::read_dir(&dir) .unwrap() .flatten() @@ -843,8 +773,6 @@ mod tests { .await .expect_err("a dropped stream must fail"); - // The whole point: no truncated file at the real path for a later - // `exists()` check to mistake for a finished download. assert!(!target.exists(), "left a truncated file at the destination"); let leftovers: Vec<_> = std::fs::read_dir(&dir) @@ -867,7 +795,6 @@ mod tests { .await .expect_err("a dropped stream must fail"); - // A failed repair must not be worse than no repair. assert_eq!(std::fs::read(&target).unwrap(), b"known-good"); std::fs::remove_dir_all(&dir).unwrap(); @@ -897,7 +824,6 @@ mod tests { let file = outside.join("auth.json"); std::fs::write(&file, b"").unwrap(); - // The classic shape: a path that only escapes once resolved. let sneaky = root.join("..").join("secrets").join("auth.json"); assert_eq!(ensure_under(&sneaky, [&root]).unwrap(), None); diff --git a/packages/polyio/src/hash.rs b/packages/polyio/src/hash.rs index 907b17c8..3f50f8c3 100644 --- a/packages/polyio/src/hash.rs +++ b/packages/polyio/src/hash.rs @@ -6,13 +6,9 @@ use digest::Digest; use crate::{IOError, PolyIOResult}; -/// Largest read chunk used when hashing a file. Files smaller than this get a -/// buffer sized to fit, so hashing a 10 KiB asset doesn't allocate (and zero) -/// a quarter of a megabyte. +/// Upper bound only smaller files get a buffer sized to fit const MAX_HASH_BUFFER: usize = 256 * 1024; -/// Incremental SHA-1, so a download can be verified from the bytes as they -/// stream past instead of re-reading the finished file back off disk. pub struct Sha1Stream(sha1::Sha1); impl Sha1Stream { @@ -37,9 +33,8 @@ impl Default for Sha1Stream { } } -/// Hashing is CPU- and syscall-bound, so it runs on the blocking pool: one -/// dispatch for the whole file rather than one per read, and the digest itself -/// stays off the async workers that are driving concurrent downloads. +/// Runs on the blocking pool one dispatch for the whole file keeping the +/// digest off the async workers driving concurrent downloads pub async fn sha1_file(path: impl AsRef) -> PolyIOResult { let path = path.as_ref().to_path_buf(); tokio::task::spawn_blocking(move || sha1_file_sync(&path)) @@ -82,7 +77,6 @@ pub fn sha1_bytes(data: &[u8]) -> String { hasher.finish() } -/// Which digest a manifest published its hash in. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ChecksumAlgorithm { Sha1, @@ -90,8 +84,8 @@ pub enum ChecksumAlgorithm { } impl ChecksumAlgorithm { - /// Length of this digest written as hex, used to tell one apart from - /// another when a manifest gives a bare string with no algorithm field. + /// Hex length which is how a bare manifest string with no algorithm field + /// gets identified #[must_use] pub const fn hex_len(self) -> usize { match self { @@ -109,11 +103,10 @@ impl ChecksumAlgorithm { } } -/// A hash a manifest promised, together with the algorithm it is written in. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Checksum { pub algorithm: ChecksumAlgorithm, - /// Always normalized, so comparing two `Checksum`s never trips on case. + /// Always normalized (trimmed lowercase) pub hex: String, } @@ -136,12 +129,8 @@ impl Checksum { Self::new(ChecksumAlgorithm::Sha256, hex) } - /// Whether the hash is the right shape for its algorithm. - /// - /// A vendor that starts returning an empty string, a placeholder, or a - /// digest in another algorithm would otherwise fail every download with a - /// mismatch that looks like corruption. Callers drop malformed checksums and - /// download unverified rather than making the runtime uninstallable. + /// Whether the hash is the right shape for its algorithm + /// Callers drop malformed checksums and download unverified #[must_use] pub fn is_well_formed(&self) -> bool { self.hex.len() == self.algorithm.hex_len() @@ -154,9 +143,6 @@ impl Checksum { } } -/// Incremental hashing in whichever algorithm a [`Checksum`] calls for, so a -/// download is verified from the bytes as they stream past rather than by -/// re-reading the finished file. pub enum ChecksumStream { Sha1(sha1::Sha1), Sha256(sha2::Sha256), @@ -187,8 +173,6 @@ impl ChecksumStream { } } -/// Canonical form for a hash read from an external manifest, so a comparison -/// against a locally computed one doesn't fail on case or stray whitespace. #[must_use] pub fn normalize_hash(hash: &str) -> String { hash.trim().to_ascii_lowercase() @@ -257,9 +241,6 @@ mod tests { #[test] fn a_hash_of_the_wrong_shape_is_not_well_formed() { - // What a vendor sending a placeholder, an empty field, or a digest in - // another algorithm would look like. Verifying against these would fail - // every download with what reads to the user as corruption. assert!(!Checksum::sha256("").is_well_formed()); assert!(!Checksum::sha256("n/a").is_well_formed()); assert!(!Checksum::sha256("a9993e364706816aba3e25717850c26c9cd0d89d").is_well_formed()); diff --git a/packages/polyio/src/system.rs b/packages/polyio/src/system.rs index 77cec6c5..0d1566f8 100644 --- a/packages/polyio/src/system.rs +++ b/packages/polyio/src/system.rs @@ -2,12 +2,12 @@ use std::path::PathBuf; use crate::{IOError, PolyIOResult}; -/// Attempts to parse a path taken from an environment variable. Returns `None` if the variable is not set. pub fn env_path(name: &str) -> Option { std::env::var_os(name).map(PathBuf::from) } -/// An OS specific wrapper of [`std::fs::canonicalize`], but on Windows it outputs the most compatible form of a path instead of UNC. +/// Like [`std::fs::canonicalize`] but on Windows emits the most compatible +/// path form instead of UNC pub fn canonicalize(path: impl AsRef) -> PolyIOResult { let path = path.as_ref(); dunce::canonicalize(path).map_err(|e| IOError::PathIOError { diff --git a/packages/polyio/src/testing.rs b/packages/polyio/src/testing.rs index 2659dc28..6ac19567 100644 --- a/packages/polyio/src/testing.rs +++ b/packages/polyio/src/testing.rs @@ -1,23 +1,13 @@ -//! Helpers for tests. Gated behind the `testing` feature so they stay out of -//! release builds; enable it as a dev-dependency. - use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; -/// A uniquely-named directory under the system temp dir, removed when dropped. -/// -/// Prefer this over a bare `std::env::temp_dir().join(...)`: hand-rolled -/// versions of this leak their directory on every run, because there is nothing -/// to remove it once the test finishes. -/// -/// The name embeds the process id and a counter, so concurrent tests (and -/// concurrent *runs*) cannot collide. +/// A uniquely-named directory under the system temp dir removed when dropped #[derive(Debug)] pub struct ScratchDir(PathBuf); impl ScratchDir { - /// Creates the directory. `tag` is only there to make a stray directory - /// identifiable if a test aborts hard enough to skip the destructor. + /// `tag` only makes a leaked directory identifiable if the destructor is + /// skipped #[must_use] pub fn new(tag: &str) -> Self { static COUNTER: AtomicU64 = AtomicU64::new(0);