Skip to content

Installer: /CURRENTUSER user-mode install with 0-UAC upgrades#2030

Draft
tyrielv wants to merge 4 commits into
microsoft:masterfrom
tyrielv:tyrielv/user-mode-install-v2
Draft

Installer: /CURRENTUSER user-mode install with 0-UAC upgrades#2030
tyrielv wants to merge 4 commits into
microsoft:masterfrom
tyrielv:tyrielv/user-mode-install-v2

Conversation

@tyrielv

@tyrielv tyrielv commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds /CURRENTUSER\ flag for user-mode installation that eliminates UAC prompts for routine upgrades.

UAC budget:

  • Fresh install (new machine): 1 UAC — registers ProjFS boot task
  • Upgrade (admin task unchanged): 0 UAC
  • Upgrade (admin task hash changed): 1 UAC — re-registers boot task
  • Mount/unmount/clone: 0 UAC (always)

How it works:

  1. Drift detection: Checks PrjFlt driver registration (registry) + boot task hash marker — both work non-elevated
  2. If admin setup current: Skip elevation entirely → 0 UAC
  3. If admin setup needed: Self-elevates via /ADMINSTAGE=true\ for minimal admin work only
  4. User-mode deployment: %LocalAppData%\VFSForGit\Versions\{ver}\\ + \Current\ junction + HKCU PATH

Stacked on:

Review findings addressed:

  • \Get-WindowsOptionalFeature\ requires elevation → Fixed: uses PrjFlt registry check instead
  • \IsUserModeInstall\ uninitialized during uninstall → Fixed: set in \InitializeUninstall\
  • \StopService\ called non-elevated crashes user-mode → Fixed: gated behind
    ot IsUserModeInstall\

Part of Plan 002 — UAC-Free Install Modernization (Phase 3)

@tyrielv tyrielv force-pushed the tyrielv/user-mode-install-v2 branch from b4a6320 to 27040b8 Compare June 17, 2026 23:50
Replaces flat {app}\ deployment with versioned Versions\<ver>\ + Current junction.
Eliminates PendingUpgrade staging flow — new version goes to new folder, junction
swaps atomically, mounts continue running from old version until unmounted.

Changes:
- [Files]: Deploy to {app}\Versions\{version}\ instead of {app}\
- [Registry]: PATH points to {app}\Current (junction) instead of {app}\
- [Dirs]: ProgramData under versioned folder
- [Code]: Add CreateOrUpdateCurrentJunction() — creates/updates junction post-install
- [Code]: Add GarbageCollectOldVersions() — keeps 1 most recent old version, deletes older
  (skips versions with running mounts detected via Get-Process gvfs.mount | .Path)
- [Code]: Remove KeepMountsRunning variable, IsNormalInstall/IsStagingInstall checks
- [Code]: Remove StagingUpdateService, ShowMountChoiceDialog (no longer needed)
- [Code]: Simplify PrepareToInstall — no mount detection, no staging, just stop service
- [Code]: Update InstallGVFSService to reference {app}\Current\GVFS.Service.exe
- [Code]: Update MountRepos, MigrateConfigAndStatusCacheFiles, WriteOnDiskVersion16CapableFile
  to use Current junction paths
- CurStepChanged: Remove staging logic, call CreateOrUpdateCurrentJunction + GarbageCollectOldVersions
- CurUninstallStepChanged: Remove {app}\Current from PATH instead of {app}

Flat-layout migration stub added (detects {app}\GVFS.exe, logs version) but defers
actual file move to future PR to reduce complexity.

Assisted-by: Claude Sonnet 4.5
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
tyrielv added 3 commits June 18, 2026 11:03
Fixes 5 critical issues in the Phase 2 versioned-layout installer:

1. Forward reference violation - Move CreateOrUpdateCurrentJunction,
   GetFileVersion, IsProcessRunningFromPath, and GarbageCollectOldVersions
   before CurStepChanged. Inno Setup Pascal does not allow forward refs.

