Skip to content

Commit 84b915c

Browse files
fix(Windows): harden driver uninstall flow
Improve Windows driver uninstallation when a consumer still has the control device open. The uninstall script now logs to a shared transcript, tolerates initial device-removal failures so forced package removal can proceed, and the MSI/CI flow captures those diagnostics. This also moves transcript helpers into the shared PowerShell common module and documents the new uninstall behavior and log path.
1 parent f957126 commit 84b915c

6 files changed

Lines changed: 186 additions & 102 deletions

File tree

.github/workflows/ci-build.yml

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ jobs:
496496
--xml-pretty \
497497
-o reports/coverage.xml
498498
499-
- name: Uninstall Windows driver installer
499+
- name: Uninstall Windows driver installer with an open consumer handle
500500
if: >-
501501
always() &&
502502
runner.os == 'Windows'
@@ -507,20 +507,67 @@ jobs:
507507
-ErrorAction SilentlyContinue |
508508
Select-Object -First 1
509509
if ($installer) {
510-
$logPath = Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-uninstall.log"
511-
$process = Start-Process `
512-
-FilePath msiexec.exe `
513-
-ArgumentList @("/x", $installer.FullName, "/qn", "/norestart", "/L*v", $logPath) `
514-
-PassThru `
515-
-NoNewWindow
516-
if (!$process.WaitForExit([int] [TimeSpan]::FromMinutes(5).TotalMilliseconds)) {
517-
Get-Content -LiteralPath $logPath -Tail 200 -ErrorAction SilentlyContinue
518-
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
519-
throw "Windows driver installer uninstall timed out after 5 minutes."
510+
$nativeMethods = @'
511+
using System;
512+
using System.Runtime.InteropServices;
513+
514+
public static class LibVirtualHidUninstallTest
515+
{
516+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
517+
public static extern IntPtr CreateFile(
518+
string fileName,
519+
uint desiredAccess,
520+
uint shareMode,
521+
IntPtr securityAttributes,
522+
uint creationDisposition,
523+
uint flagsAndAttributes,
524+
IntPtr templateFile);
525+
526+
[DllImport("kernel32.dll", SetLastError = true)]
527+
[return: MarshalAs(UnmanagedType.Bool)]
528+
public static extern bool CloseHandle(IntPtr handle);
529+
}
530+
'@
531+
Add-Type -TypeDefinition $nativeMethods
532+
$invalidHandle = [IntPtr]::new(-1)
533+
$deviceHandle = [LibVirtualHidUninstallTest]::CreateFile(
534+
"\\.\LibVirtualHid",
535+
0xC0000000,
536+
0x00000003,
537+
[IntPtr]::Zero,
538+
3,
539+
0x40000080,
540+
[IntPtr]::Zero
541+
)
542+
if ($deviceHandle -eq $invalidHandle) {
543+
Write-Warning "Unable to hold the libvirtualhid control device; continuing cleanup."
544+
} else {
545+
Write-Host "Holding the libvirtualhid control device open during MSI uninstall."
520546
}
521-
if ($process.ExitCode -notin @(0, 3010)) {
522-
Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue
523-
throw "Windows driver installer uninstall exited with code $($process.ExitCode)."
547+
548+
$logPath = Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-uninstall.log"
549+
$driverLogPath = Join-Path $env:ProgramData "libvirtualhid\uninstall-driver.log"
550+
try {
551+
$process = Start-Process `
552+
-FilePath msiexec.exe `
553+
-ArgumentList @("/x", $installer.FullName, "/qn", "/norestart", "/L*v", $logPath) `
554+
-PassThru `
555+
-NoNewWindow
556+
if (!$process.WaitForExit([int] [TimeSpan]::FromMinutes(5).TotalMilliseconds)) {
557+
Get-Content -LiteralPath $logPath -Tail 200 -ErrorAction SilentlyContinue
558+
Get-Content -LiteralPath $driverLogPath -Tail 200 -ErrorAction SilentlyContinue
559+
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
560+
throw "Windows driver installer uninstall timed out after 5 minutes."
561+
}
562+
if ($process.ExitCode -notin @(0, 3010)) {
563+
Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue
564+
Get-Content -LiteralPath $driverLogPath -ErrorAction SilentlyContinue
565+
throw "Windows driver installer uninstall exited with code $($process.ExitCode)."
566+
}
567+
} finally {
568+
if ($deviceHandle -ne $invalidHandle) {
569+
[void] [LibVirtualHidUninstallTest]::CloseHandle($deviceHandle)
570+
}
524571
}
525572
}
526573

cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
Impersonate="no" />
1515
<CustomAction Id="CA_LibVirtualHidUninstallDriver"
1616
Directory="INSTALL_ROOT"
17-
ExeCommand="&quot;[WindowsFolder]System32\WindowsPowerShell\v1.0\powershell.exe&quot; -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -File &quot;[INSTALL_ROOT]scripts\windows\uninstall-driver.ps1&quot; -Force -RemoveCertificateSubject &quot;CN=libvirtualhid CI Test Driver Signing&quot;"
17+
ExeCommand="&quot;[WindowsFolder]System32\WindowsPowerShell\v1.0\powershell.exe&quot; -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -File &quot;[INSTALL_ROOT]scripts\windows\uninstall-driver.ps1&quot; -Force -RemoveCertificateSubject &quot;CN=libvirtualhid CI Test Driver Signing&quot; -LogPath &quot;[CommonAppDataFolder]libvirtualhid\uninstall-driver.log&quot;"
1818
Execute="deferred"
1919
Return="check"
2020
Impersonate="no" />
2121
<CustomAction Id="CA_LibVirtualHidUninstallDriverSilent"
2222
Directory="INSTALL_ROOT"
23-
ExeCommand="&quot;[WindowsFolder]System32\WindowsPowerShell\v1.0\powershell.exe&quot; -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -File &quot;[INSTALL_ROOT]scripts\windows\uninstall-driver.ps1&quot; -Force -RemoveCertificateSubject &quot;CN=libvirtualhid CI Test Driver Signing&quot;"
23+
ExeCommand="&quot;[WindowsFolder]System32\WindowsPowerShell\v1.0\powershell.exe&quot; -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -File &quot;[INSTALL_ROOT]scripts\windows\uninstall-driver.ps1&quot; -Force -RemoveCertificateSubject &quot;CN=libvirtualhid CI Test Driver Signing&quot; -LogPath &quot;[CommonAppDataFolder]libvirtualhid\uninstall-driver.log&quot;"
2424
Execute="deferred"
2525
Return="check"
2626
Impersonate="no" />

docs/windows-driver.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-browser-gamepad.
147147
-GamepadAdapterPath .\cmake-build-windows-driver\examples\Release\gamepad_adapter.exe `
148148
-GamepadProfile xseries
149149
powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 `
150-
-Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing"
150+
-Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" `
151+
-LogPath .\cmake-build-windows-driver\uninstall-driver.log
151152
```
152153

153154
The WiX installer also places validation files under the default install root,
@@ -174,10 +175,14 @@ The install helper also clears any legacy broker service `Environment` value so
174175
licensing configuration cannot be overridden on the user's machine. The
175176
uninstall helper stops and deletes that service before removing the driver
176177
package. It discovers staged OEM INF names through language-neutral DISM and
177-
CIM objects instead of parsing localized `pnputil` labels. Uninstall fails if a
178-
command fails or if the broker service, root device, or staged driver package
179-
is still present after cleanup, so the MSI cannot silently report a complete
180-
removal while driver state remains.
178+
CIM objects instead of parsing localized `pnputil` labels. If an application
179+
has an outstanding device handle, the helper records the initial device-removal
180+
failure and continues with the forced driver-package uninstall, which can finish
181+
or schedule the removal. Uninstall still fails if package removal fails or if
182+
the broker service, root device, or staged driver package remains after cleanup,
183+
so the MSI cannot silently report a complete removal while driver state remains.
184+
MSI uninstall diagnostics are appended to
185+
`C:\ProgramData\libvirtualhid\uninstall-driver.log`.
181186

182187
The installed-driver test fails if the root device is not started, if
183188
`\\.\LibVirtualHid` cannot be opened, or if a held `gamepad_adapter` instance

scripts/windows/install-driver.ps1

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,6 @@ $script:LibVirtualHidBrokerServiceName = "libvirtualhid_broker"
2424
$script:LibVirtualHidBrokerServiceDisplayName = "libvirtualhid Broker"
2525
. (Join-Path $PSScriptRoot "libvirtualhid-driver-common.ps1")
2626

27-
function Start-LibVirtualHidTranscript {
28-
[CmdletBinding(SupportsShouldProcess)]
29-
param([string] $Path)
30-
31-
if (-not $Path) {
32-
return
33-
}
34-
35-
try {
36-
$logDirectory = Split-Path -Parent $Path
37-
if ($logDirectory) {
38-
New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null
39-
}
40-
if ($PSCmdlet.ShouldProcess($Path, "Start libvirtualhid install transcript")) {
41-
Start-Transcript -Path $Path -Append | Out-Null
42-
$script:LibVirtualHidTranscriptStarted = $true
43-
}
44-
} catch {
45-
Write-Warning "Unable to start libvirtualhid install transcript: $($_.Exception.Message)"
46-
}
47-
}
48-
49-
function Stop-LibVirtualHidTranscript {
50-
[CmdletBinding(SupportsShouldProcess)]
51-
param()
52-
53-
if (-not $script:LibVirtualHidTranscriptStarted) {
54-
return
55-
}
56-
57-
try {
58-
if ($PSCmdlet.ShouldProcess("libvirtualhid install transcript", "Stop transcript")) {
59-
Stop-Transcript | Out-Null
60-
}
61-
} catch {
62-
Write-Warning "Unable to stop libvirtualhid install transcript: $($_.Exception.Message)"
63-
}
64-
}
65-
6627
function Invoke-CheckedCommand {
6728
param(
6829
[Parameter(Mandatory = $true)]

scripts/windows/libvirtualhid-driver-common.ps1

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
1+
function Start-LibVirtualHidTranscript {
2+
[CmdletBinding(SupportsShouldProcess)]
3+
param([string] $Path)
4+
5+
if (-not $Path) {
6+
return
7+
}
8+
9+
try {
10+
$logDirectory = Split-Path -Parent $Path
11+
if ($logDirectory) {
12+
New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null
13+
}
14+
if ($PSCmdlet.ShouldProcess($Path, "Start libvirtualhid driver transcript")) {
15+
Start-Transcript -Path $Path -Append | Out-Null
16+
$script:LibVirtualHidTranscriptStarted = $true
17+
}
18+
} catch {
19+
Write-Warning "Unable to start libvirtualhid driver transcript: $($_.Exception.Message)"
20+
}
21+
}
22+
23+
function Stop-LibVirtualHidTranscript {
24+
[CmdletBinding(SupportsShouldProcess)]
25+
param()
26+
27+
if (-not $script:LibVirtualHidTranscriptStarted) {
28+
return
29+
}
30+
31+
try {
32+
if ($PSCmdlet.ShouldProcess("libvirtualhid driver transcript", "Stop transcript")) {
33+
Stop-Transcript | Out-Null
34+
}
35+
} catch {
36+
Write-Warning "Unable to stop libvirtualhid driver transcript: $($_.Exception.Message)"
37+
}
38+
}
39+
140
function Get-LibVirtualHidRootDeviceInstanceId {
241
param([string] $TargetHardwareId)
342

scripts/windows/uninstall-driver.ps1

Lines changed: 74 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ param(
1414

1515
[string] $RemoveCertificateSubject,
1616

17+
[string] $LogPath,
18+
1719
[switch] $Force
1820
)
1921

2022
$ErrorActionPreference = "Stop"
23+
$script:LibVirtualHidTranscriptStarted = $false
2124
. (Join-Path $PSScriptRoot "libvirtualhid-driver-common.ps1")
2225

2326
function Invoke-CheckedCommand {
@@ -125,6 +128,31 @@ function Assert-PublishedName {
125128
}
126129
}
127130

131+
function Remove-LibVirtualHidDeviceInstance {
132+
[CmdletBinding(SupportsShouldProcess)]
133+
param(
134+
[Parameter(Mandatory = $true)]
135+
[string] $InstanceId
136+
)
137+
138+
if (-not $PSCmdlet.ShouldProcess($InstanceId, "Remove libvirtualhid development device with pnputil")) {
139+
return
140+
}
141+
142+
$output = @(& pnputil.exe /remove-device $InstanceId 2>&1)
143+
$exitCode = $LASTEXITCODE
144+
foreach ($line in $output) {
145+
Write-Information ([string] $line) -InformationAction Continue
146+
}
147+
148+
if ($exitCode -ne 0) {
149+
Write-Warning (
150+
"pnputil.exe /remove-device $InstanceId exited with code $exitCode. " +
151+
"Continuing with forced driver-package removal; final state verification will still fail if cleanup is incomplete."
152+
)
153+
}
154+
}
155+
128156
function Assert-LibVirtualHidRemoved {
129157
param(
130158
[string] $TargetOriginalName,
@@ -173,56 +201,60 @@ function Remove-DriverCertificate {
173201
}
174202
}
175203

176-
$publishedNames = @()
177-
if ($PublishedName) {
178-
Assert-PublishedName -Name $PublishedName
179-
$publishedNames += $PublishedName
180-
} else {
181-
try {
182-
$publishedNames = @(
183-
Find-PublishedName `
184-
-TargetOriginalName $OriginalName `
185-
-TargetHardwareId $HardwareId
186-
)
187-
} catch {
188-
throw "Unable to discover the staged libvirtualhid driver package through Windows APIs: $($_.Exception.Message)"
204+
Start-LibVirtualHidTranscript -Path $LogPath
205+
206+
try {
207+
$publishedNames = @()
208+
if ($PublishedName) {
209+
Assert-PublishedName -Name $PublishedName
210+
$publishedNames += $PublishedName
211+
} else {
212+
try {
213+
$publishedNames = @(
214+
Find-PublishedName `
215+
-TargetOriginalName $OriginalName `
216+
-TargetHardwareId $HardwareId
217+
)
218+
} catch {
219+
throw "Unable to discover the staged libvirtualhid driver package through Windows APIs: $($_.Exception.Message)"
220+
}
189221
}
190-
}
191222

192-
Remove-LibVirtualHidBrokerService -Name $BrokerServiceName
223+
Remove-LibVirtualHidBrokerService -Name $BrokerServiceName
193224

194-
$deviceInstanceIds = @(
195-
Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId
196-
Get-LibVirtualHidRegistryRootDevice -TargetHardwareId $HardwareId |
197-
Select-Object -ExpandProperty InstanceId
198-
) | Select-Object -Unique
225+
$deviceInstanceIds = @(
226+
Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId
227+
Get-LibVirtualHidRegistryRootDevice -TargetHardwareId $HardwareId |
228+
Select-Object -ExpandProperty InstanceId
229+
) | Select-Object -Unique
199230

200-
foreach ($instanceId in $deviceInstanceIds) {
201-
if ($PSCmdlet.ShouldProcess($instanceId, "Remove libvirtualhid development device with pnputil")) {
202-
Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/remove-device", $instanceId)
231+
foreach ($instanceId in $deviceInstanceIds) {
232+
Remove-LibVirtualHidDeviceInstance -InstanceId $instanceId
203233
}
204-
}
205234

206-
if ($publishedNames.Count -eq 0) {
207-
Write-Warning "No staged libvirtualhid driver package matching $OriginalName was found."
208-
} else {
209-
foreach ($driverPackage in $publishedNames) {
210-
Assert-PublishedName -Name $driverPackage
211-
$deleteArgs = @("/delete-driver", $driverPackage, "/uninstall")
212-
if ($Force) {
213-
$deleteArgs += "/force"
214-
}
235+
if ($publishedNames.Count -eq 0) {
236+
Write-Warning "No staged libvirtualhid driver package matching $OriginalName was found."
237+
} else {
238+
foreach ($driverPackage in $publishedNames) {
239+
Assert-PublishedName -Name $driverPackage
240+
$deleteArgs = @("/delete-driver", $driverPackage, "/uninstall")
241+
if ($Force) {
242+
$deleteArgs += "/force"
243+
}
215244

216-
if ($PSCmdlet.ShouldProcess($driverPackage, "Delete libvirtualhid driver package")) {
217-
Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments $deleteArgs
245+
if ($PSCmdlet.ShouldProcess($driverPackage, "Delete libvirtualhid driver package")) {
246+
Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments $deleteArgs
247+
}
218248
}
219249
}
220-
}
221250

222-
if (-not $WhatIfPreference) {
223-
Assert-LibVirtualHidRemoved `
224-
-TargetOriginalName $OriginalName `
225-
-TargetHardwareId $HardwareId `
226-
-ServiceName $BrokerServiceName
251+
if (-not $WhatIfPreference) {
252+
Assert-LibVirtualHidRemoved `
253+
-TargetOriginalName $OriginalName `
254+
-TargetHardwareId $HardwareId `
255+
-ServiceName $BrokerServiceName
256+
}
257+
Remove-DriverCertificate -Subject $RemoveCertificateSubject
258+
} finally {
259+
Stop-LibVirtualHidTranscript
227260
}
228-
Remove-DriverCertificate -Subject $RemoveCertificateSubject

0 commit comments

Comments
 (0)