fix(client/windows): "Open log folder" stops opening Documents #31

Merged
enricobuehler merged 1 commits from worktree-client-logs-folder-msix into main 2026-08-04 17:41:04 +00:00
Owner

Field report: "the View Logs button now appears on the client, but when I click it, it opens my Documents folder instead of the correct logs directory."

Nothing is wrong with the button — the path is.

Root cause

The client ships as a full-trust MSIX package (runFullTrust), and Windows redirects a packaged app's %LOCALAPPDATA% writes into its private …\Packages\<family>\LocalCache\Local\. logfile creates and appends through that redirection without ever seeing it, so the literal %LOCALAPPDATA%\punktfunk\logs it hands out is right to write to yet names a directory that never exists on disk.

Explorer runs outside the container: it resolves the literal path, finds nothing, and — instead of failing — silently falls back to the user's Documents folder. That silent fallback is the entire signature.

An unpackaged dev run creates that directory for real, which is why this only ever showed up in the field.

It was three bugs, not one

The same phantom path was handed straight to the user in two more places, both added by d839f4c2 and both wrong the same way:

  • the client log file startup line, and
  • the failed-spawn banner's Check <path> — the one people are told to follow after a session dies.

Anyone who followed either landed in an empty or absent directory.

The fix

One resolver, not three call-site patches:

  • real_dir() canonicalizes the directory it just created. That resolves through the redirection on a packaged run and changes nothing on an unpackaged one — no package identity to detect, no LocalCache path to hand-assemble.
  • log_dir() stays the write path and goes private, so a future caller can't reach for the wrong one.
  • path() resolves too, which fixes both messages.
  • strip_verbatim() undoes the \\?\ that canonicalize always returns — Explorer refuses verbatim paths, taking the same silent Documents fallback — including the \\?\UNC\ form a roaming profile on a share resolves to.
  • The button additionally guards on is_dir().

Not affected: the art-cache, os-icons and %APPDATA% trust store are app-internal, read and written through the same redirection, so they stay self-consistent. The host is an Inno Setup install and its tray uses %ProgramData%, which is not redirected.

Verification

Run on the Windows CI runner (.133), since this file is cfg(windows) and macOS never compiles it:

  • cargo clippy -p punktfunk-client-windows --all-targets -- -D warnings — clean
  • cargo test -p punktfunk-client-windows — green, incl. 5 new logfile::tests
  • cargo fmt --all --check — clean
  • Measured GetFinalPathNameByHandleW on the box directly: it does return \\?\, so the prefix stripping is load-bearing; and on an unpackaged run it resolves to the literal path, proving this change is a no-op there and cannot regress anything.

Known gap — the packaged half is unverified

That canonicalize resolves through the MSIX redirection on a genuinely packaged run is the one load-bearing assumption, and it is not yet confirmed on glass. Note the is_dir() guard does not catch it if that is wrong: from inside the container the literal path appears to exist, so a bad resolve would quietly revert to today's behaviour rather than doing nothing.

Confirming needs an MSIX install plus a real desktop session (.173/.221 were off-LAN). Two headless workarounds dead-ended and are not worth repeating: Invoke-CommandInDesktopPackage into a stock UWP package can't host a probe (AppContainer), and New-SelfSignedCertificate won't persist a key over ssh on .133, so a hand-rolled signed full-trust test package isn't buildable there either.

