Public/Test-PsGadgetEnvironment.ps1
|
# Test-PsGadgetEnvironment.ps1 # Diagnostic command to verify the PsGadget environment and hardware readiness function Test-PsGadgetEnvironment { <# .SYNOPSIS Checks this environment and reports whether PsGadget hardware is ready. .DESCRIPTION Verifies the PowerShell version, .NET runtime, FTDI driver/DLL state, native library presence (Linux/macOS), and connected devices. Default output is a clean summary. Use -Verbose for per-device hints and next-step commands you can copy directly into your session. .EXAMPLE Test-PsGadgetEnvironment .EXAMPLE Test-PsGadgetEnvironment -Verbose .OUTPUTS PSCustomObject with Platform, Backend, Devices, DeviceCount, and IsReady properties. Use the return value to script conditional setup logic. #> [CmdletBinding()] [OutputType([PSCustomObject])] param() $runningOnWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT' $runningOnMacOS = (-not $runningOnWindows) -and (& { try { (& uname -s 2>$null).Trim() -eq 'Darwin' } catch { $false } }) $psVersion = $PSVersionTable.PSVersion $dotnet = [System.Environment]::Version $platform = if ($runningOnWindows) { 'Windows' } elseif ($runningOnMacOS) { 'macOS' } else { 'Linux' } # ------------------------------------------------------------------ # Determine active backend from module-scope flags set at import time # ------------------------------------------------------------------ $backendName = 'Stub (no hardware access)' $backendOk = $false if ($script:IotBackendAvailable) { $backendName = 'IoT (Iot.Device.Bindings / .NET 8+)' $backendOk = $true } elseif ($script:D2xxLoaded) { $backendName = 'D2XX (FTD2XX_NET.dll)' $backendOk = $true } $readySuffix = if ($backendOk) { ' - Ready' } else { ' - hardware commands unavailable' } $backendLabel = "$backendName$readySuffix" Write-Verbose "PS version : $psVersion" Write-Verbose ".NET version: $dotnet" Write-Verbose "Platform : $platform" Write-Verbose "IotBackend : $($script:IotBackendAvailable)" Write-Verbose "D2xxLoaded : $($script:D2xxLoaded)" # ------------------------------------------------------------------ # Native library check # ------------------------------------------------------------------ $nativeStatus = 'N/A (Windows)' $nativeOk = $true $nativePath = $null if ($runningOnWindows) { # On Windows the bundled lib/native/FTD2XX.dll is the native D2XX driver. # Locate it so NativeLibPath is populated in the return object. $moduleRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $windowsDll = Join-Path $moduleRoot 'lib\native\FTD2XX.dll' if (([System.IO.FileInfo]::new($windowsDll)).Exists) { $nativePath = $windowsDll } } if (-not $runningOnWindows) { $moduleNet8Dir = Join-Path (Join-Path $PSScriptRoot '..') 'lib/net8' $moduleNet8Dir = [System.IO.Path]::GetFullPath($moduleNet8Dir) if ($runningOnMacOS) { $nativeLibLocations = @( (Join-Path $moduleNet8Dir 'libftd2xx.dylib'), '/usr/local/lib/libftd2xx.dylib', '/usr/lib/libftd2xx.dylib' ) } else { $nativeLibLocations = @( # Local copy inside lib/net8/ - always readable by pwsh regardless of snap confinement (Join-Path $moduleNet8Dir 'libftd2xx.so'), '/usr/local/lib/libftd2xx.so', '/usr/lib/libftd2xx.so', '/usr/lib/x86_64-linux-gnu/libftd2xx.so', '/usr/lib/aarch64-linux-gnu/libftd2xx.so', '/usr/lib/arm-linux-gnueabihf/libftd2xx.so' ) } # Use [System.IO.FileInfo]::Exists instead of Test-Path. # Snap-confined pwsh overrides Test-Path with a provider that returns $true # for paths outside the snap tree even when those files are not accessible # to .NET P/Invoke or bash. FileInfo.Exists uses System.IO stat(), which # matches what the runtime actually sees. $nativePath = $nativeLibLocations | Where-Object { try { ([System.IO.FileInfo]::new($_)).Exists } catch { $false } } | Select-Object -First 1 if ($nativePath) { $nativeStatus = "[OK] $nativePath" $nativeOk = $true } else { $nativeLibName = if ($runningOnMacOS) { 'libftd2xx.dylib' } else { 'libftd2xx.so' } $nativeStatus = "[MISSING] $nativeLibName not found" $nativeOk = $false } if ($runningOnMacOS) { # Check if AppleUSBFTDI kext is claiming the device try { $kextOut = & kextstat 2>$null if ($kextOut -match 'AppleUSBFTDI') { $nativeStatus += ' [AppleUSBFTDI loaded]' Write-Verbose 'AppleUSBFTDI kext is loaded - it may claim VCP devices before D2XX can open them.' Write-Verbose 'To unload: sudo kextunload -b com.apple.driver.AppleUSBFTDI' } } catch {} if (-not $nativeOk) { Write-Verbose 'libftd2xx.dylib is required for FTDI hardware access on macOS.' Write-Verbose 'Download the D2XX macOS package from: https://ftdichip.com/drivers/d2xx-drivers/' Write-Verbose 'Open the DMG and run the installer, or: sudo cp libftd2xx.dylib /usr/local/lib/' Write-Verbose 'NOTE: macOS ships AppleUSBFTDI which claims FTDI devices before D2XX can open them.' Write-Verbose 'After installing libftd2xx.dylib, unload it with: sudo kextunload -b com.apple.driver.AppleUSBFTDI' } } else { # Check if ftdi_sio is blocking D2XX access (Linux only) try { $lsmodOut = & lsmod 2>/dev/null if ($lsmodOut -match 'ftdi_sio') { $nativeStatus += ' [ftdi_sio loaded]' Write-Verbose 'ftdi_sio kernel module is loaded - it claims VCP devices before D2XX can open them.' Write-Verbose 'If hardware does not respond: sudo rmmod ftdi_sio' Write-Verbose 'To make the change permanent: echo "blacklist ftdi_sio" | sudo tee /etc/modprobe.d/ftdi-psgadget.conf' } } catch {} if (-not $nativeOk) { Write-Verbose 'libftd2xx.so is required for FTDI hardware access on Linux.' Write-Verbose 'Download from: https://ftdichip.com/drivers/d2xx-drivers/' Write-Verbose 'Install: sudo cp libftd2xx.so /usr/local/lib && sudo ldconfig' } } } # ------------------------------------------------------------------ # Enumerate connected devices # ------------------------------------------------------------------ $devices = @() $deviceNote = 'None found' try { $devices = @(Get-PsGadgetFtdi -ErrorAction SilentlyContinue) if ($devices.Count -gt 0) { $deviceNote = "$($devices.Count) device(s) found" } } catch { $deviceNote = "Enumeration failed: $($_.Exception.Message)" Write-Verbose "Device enumeration error: $($_.Exception.Message)" } # ------------------------------------------------------------------ # Config check # ------------------------------------------------------------------ $userHome = [Environment]::GetFolderPath('UserProfile') $configPath = Join-Path $userHome (Join-Path '.psgadget' 'config.json') $configOk = Test-Path $configPath $configNote = if ($configOk) { "[OK] $configPath" } else { '[MISSING] Run Set-PsGadgetConfig to create one' } # ------------------------------------------------------------------ # Print summary block # ------------------------------------------------------------------ $line = '-' * 52 Write-Host '' Write-Host 'PsGadget Setup Check' Write-Host $line Write-Host ("Platform : {0} / PS {1} / .NET {2}" -f $platform, $psVersion, $dotnet) Write-Host ("Driver : {0}" -f $backendLabel) if (-not $runningOnWindows) { Write-Host ("Native lib: {0}" -f $nativeStatus) } Write-Host ("Devices : {0}" -f $deviceNote) Write-Host ("Config : {0}" -f $configNote) Write-Host $line # Per-device detail rows foreach ($dev in $devices) { $caps = Get-FtdiChipCapabilities -TypeName $dev.Type Write-Host (" [{0}] {1,-10} SN={2,-14} GPIO={3}" -f ` $dev.Index, $dev.Type, $dev.SerialNumber, $caps.GpioMethod) Write-Verbose (" Pins : {0}" -f $caps.GpioPins) if ($caps.CapabilityNote) { Write-Verbose (" Note : {0}" -f $caps.CapabilityNote) } # Actionable next-step hints if ($dev.SerialNumber) { Write-Verbose (" Connect : `$dev = New-PsGadgetFtdi -SerialNumber '{0}'" -f $dev.SerialNumber) } elseif ($dev.LocationId) { Write-Verbose (" Connect : `$dev = New-PsGadgetFtdi -LocationId '{0}'" -f $dev.LocationId) } else { Write-Verbose (" Connect : `$dev = New-PsGadgetFtdi -Index {0}" -f $dev.Index) } if ($caps.HasMpsse) { Write-Verbose (" I2C scan: `$dev.ScanI2CBus()") Write-Verbose (" Display : `$dev.Display('Hello world', 0)") } } # Pad before status line if ($devices.Count -gt 0) { Write-Host '' } # Backend guidance when in stub mode if (-not $backendOk) { Write-Verbose 'No FTDI backend loaded. Re-import with: Remove-Module PSGadget; Import-Module PSGadget -Verbose' Write-Verbose 'Verbose import output shows exactly which DLL paths were tried.' } # ------------------------------------------------------------------ # Detect snap-confined PowerShell (causes GLIBC mismatch on import) # ------------------------------------------------------------------ $isSnapPwsh = $false if (-not $runningOnWindows) { $snapEnv = [System.Environment]::GetEnvironmentVariable('SNAP') if ($snapEnv) { $isSnapPwsh = $true } else { try { $pwshPath = (Get-Process -Id $PID -ErrorAction SilentlyContinue).MainModule.FileName if ($pwshPath -and $pwshPath -like '*/snap/*') { $isSnapPwsh = $true } } catch {} } } # Overall status $isReady = $backendOk -and $nativeOk -and ($devices.Count -gt 0) # Derive Status / Reason / NextStep for structured return if ($isReady) { $resultStatus = 'OK' $resultReason = 'All checks passed' $resultNextStep = 'Run: Get-PsGadgetFtdi | Format-Table' } elseif (-not $backendOk -and $isSnapPwsh) { $resultStatus = 'Fail' $resultReason = 'snap-confined pwsh: GLIBC mismatch prevents libftd2xx.so from loading (snap bundled glibc is older than library requirement)' $resultNextStep = 'exit from this session then use non-snap PowerShell (apt-get install -y powershell)then run `$ /usr/bin/pwsh` instead of `$ powershell` (snap alias)' Write-Verbose 'snap-confined pwsh detected. The snap sandbox bundles an older glibc that is' Write-Verbose 'incompatible with the libftd2xx.so in lib/net8/. Two options:' Write-Verbose ' A) Switch to non-snap PowerShell:' Write-Verbose ' sudo apt-get install -y powershell' Write-Verbose ' /usr/bin/pwsh (not the snap alias)' Write-Verbose ' B) Replace lib/net8/libftd2xx.so with an older build (compiled for glibc <= 2.35):' Write-Verbose ' cd /tmp && wget https://ftdichip.com/wp-content/uploads/2024/04/libftd2xx-linux-x86_64-1.4.30.tgz' Write-Verbose ' tar xzf libftd2xx-linux-x86_64-1.4.30.tgz' Write-Verbose ' cp linux-x86_64/libftd2xx.so.1.4.30 <module-root>/lib/net8/libftd2xx.so' } elseif (-not $backendOk) { $resultStatus = 'Fail' $resultReason = 'No FTDI backend loaded' $resultNextStep = 'Remove-Module PSGadget; Import-Module PSGadget -Verbose' } elseif (-not $nativeOk) { $resultStatus = 'Fail' if ($runningOnMacOS) { $resultReason = 'Native FTDI library not found (libftd2xx.dylib)' $resultNextStep = 'Download D2XX macOS package from https://ftdichip.com/drivers/d2xx-drivers/ then: sudo cp libftd2xx.dylib /usr/local/lib/' } else { $resultReason = 'Native FTDI library not found (libftd2xx.so)' $resultNextStep = 'Download from https://ftdichip.com/drivers/d2xx-drivers/ then: sudo cp libftd2xx.so /usr/local/lib && sudo ldconfig' } } else { $resultStatus = 'Fail' $resultReason = 'No FTDI devices detected' $resultNextStep = 'Connect an FTDI device and retry, or run Test-PsGadgetEnvironment -Verbose for diagnostics' } $statusLabel = if ($isReady) { 'READY' } else { 'NOT READY - run with -Verbose for details' } Write-Host ("Status : {0}" -f $statusLabel) if (-not $isReady) { Write-Host ("Next step : {0}" -f $resultNextStep) } Write-Host '' if ($isReady) { Write-Verbose 'All checks passed. Hardware is ready.' Write-Verbose 'Quick start: Get-PsGadgetFtdi | Format-Table' Write-Verbose 'Then: $dev = New-PsGadgetFtdi -SerialNumber <SN>' } return [PSCustomObject]@{ Status = $resultStatus Reason = $resultReason NextStep = $resultNextStep IsSnapPwsh = $isSnapPwsh Platform = $platform PsVersion = $psVersion.ToString() DotNetVersion = $dotnet.ToString() Backend = $backendLabel BackendReady = $backendOk NativeLibOk = $nativeOk NativeLibPath = $nativePath Devices = $devices DeviceCount = $devices.Count ConfigPresent = $configOk IsReady = $isReady } } # Backward-compatibility alias Set-Alias -Name 'Test-PsGadgetSetup' -Value 'Test-PsGadgetEnvironment' |