From 84b915c59c3fc7f35a5bbdb03511d757bff895fe Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:57:36 -0400 Subject: [PATCH 1/2] 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. --- .github/workflows/ci-build.yml | 75 ++++++++--- .../libvirtualhid-driver-installer-patch.xml | 4 +- docs/windows-driver.md | 15 ++- scripts/windows/install-driver.ps1 | 39 ------ .../windows/libvirtualhid-driver-common.ps1 | 39 ++++++ scripts/windows/uninstall-driver.ps1 | 116 +++++++++++------- 6 files changed, 186 insertions(+), 102 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 58f1dc0..fff727a 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -496,7 +496,7 @@ jobs: --xml-pretty \ -o reports/coverage.xml - - name: Uninstall Windows driver installer + - name: Uninstall Windows driver installer with an open consumer handle if: >- always() && runner.os == 'Windows' @@ -507,20 +507,67 @@ jobs: -ErrorAction SilentlyContinue | Select-Object -First 1 if ($installer) { - $logPath = Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-uninstall.log" - $process = Start-Process ` - -FilePath msiexec.exe ` - -ArgumentList @("/x", $installer.FullName, "/qn", "/norestart", "/L*v", $logPath) ` - -PassThru ` - -NoNewWindow - if (!$process.WaitForExit([int] [TimeSpan]::FromMinutes(5).TotalMilliseconds)) { - Get-Content -LiteralPath $logPath -Tail 200 -ErrorAction SilentlyContinue - Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue - throw "Windows driver installer uninstall timed out after 5 minutes." + $nativeMethods = @' + using System; + using System.Runtime.InteropServices; + + public static class LibVirtualHidUninstallTest + { + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr CreateFile( + string fileName, + uint desiredAccess, + uint shareMode, + IntPtr securityAttributes, + uint creationDisposition, + uint flagsAndAttributes, + IntPtr templateFile); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr handle); + } + '@ + Add-Type -TypeDefinition $nativeMethods + $invalidHandle = [IntPtr]::new(-1) + $deviceHandle = [LibVirtualHidUninstallTest]::CreateFile( + "\\.\LibVirtualHid", + 0xC0000000, + 0x00000003, + [IntPtr]::Zero, + 3, + 0x40000080, + [IntPtr]::Zero + ) + if ($deviceHandle -eq $invalidHandle) { + Write-Warning "Unable to hold the libvirtualhid control device; continuing cleanup." + } else { + Write-Host "Holding the libvirtualhid control device open during MSI uninstall." } - if ($process.ExitCode -notin @(0, 3010)) { - Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue - throw "Windows driver installer uninstall exited with code $($process.ExitCode)." + + $logPath = Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-uninstall.log" + $driverLogPath = Join-Path $env:ProgramData "libvirtualhid\uninstall-driver.log" + try { + $process = Start-Process ` + -FilePath msiexec.exe ` + -ArgumentList @("/x", $installer.FullName, "/qn", "/norestart", "/L*v", $logPath) ` + -PassThru ` + -NoNewWindow + if (!$process.WaitForExit([int] [TimeSpan]::FromMinutes(5).TotalMilliseconds)) { + Get-Content -LiteralPath $logPath -Tail 200 -ErrorAction SilentlyContinue + Get-Content -LiteralPath $driverLogPath -Tail 200 -ErrorAction SilentlyContinue + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + throw "Windows driver installer uninstall timed out after 5 minutes." + } + if ($process.ExitCode -notin @(0, 3010)) { + Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue + Get-Content -LiteralPath $driverLogPath -ErrorAction SilentlyContinue + throw "Windows driver installer uninstall exited with code $($process.ExitCode)." + } + } finally { + if ($deviceHandle -ne $invalidHandle) { + [void] [LibVirtualHidUninstallTest]::CloseHandle($deviceHandle) + } } } diff --git a/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml b/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml index 05ac291..8b54ba9 100644 --- a/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml +++ b/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml @@ -14,13 +14,13 @@ Impersonate="no" /> diff --git a/docs/windows-driver.md b/docs/windows-driver.md index 70e0dd9..075a0b5 100644 --- a/docs/windows-driver.md +++ b/docs/windows-driver.md @@ -147,7 +147,8 @@ powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-browser-gamepad. -GamepadAdapterPath .\cmake-build-windows-driver\examples\Release\gamepad_adapter.exe ` -GamepadProfile xseries powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 ` - -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" + -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" ` + -LogPath .\cmake-build-windows-driver\uninstall-driver.log ``` 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 licensing configuration cannot be overridden on the user's machine. The uninstall helper stops and deletes that service before removing the driver package. It discovers staged OEM INF names through language-neutral DISM and -CIM objects instead of parsing localized `pnputil` labels. Uninstall fails if a -command fails or if the broker service, root device, or staged driver package -is still present after cleanup, so the MSI cannot silently report a complete -removal while driver state remains. +CIM objects instead of parsing localized `pnputil` labels. If an application +has an outstanding device handle, the helper records the initial device-removal +failure and continues with the forced driver-package uninstall, which can finish +or schedule the removal. Uninstall still fails if package removal fails or if +the broker service, root device, or staged driver package remains after cleanup, +so the MSI cannot silently report a complete removal while driver state remains. +MSI uninstall diagnostics are appended to +`C:\ProgramData\libvirtualhid\uninstall-driver.log`. The installed-driver test fails if the root device is not started, if `\\.\LibVirtualHid` cannot be opened, or if a held `gamepad_adapter` instance diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index 26515f1..ed0eb60 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -24,45 +24,6 @@ $script:LibVirtualHidBrokerServiceName = "libvirtualhid_broker" $script:LibVirtualHidBrokerServiceDisplayName = "libvirtualhid Broker" . (Join-Path $PSScriptRoot "libvirtualhid-driver-common.ps1") -function Start-LibVirtualHidTranscript { - [CmdletBinding(SupportsShouldProcess)] - param([string] $Path) - - if (-not $Path) { - return - } - - try { - $logDirectory = Split-Path -Parent $Path - if ($logDirectory) { - New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null - } - if ($PSCmdlet.ShouldProcess($Path, "Start libvirtualhid install transcript")) { - Start-Transcript -Path $Path -Append | Out-Null - $script:LibVirtualHidTranscriptStarted = $true - } - } catch { - Write-Warning "Unable to start libvirtualhid install transcript: $($_.Exception.Message)" - } -} - -function Stop-LibVirtualHidTranscript { - [CmdletBinding(SupportsShouldProcess)] - param() - - if (-not $script:LibVirtualHidTranscriptStarted) { - return - } - - try { - if ($PSCmdlet.ShouldProcess("libvirtualhid install transcript", "Stop transcript")) { - Stop-Transcript | Out-Null - } - } catch { - Write-Warning "Unable to stop libvirtualhid install transcript: $($_.Exception.Message)" - } -} - function Invoke-CheckedCommand { param( [Parameter(Mandatory = $true)] diff --git a/scripts/windows/libvirtualhid-driver-common.ps1 b/scripts/windows/libvirtualhid-driver-common.ps1 index ad5caa2..4017325 100644 --- a/scripts/windows/libvirtualhid-driver-common.ps1 +++ b/scripts/windows/libvirtualhid-driver-common.ps1 @@ -1,3 +1,42 @@ +function Start-LibVirtualHidTranscript { + [CmdletBinding(SupportsShouldProcess)] + param([string] $Path) + + if (-not $Path) { + return + } + + try { + $logDirectory = Split-Path -Parent $Path + if ($logDirectory) { + New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null + } + if ($PSCmdlet.ShouldProcess($Path, "Start libvirtualhid driver transcript")) { + Start-Transcript -Path $Path -Append | Out-Null + $script:LibVirtualHidTranscriptStarted = $true + } + } catch { + Write-Warning "Unable to start libvirtualhid driver transcript: $($_.Exception.Message)" + } +} + +function Stop-LibVirtualHidTranscript { + [CmdletBinding(SupportsShouldProcess)] + param() + + if (-not $script:LibVirtualHidTranscriptStarted) { + return + } + + try { + if ($PSCmdlet.ShouldProcess("libvirtualhid driver transcript", "Stop transcript")) { + Stop-Transcript | Out-Null + } + } catch { + Write-Warning "Unable to stop libvirtualhid driver transcript: $($_.Exception.Message)" + } +} + function Get-LibVirtualHidRootDeviceInstanceId { param([string] $TargetHardwareId) diff --git a/scripts/windows/uninstall-driver.ps1 b/scripts/windows/uninstall-driver.ps1 index 7e40c77..3077002 100644 --- a/scripts/windows/uninstall-driver.ps1 +++ b/scripts/windows/uninstall-driver.ps1 @@ -14,10 +14,13 @@ param( [string] $RemoveCertificateSubject, + [string] $LogPath, + [switch] $Force ) $ErrorActionPreference = "Stop" +$script:LibVirtualHidTranscriptStarted = $false . (Join-Path $PSScriptRoot "libvirtualhid-driver-common.ps1") function Invoke-CheckedCommand { @@ -125,6 +128,31 @@ function Assert-PublishedName { } } +function Remove-LibVirtualHidDeviceInstance { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [string] $InstanceId + ) + + if (-not $PSCmdlet.ShouldProcess($InstanceId, "Remove libvirtualhid development device with pnputil")) { + return + } + + $output = @(& pnputil.exe /remove-device $InstanceId 2>&1) + $exitCode = $LASTEXITCODE + foreach ($line in $output) { + Write-Information ([string] $line) -InformationAction Continue + } + + if ($exitCode -ne 0) { + Write-Warning ( + "pnputil.exe /remove-device $InstanceId exited with code $exitCode. " + + "Continuing with forced driver-package removal; final state verification will still fail if cleanup is incomplete." + ) + } +} + function Assert-LibVirtualHidRemoved { param( [string] $TargetOriginalName, @@ -173,56 +201,60 @@ function Remove-DriverCertificate { } } -$publishedNames = @() -if ($PublishedName) { - Assert-PublishedName -Name $PublishedName - $publishedNames += $PublishedName -} else { - try { - $publishedNames = @( - Find-PublishedName ` - -TargetOriginalName $OriginalName ` - -TargetHardwareId $HardwareId - ) - } catch { - throw "Unable to discover the staged libvirtualhid driver package through Windows APIs: $($_.Exception.Message)" +Start-LibVirtualHidTranscript -Path $LogPath + +try { + $publishedNames = @() + if ($PublishedName) { + Assert-PublishedName -Name $PublishedName + $publishedNames += $PublishedName + } else { + try { + $publishedNames = @( + Find-PublishedName ` + -TargetOriginalName $OriginalName ` + -TargetHardwareId $HardwareId + ) + } catch { + throw "Unable to discover the staged libvirtualhid driver package through Windows APIs: $($_.Exception.Message)" + } } -} -Remove-LibVirtualHidBrokerService -Name $BrokerServiceName + Remove-LibVirtualHidBrokerService -Name $BrokerServiceName -$deviceInstanceIds = @( - Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId - Get-LibVirtualHidRegistryRootDevice -TargetHardwareId $HardwareId | - Select-Object -ExpandProperty InstanceId -) | Select-Object -Unique + $deviceInstanceIds = @( + Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId + Get-LibVirtualHidRegistryRootDevice -TargetHardwareId $HardwareId | + Select-Object -ExpandProperty InstanceId + ) | Select-Object -Unique -foreach ($instanceId in $deviceInstanceIds) { - if ($PSCmdlet.ShouldProcess($instanceId, "Remove libvirtualhid development device with pnputil")) { - Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/remove-device", $instanceId) + foreach ($instanceId in $deviceInstanceIds) { + Remove-LibVirtualHidDeviceInstance -InstanceId $instanceId } -} -if ($publishedNames.Count -eq 0) { - Write-Warning "No staged libvirtualhid driver package matching $OriginalName was found." -} else { - foreach ($driverPackage in $publishedNames) { - Assert-PublishedName -Name $driverPackage - $deleteArgs = @("/delete-driver", $driverPackage, "/uninstall") - if ($Force) { - $deleteArgs += "/force" - } + if ($publishedNames.Count -eq 0) { + Write-Warning "No staged libvirtualhid driver package matching $OriginalName was found." + } else { + foreach ($driverPackage in $publishedNames) { + Assert-PublishedName -Name $driverPackage + $deleteArgs = @("/delete-driver", $driverPackage, "/uninstall") + if ($Force) { + $deleteArgs += "/force" + } - if ($PSCmdlet.ShouldProcess($driverPackage, "Delete libvirtualhid driver package")) { - Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments $deleteArgs + if ($PSCmdlet.ShouldProcess($driverPackage, "Delete libvirtualhid driver package")) { + Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments $deleteArgs + } } } -} -if (-not $WhatIfPreference) { - Assert-LibVirtualHidRemoved ` - -TargetOriginalName $OriginalName ` - -TargetHardwareId $HardwareId ` - -ServiceName $BrokerServiceName + if (-not $WhatIfPreference) { + Assert-LibVirtualHidRemoved ` + -TargetOriginalName $OriginalName ` + -TargetHardwareId $HardwareId ` + -ServiceName $BrokerServiceName + } + Remove-DriverCertificate -Subject $RemoveCertificateSubject +} finally { + Stop-LibVirtualHidTranscript } -Remove-DriverCertificate -Subject $RemoveCertificateSubject From 0491a35990d888110f497cda837951812d17423e Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:26:09 -0400 Subject: [PATCH 2/2] fix(ci): type Windows access mask as uint --- .github/workflows/ci-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index fff727a..8acb234 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -513,6 +513,8 @@ jobs: public static class LibVirtualHidUninstallTest { + public const uint GenericReadWriteAccess = 0xC0000000; + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr CreateFile( string fileName, @@ -532,7 +534,7 @@ jobs: $invalidHandle = [IntPtr]::new(-1) $deviceHandle = [LibVirtualHidUninstallTest]::CreateFile( "\\.\LibVirtualHid", - 0xC0000000, + [LibVirtualHidUninstallTest]::GenericReadWriteAccess, 0x00000003, [IntPtr]::Zero, 3,