feat(update): Windows auto-update mechanism
Summary
Windows-only auto-update for yalla-sip-phone. Polls a backend manifest endpoint hourly, downloads the MSI with resumable Range requests, verifies SHA256, waits for idle call, installs via a C# .NET bootstrapper — never interrupts an active SIP call.
- 47 new tests, all green (`./gradlew clean build` 0 failures)
- 19 invariants enforced — I1 active-call gate (test-covered), I13 version blacklist, I15 downgrade refusal, I16 skip-during-call, I17 schema validation, I18 MOTW strip, I19 pinned UpgradeCode
- Per-user install (`%LOCALAPPDATA%\YallaSipPhone`) — no UAC prompt, no admin rights required
- No code signing — cert-free path via Zone.Identifier stripping + download-host allow-list + SHA256 integrity
-
Hidden toggles: `Ctrl+Shift+Alt+B` channel (stable
↔️ beta), `Ctrl+Shift+Alt+D` diagnostics dialog - uz + ru + en strings for operator-facing UI
- C# bootstrapper cross-compiled from macOS to win-x64 (TFM `net8.0`, not `net8.0-windows`, because Program.cs uses only cross-platform System.* APIs)
Design
Full design lives at `docs/superpowers/specs/2026-04-13-auto-update-design.md` (536 lines). Written after multi-round brainstorming + 4 parallel critical reviewers (security threat model, Windows MSI feasibility, simplicity/YAGNI cut, field-reliability stress test).
Implementation plan: `docs/superpowers/plans/2026-04-13-auto-update.md` (2871 lines, 21 TDD tasks).
Commits
``` 140edc86 feat(update): wire UI into toolbar, add disk pre-flight, diagnostics state 4b76c929 build(bootstrapper): compile yalla-update-bootstrap.exe from macOS 5bdb1372 feat(update): Phase 6 — C# .NET bootstrapper source 432de988 feat(update): Phase 5 — pin WiX UpgradeCode, enable per-user MSI install 565af4ba feat(update): Phase 4 — UI badge, dialog, diagnostics, Main.kt wiring e678dbe8 feat(update): Phase 3 — orchestrator, Koin module, installId, channel setting 2b764399 feat(update): Phase 2 — data layer (paths, sha256, api, downloader, installer) 63720187 feat(update): Phase 1 — domain types, semver, manifest validation, strings cdb13d48 docs(update): auto-update design spec + implementation plan ```
Architecture
Client → `GET /api/v1/app-updates/latest` → 200 envelope `{updateAvailable, release: {version, minSupportedVersion, releaseNotes, installer: {url, sha256, size}}}` → validate schema + host allow-list → pre-flight disk space (size × 2) → resumable Ktor download (Range + `.part`/`.meta` sidecar) → SHA256 verify → wait for `CallState.Idle` → spawn `yalla-update-bootstrap.exe` → `exitProcess(0)` → bootstrapper waits for parent PID + 3s file-lock drain → strips MOTW → quarantines old install → runs `msiexec /i /qn /norestart` → relaunches new exe (or restores backup on failure).
Bootstrapper is mandatory because JCEF (`libcef.dll`, `jcef_helper.exe`, `jcef-cache` lock) and pjsip (`libpjsua2.dll` mapped in JVM) hold native file locks that RestartManager cannot evict within its ~30 s window.
Simplicity cuts baked in per Agent 3's review: 6-field manifest (not 15), 10 Kotlin files (not 18), collapsed state machine, no markdown rendering, no staged rollout, no kill switch (server manages rollback by republishing). Backend team implements §6 contract via 1 REST endpoint.
Test plan
Automated (all green on merge):
-
`./gradlew clean build` — full suite green (47 new update tests + existing) -
SemverComparator, UpdateManifestTest (schema validation, 11 cases) -
UpdatePathsTest (GC for `.part`/`.msi`/`.meta` orphans) -
Sha256VerifierTest (verify/compute) -
UpdateApiTest (5 cases with ktor-client-mock: 200 envelope, 204, malformed, untrusted host, 5xx) -
UpdateDownloaderTest (Range resume, stale `.meta` discard, verify fail) -
MsiBootstrapperInstallerTest (command construction, missing-bootstrapper throw, process launch) -
UpdateManagerTest (8 cases: I1 idle-call gate, I13 blacklist after 3 verify fails, I15 downgrade refuse, I16 skip during call)
Manual (Windows-side verification — pending):
-
`./gradlew packageMsi` on Windows produces a valid MSI -
Orca inspection: `UpgradeCode = E7A4F1B2-9C5D-4E8A-B1F6-2D3E4F5A6B7C` (pinned), `InstallScope=perUser`, MajorUpgrade element blocks downgrades -
Clean install on Win 11 VM → no UAC prompt, SmartScreen "More info → Run anyway" flow documented for first install -
Mock manifest server (`python3 -m http.server`) end-to-end: download, verify, bootstrap, install, relaunch -
Kill msiexec mid-install → bootstrapper restores quarantine + relaunches old exe -
Operator credentials survive MSI upgrade (Java Preferences `HKCU\Software\JavaSoft\Prefs\...` is MSI-independent, but confirm empirically) -
`Ctrl+Shift+Alt+B` channel toggle + `Ctrl+Shift+Alt+D` diagnostics dialog render correctly -
UpdateBadge visibility across all states (Downloading, ReadyToInstall, Failed) -
JCEF + pjsip file-lock behavior — confirm bootstrapper pattern is actually needed (or RestartManager suffices) on real Win 11
Backend team handoff (spec §17 open questions):
-
`BACKEND_BASE_URL` for `/api/v1/app-updates/latest` -
MSI hosting host(s) — update `UPDATE_URL_ALLOWLIST` in `UpdateManifest.kt` (currently placeholders: `downloads.yalla.uz`, `updates.yalla.local`) -
Whether JWT auth is required on the manifest endpoint -
Backend request logging (spec §16 — poll logs are the only observability)
Known gaps / future work
- Manifest signing (minisign / cosign) — deferred to v2 as documented security debt (spec §15.2)
- Authenticode MSI signing — permanently out of scope per product stance; mitigated by LAN-only + host allow-list + MOTW strip
- Staged rollout / kill switch — backend can implement server-side if needed; client treats any manifest as authoritative
- Git-tag-driven versioning — `BuildVersion.CURRENT` and `build.gradle.kts` still hardcode `1.0.0`; wire to a tag plugin later
- Release CI pipeline — manual `./gradlew packageMsi` + scp for MVP; a GitHub Actions Windows runner workflow is future work