Repository navigation
fix(desktop): keep a portable run's temp files, tool caches and settings copy in its folder - #904
Merged
Merged
Conversation
…ngs copy in its folder The portable Windows build still wrote outside its own folder: the backend kept a settings.json copy in %LOCALAPPDATA%\StemDeck, matplotlib its font cache in %LOCALAPPDATA%\matplotlib, WebView2 and Python their temp files (an upload over 1 MB among them) in the Windows temp folder, and the NVIDIA setup pip's cache of the CUDA torch wheels in %LOCALAPPDATA%\pip. One place sets this now: first thing in main, a portable package points TEMP, TMP, TMPDIR, PIP_CACHE_DIR and MPLCONFIGDIR into data\, so the shell, WebView2 and every child inherit them. The backend gets no settings mirror in a portable package. Explorer, started for links and Reveal, gets the user's own values back. Installed builds are unchanged.
thcp
added a commit
that referenced
this pull request
Nov 3, 2026
…gs or library (#909) 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 - A new portable folder starts with default settings and keeps its library in `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_library` brings nothing and adopts it). - An in-place upgrade (unzip over the old folder, or the in-app update) is unchanged: `data\settings.json` stays and names the same library. - Installed builds (Windows installer, macOS, Linux) are unchanged: the library default is still `~/Documents/StemDeck/jobs`. ## For the release notes - A portable user whose library sat in `Documents\StemDeck\jobs` only 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 in `data\jobs`, the move refuses a folder that holds another library). - A portable folder an older version already seeded from an installed app keeps that `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.json` from an installed StemDeck in `ensure_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\jobs` had anything in it, a portable package adopted it as its default library, and copied `~\Documents\StemDeck\user-data.json` into 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, and `directory_has_entries`, which only it used. - The settings mirror (`shared_settings_dir`, `settings_mirror`, `STEMDECK_SETTINGS_MIRROR`, and in `app/core/settings.py` `_mirror_path`, `_mirror_settings` and the recovery from the mirror). Its only purpose was to feed the portable seed. A portable package already had no mirror (#904), and for an installed build the mirror path is exactly its own `settings.json` (`shared_settings_dir` returned what `local_data_dir` returns 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 with `STEMDECK_DATA_DIR` pointed at a test folder wrote that run's settings into the real `%LOCALAPPDATA%\StemDeck\settings.json`. A corrupt `settings.json` is still moved aside for diagnosis (`settings.json.corrupt-*`), as before. ## How it was verified - Red then green, Rust: `a_new_portable_folder_never_takes_an_installed_apps_settings` runs the real `ensure_workspace` in a child test process with `STEMDECK_ROOT` set to a throwaway portable folder and `LOCALAPPDATA` set to a throwaway folder holding an installed app's `settings.json`. On `big-release` it fails with "a portable folder copied the installed app's settings"; with this change it passes. - Red then green, Rust: `a_portable_package_never_looks_in_documents` fails while a portable package still consults Documents, and passes with the gate. `an_installed_app_keeps_its_library_in_documents` covers the unchanged installed path. - `cargo fmt --check`, `cargo clippy --tests -- -D warnings` and `cargo test` (233 passed) on Windows. Linux and macOS compile on PR CI only. - Full pytest suite on Windows (3860 passed, 12 skipped), ruff check and ruff format. - The child test only runs `ensure_workspace` when its root is a throwaway portable folder and its data folder is that root's `data`, so `cargo test -- --ignored` in a real environment does nothing. - The acceptance check UPG-01 (not in CI, needs a real build) now upgrades in place: the 0.20.0 `settings.json` goes in the package's own data folder instead of a fake `LOCALAPPDATA`. 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.
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.
The portable Windows build now keeps what it writes inside its own folder. Before this change a portable run still wrote a copy of settings.json to
%LOCALAPPDATA%\StemDeck, matplotlib's font cache to%LOCALAPPDATA%\matplotlib, its temporary files (WebView2, Python, uploads over 1 MB) to the Windows temp folder, and on the NVIDIA build pip's cache of the CUDA torch wheels to%LOCALAPPDATA%\pip. Follows #897, which moved the WebView2 profile todata\webview. Part of #399.What changes
desktop/src-tauri/src/main.rsportable_envandapply_portable_env: one place, first thing inmain, before Tauri starts any thread. For a portable package only, the process setsTEMP,TMP,TMPDIRtodata\tmp,PIP_CACHE_DIRtodata\cache\pipandMPLCONFIGDIRtodata\cache\matplotlib. The shell, WebView2 and every child (backend, model warmup, pip) inherit them, so no spawn site needs its own patch.data\tmpcannot be created, nothing is redirected anddata\logs\setup.logsays so.with_user_env:open_urland Reveal startexplorer.exewith the user's ownTEMP,TMP,TMPDIR,PIP_CACHE_DIRandMPLCONFIGDIR, so a browser started cold from it does not keep the portable folder as its temp folder.settings_mirror: the backend is no longer givenSTEMDECK_SETTINGS_MIRRORin a portable package. An installed app gets exactly the value it got before.app/core/settings.py: docstring of_mirror_settingsonly, to match.packaging/windows/README-WINDOWS.txt: says what stays in the folder, what is outside only because the user chose it, and what Windows writes that StemDeck cannot move.Nothing changes for installed builds, macOS or Linux:
portable.txtonly ships in the Windows zip, and without itportable_envis empty andsettings_mirroris unchanged.Every location a portable run writes
Measured by running the real CPU package, built with
scripts/windows/make-portable.ps1 -CpuOnly, withLOCALAPPDATA,APPDATA,TEMP,TMP,USERPROFILEandHOMEpointed at empty throwaway folders, through first-run setup, one upload separated end to end (a 12 s, 2.1 MB WAV), and quit. Two runs of the old build and three of the fixed build (the last one on the final commit); listings after each phase.data\settings, config, logs, ffmpeg, models, jobs, certs, downloads, webviewdata\listingdata\cache\huggingface(model downloads)data\cache\listingXDG_CACHE_HOME,TORCH_HOME)%LOCALAPPDATA%\StemDeck\settings.jsonLocalAppData\StemDeck\settings.json (14 B)written when the backend started; fixed runs: absentsettings_mirrorreturns None for portable%LOCALAPPDATA%\matplotlib\fontlist-v3.11.0.jsondata\cache\matplotlibdata\cache\matplotlibpresentMPLCONFIGDIR%TEMP%data\tmpTemp\<guid>.tmpwhile running; fixed runs:Tempempty in every phase,data\tmpholdsmsedgeedge_BITS_*(WebView2) and the<guid>.tmpTEMP,TMP,TMPDIRtempfile)data\tmptempfile.gettempdir()under the same environment printed the sandboxTempfolder%LOCALAPPDATA%\pip\Cache(NVIDIA build, CUDA torch install)data\cache\pippython -m pip cache dirprinted the profile path without the variable anddata\cache\pipwith itPIP_CACHE_DIR%USERPROFILE%\AppData\Local\Microsoft\{IdentityCache, Windows\INetCache, INetCookies, History, Feeds Cache, Internet Explorer}%LOCALAPPDATA%\Microsoft\Windows\Caches\cversions.3.db%USERPROFILE%\AppData\Local\NVIDIA\DXCache,LocalLow\NVIDIA\DXCacheDocuments\StemDeck\jobs, only when it already holds a library from an older installdocuments_dir_for_jobs%LOCALAPPDATA%\StemDeck\settings.jsonread on first run of a fresh folderensure_workspaceThe real profile was never used: during every run the real
%LOCALAPPDATA%\app.stemdeck.desktop\EBWebViewand%LOCALAPPDATA%\StemDeckhad no new or modified entries.Tests
main.rs:a_portable_package_sends_temp_and_tool_caches_into_data,an_installed_app_keeps_the_system_temp_and_caches,only_an_installed_app_mirrors_its_settings_to_the_profile,a_program_started_for_the_user_gets_the_users_temp_back. Against the old code they do not build (error[E0425]: cannot find function portable_envandsettings_mirror), and the old code passed the mirror to every backend; with the change all three pass.cargo fmt --check,cargo clippy --tests -- -D warnings,cargo test(216 passed, 0 failed) indesktop/src-tauri.STEMDECK_DATA_DIR(tests/test_config.py::test_data_dir_moves_default_runtime_dirs); the stray writes came from what the shell handed it and from third-party libraries, which the shell now points into the folder.How installed behaviour was kept
portable_envreturns nothing withoutportable.txtin the app root, soapply_portable_envsets no variable (an_installed_app_keeps_the_system_temp_and_caches).settings_mirrorreturns the sameshared_settings_dir()\settings.jsonthe old inline code passed (only_an_installed_app_mirrors_its_settings_to_the_profile).%LOCALAPPDATA%\app.stemdeck.desktop.Not verified
PIP_CACHE_DIRis proven bypip cache dironly.explorer.exe(unit-tested throughrestore_env, not clicked by hand).data\tmpis not swept at startup. After quit it was empty in two of the three fixed runs; the third left one WebView2msedgeedge_BITS_*folder left by WebView2.Review
Rust and security reviewers read the diff. Taken: log a failed
data\tmpto setup.log instead of stderr; give Explorer the user's environment back; lookTEMPup by name; update the Python docstring. Not taken: both suggested matplotlib uses%USERPROFILE%\.matplotlibon Windows, but matplotlib 3.11 in this package was measured writing%LOCALAPPDATA%\matplotlib\fontlist-v3.11.0.json, so the comment stays. The security reviewer also noted thatC:\StemDeck, the folder the not-writable message suggests, inherits Modify for Authenticated Users fromC:\; that predates this change and applies equally topython\andStemDeck.exe, so it is a separate item.Does not touch
uv.lock. Changes the shell binary, so it reaches users through the app update.