mirror of
https://github.com/LizardByte/libvirtualhid.git
synced 2026-10-23 03:27:34 +09:00
349 lines
9.2 KiB
PowerShell
349 lines
9.2 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Validates that the installed libvirtualhid Windows driver starts and can expose a gamepad child device.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string] $HardwareId = "ROOT\LIBVIRTUALHID",
|
|
|
|
[string] $ControlDevicePath = "\\.\LibVirtualHid",
|
|
|
|
[string] $GamepadAdapterPath,
|
|
|
|
[ValidateSet("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch")]
|
|
[Alias("Profile")]
|
|
[string] $GamepadProfile = "xseries",
|
|
|
|
[int] $HoldSeconds = 12,
|
|
|
|
[int] $DeviceStartTimeoutSeconds = 20
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function Invoke-PnPUtil {
|
|
param([string[]] $Arguments)
|
|
|
|
$output = @(pnputil.exe @Arguments 2>&1)
|
|
$exitCode = $LASTEXITCODE
|
|
|
|
if ($exitCode -ne 0) {
|
|
throw "pnputil.exe $($Arguments -join ' ') exited with code $exitCode`n$($output -join "`n")"
|
|
}
|
|
|
|
return $output
|
|
}
|
|
|
|
function ConvertTo-PnPUtilDeviceRecord {
|
|
param([string] $InstanceId)
|
|
|
|
return @{
|
|
InstanceId = $InstanceId.Trim()
|
|
DeviceDescription = $null
|
|
DriverName = $null
|
|
HardwareIds = @()
|
|
Status = $null
|
|
ProblemCode = $null
|
|
ProblemStatus = $null
|
|
}
|
|
}
|
|
|
|
function ConvertFrom-PnPUtilDeviceLine {
|
|
param(
|
|
[hashtable] $Record,
|
|
[string] $Line,
|
|
[string] $Section
|
|
)
|
|
|
|
$singleValueLabels = @{
|
|
"Device Description" = "DeviceDescription"
|
|
"Driver Name" = "DriverName"
|
|
"Status" = "Status"
|
|
"Problem Code" = "ProblemCode"
|
|
"Problem Status" = "ProblemStatus"
|
|
}
|
|
|
|
if ($Line -match "^\s{0,2}([^:]+):\s*(.*)\s*$") {
|
|
$label = $Matches[1].Trim()
|
|
$value = $Matches[2].Trim()
|
|
if ($label -eq "Hardware IDs") {
|
|
if ($value) {
|
|
$Record.HardwareIds += $value
|
|
}
|
|
return "HardwareIds"
|
|
}
|
|
|
|
if ($singleValueLabels.ContainsKey($label)) {
|
|
$Record[$singleValueLabels[$label]] = $value
|
|
}
|
|
return $null
|
|
}
|
|
|
|
if ($Section -eq "HardwareIds" -and $Line -match "^\s+(.+)\s*$") {
|
|
$Record.HardwareIds += $Matches[1].Trim()
|
|
}
|
|
|
|
return $Section
|
|
}
|
|
|
|
function ConvertFrom-PnPUtilDeviceOutput {
|
|
param([string[]] $Output)
|
|
|
|
$records = @()
|
|
$current = $null
|
|
foreach ($line in $Output) {
|
|
if ($line -match "^\s*Instance ID:\s*(.+)\s*$") {
|
|
if ($current) {
|
|
$records += [pscustomobject] $current
|
|
}
|
|
$current = ConvertTo-PnPUtilDeviceRecord -InstanceId $Matches[1]
|
|
$section = $null
|
|
continue
|
|
}
|
|
|
|
if (-not $current) {
|
|
continue
|
|
}
|
|
|
|
$section = ConvertFrom-PnPUtilDeviceLine -Record $current -Line $line -Section $section
|
|
}
|
|
|
|
if ($current) {
|
|
$records += [pscustomobject] $current
|
|
}
|
|
|
|
return $records
|
|
}
|
|
|
|
function Write-PnPRecordVerbose {
|
|
param([object] $Record)
|
|
|
|
Write-Verbose "Matched device: $($Record.InstanceId)"
|
|
if ($Record.DeviceDescription) {
|
|
Write-Verbose " Description: $($Record.DeviceDescription)"
|
|
}
|
|
if ($Record.Status) {
|
|
Write-Verbose " Status: $($Record.Status)"
|
|
}
|
|
if ($Record.DriverName) {
|
|
Write-Verbose " Driver Name: $($Record.DriverName)"
|
|
}
|
|
if ($Record.HardwareIds) {
|
|
Write-Verbose " Hardware IDs: $($Record.HardwareIds -join ', ')"
|
|
}
|
|
if ($Record.ProblemCode) {
|
|
Write-Verbose " Problem Code: $($Record.ProblemCode)"
|
|
}
|
|
if ($Record.ProblemStatus) {
|
|
Write-Verbose " Problem Status: $($Record.ProblemStatus)"
|
|
}
|
|
}
|
|
|
|
function Get-PnPUtilDevicesByDeviceId {
|
|
param([string] $DeviceId)
|
|
|
|
$output = Invoke-PnPUtil -Arguments @(
|
|
"/enum-devices",
|
|
"/deviceids",
|
|
"/drivers"
|
|
)
|
|
$matchingDevices = @(ConvertFrom-PnPUtilDeviceOutput -Output $output |
|
|
Where-Object {
|
|
$_.InstanceId -like "$DeviceId\*" -or
|
|
$_.HardwareIds -contains $DeviceId
|
|
}
|
|
)
|
|
foreach ($device in $matchingDevices) {
|
|
Write-PnPRecordVerbose -Record $device
|
|
}
|
|
|
|
return $matchingDevices
|
|
}
|
|
|
|
function Assert-StartedPnPRecord {
|
|
param(
|
|
[object] $Record,
|
|
[string] $Description
|
|
)
|
|
|
|
if ($Record.Status -ne "Started") {
|
|
$details = @(
|
|
"$Description did not report Status: Started.",
|
|
"Instance ID: $($Record.InstanceId)",
|
|
"Status: $($Record.Status)"
|
|
)
|
|
if ($Record.ProblemCode) {
|
|
$details += "Problem Code: $($Record.ProblemCode)"
|
|
}
|
|
if ($Record.ProblemStatus) {
|
|
$details += "Problem Status: $($Record.ProblemStatus)"
|
|
}
|
|
|
|
throw ($details -join "`n")
|
|
}
|
|
}
|
|
|
|
function Assert-RootDeviceStarted {
|
|
param([string] $TargetHardwareId)
|
|
|
|
$rootDevices = @(Get-PnPUtilDevicesByDeviceId -DeviceId $TargetHardwareId)
|
|
if (-not $rootDevices) {
|
|
throw "No installed libvirtualhid root device was found for $TargetHardwareId."
|
|
}
|
|
|
|
foreach ($device in $rootDevices) {
|
|
Assert-StartedPnPRecord -Record $device -Description "Root device $($device.InstanceId)"
|
|
}
|
|
}
|
|
|
|
function Assert-ControlDeviceOpen {
|
|
param([string] $Path)
|
|
|
|
try {
|
|
$stream = [System.IO.File]::Open(
|
|
$Path,
|
|
[System.IO.FileMode]::Open,
|
|
[System.IO.FileAccess]::ReadWrite,
|
|
[System.IO.FileShare]::ReadWrite
|
|
)
|
|
$stream.Dispose()
|
|
} catch {
|
|
throw "Could not open ${Path}: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
function Get-ExpectedGamepadHardwareId {
|
|
param([string] $ProfileName)
|
|
|
|
switch ($ProfileName) {
|
|
"generic" { return @("HID\VID_1209&PID_0001") }
|
|
"xone" { return @("HID\VID_045E&PID_02EA&IG_00") }
|
|
"xseries" { return @("HID\VID_045E&PID_0B12&IG_00") }
|
|
"ds4" { return @("HID\VID_054C&PID_05C4") }
|
|
"ds5" { return @("HID\VID_054C&PID_0CE6") }
|
|
"switch" { return @("HID\VID_057E&PID_2009") }
|
|
}
|
|
|
|
throw "Unsupported profile: $ProfileName"
|
|
}
|
|
|
|
function Wait-ForStartedGamepadChild {
|
|
param(
|
|
[string] $ProfileName,
|
|
[int] $TimeoutSeconds
|
|
)
|
|
|
|
$deviceIds = @(Get-ExpectedGamepadHardwareId -ProfileName $ProfileName)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
$latestRecords = @()
|
|
|
|
do {
|
|
$latestRecords = @()
|
|
foreach ($deviceId in $deviceIds) {
|
|
$latestRecords += @(Get-PnPUtilDevicesByDeviceId -DeviceId $deviceId)
|
|
}
|
|
|
|
$started = $latestRecords |
|
|
Where-Object {
|
|
$_.Status -eq "Started" -and
|
|
$_.DriverName -ne "hidvhf.inf" -and
|
|
$_.DeviceDescription -ne "Virtual HID Framework (VHF) HID device"
|
|
} |
|
|
Select-Object -First 1
|
|
if ($started) {
|
|
Write-Information "Gamepad child device started: $($started.InstanceId) ($($started.DriverName))" -InformationAction Continue
|
|
return
|
|
}
|
|
|
|
Start-Sleep -Milliseconds 500
|
|
} while ((Get-Date) -lt $deadline)
|
|
|
|
if (-not $latestRecords) {
|
|
throw "No gamepad child device was found for $($deviceIds -join ', ')."
|
|
}
|
|
|
|
foreach ($record in $latestRecords) {
|
|
Assert-StartedPnPRecord -Record $record -Description "Gamepad child device $($deviceIds -join ', ')"
|
|
}
|
|
}
|
|
|
|
function Wait-ForStartedXbox360Companion {
|
|
param([int] $TimeoutSeconds)
|
|
|
|
$hardwareId = "LIBVIRTUALHID_XBOX360"
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
$latestRecords = @()
|
|
do {
|
|
$latestRecords = @(Get-PnPUtilDevicesByDeviceId -DeviceId $hardwareId)
|
|
$started = $latestRecords |
|
|
Where-Object { $_.Status -eq "Started" } |
|
|
Select-Object -First 1
|
|
if ($started) {
|
|
Write-Information "Xbox 360 XUSB companion started: $($started.InstanceId) ($($started.DriverName))" -InformationAction Continue
|
|
return
|
|
}
|
|
Start-Sleep -Milliseconds 500
|
|
} while ((Get-Date) -lt $deadline)
|
|
|
|
if (-not $latestRecords) {
|
|
throw "No Xbox 360 XUSB companion was found for $hardwareId."
|
|
}
|
|
foreach ($record in $latestRecords) {
|
|
Assert-StartedPnPRecord -Record $record -Description "Xbox 360 XUSB companion $($record.InstanceId)"
|
|
}
|
|
}
|
|
|
|
function Invoke-GamepadAdapterSmoke {
|
|
param(
|
|
[string] $Path,
|
|
[string] $ProfileName,
|
|
[int] $HoldSeconds,
|
|
[int] $DeviceStartTimeoutSeconds
|
|
)
|
|
|
|
if (-not $Path) {
|
|
return
|
|
}
|
|
|
|
$resolvedGamepadAdapterPath = (Resolve-Path -LiteralPath $Path).Path
|
|
$stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-gamepad-adapter.out"
|
|
$stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-gamepad-adapter.err"
|
|
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
|
|
|
$process = Start-Process `
|
|
-FilePath $resolvedGamepadAdapterPath `
|
|
-ArgumentList @($ProfileName, "--hold-seconds", "$HoldSeconds") `
|
|
-PassThru `
|
|
-RedirectStandardOutput $stdoutPath `
|
|
-RedirectStandardError $stderrPath `
|
|
-WindowStyle Hidden
|
|
|
|
try {
|
|
Start-Sleep -Seconds 2
|
|
if ($process.HasExited) {
|
|
$stdout = Get-Content -LiteralPath $stdoutPath -Raw -ErrorAction SilentlyContinue
|
|
$stderr = Get-Content -LiteralPath $stderrPath -Raw -ErrorAction SilentlyContinue
|
|
throw "gamepad_adapter exited with code $($process.ExitCode).`nstdout:`n$stdout`nstderr:`n$stderr"
|
|
}
|
|
|
|
if ($ProfileName -eq "x360") {
|
|
Wait-ForStartedXbox360Companion -TimeoutSeconds $DeviceStartTimeoutSeconds
|
|
} else {
|
|
Wait-ForStartedGamepadChild -ProfileName $ProfileName -TimeoutSeconds $DeviceStartTimeoutSeconds
|
|
}
|
|
} finally {
|
|
if (-not $process.HasExited) {
|
|
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
|
Wait-Process -Id $process.Id -Timeout 5 -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
Assert-RootDeviceStarted -TargetHardwareId $HardwareId
|
|
Assert-ControlDeviceOpen -Path $ControlDevicePath
|
|
Invoke-GamepadAdapterSmoke `
|
|
-Path $GamepadAdapterPath `
|
|
-ProfileName $GamepadProfile `
|
|
-HoldSeconds $HoldSeconds `
|
|
-DeviceStartTimeoutSeconds $DeviceStartTimeoutSeconds
|