Skip to content

[2/12] feat: core primitives — atomic JSON, DPAPI secrets, single-instance, window placement - #11

Open
Mvkd108 wants to merge 4 commits into
docs/windows-execution-planfrom
feat/window-placement-service
Open

[2/12] feat: core primitives — atomic JSON, DPAPI secrets, single-instance, window placement#11
Mvkd108 wants to merge 4 commits into
docs/windows-execution-planfrom
feat/window-placement-service

Conversation

@Mvkd108

@Mvkd108 Mvkd108 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Status: REVIEW ONLY — do not merge. PR 2 of 12 in a stacked series; stacked on #10. This PR shows only its own chunk. Series intro, divergence notes and full map: #10. Next: #12.

This PR (2/12): core primitives

Commits:

  • 437c712 feat: atomic JSON persistence, cross-process locking and path service — Services/AtomicJsonFile.cs, Services/CrossProcessFileLock.cs, Services/MuesliPathService.cs
  • d6a7664 feat: DPAPI-backed secret store — Services/SecretStore.cs
  • a8a677a feat: single-instance coordinator — Services/SingleInstanceCoordinator.cs
  • eda67f4 feat: window placement service — Services/WindowPlacementService.cs

6 files, +800. All paths under windows-native/Muesli.Windows/Services/.

Review focus

  • AtomicJsonFile + CrossProcessFileLock — write protocol (temp file + atomic replace), lock scope and contention behavior; corruption recovery on malformed JSON.
  • SecretStore — DPAPI scope (CurrentUser), entropy handling, failure modes when DPAPI is unavailable.
  • SingleInstanceCoordinator — second-instance handoff protocol and race windows at startup.
  • WindowPlacementService — off-screen/monitor-removed rescue logic, DPI awareness of saved coordinates.

Expected — do not flag: these primitives have almost no callers yet; they are wired into the app in PR 9 (feat/rebuild-app-shell). Forward references are intentional.

Test evidence

  • Full stack at tip: dotnet build windows-native\Muesli.Windows\Muesli.Windows.csproj --no-restore clean; dotnet test windows-native\Muesli.Windows.Tests\Muesli.Windows.Tests.csproj --no-restore484/484 passing.
  • The suite lands in PR 11; classes covering this chunk: AtomicPersistenceTests, SecretsAndSettingsTests, ModelAndSingleInstanceTests.

Previous: #10 | Next: #12

Copilot AI lite review requested due to automatic review settings August 4, 2026 09:03
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 519b4767-1189-404b-a6c0-c8cbc9ad843e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The single-instance mutex acquisition can block/hang secondary launches and mishandles mutex ownership, and the DPI conversion fallback incorrectly assumes uniform X/Y scaling.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Adds foundational Windows-native “core primitives” under windows-native/Muesli.Windows/Services/ to support future app-shell wiring (atomic JSON persistence, secret storage, single-instance activation, and window/work-area placement helpers).

Changes:

  • Introduces AtomicJsonFile for recoverable, atomic JSON persistence (temp write + replace, backup/quarantine handling).
  • Adds Windows secrets storage (ISecretStore + Credential Manager-backed implementation) and a simple in-memory fallback.
  • Adds primitives for single-instance coordination (named mutex + activation pipe), cross-process file locking, and DPI-aware work-area/window placement.
File summaries
File Description
windows-native/Muesli.Windows/Services/AtomicJsonFile.cs Atomic JSON read/write with backup/quarantine recovery and privacy-sensitive purge mode.
windows-native/Muesli.Windows/Services/CrossProcessFileLock.cs Async cross-process lock acquisition via exclusive lock file.
windows-native/Muesli.Windows/Services/MuesliPathService.cs User profile path resolution helper with validation.
windows-native/Muesli.Windows/Services/SecretStore.cs Secret store abstraction plus Windows Credential Manager and in-memory implementations.
windows-native/Muesli.Windows/Services/SingleInstanceCoordinator.cs Named mutex + named pipe activation signaling for single-instance behavior.
windows-native/Muesli.Windows/Services/WindowPlacementService.cs Monitor/work-area helpers and pixel↔DIP conversions for window placement.
Review details

Suppressed comments (1)