Field report: *"the View Logs button now appears on the client, but when I click it, it opens my Documents folder instead of the correct logs directory."* Nothing is wrong with the button — the path is. ## Root cause The client ships as a **full-trust MSIX package** (`runFullTrust`), and Windows redirects a packaged app's `%LOCALAPPDATA%` writes into its private `…\Packages\<family>\LocalCache\Local\`. `logfile` creates and appends through that redirection without ever seeing it, so the literal `%LOCALAPPDATA%\punktfunk\logs` it hands out is right to **write** to yet names a directory that **never exists on disk**. Explorer runs *outside* the container: it resolves the literal path, finds nothing, and — instead of failing — **silently falls back to the user's Documents folder**. That silent fallback is the entire signature. An unpackaged dev run creates that directory for real, which is why this only ever showed up in the field. ## It was three bugs, not one The same phantom path was handed straight to the user in two more places, both added by `d839f4c2` and both wrong the same way: * the `client log file` startup line, and * the failed-spawn banner's `Check <path>` — the one people are told to follow *after a session dies*. Anyone who followed either landed in an empty or absent directory. ## The fix One resolver, not three call-site patches: * **`real_dir()`** canonicalizes the directory it just created. That resolves *through* the redirection on a packaged run and changes nothing on an unpackaged one — no package identity to detect, no LocalCache path to hand-assemble. * **`log_dir()`** stays the write path and goes **private**, so a future caller can't reach for the wrong one. * **`path()`** resolves too, which fixes both messages. * **`strip_verbatim()`** undoes the `\\?\` that `canonicalize` always returns — Explorer refuses verbatim paths, taking the *same* silent Documents fallback — including the `\\?\UNC\` form a roaming profile on a share resolves to. * The button additionally guards on `is_dir()`. Not affected: the art-cache, os-icons and `%APPDATA%` trust store are app-internal, read and written through the same redirection, so they stay self-consistent. The host is an Inno Setup install and its tray uses `%ProgramData%`, which is not redirected. ## Verification Run on the Windows CI runner (.133), since this file is `cfg(windows)` and macOS never compiles it: * `cargo clippy -p punktfunk-client-windows --all-targets -- -D warnings` — clean * `cargo test -p punktfunk-client-windows` — green, incl. 5 new `logfile::tests` * `cargo fmt --all --check` — clean * Measured `GetFinalPathNameByHandleW` on the box directly: it does return `\\?\`, so the prefix stripping is load-bearing; and on an unpackaged run it resolves to the literal path, proving this change is a **no-op there** and cannot regress anything. ## ⏳ Known gap — the packaged half is unverified That `canonicalize` resolves *through* the MSIX redirection on a genuinely packaged run is the one load-bearing assumption, and it is not yet confirmed on glass. Note the `is_dir()` guard does **not** catch it if that is wrong: from inside the container the literal path *appears* to exist, so a bad resolve would quietly revert to today's behaviour rather than doing nothing. Confirming needs an MSIX install plus a real desktop session (.173/.221 were off-LAN). Two headless workarounds dead-ended and are not worth repeating: `Invoke-CommandInDesktopPackage` into a stock UWP package can't host a probe (AppContainer), and `New-SelfSignedCertificate` won't persist a key over ssh on .133, so a hand-rolled signed full-trust test package isn't buildable there either.
enricobuehler added 1 commit 2026-08-04 06:14:09 +00:00
fix(client/windows): "Open log folder" stops opening Documents
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 3m5s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 3m55s
ci / rust (pull_request) Successful in 7m46s
ci / web (pull_request) Successful in 59s
ci / docs-site (pull_request) Successful in 1m18s
ci / rust-arm64 (pull_request) Successful in 1m36s
e5453aebb7
The button shipped in d839f4c2 opens the user's Documents folder instead of the log
directory on every packaged install. Nothing is wrong with the button — the path is.

The client ships as a full-trust MSIX package, and Windows redirects a packaged app's
%LOCALAPPDATA% writes into its private ...\Packages\<family>\LocalCache\Local\. The log
module creates and appends through that redirection without ever seeing it, so the
literal %LOCALAPPDATA%\punktfunk\logs it hands out is right to WRITE to and names a
directory that never exists on disk. Explorer runs outside the container: it resolves the
literal path, finds nothing, and — instead of failing — silently falls back to Documents.
An unpackaged dev run creates that directory for real, which is why this only ever showed
up in the field.

Two more places handed the same phantom path straight to the user, both added by the same
commit and both wrong in the same way: the "client log file" startup line, and the
failed-spawn banner's "Check <path>" — the one people are told to follow after a session
dies. Anyone who did landed in an empty or absent directory.

So the fix is one resolver, not three call-site patches. `real_dir` canonicalizes the
directory it just created, which resolves through the redirection on a packaged run and
changes nothing on an unpackaged one — no package identity to detect, no LocalCache path
to hand-assemble. `log_dir` stays as the write path and goes private so a future caller
can't reach for the wrong one; `path` now resolves too, which fixes both messages.

`canonicalize` always returns a `\\?\` verbatim path and Explorer refuses those (taking
the same silent Documents fallback), so `strip_verbatim` undoes the prefix — including
the `\\?\UNC\` form a roaming profile on a share resolves to. The button additionally
guards on `is_dir()`: if the resolve ever comes back wrong, the click does nothing rather
than landing the user somewhere misleading again.
enricobuehler merged commit f3c0ee47d7 into main 2026-08-04 17:41:04 +00:00
enricobuehler deleted branch worktree-client-logs-folder-msix 2026-08-04 17:41:06 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#31