Repository navigation
fix(desktop): a portable folder never reads an installed app's settings or library - #909
Merged
Merged
Conversation
…gs or library A fresh portable folder copied %LOCALAPPDATA%\StemDeck\settings.json from an installed StemDeck in ensure_workspace. That file can name the installed app's library, so the portable run opened it, and its factory reset or auto-delete sweep would delete those songs. A portable package also adopted ~\Documents\StemDeck\jobs as its default library whenever it had entries, and copied Documents\StemDeck\user-data.json into its own jobs folder. A portable package now starts from its own data folder only, with its own defaults, or the library the user picks in Settings. It never resolves Documents. Installed builds keep ~/Documents/StemDeck/jobs as before. The settings mirror (STEMDECK_SETTINGS_MIRROR) only existed to feed the portable seed. A portable package already had none, and for an installed build it was the same file as settings.json, so its write and its corrupt file recovery did nothing. It is removed from the shell and the backend. A corrupt settings.json is still moved aside for diagnosis.
thcp
added a commit
that referenced
this pull request
Nov 4, 2026
…e StemDeck writes nothing outside its folder (#1002) 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A portable StemDeck folder now starts from its own data folder only. It no longer reads the settings or the library of an installed StemDeck on the same Windows account.
What changes for the user
data\jobs. To use a library you already have, choose its folder in Settings, StemData location (an empty current library can point at an existing one;move_librarybrings nothing and adopts it).data\settings.jsonstays and names the same library.~/Documents/StemDeck/jobs.For the release notes
Documents\StemDeck\jobsonly because an older version adopted it (no saved StemData location) opens an empty library after updating. Nothing is lost or touched: choose that folder once in Settings, StemData location, before importing a new song (with a song indata\jobs, the move refuses a folder that holds another library).data\settings.json, which can still name the installed app's library. It cannot be detected without reading outside the folder, which is the thing this change forbids, so the note should ask portable users to check Settings, StemData location.Why
A fresh portable folder copied
%LOCALAPPDATA%\StemDeck\settings.jsonfrom an installed StemDeck inensure_workspace. That file can name the installed app's library, so the portable run opened it, and a factory reset or an auto-delete sweep in the portable run would delete those songs. PR #904 stopped the portable package from writing that copy; this stops the read.The same audit found a second path out of the folder: when
~\Documents\StemDeck\jobshad anything in it, a portable package adopted it as its default library, and copied~\Documents\StemDeck\user-data.jsoninto its own. That folder is the installed app's default library. A portable package now never resolves Documents at all.What was removed, and why that is safe
ensure_workspace: the portable seed from%LOCALAPPDATA%\StemDeck.documents_dir_for_jobs: the "adopt Documents if it has entries" rule, anddirectory_has_entries, which only it used.shared_settings_dir,settings_mirror,STEMDECK_SETTINGS_MIRROR, and inapp/core/settings.py_mirror_path,_mirror_settingsand the recovery from the mirror). Its only purpose was to feed the portable seed. A portable package already had no mirror (fix(desktop): keep a portable run's temp files, tool caches and settings copy in its folder #904), and for an installed build the mirror path is exactly its ownsettings.json(shared_settings_dirreturned whatlocal_data_dirreturns for a non-portable install), so the mirror wrote the same file twice and its recovery read the file it had just quarantined. One real effect went away with it: a desktop run withSTEMDECK_DATA_DIRpointed at a test folder wrote that run's settings into the real%LOCALAPPDATA%\StemDeck\settings.json.A corrupt
settings.jsonis still moved aside for diagnosis (settings.json.corrupt-*), as before.How it was verified
a_new_portable_folder_never_takes_an_installed_apps_settingsruns the realensure_workspacein a child test process withSTEMDECK_ROOTset to a throwaway portable folder andLOCALAPPDATAset to a throwaway folder holding an installed app'ssettings.json. Onbig-releaseit fails with "a portable folder copied the installed app's settings"; with this change it passes.a_portable_package_never_looks_in_documentsfails while a portable package still consults Documents, and passes with the gate.an_installed_app_keeps_its_library_in_documentscovers the unchanged installed path.cargo fmt --check,cargo clippy --tests -- -D warningsandcargo test(233 passed) on Windows. Linux and macOS compile on PR CI only.ensure_workspacewhen its root is a throwaway portable folder and its data folder is that root'sdata, socargo test -- --ignoredin a real environment does nothing.settings.jsongoes in the package's own data folder instead of a fakeLOCALAPPDATA. Not run here, since it needs a packaged build.Update path
No change to
uv.lock. The shell binary changes (main.rs), as any shell fix does.