windows-native/Muesli.Windows/Services/WindowPlacementService.cs:134

  • This DPI transform fallback assumes uniform scaling by using only DpiScaleX and applying it to both axes. If DpiScaleX and DpiScaleY differ, work-area conversion will be wrong in Y. Build the matrix with separate X/Y scale values (with a safe fallback).
        var dpi = VisualTreeHelper.GetDpi(window);
        var scale = dpi.DpiScaleX > 0 ? dpi.DpiScaleX : Math.Max(1d, GetDpiForWindow(handle) / 96d);
        return new Matrix(scale, 0, 0, scale, 0, 0);
  • Files reviewed: 6/6 changed files
  • Comments generated: 3
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment on lines +38 to +44
public static SingleInstanceCoordinator AcquireForCurrentUser()
{
var identity = WindowsIdentity.GetCurrent().User?.Value ?? Environment.UserName;
var suffix = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity)))[..20];
var mutex = new Mutex(initiallyOwned: true, $"Local\\Muesli.Windows.UI.{suffix}", out var createdNew);
return new SingleInstanceCoordinator(mutex, $"Muesli.Windows.Activation.{suffix}", createdNew);
}
Comment on lines +119 to +126
private static Matrix GetTransformFromDevice(Window window, IntPtr handle)
{
var source = GetHwndSource(window, handle);
if (source?.CompositionTarget is not null) return source.CompositionTarget.TransformFromDevice;
var dpi = VisualTreeHelper.GetDpi(window);
var scale = dpi.DpiScaleX > 0 ? dpi.DpiScaleX : Math.Max(1d, GetDpiForWindow(handle) / 96d);
return new Matrix(1 / scale, 0, 0, 1 / scale, 0, 0);
}
}
catch (IOException exception)
{
throw new TimeoutException($"Timed out waiting for the model setup lock '{Path.GetFileName(path)}'.", exception);
@pHequals7
pHequals7 force-pushed the feat/window-placement-service branch from eda67f4 to b84e056 Compare August 4, 2026 15:22
@claude

claude Bot commented Aug 4, 2026

Copy link
Copy Markdown

Claude finished @Mvkd108's task in 4m 6s —— View job


Review: core primitives (AtomicJsonFile, CrossProcessFileLock, MuesliPathService, SecretStore, SingleInstanceCoordinator, WindowPlacementService)

  • Gather context (diff, files, prior Copilot review comments)
  • Review AtomicJsonFile.cs + CrossProcessFileLock.cs
  • Review SecretStore.cs
  • Review SingleInstanceCoordinator.cs
  • Review WindowPlacementService.cs (incl. Copilot's DPI-scale finding)
  • Post final review

Reviewed against origin/docs/windows-execution-plan (the stated base for this stacked PR). No test/build tooling available in this sandbox (Linux, no dotnet/Windows SDK), so I could not independently re-run dotnet build/dotnet test — relying on the author's reported 484/484 passing. Per the PR notes, I did not flag the lack of callers for these primitives (that's expected; wiring lands in PR 9).

Findings

1. AtomicJsonFile.Load<T>() doesn't handle a persistent IO error — only JSON corruption (Medium)
windows-native/Muesli.Windows/Services/AtomicJsonFile.cs:43-76

Load only catches JsonException to trigger the backup/quarantine recovery path. DeserializeWithRetry (lines 166-179) retries on IOException/UnauthorizedAccessException up to IoAttemptCount (5) times, but once retries are exhausted it lets that exception propagate — and Load has no catch for it. So a transient-but-persistent lock (AV scan, backup software, OneDrive sync, etc. holding the file past ~500ms of retries) surfaces as an unhandled exception instead of falling back to the caller's fallback value the way the corruption path does. That undermines the "recoverable" contract this class advertises for exactly the kind of file-locking scenario Windows is prone to.

Fix this →

2. SecretStore.Read/IsConfigured throw on any Credential Manager failure other than "not found" (Medium)
windows-native/Muesli.Windows/Services/SecretStore.cs:21-26, :96

Read treats only Win32 error 1168 (ERROR_NOT_FOUND) as "no value" and throws Win32Exception for everything else. IsConfigured (line 96) just calls Read, so it inherits that throw. The PR description explicitly calls out "failure modes when DPAPI is unavailable" as a review focus — Credential Manager can fail for reasons besides "not found" (group policy disabling it, ERROR_ACCESS_DENIED, no logon session, etc.), and a settings-check helper like IsConfigured throwing in that case is a rougher failure mode than callers will likely expect from a boolean-returning check.

Fix this →

Side note: this class is Windows Credential Manager backed (CredWrite/CredRead, CRED_PERSIST_LOCAL_MACHINE), not raw DPAPI (ProtectedData.Protect). Functionally it's fine — Credential Manager uses DPAPI under the hood and LOCAL_MACHINE persistence is the right choice for a per-user, non-roaming secret — but the PR title/description says "DPAPI-backed secret store," which is a bit misleading for anyone expecting to find ProtectedData calls here.

3. SingleInstanceCoordinator — Copilot's "mutex can block/hang" claim looks like a false positive (Informational)
windows-native/Muesli.Windows/Services/SingleInstanceCoordinator.cs:38-44

I looked into this since it was flagged as blocking. Per Win32 CreateMutex semantics (which new Mutex(bool initiallyOwned, string name, out bool createdNew) wraps directly): if a mutex with that name already exists, bInitialOwner/initiallyOwned is ignored, the call does not block, createdNew is false, and the caller does not gain ownership. This is also the standard, widely-used .NET single-instance pattern. So the second instance's constructor call returns immediately with _ownsMutex = false, and Dispose() correctly skips ReleaseMutex() for it (SingleInstanceCoordinator.cs:121). I don't think this hangs in practice — worth confirming with the reviewer, but I'd push back on treating it as blocking.

One smaller, real observation in the same file: the pipe listener's fallback catch block (SingleInstanceCoordinator.cs:77-83) retries immediately with no delay when an unexpected (non-cancellation) exception occurs and cancellation hasn't been requested — a persistent pipe-creation failure would spin a tight retry loop. Low real-world impact (this only runs on the primary instance's activation-pipe listener), but a small Task.Delay before re-looping would be cheap insurance. Also, Dispose() (lines 118-134) cancels but never awaits _listenerTask, so disposal doesn't guarantee the background loop has actually exited — harmless at process-exit, but worth knowing if this class is ever reused in a longer-lived scenario.

4. WindowPlacementService DPI transform — Copilot's X/Y scale finding is correct but low real-world severity (Low)
windows-native/Muesli.Windows/Services/WindowPlacementService.cs:119-135

Confirmed: both GetTransformFromDevice and GetTransformToDevice's fallback branches (used only when there's no connected HwndSource/CompositionTarget) build the matrix from DpiScaleX alone and apply it to both axes. This is only reachable in the "unshown window" fallback path (see the comment at line 106-108), and Windows' per-monitor DPI model applies uniform X/Y scaling in every practical desktop configuration today, so I wouldn't expect this to misbehave on real hardware — but it's not guaranteed by the API contract, and it's a one-line fix (dpi.DpiScaleY) to make the code actually match its own doc comment about not assuming uniform scaling elsewhere in the file (e.g. line 8, 15). Worth taking since it's cheap.

5. CrossProcessFileLock timeout message is hard-coded to "model setup lock" (Low, already flagged by Copilot)
windows-native/Muesli.Windows/Services/CrossProcessFileLock.cs:34

Agreed with Copilot here — this is a generic file-lock helper (used generically per its name), but the TimeoutException message assumes it's always guarding "the model setup lock." Worth genericizing (e.g. $"Timed out waiting for the lock file '{Path.GetFileName(path)}'.") so future call sites don't get a misleading error.

Not flagged

  • MuesliPathService.cs — straightforward, no issues found.
  • AtomicJsonFile's write path (temp file + File.Replace/backup), privacy-sensitive purge ordering, and quarantine-on-corruption logic all look correct — purge only targets .bak/.tmp/.recovery.tmp/.corrupt-* artifacts and never the live file itself, and it runs before the atomic replace so a failed purge leaves the original file authoritative, as the inline comment intends.
  • WindowPlacementService's off-screen/monitor-removed rescue logic (GetWorkAreaForBounds + ClampToVisibleWorkArea, using MonitorFromRect(..., MonitorDefaultToNearest)) correctly falls back to the nearest remaining monitor's work area when a saved position is now off-screen.
  • No callers exist yet for any of this — expected per the PR description, not flagged.

Mvkd108 and others added 4 commits August 18, 2026 19:42
…vice

Introduces AtomicJsonFile for crash-safe JSON writes, CrossProcessFileLock
for coordinating access across app instances, and MuesliPathService as the
single source of truth for app data locations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stores API keys encrypted at rest with the current user's Windows DPAPI
key rather than in plaintext settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ensures only one Muesli instance owns the hotkeys and capture devices,
handing activation to the running instance instead of starting a second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Centralises monitor-aware window positioning and restores placement across
DPI and display-topology changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Mvkd108
Mvkd108 force-pushed the feat/window-placement-service branch from b84e056 to 7b12d57 Compare August 18, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants