Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/tests/test_cua_driver_release_wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ def test_driver_tag_build_cannot_publish_before_manual_e2e_gate(self) -> None:
self.assertIn("CUA_DRIVER_LOCAL_HOME: ${{ runner.temp }}/cua-driver-local-home", linux)

windows = self.read(".github/workflows/e2e-rust-windows.yml")
self.assertIn('name: "Windows / install-local.ps1 smoke"', windows)
self.assertIn('name: "Windows / installer and update smoke"', windows)
self.assertIn("install-local.ps1 -NoAutoStart -NoPathUpdate", windows)
self.assertIn('CUA_DRIVER_LOCAL_HOME = Join-Path $env:RUNNER_TEMP', windows)

Expand Down
45 changes: 44 additions & 1 deletion .github/workflows/e2e-rust-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ jobs:

installer:
if: inputs.lane == 'all' || inputs.lane == 'installer'
name: "Windows / install-local.ps1 smoke"
name: "Windows / installer and update smoke"
needs: source
runs-on: windows-latest
timeout-minutes: 45
Expand All @@ -181,6 +181,47 @@ jobs:
- name: Verify installer runner is interactive
shell: pwsh
run: .\scripts\ci\windows\verify-user-session.ps1
- name: Run Windows installer regression contracts
shell: pwsh
run: .\libs\cua-driver\scripts\tests\install-windows-regression.ps1
- name: Verify released install and legacy migration preserve state
shell: pwsh
run: |
$savedUserProfile = $env:USERPROFILE
$savedLocalAppData = $env:LOCALAPPDATA
$testRoot = Join-Path $env:RUNNER_TEMP "cua-driver-release-installer"
$env:USERPROFILE = Join-Path $testRoot "profile"
$env:LOCALAPPDATA = Join-Path $testRoot "localappdata"
$legacyHome = Join-Path $env:USERPROFILE ".cua-driver-rs"
$modernHome = Join-Path $env:USERPROFILE ".cua-driver"
$installed = Join-Path $env:LOCALAPPDATA "Programs\Cua\cua-driver\bin\cua-driver.exe"
try {
New-Item -ItemType Directory -Force -Path $legacyHome | Out-Null
Set-Content -LiteralPath (Join-Path $legacyHome "version_check.json") -Value "{}"
Set-Content -LiteralPath (Join-Path $legacyHome ".telemetry_id") -Value "synthetic-e2e-id"

$firstOutput = (& .\libs\cua-driver\scripts\install.ps1 -NoPathUpdate *>&1 | Out-String)
$firstOutput | Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP "cua-driver-release-install.txt")
if ($firstOutput -match "detected legacy install layout") { throw "cache-only home was treated as legacy" }
if (-not (Test-Path -LiteralPath $installed)) { throw "missing installed binary: $installed" }
if (-not (Test-Path -LiteralPath (Join-Path $modernHome "packages\current"))) { throw "missing current package junction" }
& schtasks.exe /Query /TN "cua-driver-serve" | Out-Null
if ($LASTEXITCODE -ne 0) { throw "install did not register cua-driver-serve" }

New-Item -ItemType Directory -Force -Path (Join-Path $legacyHome "packages") | Out-Null
$secondOutput = (& .\libs\cua-driver\scripts\install.ps1 -NoPathUpdate *>&1 | Out-String)
$secondOutput | Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP "cua-driver-release-reinstall.txt")
if ($secondOutput -notmatch "detected legacy install layout") { throw "true legacy marker was not detected" }
if (-not (Test-Path -LiteralPath $installed)) { throw "reinstall removed the installed binary" }
& schtasks.exe /Query /TN "cua-driver-serve" | Out-Null
if ($LASTEXITCODE -ne 0) { throw "legacy migration did not restore cua-driver-serve" }
}
finally {
& schtasks.exe /End /TN "cua-driver-serve" 2>$null | Out-Null
& schtasks.exe /Delete /TN "cua-driver-serve" /F 2>$null | Out-Null
$env:USERPROFILE = $savedUserProfile
$env:LOCALAPPDATA = $savedLocalAppData
}
- name: Install into an isolated local namespace
shell: pwsh
env:
Expand Down Expand Up @@ -236,6 +277,8 @@ jobs:
${{ runner.temp }}/cua-driver-local-config.json
${{ runner.temp }}/cua-driver-local-daemon.out.log
${{ runner.temp }}/cua-driver-local-daemon.err.log
${{ runner.temp }}/cua-driver-release-install.txt
${{ runner.temp }}/cua-driver-release-reinstall.txt
if-no-files-found: ignore
retention-days: 14

