Skip to content

Refuse Windows uninstall while GraphCode runs and make removal all-or-nothing - #662

Merged
coneilen merged 1 commit into
mainfrom
coneilen-fix-uninstall-with-live-sessions
Oct 8, 2026
Merged

coneilen merged 1 commit into
mainfrom
coneilen-fix-uninstall-with-live-sessions

Conversation

@coneilen

@coneilen coneilen commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Dev Box qualification of 0.1.78-windows.beta17 (D10 Lifecycle) ran GraphCode-Setup.ps1 -Command Uninstall while 13 loop zmx.exe session hosts were running from the installed bin. Uninstall removed the scheduled task, Start-menu shortcut and user PATH entry, then failed (exit 1) at Remove-Item $InstallRoot -Recurse -Force with Access to the path '...\current\bin\zmx.exe' is denied, leaving a partial installation of 13 files. This PR makes Windows uninstall all-or-nothing: it refuses up front, without changing anything, while GraphCode processes run from the install root, and it rolls back fully if removal fails after that point.

Changes

  • PackageRuntime.ps1 Uninstall-Package (also embedded verbatim in the generated GraphCode-Setup.ps1):
    • Up-front refusal. Before touching the daemon, task, PATH or shortcut, it lists every process whose executable is under the install root. Session hosts (zmx.exe), the shell and the CLI are the user's live work, so Uninstall does not kill them. It exits 1 with Uninstall changed nothing, names each process and PID, and says how to end the sessions (zmx.exe ls, then zmx.exe kill NAME) and to rerun from a terminal outside GraphCode. The managed daemon is still stopped automatically. With -NoScheduledTask, a running graphcoded.exe also blocks.
    • Transactional removal. After stopping the daemon it re-checks processes, confirms each installed file can be opened exclusively (this catches DLLs or files held by processes outside the root), then renames the whole root to a sibling .GraphCode-uninstall-<guid> in one step, and only then removes the task, PATH and shortcut. Any failure up to that point moves the root back, restores PATH and the shortcut (snapshot in %TEMP%), and restarts the daemon if it was running. The renamed tree is deleted last. If a file is opened in that short window, Uninstall still succeeds and warns with the leftover path.
    • I verified on Windows that renaming a directory works while an .exe inside it is running, so the rename alone can't guard against live session hosts. That's why the process check is there. An exclusive ReadWrite open does fail for running images and open handles.
    • User-data semantics are unchanged: preserved by default, removed only with -RemoveUserData.
  • New fixture contract Tools/windows/Tests/Packaging.Uninstall.Tests.ps1 (4 cases), registered in validate.ps1 -Task packaging contracts and guarded in ValidationRunner.Tests.ps1:
    • live-session: a real process launched from the fixture's installed bin\zmx.exe (a copy of PING.EXE) is running. Uninstall must refuse, name zmx.exe and its PID, leave the install tree, task, PATH, shortcut and daemon untouched, and leave the session alive.
    • locked-file: bin\_FoundationICU.dll is held open by another process. Uninstall must refuse, report the file, leave everything intact and restart the daemon.
    • task-removal-failure: failure is injected after the rename. The install must be restored byte-for-byte and the daemon restarted.
    • success: everything is removed, no transaction debris is left, and user data is byte-identical.
    • The scheduler and registry PATH are modelled. Files, shortcuts (fixture APPDATA) and processes are real.
  • PACKAGING.md documents the behavior. validation-matrix.md lists the new contract.

Test plan

RED: git stash push -- Tools/windows/PackageRuntime.ps1; pwsh -NoProfile -File Tools\windows\Tests\Packaging.Uninstall.Tests.ps1 -> exit 1, live-session: left a partial installation: Access to the path '...\live-session\GraphCode\current\bin\zmx.exe' is denied. locked-file: left a partial installation: The process cannot access the file '...\bin_FoundationICU.dll' because it is being used by another process. task-removal-failure: left the daemon stopped after refusing
GREEN: pwsh -NoProfile -File Tools\windows\Tests\Packaging.Uninstall.Tests.ps1 -> Uninstall live-session refusal and all-or-nothing removal contracts (4 cases): PASS, exit 0 (also PASS under powershell.exe 5.1)
REGRESSION: pwsh -NoProfile -File Tools\windows\validate.ps1 -Task packaging -PackagingPart contracts -> exit 0, all contracts PASS incl. rollback, scheduler, signing and standalone setup executed=18; pwsh -NoProfile -File Tools\windows\Tests\ValidationRunner.Tests.ps1 -> ValidationRunner.Tests.ps1: PASS

What these runs did not cover:

  • I did not run the real-product packaging part (-PackagingPart real: Packaging.Tests.ps1 plus Packaging.RealLifecycle.Tests.ps1, which covers real scheduled-task self-uninstall) locally. It rebuilds the shared provider trees. This PR's Windows packaging CI job covers it.
  • The new contract models the scheduler and user PATH. It does not drive the real installed GraphCode-Setup.ps1 with real loop zmx sessions.
  • CI does not replace the Dev Box D10 rerun. The qualification owner still needs to repeat D10 with live loop sessions on a candidate that includes this fix. Expected result: Uninstall exits 1 with the refusal message and changes nothing. After the sessions are ended, Uninstall succeeds, user data is byte-identical, and reinstall works.

Checklist

  • I have read the Contributing Guidelines
  • I have signed off my commits (git commit -s) per the DCO
  • Tests pass locally (make test): macOS only, not applicable to this Windows packaging change and not run
  • Code follows the existing style (make check): macOS Swift lint, not run, and no Swift changed
  • I added the test/contract before the implementation and observed the intended RED failure: the contract was written after a first draft of the fix, then RED was observed by running it against the unfixed PackageRuntime.ps1 from HEAD (stashed), as recorded above

…-nothing

Uninstall removed the scheduled task, Start-menu shortcut and PATH entry and
then failed deleting the install root because live zmx session hosts held
bin\zmx.exe, leaving a partial installation (Dev Box D10, 0.1.78-windows.beta17).

Uninstall now refuses up front, changing nothing, while any process other than
the managed daemon runs from the install root, naming each process and PID and
how to end the sessions. After stopping the daemon it re-checks, probes every
installed file for exclusive access, and renames the whole root aside in one
step before removing integration; any failure moves it back, restores PATH and
the shortcut, and restarts the daemon. User data preservation is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
@coneilen
coneilen merged commit 4c94c22 into main Oct 8, 2026
24 checks passed
coneilen added a commit that referenced this pull request Oct 9, 2026
Record the 0.1.78-windows.beta18 candidate at 880e6a3 (#662-#665), its
local validation, package, custody and Dev Box handoff identities, and the
57/41 parity count. Move beta17 to history.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Colin Neilens <coneilen@microsoft.com>
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