Upstream shared_preferences_windows and _linux write the whole preference
document with a bare `writeAsStringSync`. That opens with the default
`FileMode.write`, which truncates the live file before writing it, so every
single preference write has a window in which the only copy on disk is empty
or half-written. A crash, power loss, forced reboot or antivirus interception
inside that window leaves a document that fails to parse on every subsequent
launch — and the store holds the credential-vault key, so the loss is not
recoverable by rewriting it. This is the corruption class behind #1732; the
recovery path already landed is a band-aid over it.
Vendor both packages under packages/ — the convention saf_util and
wakelock_plus already follow — and stage, flush, then rename over the target.
The flush has to precede the rename or it could publish contents that were
never committed, the same corruption by another route. Staging uses one fixed
sibling name rather than a stamped one, because the file is a plaintext copy
of the vault key, tracker refresh tokens and Seerr cookies; it is created in
the target's own directory so rename stays on one volume and the mode matches
what the canonical file would have had, and a stale one is swept once the
canonical document has been read cleanly. Both deltas are marked in-source and
in provenance.json with the refresh contract.
Atomicity is proven, not asserted. A hard link to the store observes the old
document after a write, which only holds when the directory entry was replaced
— truncate-in-place would have rewritten the shared inode, and that test does
fail against unpatched upstream. Upstream's own suites still pass unchanged in
both packages and now run in CI, so the patch keeps the contract it inherited.
Windows `MoveFileExW` replacement semantics cannot be proven on a POSIX runner
or a memory file system, so they get their own test on the existing
windows-latest job, including replacement while a reader holds the file open —
antivirus and Search Indexer both do.
Since 2.10.0 the whole app sits behind one all-or-nothing initialization
gate, and that gate discarded the only evidence of its own failure. It
caught the error, logged nothing but `error.runtimeType`, rendered an
icon plus the word "Error" plus Retry, and never reported the error
because catching it kept the crash reporter from ever seeing it. There
is no log file on any platform, the buffer is in memory only, a
double-clicked Windows release build has no console, and the log viewer
lives in Settings, behind the gate that just failed. #1732 is the result:
a Windows 11 user whose app will not boot and who cannot produce a single
byte of diagnostic detail.
The gate now names its phases. Each step is wrapped so a throw carries
the phase it came from, replacing a `Future.wait` that discarded every
error but the first and could not attribute it to any of four concurrent
steps. The failure screen renders the phase, the exception type, the
message and an expandable stack, plus copy and upload actions that reuse
the existing log-relay flow. The record is persisted next to the database
so the next successful launch can surface it in Settings > Logs, and it
is reported to the crash reporter explicitly.
Only preferences and the database still gate the launch. Window chrome,
locale, crash-reporting init, TV/performance detection, the image-cache
budget and download storage are best-effort and time-bounded, so a
stalled platform thread degrades instead of holding the splash forever.
Sentry no longer receives the startup work as its `appRunner`: that made
a startup failure indistinguishable from a Sentry failure, and the guard
would then have re-run migrations and the database open a second time.
The two remaining fatal steps become recoverable. Preference reads
tolerate a value whose stored type no longer matches, dropping the key
and defaulting instead of failing the boot. A store that cannot be parsed
is detected before either desktop plugin backend can memoise it, which is
what makes an in-process repair possible at all. Repair is never
automatic: it states what it will cost, salvages the credential-vault key
and every tracker and Seerr session it can validate out of the damaged
bytes, reseeds them, and moves the original aside rather than deleting
it. Servers and profiles survive a salvaged key because their tokens are
ciphertext in the database; tracker and Seerr sessions are plaintext
preference entries, so the copy says they may still need reconnecting.
Nothing derived from the store reaches a diagnostic. `FormatException`
prints an excerpt of whatever it failed to parse, and during startup that
document holds the vault key, refresh tokens and session cookies while
the redaction manager still has nothing registered, so the wrapper keeps
only the cause's type and offset and the record is an allowlist of
already-redacted fields. The quarantined copy is labelled as containing
credentials, is never offered for upload, and can be deleted from the
dialog.
Also self-heals orphaned WAL/SHM sidecars on desktop rather than only
tvOS, makes every `createTable` migration step idempotent, keeps MSVC
link by-products out of the Windows bundle, and asserts bundle contents
in CI.
Refs #1732
The Store's unpackaged EXE path would require Authenticode-signing the
installer and every PE file inside it. MSIX submissions are re-signed by the
Store instead, so this route needs no code-signing certificate. build-msix.ps1
mirrors build-installer.ps1 and consumes the same per-architecture build
artifacts, leaving the installer, portable archives and WinSparkle appcast
untouched.
One template generates the manifest for both architectures, carrying the
identity reserved in Partner Center. check_windows_msix.py recomputes the
package family name from the publisher DN, so a mistyped identity fails CI
rather than a submission, and it parses the script rather than running it
because root CI is Linux. Qualified logo assets are indexed into
resources.pri; without the altform-unplated variants the shell draws the
taskbar icon on an accent-coloured plate.
PlatformDetector.isPackagedInstall gates the in-app updater and the Liberapay
tile, which the read-only package directory and Store commerce policy
respectively rule out. Gating at runtime keeps one Windows build feeding both
the installer and the Store package.
R8 only ever ran on `release`, so every automated gate in this repository
exercised code the shipped APK does not contain. Reflective lookups, JNI
callbacks and native library loading can all break under shrinking while
`flutter test`, the Robolectric suites and `connectedDebugAndroidTest`
stay green — which is exactly how #1703 shipped, with the bundled FFmpeg
audio renderer shrunk out of release builds for TrueHD and DTS-HD.
Add a `minified` build type that inherits release's shrinker
configuration but stays debuggable and debug-signed, so it is an ordinary
test artifact and never a publishable one. Three integration details
took a run each to find: the Flutter plugin copies app build types into
every plugin module, so library-level shrinking deleted the plugin entry
points that only GeneratedPluginRegistrant references; the harness must
not be shrunk or the runner disappears; and androidx.test has to survive
in the app under test, or the runner cannot link its own supertype and
the run reports zero tests instead of failing.
Instrumentation still defaults to `debug`, because only one build type
can host androidTest and the existing playback suites drive media3
builder APIs the app never calls, which R8 shrinks legitimately. The new
reachability test opts into the minified variant instead and touches no
builder API, so the only keeps it depends on are the ones under test.
Emptying proguard-rules.pro was verified to fail it.
The Dart suite spent 77% of its cost compiling one isolate per test file
while `flutter test` used half the cores, and every Maestro flow replayed
a full Jellyfin onboarding before its first real assertion.
- Add scripts/run_tests.sh, which runs `flutter test` with -j set to the
cores the process may actually use instead of the ncpu/2 default.
Measured on 8 cores: 190s -> 136s; -j 12 regresses to 165s, so it scales
to the core count rather than hard-coding one. CI and CONTRIBUTING use it.
A cgroup v2 quota, a cgroup v1 quota, and the cpuset/affinity nproc
reports can each be the binding limit independently, so the detector
takes the smallest; trusting whichever it found first would oversubscribe
4x on a container holding an 8-CPU quota while pinned to 2. Covered by
scripts/test_run_tests.py, which the ci_guard_checks.sh glob picks up.
- Add .maestro/subflows/ensure_onboarded.yaml: cold-start the app and only
onboard when no session is stored. Flows that just need a signed-in Home
use it; 02_onboarding_home, 08_logout, 09_download_offline_playback and
the profile regressions keep clearing state. 59s -> 16s per flow.
- Guard onboarding's two optional taps behind visibility checks. A missed
`optional: true` tap still runs the full element search, costing 3.0s
and 7.8s per onboarding to find nothing.
- Disable device animation scales in run_maestro.py, restored by the
existing cleanup path. CI's emulator got this from the runner flag;
physical devices never did.
- Shorten the watch_together setup-timeout replacement from 500ms to the
10ms the same file already proves sufficient, and shorten the retry
backoff at the one site that missed it: 8.04s -> 1.59s of execution.
- Make the LAN discovery waits deadline-based and resend the beacon while
polling. Loopback UDP drops datagrams under load, which timed out a
wait that could never be satisfied; this was the suite's one flaky test.
- Fix 08_logout, which searched for "Logout" and "Are you sure you want to
logout?" after both strings became "Log out". The flow had been failing
and aborting the suite before 09 ever ran.
flutter test 190s -> 131s. Maestro's Android suite 621s -> 385s across the
eight flows the baseline reached, and now runs all nine green.
Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.
Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.
Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.