2. Service starts before junction exists - Create Current junction in
   CurStepChanged(ssInstall) BEFORE file extraction, ensuring the junction
   exists when InstallGVFSService runs (AfterInstall callback during file
   copy). Service binPath references {app}\Current\GVFS.Service.exe.

3. Old PATH entry not cleaned up - On upgrade from flat to versioned layout,
   remove legacy {app} PATH entry in CurStepChanged(ssPostInstall). New
   entry points to {app}\Current.

4. Non-atomic junction swap - Use Current.new temporary: create junction at
   Current.new, rmdir old Current (check ResultCode), rename Current.new to
   Current. Eliminates window where Current doesn't exist.

5. Add upgrade test scenarios - Add versioned-fresh-install,
   versioned-upgrade, and flat-to-versioned-upgrade scenarios to
   .github/workflows/upgrade-tests.yaml with full test implementations.

Assisted-by: Claude Sonnet 4.5
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Replace the GVFS.Service Windows service with a simpler architecture:

Infrastructure:
- LocalRepoRegistry: file-based repo tracking, wire-compatible with old
  service format. SYSTEM uses ProgramData; per-user uses platform default.
  Seed-on-first-use copies accessible entries from system registry.
- LogonTaskRegistration: machine-wide \GVFS\AutoMount task fires for all
  interactive users (GroupId S-1-5-4) at logon, runs gvfs service --mount-all.
  Each user's repos loaded from their own LocalRepoRegistry.
- ProjFS boot task: enable-projfs-on-all-drives.ps1 enables ProjFS and
  attaches PrjFlt on all volumes. Embedded in task XML via build-task-xml.ps1
  with SHA-256 hash marker for drift detection.
- CLI verb fallbacks: mount/unmount/service verbs fall back to LocalRepoRegistry
  when the service named pipe is unavailable.
- GVFSVerb: silent-success fallback for PrjFlt FilterAttach.
- InProcessMount: restore exception safety net in HandleRequest.

Installer:
- Stop and delete GVFS.Service on upgrade from older versions.
- Register \GVFS\AutoMount logon task.
- Remove service deployment, install, start from [Files]/[Run].
- Remove PendingUpgrade staging logic and ShowMountChoiceDialog.
- Exclude GVFS.Service.exe from payload (layout.bat).

Functional tests:
- Remove service install/uninstall (no service to test against).
- Settings.cs auto-detects user-mode gvfs at %LocalAppData%\VFSForGit\Current.

926 unit tests pass.

Assisted-by: Claude Sonnet 4.5
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Add user-mode install support via Inno Setup's /CURRENTUSER flag:

Admin-layer drift detection (0 UAC when current):
- IsProjFSEnabled(): checks Client-ProjFS Windows optional feature
- IsEnableProjFSTaskCurrent(): queries registered task XML for hash marker
- NeedsAdminSetup(): combines both checks
- Self-elevates via /ADMINSTAGE=true only when drift detected

/CURRENTUSER mode (non-elevated):
- Deploys payload to %LocalAppData%\VFSForGit\Versions\{ver}\
- Creates Current junction + user PATH (HKCU)
- Sets GVFS_COMMON_APPDATA_ROOT and GVFS_SECURE_DATA_ROOT env vars
- Registers per-user \GVFS\AutoMount logon task

/ADMINSTAGE mode (elevated, minimal):
- Enables ProjFS feature
- Registers EnableProjFSOnAllDrives boot task from embedded XML
- Exits without deploying payload (caller handles deployment)

Build integration:
- BuildProjFSTaskXml MSBuild target generates task XML with hash marker
- Passes /DProjFSTaskXml to ISCC

System-mode (elevated) unchanged: deploys to Program Files with versioned
layout, same as Phase 2. PrivilegesRequiredOverridesAllowed=commandline
enables the /CURRENTUSER flag without changing default behavior.

Assisted-by: Claude Sonnet 4.5
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
@tyrielv tyrielv force-pushed the tyrielv/user-mode-install-v2 branch from 0a091af to b727e4a Compare June 18, 2026 19:00
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