Skip to content

fix(desktop): Export logs saves through the save dialog, so a portable StemDeck writes nothing outside its folder - #1002

Merged
thcp merged 5 commits into
big-releasefrom
fix/b25d-portable-no-outside-writes
Nov 4, 2026
Merged

thcp merged 5 commits into
big-releasefrom
fix/b25d-portable-no-outside-writes

Conversation

@thcp

@thcp thcp commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

A portable StemDeck now writes nothing outside its own folder in a normal session, measured on a package built from this branch. The one write I found outside the folder was Settings > Logs > Export logs: it is fixed here, and the repository gets a script that measures this on any build.

Refs #399

What changes for the user

  • Export logs in the desktop app now asks where to save the zip, with the same save dialog as every other export. Before, the window downloaded the zip by itself, and WebView2 put it in the Downloads folder without asking. In a portable StemDeck that was a file outside its folder, in a place the user never chose.
  • The browser keeps its own download, unchanged.

How I looked for writes outside the folder

I extracted the hand-check zip (built from big-release 55a0d97) into new empty folders and ran it, three ways:

  1. Real profile, first run. A full listing, before and after, of every file changed that day and every folder under %LOCALAPPDATA%, %APPDATA%, %USERPROFILE%, Documents and %ProgramData%, the entries beside the package folder, and every HKEY_CURRENT_USER registry value that names StemDeck. The session: setup, Settings opened, one 6 second WAV imported and split, the window closed normally.
  2. Redirected profile, first run, with the AI model step run in full. LOCALAPPDATA, APPDATA, TEMP, TMP and USERPROFILE pointed at an empty folder. Nothing else on the machine writes there, so every file in it comes from the package: Python, pip, Hugging Face, torch hub, matplotlib, yt-dlp, WebView2 and the shell.
  3. Settings > Export logs, pressed through the DevTools protocol without anything else changing how the window downloads.

What I found

Written outside the folder Who writes it Outcome
Downloads\stemdeck-logs-<time>.zip on Export logs WebView2, because exportLogs let the window download Fixed here
HKCU\...\Shell\MuiCache: the exe's name and company Windows, for every program started Cannot be prevented by any program
NVIDIA DXCache, Direct3D D3DSCache, the shell's Microsoft\Windows\Caches, WinINet and account caches (seen only in the redirected run) The graphics driver, Direct3D and Windows components the WebView2 GPU process uses Cannot be redirected per program; listed as "Windows" by the script

Nothing else. In particular, with the package's own redirects in place (#897, #904, #909): no %LOCALAPPDATA%\StemDeck, no %LOCALAPPDATA%\app.stemdeck.desktop, no pip, matplotlib, Hugging Face, torch hub, yt-dlp, numba or Python cache in the profile, no Documents folder, no Start menu or autostart entry, nothing beside the package folder.

Where the %LOCALAPPDATA% writes on this PC come from

The folder that holds the everyday StemDeck on this PC is a 0.20.0 portable (released before #897, #904 and #909). Its own logs show a start at 19:46:57 and a backend start at 19:47:10 on 3 November. At those same seconds %LOCALAPPDATA%\app.stemdeck.desktop\EBWebView and %LOCALAPPDATA%\StemDeck\settings.json were written. More than ten launches of portable packages that I made afterwards, from 55a0d97 and from this branch, did not change either one. So the writes still seen in %LOCALAPPDATA% come from the 0.20.0 build, and stop once it runs a build with these fixes. The fixed builds leave the folders that 0.20.0 already created in place. Removing them is a manual step, because the app cannot tell them apart from an installed StemDeck's.

Why not more

  • Disabling the GPU in the window would stop the shader caches, but every waveform and animation would then draw in software. That is a real cost for a write that Windows makes for every program that draws, so I left it out. If you want it anyway, it is one browser argument, and the risk is slower drawing.
  • Pointing USERPROFILE and LOCALAPPDATA at the package folder would move those caches too. It would also copy the Windows account cache (sign-in tokens) into a folder that is carried around on USB sticks, and it would change what Python treats as home. I did not do it.

The leak check (new, Windows only, manual)

scripts/windows/check-portable-leaks.ps1 runs one extracted package through the session above, using scripts/windows/drive-portable.mjs, and exits 1 if anything is written outside the folder. It sorts each change into one of these groups:

  • StemDeck: a path that names it, a download the window made by itself, or anything in the redirected profile. These fail the check.
  • Windows: registry and caches no program can prevent.
  • Review: names a tool StemDeck uses.
  • Other: another program on the machine.

It refuses a package whose library points outside it, and a DevTools port that is already taken. It never deletes anything: robocopy /L only lists, and reg query only reads. It needs a desktop session and a built package, so it is not in CI.

How it was verified

  • Red then green, browser tests: tests/e2e/export-logs.spec.mjs. On 55a0d97 both desktop tests fail: no save dialog is asked for, and the window downloads the zip. With this change all 4 tests pass: dialog, cancel, failed save, browser download.
  • Red then green, on real packages, with the new check:
    • 55a0d97 package: exit 1, StemDeck (1): [window download] ...playwright-artifacts-...\<zip>.
    • Package built from this branch (make-portable.ps1 -CpuOnly), first run: exit 0, StemDeck (0), and "Export logs: save dialog shown and closed".
    • The same build, first run, with the redirected profile and the full model step. The window's DevTools port did not open in this mode, so the check ran the session over HTTP: settings read, one import split to done.
      • In the redirected profile there were only the NVIDIA shader cache and the shell's Microsoft\Windows\Caches, so nothing from Python, pip, Hugging Face, torch hub or matplotlib.
      • The one line counted as a leak was the stemdeck.log of my own browser-test server in the temp folder, from another run. I narrowed the check's name rule so that it no longer counts that file. This run used the older rule.
  • Full browser suite in 3 shards: 847 passed, 7 skipped, 1 failed.
    • The failure was live-wave.spec.mjs (recording waveform), which this change does not touch.
    • Run alone, all 4 of its tests passed, so I count it as a flake under machine load.
  • Reviews: frontend and security reviewers. I applied their findings: more test assertions, plus the leak check's guards against a reused package, a taken port and a failed session.

Update path

No change to uv.lock or to the desktop shell. The change is in backend/static, which the in-app update replaces.

Thales and others added 5 commits November 3, 2026 21:01
Settings > Logs > Export logs let the window download the zip by itself.
WebView2 saves such a download straight to the user's Downloads folder
without asking, so a portable StemDeck wrote a file outside its own folder
in a place the user never chose. The desktop app now saves the zip through
the native save dialog, the same path every other export takes. The browser
keeps its own download.

Adds scripts/windows/check-portable-leaks.ps1 (with drive-portable.mjs), a
Windows-only check that runs an extracted portable package through one
session and lists everything it wrote outside its folder: profile folders,
registry, files beside the package, downloads the window made by itself, and
with -RedirectProfile everything that follows the profile environment.
…ak check

The Export logs tests now also check the label during the transfer, a
failed save and a cancelled one.

The leak check refuses a package whose library points outside it and a
DevTools port that is already taken, always closes the app and writes the
report when the session fails, keeps the driver's stderr out of PowerShell's
error handling, waits for the save dialog instead of a fixed pause, and runs
PowerShell by its full path with an encoded command.
Windows PowerShell 5.1 mangles embedded quotes in a native argument, so the curl form field carried a broken path and the import was refused.
…package's

A test server's stemdeck.log in the temp folder was counted as a leak of the package under test.
@thcp
thcp merged commit caae319 into big-release Nov 4, 2026
19 of 20 checks passed
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.

1 participant