Windows portable build - #275
Merged
Merged
Conversation
Integration branch for the Windows portable build. Each module is a no-op placeholder that one work path fills in: - portable: data next to the exe, write check (path A) - dialog: native dialogs before Tauri starts (path B; MessageBoxW stub) - single: one running instance, prompt and switch (path B) - winreg: HKCU protocol, notification registration, autostart (path B) - cleanup: --uninstall-cleanup for the system uninstaller (path C) - webview2: refuse to start without the WebView2 runtime (path C) run() calls them in order before the builder; setup() claims the HKCU entries before the notification sink is chosen and starts the switch listener once AppState exists. TEMP: ci.yml also runs on pushes to this branch and portable/**; revert before merging into dev. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…set names The portable copy is a release build on Windows with no uninstall.exe beside it (update::windows_kind). It keeps its data in <exe dir>\data, checked for write access at startup (a native dialog and exit when the folder is not writable), runs only the twcore.exe next to it, and gives WebView2 its own data\webview directory at all three window sites. Self-update asks the updater for the windows-<arch>-portable key, checks that the zip holds exactly thinkwatch-lite.exe and twcore.exe at its root, waits for requests in flight, stops core, renames both exes to <name>.old, writes the new ones and restarts. Any failure renames them back and brings the gateway back up; the .old files are removed on the next start. The rename/write/rollback step does not need an AppHandle and is tested on every platform, plus on Windows against two running executables. Release assets are renamed to ThinkWatch-Lite-<v>-<os>-<arch>... with darwin/windows/linux, and each Windows job also publishes a portable zip (sha256 + signature) built from the exes inside the installer and checked for shape, PE architecture, versions and Visual C++ runtime imports. latest.json keeps every existing key and adds windows-x86_64-portable and windows-aarch64-portable; manifest.py now pins which file suffix each key may point to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dialog: TaskDialogIndirect with our own button labels, a main instruction and content, loaded from comctl32 at call time so the test binaries (no v6 manifest) still start; MessageBoxW as fallback. dialog::wait shows a marquee dialog that cannot be dismissed while a closure runs on another thread and closes itself when it returns. winreg: claim() points thinkwatch:// at the running exe under HKCU, repoints an existing autostart Run entry to it, and registers the notification AUMID (DisplayName + IconUri to a png written into the data dir). release_all() removes only entries that point at the given exe, plus this copy's AUMID registration and icon. Dev builds never touch the registry. Autostart on Windows no longer goes through the plugin: the toggle is the one shared Run entry (on = present anywhere and not disabled in Settings > Apps > Startup), enabling writes a quoted command for this exe, disabling deletes it. Notifications: AUMID app.thinkwatch.lite (installed) or app.thinkwatch.lite.portable; availability follows the registration instead of the Start menu shortcut. single: precheck() asks when the running instance is a different exe (pure verdict() decides, tested everywhere), signals a named event the running side creates in listen(), shows the waiting dialog until the plugin's mutex is released (abandoned counts), and releases and closes its own handle before Tauri starts. The running side waits for in-flight requests (up to 3 minutes), marks the exit as user-initiated, stops core and exits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ebView2 check - cleanup: `--uninstall-cleanup [--delete-data]` runs before Tauri. It restores this copy's clients (WSL too), calls winreg::release_all for the running exe, waits for this copy's twcore.exe to exit, deletes the data directory only when asked and every client came back (retrying while the core lets go), and exits 0/1. - uninstall: the in-app uninstall and the cleanup share the steps (restore_everywhere, restore_steps, drop_data, last_line). On Windows the in-app uninstall releases the HKCU entries through winreg::release_all; the portable build ends with "the whole folder can now be deleted" and keeps its in-use data\webview when deleting data. - NSIS: PREUNINSTALL hook (skipped on /UPDATE) closes the running app and runs the cleanup, with --delete-data when the checkbox is ticked. English and SimpChinese language files copied from Tauri CLI 2.11.4 with the checkbox reworded. - Clients taken over by another ThinkWatch Lite (the full backup named in the sidecar is not in this copy's backups dir) are marked as such: no restore or adopt here, restore-all skips and lists them, key sync and retargeting leave them alone, and the backend refuses to touch them (adopt.other_instance). - webview2: refuse to start without the WebView2 runtime, offering Microsoft's download page. - Settings: the uninstall section says the system uninstaller now does the same steps (installed) or that the folder can be deleted (portable). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able/c-uninstall # Conflicts: # src-tauri/Cargo.toml
Tauri's uninstall section deletes the HKCU Run value named after the product whatever it points at. There is one such entry for both the installed and the portable copy, so uninstalling the installed copy would also turn off launch at login for a portable copy. The pre-uninstall hook now remembers the value when it does not point into $INSTDIR (with or without quotes, case-insensitive) and the post-uninstall hook writes it back. StartupApproved is not touched by the template and stays as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The download table uses the ThinkWatch-Lite-<version>-<os>-<arch> names and lists the portable zip next to each Windows installer. A portable section covers data in data\, separate settings from the installed copy, one copy running at a time and how to remove it; an uninstall section covers the in-app uninstall, the system uninstaller's checkbox and the case of a standard account entering an administrator's password. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The portable copy keeps data\webview while the app is open. Compare the folder by its real path when the two spellings differ (a \\?\ prefix, a symlinked temp dir), and skip it by name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore publishing A windows-e2e job takes the x64 setup.exe and portable zip from the same run and uses them on windows-latest with Windows PowerShell 5.1: portable start and data folder, second copy switch / cancel / same exe, a folder that is not writable, silent install, and (once uninstall cleanup lands) silent uninstall plus --uninstall-cleanup. Screenshots go to e2e-shots, the app's own logs to e2e-logs. publish now needs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…names ignore case) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heck, docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dialog's UI tree Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pre-uninstall hook now keeps the cleanup's exit code. When it is not 0, or the program could not be started at all (ExecWait's error flag), an interactive uninstall shows a warning that points to the in-app Uninstall for the remaining steps, then carries on; a silent uninstall carries on without it. The sentence is a new LangString in both language files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…og buttons with the mouse TaskDialog buttons show up in UI Automation as CCPushButton panes without an Invoke pattern, so they are clicked where they are drawn and the dialog must close; TDM_CLICK_BUTTON by id stays only as a reported fallback. The cleanup of the portable copy waits for its WebView2 processes first, as a user who quit the app would. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not complete Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dialog: the MessageBoxW fallback asks Yes/No with the first button's label as the question, so "OK" no longer silently means the first choice. - build.rs: our own app manifest keeps the Common-Controls v6 dependency and declares PerMonitorV2 DPI awareness. tao sets it at runtime, which Windows requires before any UI exists; the switch dialogs run earlier. The per-thread DPI switch in dialog is gone. A Windows test builds an activation context from the manifest. - winreg: IconUri is written even when the icon file cannot be, so release_all still recognises the registration as this copy's. - portable: an unknown exe folder refuses to start instead of falling back to the installed copy's data dir; a data folder created by another Windows account gets its own message (folder writable, data\ listed but denied); THINKWATCH_HOME is set together with a THINKWATCH_PORTABLE_EXE marker and both are dropped at startup when the marker names another exe, so programs launched from a portable copy do not hand its data dir to the installed one. - single: a stale signalled handoff event is reset when recreated; an unreachable mutex or event (running instance elevated) is reported and this process exits instead of starting a second instance; when the event cannot be opened, a mutex already released means the other copy quit, so start normally; stepping aside has a 190 s overall deadline, and wait_for_quiet times out each status call after 5 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bool::then_some evaluated Self(h) eagerly, so a failed Open* call dropped a Handle(null) whose CloseHandle(NULL) replaced the last error with ERROR_INVALID_HANDLE before open_mutex read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up dev (core v0.60.0) and path C's uninstall notice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI (Windows) now makes a FAT32 and an exFAT VHD with diskpart and a local SMB share, and runs the portable tests (including the one that renames running executables) on each. TW_PORTABLE_TEST_DIR picks the folder the tests use; without it they use the system temp dir as before. Supervisor tests: macOS holds back the first run of a freshly written executable (measured up to 49 s on a busy machine; the second run of the same file takes under a millisecond). The fake cores were written and started inside the tests' 5-second windows, so under load the two tests that count starts timed out. Each fake core now runs once with `warm` before the clock starts, liveness waits use a generous shared bound, and "returns at once" is measured on a paused clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A release build with no uninstall.exe beside it was treated as the portable copy, so a developer's own `cargo build --release` wrote its data into target\release\data and pointed the HKCU link and autostart at target\. The Windows build step in release.yml now sets TW_OFFICIAL_BUILD=1, read with option_env! (build.rs reruns when it changes); windows_kind needs it for Portable, and on Windows the registry claims follow the same answer. Installed copies are unchanged: uninstall.exe beside the exe still decides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NSIS template's CheckIfAppIsRunning finds and ends processes by file name (thinkwatch-lite.exe; for a per-machine install, any user's), so silently installing or uninstalling the installed copy also closed a running portable copy. The zip now holds the same exe under the product name; twcore.exe keeps its name. The self-update expects the new name and still writes over the running exe's own path. Release notes and both READMEs name the file; winreg tests cover a file name with a space. The e2e script runs the portable copy under the new name and adds a check: with the installed copy installed and a portable copy running, a silent install and a silent uninstall leave the portable process and its gateway running and its link untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…supervisor tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new push to a branch or PR makes the run still in progress for the previous push worthless, yet it keeps holding concurrent job slots (the free plan allows 20 at a time, 5 of them macOS). main and dev keep a result for every commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e old rehearsal Branches under rehearse/windows/ skip the macOS and Linux builds and run the two Windows builds, windows-e2e and the release page text, for work that only touches Windows. Runs are grouped by ref and a newer push to the same branch cancels the older rehearsal; tag runs are never cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
About 25 test helpers made directories (and a few files) under the system temp dir with fixed names and either never removed them or removed them only at the end of a passing run. On a busy Mac that left thousands of tw-* entries behind. Each now uses a tempfile::TempDir held for the test's lifetime, with a recognisable tw-<name>- prefix. Where the directory has to outlive something else, it is declared first: the supervisor's fake cores (the supervisor runs them from there), the D-Bus notifier tasks (they write into it), and the control plane test's core (killed and waited for in Drop before the field goes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts dir When an assertion fails part-way, the notifier tasks are still running and one may be saving the table; removing the temporary directory first let that write create it again. A guard now aborts the tasks, waits for them to finish, then removes the directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r, portable exe name Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
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.
What
A portable Windows build next to the installer: one zip per architecture (
ThinkWatch-Lite-<v>-windows-<x64|arm64>-portable.zip, containingThinkWatch Lite.exeandtwcore.exe). Extract it and run; no administrator rights needed.Portable build
TW_OFFICIAL_BUILD) with nouninstall.exebeside it. A developer's own release build counts as a dev build.data\next to the exe, including the WebView2 profile, separate from the installed copy (%APPDATA%\ThinkWatch). An unwritable folder, or adata\folder owned by another Windows account, gets an accurate native dialog. ATHINKWATCH_HOMEinherited from another copy is dropped (THINKWATCH_PORTABLE_EXEmarker).twcore.exeis taken only from the exe's own folder.CI exercises the rename on NTFS, FAT32, exFAT and an SMB share.
One running instance, whoever runs owns the system hooks
thinkwatch://, the launch-at-login entry and the toast AUMID registration (HKCU) point at the running exe. They are claimed on every start, and cleanup removes only entries pointing at this exe.HKCU\Software\Classes\AppUserModelId\<AUMID>carries the display name and icon:app.thinkwatch.litefor the installed copy,.portablefor the portable one.app.manifest: PerMonitorV2 DPI awareness and Common-Controls v6.Uninstall
--uninstall-cleanup [--delete-data]headless mode, shared with the in-app uninstall. It restores the clients this copy took over (WSL included), removes HKCU entries pointing at this exe, and deletes the data dir only when asked and every client was restored.adopt.other_instance).Other
ThinkWatch-Lite-<v>-<darwin|windows|linux>-<arch>…. latest.json keeps every existing key and addswindows-<arch>-portable; installed apps keep updating as before.windows-e2ejob in release.yml, whichpublishdepends on. It runs 43 checks against the real installer and zip:rehearse/windows/builds only Windows.Verification
cargo test,pnpm typecheck,pnpm test(747),scripts/release_notes_test.py.Release coordination
Do not merge homebrew-tap
asset-namesor the website'slite-portablebranch before the release is published. The tap needs a rebase +scripts/bump.shon that branch, then a push to main.🤖 Generated with Claude Code