Expand Down
41 changes: 37 additions & 4 deletions libs/cua-driver/scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,22 @@ Write-Step "cua-driver-rs installer (Windows)"
Write-Step " install dir : $VisibleBinDir"
Write-Step " package home: $HomeDir"

function Get-AncestorProcessIds {
# PIDs of this process and its ancestors, so cleanup never kills the caller.
# `cua-driver update --apply` runs the installer as a child of cua-driver.exe.
$ids = @()
$seen = @{}
$currentPid = $PID
while ($currentPid -and -not $seen.ContainsKey([int]$currentPid)) {
$seen[[int]$currentPid] = $true
$ids += $currentPid
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$currentPid" -ErrorAction SilentlyContinue
if (-not $proc -or -not $proc.ParentProcessId) { break }
$currentPid = $proc.ParentProcessId
}
return $ids
}

function Remove-LegacyInstall {
# Best-effort cleanup of v0.2.13-and-earlier install paths. Runs before
# any new install when default paths are in use (so users who override
Expand All @@ -1143,8 +1159,14 @@ function Remove-LegacyInstall {
if ($env:CUA_DRIVER_RS_INSTALL_DIR -or $env:CUA_DRIVER_RS_HOME) {
return
}
$hasLegacy = (Test-Path -LiteralPath $LegacyVisibleBinDir) -or `
(Test-Path -LiteralPath $LegacyHomeDir)
# A bare `~/.cua-driver-rs` is NOT evidence of a legacy install: the current
# version still writes its update-check cache and telemetry ids there
# (see crates/cua-driver/src/version_check.rs, HOME_SUBDIRECTORY). Requiring
# an actual legacy artifact keeps `update --apply` from walking into this
# branch on every single run once the update banner has been checked once.
$hasLegacyHome = (Test-Path -LiteralPath (Join-Path $LegacyHomeDir 'packages')) -or `
(Test-Path -LiteralPath (Join-Path $LegacyHomeDir 'bin'))
$hasLegacy = (Test-Path -LiteralPath $LegacyVisibleBinDir) -or $hasLegacyHome
if (-not $hasLegacy) { return }

Write-Step "detected legacy install layout (v0.2.13 or earlier); migrating to Cua\cua-driver"
Expand All @@ -1168,21 +1190,32 @@ function Remove-LegacyInstall {
# c. Stop-Process last — catches anything taskkill missed.
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
# Never kill the process tree we are running inside: when the installer is
# launched by `cua-driver update --apply`, our own parent IS a cua-driver.exe,
# and an unfiltered kill terminates the update mid-flight (the script dies
# right after the step banner above). Both cleanup passes below skip these.
$ancestorPids = @(Get-AncestorProcessIds)
try {
# Ends the running task instance. Returns non-zero when the task
# isn't running or doesn't exist, both of which we swallow.
& schtasks.exe /End /TN "cua-driver-serve" 2>$null | Out-Null
Start-Sleep -Milliseconds 250
# Force-kill via taskkill — handles High-IL processes that
# Stop-Process can't touch from a Medium-IL caller.
& taskkill.exe /F /IM "cua-driver.exe" /T 2>$null | Out-Null
& taskkill.exe /F /IM "cua-driver-uia.exe" /T 2>$null | Out-Null
$selfFilters = @()
foreach ($ancestorPid in $ancestorPids) {
$selfFilters += '/FI'
$selfFilters += "PID ne $ancestorPid"
}
Comment on lines +1205 to +1209

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 2935965. The ancestor PID list is now computed once before the try block and applied to both passes: taskkill via /FI "PID ne <pid>" filters, and the Stop-Process backstop via an explicit -contains skip.

& taskkill.exe /F /IM "cua-driver.exe" /T @selfFilters 2>$null | Out-Null
& taskkill.exe /F /IM "cua-driver-uia.exe" /T @selfFilters 2>$null | Out-Null
} finally {
$ErrorActionPreference = $prevEAP
}
$procs = Get-Process -Name "cua-driver","cua-driver-uia" -ErrorAction SilentlyContinue
if ($procs) {
foreach ($p in $procs) {
if ($ancestorPids -contains $p.Id) { continue }
try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {}
}
}
Expand Down
164 changes: 164 additions & 0 deletions libs/cua-driver/scripts/tests/install-windows-regression.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
[CmdletBinding()]
param(
[string]$InstallerPath = (Join-Path (Split-Path -Parent $PSScriptRoot) "install.ps1")
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

function Assert-True {
param(
[Parameter(Mandatory = $true)][bool]$Condition,
[Parameter(Mandatory = $true)][string]$Message
)
if (-not $Condition) { throw $Message }
}

function Import-InstallerFunction {
param(
[Parameter(Mandatory = $true)]$Ast,
[Parameter(Mandatory = $true)][string]$Name
)
$definition = $Ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -eq $Name
}, $true) | Select-Object -First 1
if (-not $definition) { throw "function $Name not found in $InstallerPath" }
$scriptDefinition = $definition.Extent.Text -replace "^function\s+$([regex]::Escape($Name))", "function script:$Name"
. ([scriptblock]::Create($scriptDefinition))
}

$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
$InstallerPath,
[ref]$tokens,
[ref]$parseErrors)
if ($parseErrors.Count -ne 0) {
throw "install.ps1 parse errors: $($parseErrors -join '; ')"
}

Import-InstallerFunction -Ast $ast -Name "Get-AncestorProcessIds"
Import-InstallerFunction -Ast $ast -Name "Remove-LegacyInstall"

$script:Steps = @()
$script:TaskkillCalls = @()
$script:ScheduledTaskCalls = @()
$script:StoppedProcessIds = @()
$script:Processes = @()
$script:ParentByPid = @{}

function Write-Step { param($Message); $script:Steps += [string]$Message }
function Start-Sleep { [CmdletBinding()] param([int]$Milliseconds, [int]$Seconds) }
function taskkill.exe { $script:TaskkillCalls += ,@($args); $global:LASTEXITCODE = 0 }
function schtasks.exe { $script:ScheduledTaskCalls += ,@($args); $global:LASTEXITCODE = 0 }
function Get-CimInstance {
[CmdletBinding()]
param([string]$ClassName, [string]$Filter)
$processId = [int]($Filter -replace '^ProcessId=', '')
if (-not $script:ParentByPid.ContainsKey($processId)) { return $null }
[pscustomobject]@{
ProcessId = $processId
ParentProcessId = $script:ParentByPid[$processId]
}
}
function Get-Process {
[CmdletBinding()]
param([string[]]$Name)
return $script:Processes
}
function Stop-Process {
[CmdletBinding()]
param([int]$Id, [switch]$Force)
$script:StoppedProcessIds += $Id
}

$savedInstallDir = $env:CUA_DRIVER_RS_INSTALL_DIR
$savedHome = $env:CUA_DRIVER_RS_HOME
$env:CUA_DRIVER_RS_INSTALL_DIR = $null
$env:CUA_DRIVER_RS_HOME = $null

try {
$root = Join-Path ([System.IO.Path]::GetTempPath()) ("cua-driver-installer-regression-" + [guid]::NewGuid().ToString("N"))
$HomeDir = Join-Path $root "modern-home"
$LegacyHomeDir = Join-Path $root "profile\.cua-driver-rs"
$LegacyVendorDir = Join-Path $root "localappdata\Programs\trycua"
$LegacyVisibleBinDir = Join-Path $LegacyVendorDir "cua-driver-rs\bin"

# Current releases write cache and telemetry state here. Neither file is a
# legacy installation marker, and migration must leave both untouched.
New-Item -ItemType Directory -Force -Path $LegacyHomeDir | Out-Null
Set-Content -LiteralPath (Join-Path $LegacyHomeDir "version_check.json") -Value "{}"
Set-Content -LiteralPath (Join-Path $LegacyHomeDir ".telemetry_id") -Value "synthetic-test-id"
Remove-LegacyInstall
Assert-True ($script:Steps.Count -eq 0) "cache/telemetry-only home was detected as legacy"
Assert-True ($script:TaskkillCalls.Count -eq 0) "cache/telemetry-only home triggered process cleanup"
Assert-True (Test-Path -LiteralPath $LegacyHomeDir) "cache/telemetry-only home was removed"

# Every supported on-disk legacy marker independently enters migration.
foreach ($marker in @("packages", "bin", "visible-bin")) {
$scenarioRoot = Join-Path $root $marker
$HomeDir = Join-Path $scenarioRoot "modern-home"
$LegacyHomeDir = Join-Path $scenarioRoot "profile\.cua-driver-rs"
$LegacyVendorDir = Join-Path $scenarioRoot "localappdata\Programs\trycua"
$LegacyVisibleBinDir = Join-Path $LegacyVendorDir "cua-driver-rs\bin"
if ($marker -eq "visible-bin") {
New-Item -ItemType Directory -Force -Path $LegacyVisibleBinDir | Out-Null
} else {
New-Item -ItemType Directory -Force -Path (Join-Path $LegacyHomeDir $marker) | Out-Null
}
$script:Steps = @()
$script:TaskkillCalls = @()
$script:ScheduledTaskCalls = @()
$script:Processes = @()
$script:StoppedProcessIds = @()
$script:ParentByPid = @{}
$script:ParentByPid[[int]$PID] = 0
Remove-LegacyInstall
Assert-True ($script:Steps -contains "detected legacy install layout (v0.2.13 or earlier); migrating to Cua\cua-driver") `
"$marker was not detected as a legacy marker"
}

# Model update --apply: PowerShell is a child of cua-driver.exe, which in
# turn has another ancestor. Both must be excluded from taskkill and the
# Stop-Process backstop, while an unrelated daemon must still be stopped.
$HomeDir = Join-Path $root "process-tree\modern-home"
$LegacyHomeDir = Join-Path $root "process-tree\profile\.cua-driver-rs"
$LegacyVendorDir = Join-Path $root "process-tree\localappdata\Programs\trycua"
$LegacyVisibleBinDir = Join-Path $LegacyVendorDir "cua-driver-rs\bin"
New-Item -ItemType Directory -Force -Path (Join-Path $LegacyHomeDir "packages") | Out-Null
$script:Steps = @()
$script:TaskkillCalls = @()
$script:ScheduledTaskCalls = @()
$script:StoppedProcessIds = @()
$script:ParentByPid = @{}
$script:ParentByPid[[int]$PID] = 4100
$script:ParentByPid[4100] = 4200
$script:ParentByPid[4200] = 0
$script:Processes = @(
[pscustomobject]@{ Id = 4100; ProcessName = "cua-driver" },
[pscustomobject]@{ Id = 4300; ProcessName = "cua-driver" }
)

$ancestors = @(Get-AncestorProcessIds)
Assert-True (($ancestors -join ',') -eq "$PID,4100,4200") "ancestor discovery did not traverse the complete synthetic chain"
Remove-LegacyInstall

Assert-True ($script:TaskkillCalls.Count -eq 2) "expected taskkill calls for driver and UIA processes"
foreach ($call in $script:TaskkillCalls) {
$commandLine = $call -join ' '
foreach ($protectedPid in @($PID, 4100, 4200)) {
Assert-True ($commandLine -match "PID ne $protectedPid(?: |$)") `
"taskkill did not exclude protected ancestor PID $protectedPid"
}
}
Assert-True (-not ($script:StoppedProcessIds -contains 4100)) "Stop-Process targeted the cua-driver launcher"
Assert-True ($script:StoppedProcessIds -contains 4300) "unrelated cua-driver daemon was not stopped"

Write-Host "Windows installer legacy/update regression checks passed."
}
finally {
$env:CUA_DRIVER_RS_INSTALL_DIR = $savedInstallDir
$env:CUA_DRIVER_RS_HOME = $savedHome
}
Loading