From 82d17d0b04bab9a670ed52be99cbae9e772ab3d9 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:02:46 -0400 Subject: [PATCH 1/5] fix(windows): handle driver reinstalls with active consumers --- .github/workflows/ci-build.yml | 46 +++++ docs/windows-driver.md | 7 + scripts/windows/install-driver.ps1 | 156 +++++++++++++---- tests/scripts/install-driver.Tests.ps1 | 226 +++++++++++++++++++++++++ 4 files changed, 401 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 47b1a327..a1a38b8d 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -513,6 +513,52 @@ jobs: --xml-pretty \ -o reports/coverage.xml + - name: Repair Windows driver installer with an open consumer handle + if: runner.os == 'Windows' + shell: pwsh + timeout-minutes: 10 + run: | + $installer = Get-ChildItem -LiteralPath .\windows-driver-installer -Filter *.msi | + Select-Object -First 1 + if (!$installer) { + throw "Windows driver installer artifact did not contain an MSI." + } + if ((Get-Service -Name libvirtualhid_broker).Status -ne "Running") { + throw "The broker must be running before the MSI repair regression test." + } + + $deviceHandle = [System.IO.File]::Open( + "\\.\LibVirtualHid", + [System.IO.FileMode]::Open, + [System.IO.FileAccess]::ReadWrite, + [System.IO.FileShare]::ReadWrite + ) + $logPath = Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-repair.log" + $driverLogPath = Join-Path $env:ProgramData "libvirtualhid\install-driver.log" + $setupApiLogPath = Join-Path $env:windir "inf\setupapi.dev.log" + try { + $process = Start-Process ` + -FilePath msiexec.exe ` + -ArgumentList @("/i", $installer.FullName, "REINSTALL=ALL", "REINSTALLMODE=amus", "/qn", "/norestart", "/L*v", $logPath) ` + -PassThru ` + -WindowStyle Hidden + 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 repair timed out after 5 minutes." + } + if ($process.ExitCode -notin @(0, 3010)) { + Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue + Get-Content -LiteralPath $driverLogPath -ErrorAction SilentlyContinue + Get-Content -LiteralPath $setupApiLogPath -Tail 300 -ErrorAction SilentlyContinue + throw "Windows driver installer repair exited with code $($process.ExitCode)." + } + (Get-Service -Name libvirtualhid_broker).WaitForStatus("Running", [TimeSpan]::FromSeconds(15)) + } finally { + $deviceHandle.Dispose() + } + - name: Uninstall Windows driver installer with an open consumer handle if: >- always() && diff --git a/docs/windows-driver.md b/docs/windows-driver.md index e4b556e0..983fe82f 100644 --- a/docs/windows-driver.md +++ b/docs/windows-driver.md @@ -45,6 +45,13 @@ Restart Windows if the installer requests it. The package installs the default. Applications can then use the normal C++ API without administrator privileges. +During a reinstall or upgrade, the installer stops the broker before updating +the driver. This disconnects existing virtual devices. Close applications +using libvirtualhid, including Sunshine, before upgrading, then restart them +afterward. If an open device handle prevents Windows from reloading the driver, +installation can finish with a reboot warning in `install-driver.log`; +restart Windows before using the updated driver. + A machine license is required to create a driver-backed device. Open `virtualhid_control.exe` to activate or refresh a license, select a gamepad profile, and create a test controller. The tool can submit buttons, axes, diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index 62883199..e121d792 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -1,6 +1,11 @@ <# .SYNOPSIS Stages and installs the libvirtualhid Windows UMDF development driver. +.DESCRIPTION +Stops the broker before changing devices and starts it after installation. +Driver reloads that Windows defers until reboot produce a warning rather than +failing an otherwise successful installation. Restart Windows before using the +driver when this warning appears. #> [CmdletBinding(SupportsShouldProcess)] param( @@ -24,6 +29,7 @@ $ErrorActionPreference = "Stop" $script:LibVirtualHidTranscriptStarted = $false $script:LibVirtualHidBrokerServiceName = "libvirtualhid_broker" $script:LibVirtualHidBrokerServiceDisplayName = "libvirtualhid Broker" +$script:LibVirtualHidRebootRequired = $false . (Join-Path $PSScriptRoot "libvirtualhid-driver-common.ps1") function Invoke-CheckedCommand { @@ -261,7 +267,7 @@ function Restart-RootDevice { [CmdletBinding(SupportsShouldProcess)] param([string] $InstanceId) - if (-not $InstanceId) { + if (-not $InstanceId -or $script:LibVirtualHidRebootRequired) { return } @@ -275,11 +281,21 @@ function Restart-RootDevice { Write-Information $line -InformationAction Continue } + # ERROR_NOT_SUPPORTED (50) is also returned when an earlier driver update + # left the instance pending reboot. A reboot can reload the installed driver + # even when this OS cannot restart the device in place. + if ($exitCode -in @(50, 3010)) { + $script:LibVirtualHidRebootRequired = $true + Write-Warning "Windows could not reload $InstanceId without a reboot (pnputil exit code $exitCode). Restart Windows to complete the libvirtualhid driver installation." + return + } + if ($exitCode -ne 0) { throw "pnputil.exe /restart-device $InstanceId exited with code $exitCode" } if ($output -match "reboot is needed") { + $script:LibVirtualHidRebootRequired = $true Write-Warning "Windows reported that a reboot is required to reload the libvirtualhid UMDF driver." } } @@ -303,6 +319,7 @@ function Update-RootDeviceDriverWithSetupApi { -Arguments @("update", $Path, $TargetHardwareId) ` -SuccessExitCodes @(0, 3010) if ($LASTEXITCODE -eq 3010) { + $script:LibVirtualHidRebootRequired = $true Write-Warning "Windows reported that a reboot is required to finish installing the libvirtualhid driver." } } @@ -323,13 +340,54 @@ function Install-RootDeviceWithSetupApi { Invoke-CheckedCommand -FilePath $SetupHelperPath -Arguments @("install", $Path, $TargetHardwareId) } -if ($MyInvocation.InvocationName -eq ".") { - return +<# +.SYNOPSIS +Records a terminating installation error even when transcription is unavailable. +.PARAMETER Path +The installation log path, if one was requested. +.PARAMETER Message +The original installation error to record without replacing it with a logging error. +#> +function Write-LibVirtualHidInstallFailure { + [CmdletBinding(SupportsShouldProcess)] + param([string] $Path, [string] $Message) + + Write-Warning "libvirtualhid driver installation failed: $Message" -WarningAction Continue + if (-not $Path -or $script:LibVirtualHidTranscriptStarted) { + return + } + + try { + if ($PSCmdlet.ShouldProcess($Path, "Record libvirtualhid installation failure")) { + Add-Content -LiteralPath $Path -Value "$(Get-Date -Format o) libvirtualhid driver installation failed: $Message" -ErrorAction Stop + } + } catch { + Write-Warning "Unable to write the libvirtualhid installation error log: $($_.Exception.Message)" -WarningAction Continue + } } -Start-LibVirtualHidTranscript -Path $LogPath +<# +.SYNOPSIS +Installs or updates the driver while the broker has released its device handles. +.DESCRIPTION +Stages the package, stops the broker before changing any device, and registers +the broker after the driver update. A required reboot skips further restarts. +Restores a previously running broker if installation fails or no broker binary +is supplied. StageOnly and WhatIf do not stop the service. +#> +function Install-LibVirtualHidDriver { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [string] $InfPath, + [string] $CertificatePath, + [string] $HardwareId = "ROOT\LIBVIRTUALHID", + [string] $BrokerPath, + [string] $SetupPath, + [switch] $StageOnly + ) -try { + $script:LibVirtualHidRebootRequired = $false $resolvedInf = (Resolve-Path -LiteralPath $InfPath).Path Import-DriverCertificate -Path $CertificatePath @@ -342,48 +400,78 @@ try { } $resolvedSetup = Resolve-LibVirtualHidDriverSetupPath -Path $SetupPath + $resolvedBroker = Resolve-LibVirtualHidBrokerPath -Path $BrokerPath + $brokerService = Get-Service -Name $script:LibVirtualHidBrokerServiceName -ErrorAction SilentlyContinue + $brokerWasRunning = $brokerService -and $brokerService.Status -ne "Stopped" - $registryRootDevices = @(Get-LibVirtualHidRegistryRootDevice -TargetHardwareId $HardwareId) - foreach ($device in ($registryRootDevices | Where-Object { $_.HasCorruptHardwareId -or $_.HasLegacyHidClass })) { - Remove-DeviceInstance -InstanceId $device.InstanceId - } + try { + Stop-LibVirtualHidBrokerService -Name $script:LibVirtualHidBrokerServiceName + + $registryRootDevices = @(Get-LibVirtualHidRegistryRootDevice -TargetHardwareId $HardwareId) + foreach ($device in ($registryRootDevices | Where-Object { $_.HasCorruptHardwareId -or $_.HasLegacyHidClass })) { + Remove-DeviceInstance -InstanceId $device.InstanceId + } + + $rootDevices = @(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId) + if ($rootDevices.Count -eq 0) { + if ($PSCmdlet.ShouldProcess($HardwareId, "Create libvirtualhid development device with SetupAPI")) { + Install-RootDeviceWithSetupApi ` + -Path $resolvedInf ` + -TargetHardwareId $HardwareId ` + -SetupHelperPath $resolvedSetup + } + $rootDevices = @(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId) + } else { + Write-Information "Updating the existing $HardwareId device driver." -InformationAction Continue + } - $rootDevices = @(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId) - if ($rootDevices.Count -gt 0) { - Write-Information "Updating the existing $HardwareId device driver." -InformationAction Continue foreach ($rootDevice in $rootDevices) { Set-RootDeviceVhfMode -InstanceId $rootDevice } + Update-RootDeviceDriverWithSetupApi ` -Path $resolvedInf ` -TargetHardwareId $HardwareId ` -SetupHelperPath $resolvedSetup - foreach ($rootDevice in $rootDevices) { - Restart-RootDevice -InstanceId $rootDevice + if (-not $script:LibVirtualHidRebootRequired) { + foreach ($rootDevice in $rootDevices) { + Restart-RootDevice -InstanceId $rootDevice + } + } + Install-LibVirtualHidBrokerService -Path $resolvedBroker + } finally { + # Restore an existing broker after a failed update or a driver-only install. + # Do not let a recovery failure hide the original installation error. + if ($brokerWasRunning) { + try { + $service = Get-Service -Name $script:LibVirtualHidBrokerServiceName -ErrorAction Stop + if ($service.Status -eq "Stopped" -and $PSCmdlet.ShouldProcess($script:LibVirtualHidBrokerServiceName, "Restore libvirtualhid broker service")) { + Start-Service -Name $script:LibVirtualHidBrokerServiceName -ErrorAction Stop + } + } catch { + Write-Warning "Unable to restore the libvirtualhid broker service: $($_.Exception.Message)" + } } - Install-LibVirtualHidBrokerService -Path $BrokerPath - return } +} - if ($PSCmdlet.ShouldProcess($HardwareId, "Create libvirtualhid development device with SetupAPI")) { - Install-RootDeviceWithSetupApi ` - -Path $resolvedInf ` - -TargetHardwareId $HardwareId ` - -SetupHelperPath $resolvedSetup - } +if ($MyInvocation.InvocationName -eq ".") { + return +} - $rootDevices = @(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId) - foreach ($rootDevice in $rootDevices) { - Set-RootDeviceVhfMode -InstanceId $rootDevice - } - Update-RootDeviceDriverWithSetupApi ` - -Path $resolvedInf ` - -TargetHardwareId $HardwareId ` - -SetupHelperPath $resolvedSetup - foreach ($rootDevice in $rootDevices) { - Restart-RootDevice -InstanceId $rootDevice - } - Install-LibVirtualHidBrokerService -Path $BrokerPath +Start-LibVirtualHidTranscript -Path $LogPath + +try { + Install-LibVirtualHidDriver ` + -InfPath $InfPath ` + -CertificatePath $CertificatePath ` + -HardwareId $HardwareId ` + -BrokerPath $BrokerPath ` + -SetupPath $SetupPath ` + -StageOnly:$StageOnly +} catch { + Write-LibVirtualHidInstallFailure -Path $LogPath -Message $_.Exception.Message + throw } finally { Stop-LibVirtualHidTranscript } diff --git a/tests/scripts/install-driver.Tests.ps1 b/tests/scripts/install-driver.Tests.ps1 index 761df180..9812f84d 100644 --- a/tests/scripts/install-driver.Tests.ps1 +++ b/tests/scripts/install-driver.Tests.ps1 @@ -55,6 +55,7 @@ Describe "install-driver.ps1 entry point" { } Mock Get-CimInstance { @() } Mock Get-ChildItem { @() } + Mock Get-Service { $null } try { { @@ -253,6 +254,10 @@ Describe "Broker service helpers" { } Describe "Driver installation helpers" { + BeforeEach { + $script:LibVirtualHidRebootRequired = $false + } + It "imports an existing certificate into both required stores" { $path = Join-Path $TestDrive "driver.cer" New-Item -ItemType File -Path $path | Out-Null @@ -320,6 +325,41 @@ Describe "Driver installation helpers" { Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { $Message -like "Windows reported that a reboot is required*" } + $script:LibVirtualHidRebootRequired | Should -BeTrue + } + + It "defers a restart with exit code until reboot" -TestCases @( + @{ ExitCode = 50 }, + @{ ExitCode = 3010 } + ) { + param($ExitCode) + Mock pnputil.exe { $global:LASTEXITCODE = $ExitCode } + Mock Write-Warning {} + + Restart-RootDevice -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false + + $script:LibVirtualHidRebootRequired | Should -BeTrue + Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $Message -like "*Restart Windows to complete*" + } + } + + It "does not restart a device once a reboot is required" { + $script:LibVirtualHidRebootRequired = $true + Mock pnputil.exe { throw "A pending-reboot device must not be restarted" } + + Restart-RootDevice -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false + + Should -Invoke pnputil.exe -Times 0 -Exactly -Scope It + } + + It "restarts a device successfully without requiring a reboot" { + Mock pnputil.exe { $global:LASTEXITCODE = 0 } + + Restart-RootDevice -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false + + $script:LibVirtualHidRebootRequired | Should -BeFalse + Should -Invoke pnputil.exe -Times 1 -Exactly -Scope It } It "throws when restarting the device fails" { @@ -346,6 +386,7 @@ Describe "Driver installation helpers" { $SuccessExitCodes -contains 3010 } Should -Invoke Write-Warning -Times 1 -Exactly -Scope It + $script:LibVirtualHidRebootRequired | Should -BeTrue } It "installs a root device through the SetupAPI helper" { @@ -361,3 +402,188 @@ Describe "Driver installation helpers" { } } } + +Describe "Install-LibVirtualHidDriver" { + BeforeEach { + $infPath = Join-Path $TestDrive "libvirtualhid.inf" + Set-Content -LiteralPath $infPath -Value "test INF" + $script:installOperations = [System.Collections.Generic.List[string]]::new() + $script:brokerStatus = "Running" + Mock Import-DriverCertificate {} + Mock Invoke-CheckedCommand { + $script:installOperations.Add("stage") + } + Mock Resolve-LibVirtualHidDriverSetupPath { "setup.exe" } + Mock Resolve-LibVirtualHidBrokerPath { "broker.exe" } + Mock Get-Service { [pscustomobject]@{ Status = $script:brokerStatus } } + Mock Stop-LibVirtualHidBrokerService { + $script:installOperations.Add("stop") + $script:brokerStatus = "Stopped" + } + Mock Get-LibVirtualHidRegistryRootDevice { @() } + Mock Get-LibVirtualHidRootDeviceInstanceId { "ROOT\LIBVIRTUALHID\0000" } + Mock Install-RootDeviceWithSetupApi { + $script:installOperations.Add("create") + } + Mock Remove-DeviceInstance { + $script:installOperations.Add("remove") + } + Mock Set-RootDeviceVhfMode { + $script:installOperations.Add("vhf") + } + Mock Update-RootDeviceDriverWithSetupApi { + $script:installOperations.Add("update") + } + Mock Restart-RootDevice { + $script:installOperations.Add("restart") + } + Mock Install-LibVirtualHidBrokerService { + $script:installOperations.Add("broker") + $script:brokerStatus = "Running" + } + Mock Start-Service { + $script:installOperations.Add("restore") + } + Mock Write-Warning {} + } + + It "stops the broker before updating an existing device and starts it last" { + Install-LibVirtualHidDriver -InfPath $infPath -Confirm:$false + + $script:installOperations.ToArray() | Should -Be @("stage", "stop", "vhf", "update", "restart", "broker") + Should -Invoke Install-RootDeviceWithSetupApi -Times 0 -Exactly -Scope It + Should -Invoke Start-Service -Times 0 -Exactly -Scope It + } + + It "stops the broker before removing a stale device and creating its replacement" { + Mock Get-LibVirtualHidRegistryRootDevice { + [pscustomobject]@{ + InstanceId = "ROOT\LIBVIRTUALHID\0000" + HasCorruptHardwareId = $true + HasLegacyHidClass = $false + } + } + $script:deviceQueries = 0 + Mock Get-LibVirtualHidRootDeviceInstanceId { + $script:deviceQueries += 1 + if ($script:deviceQueries -gt 1) { + "ROOT\LIBVIRTUALHID\0001" + } + } + + Install-LibVirtualHidDriver -InfPath $infPath -Confirm:$false + + $script:installOperations.ToArray() | Should -Be @("stage", "stop", "remove", "create", "vhf", "update", "restart", "broker") + } + + It "finishes broker installation without restarting a pending-reboot device" { + Mock Update-RootDeviceDriverWithSetupApi { + $script:installOperations.Add("update") + $script:LibVirtualHidRebootRequired = $true + } + + Install-LibVirtualHidDriver -InfPath $infPath -Confirm:$false + + $script:installOperations.ToArray() | Should -Be @("stage", "stop", "vhf", "update", "broker") + Should -Invoke Restart-RootDevice -Times 0 -Exactly -Scope It + } + + It "does not stop the broker when only staging a driver" { + Install-LibVirtualHidDriver -InfPath $infPath -StageOnly -Confirm:$false + + $script:installOperations.ToArray() | Should -Be @("stage") + Should -Invoke Get-Service -Times 0 -Exactly -Scope It + Should -Invoke Resolve-LibVirtualHidDriverSetupPath -Times 0 -Exactly -Scope It + } + + It "validates the broker path before stopping the existing service" { + Mock Resolve-LibVirtualHidBrokerPath { throw "Missing broker" } + + { Install-LibVirtualHidDriver -InfPath $infPath -Confirm:$false } | Should -Throw "Missing broker" + + Should -Invoke Stop-LibVirtualHidBrokerService -Times 0 -Exactly -Scope It + } + + It "preserves a driver update failure and restores a previously running broker" { + Mock Update-RootDeviceDriverWithSetupApi { throw "Update failed" } + + { Install-LibVirtualHidDriver -InfPath $infPath -Confirm:$false } | Should -Throw "Update failed" + + $script:installOperations.ToArray() | Should -Be @("stage", "stop", "vhf", "restore") + Should -Invoke Install-LibVirtualHidBrokerService -Times 0 -Exactly -Scope It + } + + It "does not start a previously stopped broker after an update failure" { + $script:brokerStatus = "Stopped" + Mock Update-RootDeviceDriverWithSetupApi { throw "Update failed" } + + { Install-LibVirtualHidDriver -InfPath $infPath -Confirm:$false } | Should -Throw "Update failed" + + Should -Invoke Start-Service -Times 0 -Exactly -Scope It + } + + It "preserves an update failure if broker recovery also fails" { + Mock Update-RootDeviceDriverWithSetupApi { throw "Update failed" } + Mock Start-Service { throw "Recovery failed" } + + { Install-LibVirtualHidDriver -InfPath $infPath -Confirm:$false } | Should -Throw "Update failed" + + Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $Message -eq "Unable to restore the libvirtualhid broker service: Recovery failed" + } + } + + It "restores a running broker after an install without a broker executable" { + Mock Resolve-LibVirtualHidBrokerPath { $null } + Mock Install-LibVirtualHidBrokerService {} + + Install-LibVirtualHidDriver -InfPath $infPath -Confirm:$false + + $script:installOperations[-1] | Should -Be "restore" + } +} + +Describe "Installation failure logging" { + BeforeEach { + $script:LibVirtualHidTranscriptStarted = $false + Mock Write-Warning {} + } + + It "records the error if a transcript could not be started" { + $path = Join-Path $TestDrive "failure.log" + + Write-LibVirtualHidInstallFailure -Path $path -Message "Driver update failed" + + Get-Content -LiteralPath $path | Should -BeLike "*libvirtualhid driver installation failed: Driver update failed" + } + + It "writes the error to an active transcript without appending to its file" { + $script:LibVirtualHidTranscriptStarted = $true + Mock Add-Content {} + + Write-LibVirtualHidInstallFailure -Path "driver.log" -Message "Driver update failed" + + Should -Invoke Add-Content -Times 0 -Exactly -Scope It + Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $Message -eq "libvirtualhid driver installation failed: Driver update failed" + } + } + + It "does not require a log path" { + Mock Add-Content {} + + Write-LibVirtualHidInstallFailure -Message "Driver update failed" + + Should -Invoke Add-Content -Times 0 -Exactly -Scope It + } + + It "does not replace the original error when writing the log also fails" { + Mock Add-Content { throw "Log is locked" } + + { Write-LibVirtualHidInstallFailure -Path "driver.log" -Message "Driver update failed" } | Should -Not -Throw + + Should -Invoke Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $Message -eq "Unable to write the libvirtualhid installation error log: Log is locked" + } + } +} From 319935231352002660c89e7f7f8f0738e143f373 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:12:19 -0400 Subject: [PATCH 2/5] fix(ci): resolve installer complexity and lint findings --- .github/workflows/ci-build.yml | 7 +++++- scripts/windows/install-driver.ps1 | 31 ++++++++++++++++++-------- tests/scripts/install-driver.Tests.ps1 | 3 ++- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index a1a38b8d..6a3a6350 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -537,9 +537,14 @@ jobs: $driverLogPath = Join-Path $env:ProgramData "libvirtualhid\install-driver.log" $setupApiLogPath = Join-Path $env:windir "inf\setupapi.dev.log" try { + $repairArguments = @( + "/i", $installer.FullName, + "REINSTALL=ALL", "REINSTALLMODE=amus", + "/qn", "/norestart", "/L*v", $logPath + ) $process = Start-Process ` -FilePath msiexec.exe ` - -ArgumentList @("/i", $installer.FullName, "REINSTALL=ALL", "REINSTALLMODE=amus", "/qn", "/norestart", "/L*v", $logPath) ` + -ArgumentList $repairArguments ` -PassThru ` -WindowStyle Hidden if (!$process.WaitForExit([int] [TimeSpan]::FromMinutes(5).TotalMilliseconds)) { diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index e121d792..7f0471b8 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -155,6 +155,27 @@ function Stop-LibVirtualHidBrokerService { } } +<# +.SYNOPSIS +Restores a previously running broker after installation or update failure. +.PARAMETER Name +The broker service to start if it is currently stopped. +#> +function Restore-LibVirtualHidBrokerService { + [CmdletBinding(SupportsShouldProcess)] + param([string] $Name) + + # Do not let a recovery failure hide the original installation error. + try { + $service = Get-Service -Name $Name -ErrorAction Stop + if ($service.Status -eq "Stopped" -and $PSCmdlet.ShouldProcess($Name, "Restore libvirtualhid broker service")) { + Start-Service -Name $Name -ErrorAction Stop + } + } catch { + Write-Warning "Unable to restore the libvirtualhid broker service: $($_.Exception.Message)" + } +} + function Install-LibVirtualHidBrokerService { [CmdletBinding(SupportsShouldProcess)] param([string] $Path) @@ -441,16 +462,8 @@ function Install-LibVirtualHidDriver { Install-LibVirtualHidBrokerService -Path $resolvedBroker } finally { # Restore an existing broker after a failed update or a driver-only install. - # Do not let a recovery failure hide the original installation error. if ($brokerWasRunning) { - try { - $service = Get-Service -Name $script:LibVirtualHidBrokerServiceName -ErrorAction Stop - if ($service.Status -eq "Stopped" -and $PSCmdlet.ShouldProcess($script:LibVirtualHidBrokerServiceName, "Restore libvirtualhid broker service")) { - Start-Service -Name $script:LibVirtualHidBrokerServiceName -ErrorAction Stop - } - } catch { - Write-Warning "Unable to restore the libvirtualhid broker service: $($_.Exception.Message)" - } + Restore-LibVirtualHidBrokerService -Name $script:LibVirtualHidBrokerServiceName } } } diff --git a/tests/scripts/install-driver.Tests.ps1 b/tests/scripts/install-driver.Tests.ps1 index 9812f84d..ad74cce5 100644 --- a/tests/scripts/install-driver.Tests.ps1 +++ b/tests/scripts/install-driver.Tests.ps1 @@ -333,7 +333,8 @@ Describe "Driver installation helpers" { @{ ExitCode = 3010 } ) { param($ExitCode) - Mock pnputil.exe { $global:LASTEXITCODE = $ExitCode } + $script:restartExitCode = $ExitCode + Mock pnputil.exe { $global:LASTEXITCODE = $script:restartExitCode } Mock Write-Warning {} Restart-RootDevice -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false From f4f1f498dbccb8375c35c2d369f807fa388f806e Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:03:57 -0400 Subject: [PATCH 3/5] test(windows): capture repair custom-action errors --- .github/workflows/ci-build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 6a3a6350..3d6a30f1 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -536,6 +536,7 @@ jobs: $logPath = Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-repair.log" $driverLogPath = Join-Path $env:ProgramData "libvirtualhid\install-driver.log" $setupApiLogPath = Join-Path $env:windir "inf\setupapi.dev.log" + $repairStartedAt = Get-Date try { $repairArguments = @( "/i", $installer.FullName, @@ -556,6 +557,11 @@ jobs: if ($process.ExitCode -notin @(0, 3010)) { Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue Get-Content -LiteralPath $driverLogPath -ErrorAction SilentlyContinue + Get-WinEvent -FilterHashtable @{ + LogName = "Microsoft-Windows-PowerShell/Operational" + StartTime = $repairStartedAt + Id = 4100 + } -ErrorAction SilentlyContinue | Format-List TimeCreated, Id, Message Get-Content -LiteralPath $setupApiLogPath -Tail 300 -ErrorAction SilentlyContinue throw "Windows driver installer repair exited with code $($process.ExitCode)." } From b50e308622370fb53d3e6f492a9462f74eae91a0 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:20:06 -0400 Subject: [PATCH 4/5] fix(windows): reconfigure broker through CIM during repairs --- scripts/windows/install-driver.ps1 | 76 ++++++++++++++--------- tests/scripts/install-driver.Tests.ps1 | 85 +++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 36 deletions(-) diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index 7f0471b8..4ffba986 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -98,17 +98,6 @@ function Get-LibVirtualHidQuotedServiceBinaryPath { return "`"$Path`"" } -function Get-LibVirtualHidScBinaryPathArgument { - param( - [Parameter(Mandatory = $true)] - [string] $Path - ) - - # Windows PowerShell 5.1 needs triple quotes in a native-command argument to pass one literal quote - # through to sc.exe while keeping a path containing spaces in a single argv element. - return (Get-LibVirtualHidQuotedServiceBinaryPath -Path $Path).Replace('"', '"""') -} - function Assert-LibVirtualHidBrokerServiceImagePath { param( [Parameter(Mandatory = $true)] @@ -176,6 +165,40 @@ function Restore-LibVirtualHidBrokerService { } } +<# +.SYNOPSIS +Updates the broker service without native command-line quote parsing. +.PARAMETER Path +The broker executable path to store as a quoted service binary path. +.OUTPUTS +System.Boolean. True when the service configuration was updated. +#> +function Set-LibVirtualHidBrokerServiceConfiguration { + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param([string] $Path) + + if (-not $PSCmdlet.ShouldProcess($script:LibVirtualHidBrokerServiceName, "Update libvirtualhid broker service")) { + return $false + } + + $service = Get-CimInstance -ClassName Win32_Service ` + -Filter "Name = '$script:LibVirtualHidBrokerServiceName'" -ErrorAction Stop + if (-not $service) { + throw "The $script:LibVirtualHidBrokerServiceName service was not found for reconfiguration." + } + + $result = Invoke-CimMethod -InputObject $service -MethodName Change -Arguments @{ + PathName = Get-LibVirtualHidQuotedServiceBinaryPath -Path $Path + DisplayName = $script:LibVirtualHidBrokerServiceDisplayName + StartMode = "Automatic" + } -ErrorAction Stop + if ($result.ReturnValue -ne 0) { + throw "Updating the libvirtualhid broker service failed with Win32_Service.Change return value $($result.ReturnValue)." + } + return $true +} + function Install-LibVirtualHidBrokerService { [CmdletBinding(SupportsShouldProcess)] param([string] $Path) @@ -186,24 +209,11 @@ function Install-LibVirtualHidBrokerService { return } - $serviceBinaryPath = Get-LibVirtualHidScBinaryPathArgument -Path $resolvedBroker $serviceRegistered = $false $service = Get-Service -Name $script:LibVirtualHidBrokerServiceName -ErrorAction SilentlyContinue if ($service) { Stop-LibVirtualHidBrokerService -Name $script:LibVirtualHidBrokerServiceName - if ($PSCmdlet.ShouldProcess($script:LibVirtualHidBrokerServiceName, "Update libvirtualhid broker service")) { - Invoke-CheckedCommand -FilePath "sc.exe" -Arguments @( - "config", - $script:LibVirtualHidBrokerServiceName, - "binPath=", - $serviceBinaryPath, - "start=", - "auto", - "DisplayName=", - $script:LibVirtualHidBrokerServiceDisplayName - ) - $serviceRegistered = $true - } + $serviceRegistered = Set-LibVirtualHidBrokerServiceConfiguration -Path $resolvedBroker } else { if ($PSCmdlet.ShouldProcess($script:LibVirtualHidBrokerServiceName, "Install libvirtualhid broker service")) { $quotedServicePath = Get-LibVirtualHidQuotedServiceBinaryPath -Path $resolvedBroker @@ -296,10 +306,20 @@ function Restart-RootDevice { return } - $output = @(pnputil.exe /restart-device $InstanceId 2>&1) - $exitCode = $LASTEXITCODE + $command = Get-Command pnputil.exe -ErrorAction Stop + $previousErrorActionPreference = $ErrorActionPreference + try { + # Windows PowerShell 5.1 turns redirected native stderr into error records. + # Collect them so the native exit code, including reboot statuses, decides + # whether the restart failed. + $ErrorActionPreference = "Continue" + $output = @(& $command /restart-device $InstanceId 2>&1) + $exitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } foreach ($line in $output) { - Write-Information $line -InformationAction Continue + Write-Information ([string] $line) -InformationAction Continue } # ERROR_NOT_SUPPORTED (50) is also returned when an earlier driver update diff --git a/tests/scripts/install-driver.Tests.ps1 b/tests/scripts/install-driver.Tests.ps1 index ad74cce5..de850b0d 100644 --- a/tests/scripts/install-driver.Tests.ps1 +++ b/tests/scripts/install-driver.Tests.ps1 @@ -115,11 +115,6 @@ Describe "Service path quoting" { } | Should -Throw "*must not contain quotation marks*" } - It "formats the service path for Windows PowerShell native argument parsing" { - Get-LibVirtualHidScBinaryPathArgument -Path "C:\Program Files\libvirtualhid\broker.exe" | - Should -Be '"""C:\Program Files\libvirtualhid\broker.exe"""' - } - It "accepts an exactly quoted service ImagePath" { Mock Get-ItemProperty { [pscustomobject]@{ ImagePath = '"C:\Program Files\libvirtualhid\broker.exe"' } @@ -235,24 +230,73 @@ Describe "Broker service helpers" { It "updates and configures an existing broker service" { $path = Join-Path $TestDrive "existing-libvirtualhid_broker.exe" New-Item -ItemType File -Path $path | Out-Null + $resolvedExistingPath = (Resolve-Path $path).Path Mock Get-Service { [pscustomobject]@{ Status = "Stopped" } } Mock Stop-LibVirtualHidBrokerService {} Mock Assert-LibVirtualHidBrokerServiceImagePath {} Mock Clear-LibVirtualHidBrokerServiceEnvironment {} Mock Invoke-CheckedCommand {} + Mock Set-LibVirtualHidBrokerServiceConfiguration { $true } Mock Start-Service {} Install-LibVirtualHidBrokerService -Path $path -Confirm:$false Should -Invoke Stop-LibVirtualHidBrokerService -Times 1 -Exactly -Scope It - Should -Invoke Invoke-CheckedCommand -Times 3 -Exactly -Scope It - Should -Invoke Invoke-CheckedCommand -Times 1 -Exactly -Scope It -ParameterFilter { - $FilePath -eq "sc.exe" -and $Arguments[0] -eq "config" + Should -Invoke Invoke-CheckedCommand -Times 2 -Exactly -Scope It + Should -Invoke Set-LibVirtualHidBrokerServiceConfiguration -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq $resolvedExistingPath } Should -Invoke Start-Service -Times 1 -Exactly -Scope It } } +Describe "Broker service reconfiguration" { + BeforeEach { + $cimService = New-CimInstance -ClassName Win32_Service -ClientOnly -Property @{ Name = "libvirtualhid_broker" } + Mock Get-CimInstance { $cimService } + Mock Invoke-CimMethod { [pscustomobject]@{ ReturnValue = 0 } } + } + + It "passes a quoted executable path and service settings directly to CIM" { + Set-LibVirtualHidBrokerServiceConfiguration -Path "C:\Program Files\libvirtualhid\broker.exe" -Confirm:$false | + Should -BeTrue + + Should -Invoke Get-CimInstance -Times 1 -Exactly -Scope It -ParameterFilter { + $ClassName -eq "Win32_Service" -and $Filter -eq "Name = 'libvirtualhid_broker'" + } + Should -Invoke Invoke-CimMethod -Times 1 -Exactly -Scope It -ParameterFilter { + $InputObject -eq $cimService -and $MethodName -eq "Change" -and + $Arguments.PathName -ceq '"C:\Program Files\libvirtualhid\broker.exe"' -and + $Arguments.DisplayName -eq "libvirtualhid Broker" -and $Arguments.StartMode -eq "Automatic" + } + } + + It "rejects a failed service configuration change" { + Mock Invoke-CimMethod { [pscustomobject]@{ ReturnValue = 2 } } + + { + Set-LibVirtualHidBrokerServiceConfiguration -Path "C:\broker.exe" -Confirm:$false + } | Should -Throw "*Win32_Service.Change return value 2*" + } + + It "rejects a service that disappeared before reconfiguration" { + Mock Get-CimInstance { $null } + + { + Set-LibVirtualHidBrokerServiceConfiguration -Path "C:\broker.exe" -Confirm:$false + } | Should -Throw "*service was not found for reconfiguration*" + + Should -Invoke Invoke-CimMethod -Times 0 -Exactly -Scope It + } + + It "does not reconfigure a service during WhatIf" { + Set-LibVirtualHidBrokerServiceConfiguration -Path "C:\broker.exe" -WhatIf | Should -BeFalse + + Should -Invoke Get-CimInstance -Times 0 -Exactly -Scope It + Should -Invoke Invoke-CimMethod -Times 0 -Exactly -Scope It + } +} + Describe "Driver installation helpers" { BeforeEach { $script:LibVirtualHidRebootRequired = $false @@ -354,6 +398,31 @@ Describe "Driver installation helpers" { Should -Invoke pnputil.exe -Times 0 -Exactly -Scope It } + It "handles native stderr before interpreting a reboot exit code" { + Mock pnputil.exe { + & $env:ComSpec /c "echo Device is pending system reboot. 1>&2 & exit /b 50" + } + Mock Write-Warning {} + $ErrorActionPreference = "Stop" + + Restart-RootDevice -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false + + $script:LibVirtualHidRebootRequired | Should -BeTrue + $ErrorActionPreference | Should -Be "Stop" + Should -Invoke Write-Warning -Times 1 -Exactly -Scope It + } + + It "preserves a command invocation failure and restores the error preference" { + Mock pnputil.exe { throw "Launch failed" } + $ErrorActionPreference = "Stop" + + { + Restart-RootDevice -InstanceId "ROOT\LIBVIRTUALHID\0000" -Confirm:$false + } | Should -Throw "Launch failed" + + $ErrorActionPreference | Should -Be "Stop" + } + It "restarts a device successfully without requiring a reboot" { Mock pnputil.exe { $global:LASTEXITCODE = 0 } From a26f56462de9e3c1db39b26ed64c0b275aec7a4f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:59:49 -0400 Subject: [PATCH 5/5] fix(installer): include Sunshine in applications-to-close prompts --- .github/workflows/ci-build.yml | 38 ++++++++ cmake/packaging/windows_wix.cmake | 7 +- .../libvirtualhid-driver-installer-patch.xml | 6 +- docs/windows-driver.md | 12 ++- .../test-installer-restart-manager.ps1 | 92 +++++++++++++++++++ 5 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 tests/scripts/test-installer-restart-manager.ps1 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 3d6a30f1..3d60d681 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -351,8 +351,22 @@ jobs: Write-Host "Trusted driver publisher certificate $($cert.Subject) [$($cert.Thumbprint)]." } + - name: Prepare Sunshine close-applications regression test + if: runner.os == 'Windows' + id: sunshine_fixture + shell: powershell + run: | + $installer = Get-ChildItem -LiteralPath .\windows-driver-installer -Filter *.msi | + Select-Object -First 1 + $fixtureDirectory = Join-Path $env:RUNNER_TEMP "libvirtualhid-sunshine-$([Guid]::NewGuid())" + $fixtureId = .\tests\scripts\test-installer-restart-manager.ps1 ` + -MsiPath $installer.FullName -FixtureDirectory $fixtureDirectory + @("pid=$fixtureId", "exe=$fixtureDirectory\sunshine.exe") | + Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + - name: Install Windows driver installer if: runner.os == 'Windows' + id: install_windows_driver shell: pwsh timeout-minutes: 10 run: | @@ -382,6 +396,30 @@ jobs: throw "Windows driver installer exited with code $($process.ExitCode)." } + - name: Verify Windows installer closed Sunshine + if: always() && runner.os == 'Windows' && steps.sunshine_fixture.outputs.pid != '' + shell: powershell + env: + FIXTURE_PID: ${{ steps.sunshine_fixture.outputs.pid }} + FIXTURE_EXE: ${{ steps.sunshine_fixture.outputs.exe }} + INSTALL_OUTCOME: ${{ steps.install_windows_driver.outcome }} + run: | + $fixture = Get-Process -Id ([int] $env:FIXTURE_PID) -ErrorAction SilentlyContinue + $isFixture = $fixture -and $fixture.Path -eq $env:FIXTURE_EXE + try { + if ($env:INSTALL_OUTCOME -eq "success" -and $isFixture -and !$fixture.WaitForExit(15000)) { + Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-install.log") + throw "Windows Installer did not close the registered sunshine.exe fixture." + } + } finally { + if ($isFixture -and !$fixture.HasExited) { + Stop-Process -Id $fixture.Id -Force -ErrorAction SilentlyContinue + $null = $fixture.WaitForExit(10000) + } + Remove-Item -LiteralPath $env:FIXTURE_EXE -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath (Split-Path -Parent $env:FIXTURE_EXE) -ErrorAction SilentlyContinue + } + - name: Enable GitHub Actions evaluation window if: runner.os == 'Windows' shell: pwsh diff --git a/cmake/packaging/windows_wix.cmake b/cmake/packaging/windows_wix.cmake index caacdde4..f0814324 100644 --- a/cmake/packaging/windows_wix.cmake +++ b/cmake/packaging/windows_wix.cmake @@ -12,6 +12,7 @@ set(CPACK_WIX_VERSION 4) set(CPACK_GENERATOR "WIX") set(WIX_VERSION 4.0.4) set(WIX_UI_VERSION 4.0.4) # extension versioning is independent of the WiX version +set(WIX_UTIL_VERSION 4.0.4) set(WIX_BUILD_PARENT_DIRECTORY "${CMAKE_BINARY_DIR}/wix_packaging") set(WIX_BUILD_DIRECTORY "${CPACK_PACKAGE_DIRECTORY}/_CPack_Packages/win64/WIX") @@ -55,6 +56,7 @@ function(libvirtualhid_wix_ensure_extension extension_name extension_version) endfunction() libvirtualhid_wix_ensure_extension(WixToolset.UI.wixext ${WIX_UI_VERSION}) +libvirtualhid_wix_ensure_extension(WixToolset.Util.wixext ${WIX_UTIL_VERSION}) set(CPACK_WIX_ROOT "${WIX_TOOL_PATH}") set(CPACK_WIX_UPGRADE_GUID "71D7B738-9D83-4E57-82E3-C3106D9F8053") @@ -66,7 +68,10 @@ set(CPACK_WIX_PROGRAM_MENU_FOLDER "LizardByte") # https://learn.microsoft.com/en-us/windows/win32/msi/reinstallmode set(CPACK_WIX_PROPERTY_REINSTALLMODE "amus") set(CPACK_WIX_EXTENSIONS - "WixToolset.UI.wixext") + "WixToolset.UI.wixext" + "WixToolset.Util.wixext") +set(CPACK_WIX_CUSTOM_XMLNS + "util=http://wixtoolset.org/schemas/v4/wxs/util") set(CPACK_WIX_PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/wix_resources/libvirtualhid-driver-installer-patch.xml") diff --git a/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml b/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml index 7ae86b6a..e545b2be 100644 --- a/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml +++ b/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml @@ -1,5 +1,9 @@ - + + + + + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string] $MsiPath, + + [string] $FixtureDirectory +) + +$ErrorActionPreference = "Stop" +$resolvedMsi = (Resolve-Path -LiteralPath $MsiPath).Path + +<# +.SYNOPSIS +Reads the first column of a query against a Windows Installer database. +.PARAMETER Database +The read-only Windows Installer database. +.PARAMETER Query +The query whose string results should be returned. +.OUTPUTS +System.String. The first column of each matching record. +#> +function Get-MsiQueryValue { + param($Database, [string] $Query) + + $view = $Database.OpenView($Query) + try { + $null = $view.Execute() + while ($record = $view.Fetch()) { + $record.StringData(1) + } + } finally { + $null = $view.Close() + } +} + +$installer = New-Object -ComObject WindowsInstaller.Installer +$database = $installer.OpenDatabase($resolvedMsi, 0) +$resources = @(Get-MsiQueryValue -Database $database -Query 'SELECT `Resource` FROM `Wix4RestartResource`') +foreach ($resource in @("SunshineService", "sunshine.exe")) { + if ($resource -notin $resources) { + throw "The MSI does not register $resource with Restart Manager." + } +} +$dialog = Get-MsiQueryValue -Database $database -Query "SELECT ``Dialog`` FROM ``Dialog`` WHERE ``Dialog`` = 'MsiRMFilesInUse'" +if (-not $dialog) { + throw "The MSI does not contain the native Restart Manager applications-to-close dialog." +} +$action = Get-MsiQueryValue -Database $database -Query ( + "SELECT ``Action`` FROM ``CustomAction`` WHERE ``Target`` = 'WixRegisterRestartResources'" +) +if (-not $action) { + throw "The MSI does not contain the Restart Manager registration action." +} +$registrationSequence = Get-MsiQueryValue -Database $database -Query ( + "SELECT ``Sequence`` FROM ``InstallExecuteSequence`` WHERE ``Action`` = '$action'" +) +$validationSequence = Get-MsiQueryValue -Database $database -Query ( + "SELECT ``Sequence`` FROM ``InstallExecuteSequence`` WHERE ``Action`` = 'InstallValidate'" +) +if (-not $registrationSequence -or [int] $registrationSequence -ge [int] $validationSequence) { + throw "Restart resources must be registered before InstallValidate displays files in use." +} + +Write-Information "The MSI registers Sunshine before its native applications-to-close dialog." -InformationAction Continue + +if ($FixtureDirectory) { + New-Item -ItemType Directory -Path $FixtureDirectory | Out-Null + $fixtureExecutable = Join-Path $FixtureDirectory "sunshine.exe" + Add-Type -OutputAssembly $fixtureExecutable -OutputType ConsoleApplication -TypeDefinition @' +public static class SunshineRestartManagerFixture { + public static void Main() { + System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite); + } +} +'@ + $fixtureProcess = Start-Process -FilePath $fixtureExecutable -PassThru -WindowStyle Hidden + if ($fixtureProcess.WaitForExit(250)) { + throw "The Sunshine fixture exited before the MSI could detect it." + } + $fixtureProcess.Id +}