Add self-updater: in-app update check and one-click updates - #96
Conversation
New FanaBridge.Updater project (merged into the single shipped DLL): release feed parsing with availability/installability split, whitelist zip extraction, crash-safe two-rename swap with rollback, serialized state machine with terminal ReadyToRestart. Plugin wiring runs one startup check (opt-out setting) and the settings UI offers the update banner with notify-only and access-denied fallbacks.
- Honor cancellation up to the commit point: re-check the token after extraction and clean the staging dir when a cancel lands mid-download. - Never report a committed swap as failed: the cosmetic logo step after rename 2 is now fully non-throwing (a Failed state on a live new DLL would let a retry destroy the .old rollback copy). - One updater-lifetime CTS covering manual checks/applies from the UI, not just the startup check, so FinalizePlugin cancels those too. - Move the post-update restart-prompt guard to the plugin (a fresh SettingsControl is created per page open). - Classify access-denied from the staged-DLL version read; parse asset "size" defensively (non-numeric JSON no longer throws). - Tests: real duplicate-DLL zip entries, 50 MB total-cap enforcement, IOException HRESULT access-denied classification, cancel-after- extraction, and post-commit logo failure still succeeding.
|
Warning Review limit reached
Next review available in: 53 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. 📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds a .NET Framework updater module. It parses GitHub releases, validates and extracts packages, replaces assemblies safely, integrates update state into the plugin lifecycle, and adds settings-page controls. ChangesSelf-updater
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SettingsControl
participant FanatecPlugin
participant UpdateService
participant GitHubHttpClient
participant UpdatePackage
participant UpdateFileSwapper
SettingsControl->>FanatecPlugin: check or apply update
FanatecPlugin->>UpdateService: CheckAsync or DownloadAndApplyAsync
UpdateService->>GitHubHttpClient: fetch release metadata and package
UpdateService->>UpdatePackage: verify and extract package
UpdateService->>UpdateFileSwapper: apply staged update
UpdateService-->>FanatecPlugin: publish update snapshot
FanatecPlugin-->>SettingsControl: raise UpdateStateChanged
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
tests/FanaBridge.Tests/Updater/UpdatePackageTests.cs (1)
148-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMark the total-size cap test as Slow for consistency.
This test builds three 20 MB entries and writes about 40 MB to disk before the cap trips.
ExtractToStaging_PerEntrySizeCap_Throwsat line 196 carries[Trait("Category", "Slow")]for comparable work. Apply the same trait so both heavy tests are filtered together.♻️ Proposed change
[Fact] + [Trait("Category", "Slow")] public void ExtractToStaging_TotalSizeCap_Throws()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/FanaBridge.Tests/Updater/UpdatePackageTests.cs` around lines 148 - 149, Add the [Trait("Category", "Slow")] attribute to ExtractToStaging_TotalSizeCap_Throws, matching ExtractToStaging_PerEntrySizeCap_Throws so both heavy tests are categorized consistently.src/FanaBridge.Updater/UpdateFileSwapper.cs (1)
216-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the staged
.newfile whenApplyfails before the commit.Line 158 writes
FanaBridge.dll.newbefore any live file is touched. If step 4 (_move(liveDll, oldDll)) throws, control reaches this outer catch and returns immediately, so.newstays in the SimHub install directory until the nextApplycall or the nextCleanupStaleArtifactsrun at startup.The cleanup is safe here. Every path that reaches this catch after the copy has not yet renamed the live DLL: rename 2 failures are handled by the inner catch at lines 180-208, and
CopyLogosBestEffortnever throws.♻️ Proposed change
catch (Exception ex) { + // Pre-commit failure only: the live DLL is still in place, so the + // staged .new copy is an orphan. + TryDelete(newDll); return FailClosed(ex.Message, ex); }Also extend
Apply_MoveAccessDeniedIoException_ClassifiedAccessDeniedintests/FanaBridge.Tests/Updater/UpdateFileSwapperTests.cs(lines 90-95) to assert thatFanaBridge.dll.newno longer exists after the failure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/FanaBridge.Updater/UpdateFileSwapper.cs` around lines 216 - 219, Update the outer catch in UpdateFileSwapper.Apply to delete the staged FanaBridge.dll.new file before returning FailClosed, while preserving the existing exception reporting. Extend Apply_MoveAccessDeniedIoException_ClassifiedAccessDenied to assert that the staged .new file is absent after the failed apply.src/FanaBridge.Updater/UpdatePackage.cs (1)
139-174: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlign the DLL name comparison with the rest of the whitelist, and reject invalid file-name characters.
Two small hardening points in
TryMapWhitelist:
- Line 152 matches the DLL name with
StringComparison.Ordinal, but the.pngsuffix check at line 160, theseenset at line 66, and thesawDllcheck at line 114 all useOrdinalIgnoreCase. A zip that storesfanabridge.dllis ignored and the extraction then fails with "missing root entry". The behavior fails closed, so this is a consistency fix, not a defect.- The logo file name is checked only for separators and dot segments. A name such as
a:b.pngstill reachesPath.Combineand creates an NTFS alternate data stream inside the staging directory. The write stays insidestagingDir, so there is no traversal, but rejecting invalid characters avoids the surprise.♻️ Proposed change
- if (string.Equals(name, DllName, StringComparison.Ordinal)) + if (string.Equals(name, DllName, StringComparison.OrdinalIgnoreCase)) { relativePath = DllName; return true; } string prefix = LogosDirName + "/"; - if (name.StartsWith(prefix, StringComparison.Ordinal) + if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && name.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { string file = name.Substring(prefix.Length); // Single level only — reject empty, nested, or traversal segments. if (file.Length == 0 || file.IndexOf('/') >= 0 || file.IndexOf('\\') >= 0) return false; if (file == "." || file == "..") return false; + if (file.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + return false;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/FanaBridge.Updater/UpdatePackage.cs` around lines 139 - 174, Update TryMapWhitelist to compare DllName using StringComparison.OrdinalIgnoreCase, matching the existing whitelist and seen/sawDll handling. Before accepting a logo filename, reject any invalid platform filename characters in addition to separators and dot segments, using the framework’s standard invalid-character validation, while preserving valid single-level .png mappings.tests/FanaBridge.Tests/Updater/UpdateServiceTests.cs (1)
194-217: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the concurrency test deterministic.
The test depends on
await Task.Delay(50)to guarantee that the firstCheckAsynchas entered_fetchText. On a loaded CI agent the first task may not have acquired the gate yet. The second call then acquires it andfetchesbecomes 2, so the test fails intermittently. Signal entry from inside the fetch delegate instead.🧪 Proposed fix
var tcs = new TaskCompletionSource<string>(); + var entered = new TaskCompletionSource<bool>(); int fetches = 0; var svc = CreateService( currentVersion: "0.6.0", fetchText: async (_, ct) => { Interlocked.Increment(ref fetches); + entered.TrySetResult(true); return await tcs.Task.ConfigureAwait(false); }); Task first = svc.CheckAsync(); - // Allow first to enter fetch. - await Task.Delay(50); + // Deterministic: the first call is inside the gate and inside fetch. + await entered.Task; Task second = svc.CheckAsync(); await second;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/FanaBridge.Tests/Updater/UpdateServiceTests.cs` around lines 194 - 217, Make CheckAsync_ConcurrentSecondCall_IsNoOp deterministic by replacing the timing-based Task.Delay with a TaskCompletionSource used as an entry signal inside the fetchText delegate. Await that signal before starting the second svc.CheckAsync call, while preserving the existing fetch count, completion, and final phase assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/FanaBridge.Updater/ReleaseFeed.cs`:
- Around line 116-125: Trim tagName before assigning and processing the version
in the release-feed flow, while preserving the existing single leading v/V
removal. Use the normalized value consistently for UpdateVersion.TryParse and
subsequent asset-name matching, UI version output, and error reporting in the
surrounding release handling.
In `@src/FanaBridge.Updater/UpdateService.cs`:
- Around line 290-295: Update the generic Exception catch in the update-apply
flow to call TryDeleteDir(staging), matching the InvalidDataException and
swap-failure paths. Ensure cleanup occurs before publishing the failed
UpdateSnapshot, while preserving the existing logging and failure reporting.
- Around line 180-184: Distinguish timeout cancellations from caller-requested
cancellations in both OperationCanceledException handlers in UpdateService:
src/FanaBridge.Updater/UpdateService.cs lines 180-184 and 283-289. Only restore
the previous phase silently when the caller’s cancellation token was explicitly
requested; route timeout cancellations through the existing failed check/apply
reporting path at both sites.
In `@src/FanaBridge.Updater/UpdateVersion.cs`:
- Around line 118-124: Update the public UpdateVersion value-type paths that
depend on Numeric so default-constructed instances remain safe: ensure Normalize
handles a null Numeric value, and apply equivalent null-safe behavior in
CompareTo, Equals, GetHashCode, and ToString. Preserve existing version
comparison, equality, hashing, and string formatting for initialized instances.
In `@src/FanaBridge/UI/SettingsControl.xaml.cs`:
- Around line 1260-1271: Validate snapshot.Release.HtmlUrl in OpenReleasePage
before Process.Start, allowing only absolute http or https URLs; otherwise fall
back to the existing GitHub releases URL. Preserve the current UseShellExecute
launch and warning behavior for valid URLs and launch failures.
---
Nitpick comments:
In `@src/FanaBridge.Updater/UpdateFileSwapper.cs`:
- Around line 216-219: Update the outer catch in UpdateFileSwapper.Apply to
delete the staged FanaBridge.dll.new file before returning FailClosed, while
preserving the existing exception reporting. Extend
Apply_MoveAccessDeniedIoException_ClassifiedAccessDenied to assert that the
staged .new file is absent after the failed apply.
In `@src/FanaBridge.Updater/UpdatePackage.cs`:
- Around line 139-174: Update TryMapWhitelist to compare DllName using
StringComparison.OrdinalIgnoreCase, matching the existing whitelist and
seen/sawDll handling. Before accepting a logo filename, reject any invalid
platform filename characters in addition to separators and dot segments, using
the framework’s standard invalid-character validation, while preserving valid
single-level .png mappings.
In `@tests/FanaBridge.Tests/Updater/UpdatePackageTests.cs`:
- Around line 148-149: Add the [Trait("Category", "Slow")] attribute to
ExtractToStaging_TotalSizeCap_Throws, matching
ExtractToStaging_PerEntrySizeCap_Throws so both heavy tests are categorized
consistently.
In `@tests/FanaBridge.Tests/Updater/UpdateServiceTests.cs`:
- Around line 194-217: Make CheckAsync_ConcurrentSecondCall_IsNoOp deterministic
by replacing the timing-based Task.Delay with a TaskCompletionSource used as an
entry signal inside the fetchText delegate. Await that signal before starting
the second svc.CheckAsync call, while preserving the existing fetch count,
completion, and final phase assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6b810ae7-8d7b-4398-a15f-188d2b87531b
📒 Files selected for processing (20)
Directory.Build.targetsFanaBridge.slnsrc/FanaBridge.Updater/FanaBridge.Updater.csprojsrc/FanaBridge.Updater/ReleaseFeed.cssrc/FanaBridge.Updater/UpdateFileSwapper.cssrc/FanaBridge.Updater/UpdatePackage.cssrc/FanaBridge.Updater/UpdateService.cssrc/FanaBridge.Updater/UpdateVersion.cssrc/FanaBridge/FanaBridge.csprojsrc/FanaBridge/FanatecPlugin.cssrc/FanaBridge/FanatecPluginSettings.cssrc/FanaBridge/UI/SettingsControl.xamlsrc/FanaBridge/UI/SettingsControl.xaml.cssrc/FanaBridge/Updates/GitHubHttpClient.cstests/FanaBridge.Tests/FanaBridge.Tests.csprojtests/FanaBridge.Tests/Updater/ReleaseFeedTests.cstests/FanaBridge.Tests/Updater/UpdateFileSwapperTests.cstests/FanaBridge.Tests/Updater/UpdatePackageTests.cstests/FanaBridge.Tests/Updater/UpdateServiceTests.cstests/FanaBridge.Tests/Updater/UpdateVersionTests.cs
- Distinguish HttpClient timeouts from caller cancellation in both UpdateService command handlers (a timed-out manual check now reports CheckFailed instead of silently restoring the previous phase). - Clean the staging dir in the generic apply catch, matching the other failure paths; sweep the half-staged .new in the swapper outer catch. - Trim tag_name before deriving the version/asset name; null-safe UpdateVersion for default-constructed instances. - Only shell-launch http(s) release URLs; fall back to the releases page. - Reject invalid Windows filename characters in logo entries explicitly. - Tests: deterministic concurrency test (entry signal instead of delay), Slow trait on the total-cap test, .new-absent assertion.
There was a problem hiding this comment.
Pull request overview
Adds an in-app self-updater with release checks, verified downloads, update UI, and rollback-oriented file replacement.
Changes:
- Adds updater state, release parsing, verification, extraction, and swapping.
- Integrates startup/manual checks and update controls into the plugin UI.
- Adds updater packaging configuration and comprehensive tests.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/FanaBridge.Tests/Updater/UpdateVersionTests.cs |
Tests version parsing and ordering. |
tests/FanaBridge.Tests/Updater/UpdateServiceTests.cs |
Tests updater state transitions. |
tests/FanaBridge.Tests/Updater/UpdatePackageTests.cs |
Tests secure package extraction. |
tests/FanaBridge.Tests/Updater/UpdateFileSwapperTests.cs |
Tests swapping and rollback behavior. |
tests/FanaBridge.Tests/Updater/ReleaseFeedTests.cs |
Tests release metadata parsing. |
tests/FanaBridge.Tests/FanaBridge.Tests.csproj |
References the updater project. |
src/FanaBridge/Updates/GitHubHttpClient.cs |
Implements GitHub HTTP requests. |
src/FanaBridge/UI/SettingsControl.xaml.cs |
Drives updater UI behavior. |
src/FanaBridge/UI/SettingsControl.xaml |
Adds update banner and controls. |
src/FanaBridge/FanatecPluginSettings.cs |
Adds startup-check preference. |
src/FanaBridge/FanatecPlugin.cs |
Integrates updater lifecycle. |
src/FanaBridge/FanaBridge.csproj |
Merges updater into the plugin. |
src/FanaBridge.Updater/UpdateVersion.cs |
Implements version comparison. |
src/FanaBridge.Updater/UpdateService.cs |
Implements the update state machine. |
src/FanaBridge.Updater/UpdatePackage.cs |
Verifies and extracts packages. |
src/FanaBridge.Updater/UpdateFileSwapper.cs |
Performs in-place file replacement. |
src/FanaBridge.Updater/ReleaseFeed.cs |
Parses GitHub release responses. |
src/FanaBridge.Updater/FanaBridge.Updater.csproj |
Defines the updater assembly. |
FanaBridge.sln |
Adds the updater project. |
Directory.Build.targets |
Cleans stale updater assemblies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Defer updater init (and the .old rollback-copy sweep) from the end of InitializeCore to the end of the first full Init, so the rollback copy outlives the per-manager registrations that could still fail a fresh build''s load. - Keep the About-section status line in step with the banner for the Downloading/Applying, ReadyToRestart, and Failed phases.
Replace the left-hugging width-capped callout card with a bar that spans the section content width: one row of headline (inline release-notes link) plus a right-aligned action button, and a muted detail line shown only for states that need one (installing, notify-only, failure). Drops the glyph column — the blue/amber border and tint carry the state.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/FanaBridge/FanatecPlugin.cs (1)
93-93: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake the updater’s nullable contract match its behavior.
Updatesdocuments that updater initialization can leave the value null, but the getter still returns non-nullableUpdateService. ReturnUpdateService?so callers are required to handle the unavailable-updater case before dereferencingSnapshot.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/FanaBridge/FanatecPlugin.cs` at line 93, Update the Updates property in FanatecPlugin to return nullable UpdateService? instead of non-nullable UpdateService, matching the possible uninitialized _updateService state and requiring callers to handle null before accessing Snapshot.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/FanaBridge/FanatecPlugin.cs`:
- Line 93: Update the Updates property in FanatecPlugin to return nullable
UpdateService? instead of non-nullable UpdateService, matching the possible
uninitialized _updateService state and requiring callers to handle null before
accessing Snapshot.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3a62931c-41f6-4fa7-87a2-0da5475436ca
📒 Files selected for processing (12)
src/FanaBridge.Updater/ReleaseFeed.cssrc/FanaBridge.Updater/UpdateFileSwapper.cssrc/FanaBridge.Updater/UpdatePackage.cssrc/FanaBridge.Updater/UpdateService.cssrc/FanaBridge.Updater/UpdateVersion.cssrc/FanaBridge/FanatecPlugin.cssrc/FanaBridge/UI/SettingsControl.xamlsrc/FanaBridge/UI/SettingsControl.xaml.cstests/FanaBridge.Tests/FanaBridge.Tests.csprojtests/FanaBridge.Tests/Updater/UpdateFileSwapperTests.cstests/FanaBridge.Tests/Updater/UpdatePackageTests.cstests/FanaBridge.Tests/Updater/UpdateServiceTests.cs
🚧 Files skipped from review as they are similar to previous changes (8)
- src/FanaBridge/UI/SettingsControl.xaml
- src/FanaBridge.Updater/ReleaseFeed.cs
- src/FanaBridge.Updater/UpdatePackage.cs
- src/FanaBridge.Updater/UpdateFileSwapper.cs
- tests/FanaBridge.Tests/Updater/UpdatePackageTests.cs
- src/FanaBridge.Updater/UpdateService.cs
- src/FanaBridge/UI/SettingsControl.xaml.cs
- tests/FanaBridge.Tests/FanaBridge.Tests.csproj
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Directory.Build.props`:
- Line 11: Update the VersionPrefix property in Directory.Build.props from 0.5.0
to 0.6.0 or the intended next version, ensuring the derived Version,
AssemblyVersion, and FileVersion are not lower than the released product
version.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3860bc8e-c7aa-4b2d-b45e-24da385cdc49
📒 Files selected for processing (5)
Directory.Build.propssrc/FanaBridge/FanatecPlugin.cssrc/FanaBridge/FanatecPluginSettings.cssrc/FanaBridge/UI/SettingsControl.xamlsrc/FanaBridge/UI/SettingsControl.xaml.cs
🚧 Files skipped from review as they are similar to previous changes (2)
- src/FanaBridge/UI/SettingsControl.xaml.cs
- src/FanaBridge/FanatecPlugin.cs
Rigs can leave SimHub running for weeks, so a launch-only check goes stale. A 24 h timer re-runs the check in-session; the handler re-reads EnableUpdateCheck so the About checkbox now takes effect live, and the timer is torn down (and its checks cancelled) in FinalizePlugin. Checkbox relabeled "Check for updates automatically".
Drop the redundant "you have x.y.z" from the banner headline (the installed version is already printed in About), and make the About status line report only manual-check outcomes: an available update says "see above" instead of duplicating the banner, and the in-progress / installed / failed states the banner owns leave it empty.
Everything update-related now shares one surface: installed version, the banner (offer / progress / restart / failure), the auto-check preference, and the manual check link. Accepted trade-off: on small windows the banner can sit below the fold. The "see above" pointer in the manual-check line is gone — the banner is adjacent.
50:50 grid split — About is short and the device chain does not need full width, and this keeps the update banner above the fold instead of at the bottom of the page. Experimental Features is now the last section, so it drops its trailing separator. Within About, the banner leads the section — directly under the heading and ahead of the version identity — so it opens with the actionable state when there is one. The row's own dividers replace the per-section separators: both sections set ShowSeparator="False", a vertical rule sits in the gutter between them, and one continuous horizontal rule runs under the whole row. Both rules copy SHSection's separator geometry (1px, 30% opacity, 20px template inset) and bind their brush to a section's SeparatorBrush so they track the SimHub theme.
e41cd63 to
bd97a6e
Compare
Implements the self-updater tracked in #95.
What
FanaBridge.dll+DevicesLogos\in place, then prompt to restart SimHub.Design notes
src\FanaBridge.Updaterproject — compile-time module only; ILRepack still ships the single mergedFanaBridge.dll. Keeps the network + file-swap code behind a hard project boundary with no reference to Core or the plugin.FanaBridge.dll.newfirst, then committed with two renames (live →.old,.new→ live), so the crash window is two metadata operations and a failure leaves recoverable files..oldsurvives until the next launch proves the new build initializes, then is cleaned up.ReadyToRestartis terminal — no re-check can destroy the rollback copy after a swap. Zip extraction is whitelist-only (FanaBridge.dll+DevicesLogos/*.png) with streaming size caps; user profiles underFanaBridge\are never touched.digestthe GitHub API already exposes (present on v0.6.0's asset).Testing
-p:MergePlugin=true.Remaining before merge (why draft)
VersionPrefix=0.0.1, confirm banner offers v0.6.0 and the full update → restart path works against a live SimHubCloses #95