Improve driver injection error handling and resilience

Enhances the driver installation process to be more resilient by allowing non-critical driver injection failures to not halt the entire deployment. Key improvements include:

- Adds `IgnoreExitCode` and `PassThruExitCode` parameters to the process invocation function, enabling callers to handle non-zero exit codes without throwing exceptions.

- Modifies driver injection logic (both WIM and folder-based) to capture exit codes and log warnings instead of failing the deployment when drivers fail to inject.

- Automatically collects and preserves diagnostic logs (dism.log and setupapi.offline.log) to the USB drive when driver installation encounters issues, aiding post-deployment troubleshooting.

- Wraps cleanup operations in try-catch blocks to ensure temporary resources are released even if unmounting or deletion fails.

- Fixes code formatting inconsistencies and indentation throughout the script for improved readability.

This approach prioritizes deployment completion while preserving critical diagnostic information when driver-related issues occur.
This commit is contained in:
rbalsleyMSFT
2026-01-28 18:10:18 -08:00
parent 02e429d99d
commit 7231f620c8
+224 -81
View File
@@ -23,10 +23,10 @@ function Get-HardDrive() {
if ($manufacturer -eq 'Microsoft Corporation' -and $model -eq 'Virtual Machine') { if ($manufacturer -eq 'Microsoft Corporation' -and $model -eq 'Virtual Machine') {
WriteLog 'Running in a Hyper-V VM. Getting virtual disk on Index 0 and SCSILogicalUnit 0' WriteLog 'Running in a Hyper-V VM. Getting virtual disk on Index 0 and SCSILogicalUnit 0'
$diskDriveCandidates = @(Get-CimInstance -Class 'Win32_DiskDrive' | Where-Object { $_.MediaType -eq 'Fixed hard disk media' ` $diskDriveCandidates = @(Get-CimInstance -Class 'Win32_DiskDrive' | Where-Object { $_.MediaType -eq 'Fixed hard disk media' `
-and $_.Model -eq 'Microsoft Virtual Disk' -and $_.Model -eq 'Microsoft Virtual Disk' `
-and $_.Index -eq 0 ` -and $_.Index -eq 0 `
-and $_.SCSILogicalUnit -eq 0 -and $_.SCSILogicalUnit -eq 0
}) })
} }
else { else {
WriteLog 'Not running in a VM. Getting physical disk drive' WriteLog 'Not running in a VM. Getting physical disk drive'
@@ -74,7 +74,13 @@ function Invoke-Process {
[Parameter()] [Parameter()]
[ValidateNotNullOrEmpty()] [ValidateNotNullOrEmpty()]
[string]$ArgumentList [string]$ArgumentList,
[Parameter()]
[switch]$IgnoreExitCode,
[Parameter()]
[switch]$PassThruExitCode
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@@ -96,19 +102,39 @@ function Invoke-Process {
$cmd = Start-Process @startProcessParams $cmd = Start-Process @startProcessParams
$cmdOutput = Get-Content -Path $stdOutTempFile -Raw $cmdOutput = Get-Content -Path $stdOutTempFile -Raw
$cmdError = Get-Content -Path $stdErrTempFile -Raw $cmdError = Get-Content -Path $stdErrTempFile -Raw
if ($cmd.ExitCode -ne 0) { if ($cmd.ExitCode -ne 0) {
# Non-terminating mode: capture output to Scriptlog and continue
if ($IgnoreExitCode) {
if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
WriteLog $cmdOutput
}
if ([string]::IsNullOrEmpty($cmdError) -eq $false) {
WriteLog $cmdError
}
if ($PassThruExitCode) {
return $cmd.ExitCode
}
return
}
if ($cmdError) { if ($cmdError) {
throw $cmdError.Trim() throw $cmdError.Trim()
} }
if ($cmdOutput) { if ($cmdOutput) {
throw $cmdOutput.Trim() throw $cmdOutput.Trim()
} }
throw "Process failed. ExitCode = $($cmd.ExitCode)."
} }
else { else {
if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) { if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
WriteLog $cmdOutput WriteLog $cmdOutput
} }
} }
if ($PassThruExitCode) {
return $cmd.ExitCode
}
} }
} }
catch { catch {
@@ -744,67 +770,67 @@ function Test-DriverFolderHasInstallableContent {
return $false return $false
} }
catch { catch {
WriteLog "Failed to inspect driver folder '$Path': $($_.Exception.Message)" WriteLog "Failed to inspect driver folder '$Path': $($_.Exception.Message)"
return $false return $false
}
}
function Get-AvailableDriveLetter {
$usedLetters = (Get-PSDrive -PSProvider FileSystem).Name | ForEach-Object { $_.ToUpperInvariant() }
for ($ascii = [int][char]'Z'; $ascii -ge [int][char]'A'; $ascii--) {
$candidate = [char]$ascii
if ($usedLetters -notcontains $candidate) {
return $candidate
} }
} }
return $null
}
function Get-AvailableDriveLetter { function New-DriverSubstMapping {
$usedLetters = (Get-PSDrive -PSProvider FileSystem).Name | ForEach-Object { $_.ToUpperInvariant() } [CmdletBinding()]
for ($ascii = [int][char]'Z'; $ascii -ge [int][char]'A'; $ascii--) { param(
$candidate = [char]$ascii [Parameter(Mandatory = $true)]
if ($usedLetters -notcontains $candidate) { [string]$SourcePath
return $candidate )
}
} $resolvedPath = (Resolve-Path -Path $SourcePath -ErrorAction Stop).Path
return $null $driveLetter = Get-AvailableDriveLetter
if ($null -eq $driveLetter) {
throw 'No drive letters are available for SUBST mapping.'
} }
$driveName = "$driveLetter`:"
$mappedPath = "$driveLetter`:\"
WriteLog "Mapping driver folder '$resolvedPath' to $driveName with SUBST."
$escapedPath = $resolvedPath -replace '"', '""'
$arguments = "/c subst $driveName `"$escapedPath`""
Invoke-Process -FilePath cmd.exe -ArgumentList $arguments
return [PSCustomObject]@{
DriveLetter = $driveLetter
DriveName = $driveName
DrivePath = $mappedPath
}
}
function New-DriverSubstMapping { function Remove-DriverSubstMapping {
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$SourcePath [string]$DriveLetter
) )
$resolvedPath = (Resolve-Path -Path $SourcePath -ErrorAction Stop).Path $driveName = "$DriveLetter`:"
$driveLetter = Get-AvailableDriveLetter WriteLog "Removing SUBST drive $driveName"
if ($null -eq $driveLetter) { try {
throw 'No drive letters are available for SUBST mapping.' $arguments = "/c subst $driveName /d"
}
$driveName = "$driveLetter`:"
$mappedPath = "$driveLetter`:\"
WriteLog "Mapping driver folder '$resolvedPath' to $driveName with SUBST."
$escapedPath = $resolvedPath -replace '"', '""'
$arguments = "/c subst $driveName `"$escapedPath`""
Invoke-Process -FilePath cmd.exe -ArgumentList $arguments Invoke-Process -FilePath cmd.exe -ArgumentList $arguments
return [PSCustomObject]@{
DriveLetter = $driveLetter
DriveName = $driveName
DrivePath = $mappedPath
}
} }
catch {
function Remove-DriverSubstMapping { WriteLog "Failed to remove SUBST drive $($driveName): $_"
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$DriveLetter
)
$driveName = "$DriveLetter`:"
WriteLog "Removing SUBST drive $driveName"
try {
$arguments = "/c subst $driveName /d"
Invoke-Process -FilePath cmd.exe -ArgumentList $arguments
}
catch {
WriteLog "Failed to remove SUBST drive $($driveName): $_"
}
} }
}
#Get USB Drive and create log file #Get USB Drive and create log file
$LogFileName = 'ScriptLog.txt' $LogFileName = 'ScriptLog.txt'
$USBDrive = Get-USBDrive $USBDrive = Get-USBDrive
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
@@ -856,11 +882,11 @@ else {
foreach ($currentDisk in $diskDriveCandidates) { foreach ($currentDisk in $diskDriveCandidates) {
$sizeGB = [math]::Round(($currentDisk.Size / 1GB), 2) $sizeGB = [math]::Round(($currentDisk.Size / 1GB), 2)
$displayList += [PSCustomObject]@{ $displayList += [PSCustomObject]@{
Disk = $currentDisk.Index Disk = $currentDisk.Index
'Size (GB)' = $sizeGB 'Size (GB)' = $sizeGB
'Sector' = $currentDisk.BytesPerSector 'Sector' = $currentDisk.BytesPerSector
'Bus Type' = $currentDisk.InterfaceType 'Bus Type' = $currentDisk.InterfaceType
Model = $currentDisk.Model Model = $currentDisk.Model
} }
} }
$displayList | Format-Table -AutoSize -Property Disk, 'Size (GB)', Sector, 'Bus Type', Model $displayList | Format-Table -AutoSize -Property Disk, 'Size (GB)', Sector, 'Bus Type', Model
@@ -1577,64 +1603,181 @@ if ($null -ne $DriverSourcePath) {
Write-Host "Installing drivers from WIM: $DriverSourcePath" Write-Host "Installing drivers from WIM: $DriverSourcePath"
$TempDriverDir = "W:\TempDrivers" $TempDriverDir = "W:\TempDrivers"
try { try {
# Create working folder for WIM-based drivers
WriteLog "Creating temporary directory for drivers at $TempDriverDir" WriteLog "Creating temporary directory for drivers at $TempDriverDir"
New-Item -Path $TempDriverDir -ItemType Directory -Force | Out-Null New-Item -Path $TempDriverDir -ItemType Directory -Force | Out-Null
# Mount the driver WIM read-only so DISM can recurse the extracted INF tree
WriteLog "Mounting WIM contents to $TempDriverDir" WriteLog "Mounting WIM contents to $TempDriverDir"
Write-Host "Mounting WIM contents to $TempDriverDir" Write-Host "Mounting WIM contents to $TempDriverDir"
# For some reason can't use /mount-image with invoke-process, so using dism.exe directly # For some reason can't use /mount-image with invoke-process, so using dism.exe directly
dism.exe /Mount-Image /ImageFile:$DriverSourcePath /Index:1 /MountDir:$TempDriverDir /ReadOnly /optimize dism.exe /Mount-Image /ImageFile:$DriverSourcePath /Index:1 /MountDir:$TempDriverDir /ReadOnly /optimize
$mountExitCode = $LASTEXITCODE
if ($mountExitCode -ne 0) {
throw "DISM WIM mount failed. LastExitCode = $mountExitCode."
}
WriteLog "WIM mount successful." WriteLog "WIM mount successful."
# Inject drivers into the offline Windows image; failures here should not stop deployment
WriteLog "Injecting drivers from $TempDriverDir" WriteLog "Injecting drivers from $TempDriverDir"
Write-Host "Injecting drivers from $TempDriverDir" Write-Host "Injecting drivers from $TempDriverDir"
Write-Host "This may take a while, please be patient." Write-Host "This may take a while, please be patient."
Invoke-Process dism.exe "/image:W:\ /Add-Driver /Driver:""$TempDriverDir"" /Recurse" $driverInjectExitCode = Invoke-Process -FilePath dism.exe -ArgumentList "/image:W:\ /Add-Driver /Driver:""$TempDriverDir"" /Recurse" -IgnoreExitCode -PassThruExitCode
WriteLog "Driver injection from WIM succeeded." if ($driverInjectExitCode -ne 0) {
Write-Host "Driver injection from WIM succeeded." $warningMessage = "Warning: One or more drivers failed to inject from WIM. ExitCode = $driverInjectExitCode. Continuing deployment."
WriteLog $warningMessage
Write-Host $warningMessage -ForegroundColor Yellow
# Copy setupapi.offline.log to the USB drive when driver injection fails
$setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log'
if (Test-Path -Path $setupApiLogPath) {
try {
Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y"
}
catch {
WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive. "
}
}
else {
WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath"
}
}
else {
WriteLog "Driver injection from WIM succeeded."
Write-Host "Driver injection from WIM succeeded."
}
} }
catch { catch {
WriteLog "An error occurred during WIM driver installation: $_" $warningMessage = "Warning: An error occurred during WIM driver installation. Continuing deployment."
# Copy DISM log to USBDrive for debugging WriteLog $warningMessage
invoke-process xcopy.exe "X:\Windows\logs\dism\dism.log $USBDrive /Y" Write-Host $warningMessage -ForegroundColor Yellow
throw $_
# Copy troubleshooting logs to the USB drive when driver installation fails
try {
Invoke-Process cmd.exe "/c copy /Y ""X:\Windows\logs\dism\dism.log"" ""$($USBDrive)dism_driverinject.log"""
}
catch {
WriteLog "Warning: Failed to copy dism.log to $USBDrive."
}
$setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log'
if (Test-Path -Path $setupApiLogPath) {
try {
Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y"
}
catch {
WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive."
}
}
else {
WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath"
}
} }
finally { finally {
if (Test-Path -Path $TempDriverDir) { if (Test-Path -Path $TempDriverDir) {
# Always attempt to unmount and clean up; unmount failures should not stop deployment
WriteLog "Unmounting WIM from $TempDriverDir" WriteLog "Unmounting WIM from $TempDriverDir"
Write-Host "Unmounting WIM from $TempDriverDir" Write-Host "Unmounting WIM from $TempDriverDir"
Invoke-Process dism.exe "/Unmount-Image /MountDir:""$TempDriverDir"" /Discard" try {
WriteLog "Unmount successful." Invoke-Process dism.exe "/Unmount-Image /MountDir:""$TempDriverDir"" /Discard"
Write-Host "Unmount successful." WriteLog "Unmount successful."
Write-Host "Unmount successful."
}
catch {
$warningMessage = "Warning: Failed to unmount WIM from $TempDriverDir. Continuing cleanup."
WriteLog $warningMessage
Write-Host $warningMessage -ForegroundColor Yellow
}
WriteLog "Cleaning up temporary driver directory: $TempDriverDir" WriteLog "Cleaning up temporary driver directory: $TempDriverDir"
Write-Host "Cleaning up temporary driver directory: $TempDriverDir" Write-Host "Cleaning up temporary driver directory: $TempDriverDir"
Remove-Item -Path $TempDriverDir -Recurse -Force try {
WriteLog "Cleanup successful." Remove-Item -Path $TempDriverDir -Recurse -Force
Write-Host "Cleanup successful." WriteLog "Cleanup successful."
Write-Host "Cleanup successful."
}
catch {
$warningMessage = "Warning: Failed to clean up temporary driver directory: $TempDriverDir."
WriteLog $warningMessage
Write-Host $warningMessage -ForegroundColor Yellow
}
} }
} }
} }
elseif ($DriverSourceType -eq 'Folder') { elseif ($DriverSourceType -eq 'Folder') {
$substMapping = $null $substMapping = $null
try { try {
# Use SUBST to shorten long paths for DISM /Add-Driver
$substMapping = New-DriverSubstMapping -SourcePath $DriverSourcePath $substMapping = New-DriverSubstMapping -SourcePath $DriverSourcePath
$shortDriverPath = $substMapping.DrivePath $shortDriverPath = $substMapping.DrivePath
WriteLog "Injecting drivers from folder via SUBST. Source: $DriverSourcePath, Mapped: $($substMapping.DriveName)" WriteLog "Injecting drivers from folder via SUBST. Source: $DriverSourcePath, Mapped: $($substMapping.DriveName)"
Write-Host "Injecting drivers from folder: $shortDriverPath" Write-Host "Injecting drivers from folder: $shortDriverPath"
Write-Host "This may take a while, please be patient." Write-Host "This may take a while, please be patient."
Invoke-Process dism.exe "/image:W:\ /Add-Driver /Driver:$shortDriverPath /Recurse"
WriteLog "Driver injection from folder succeeded." # Inject drivers into the offline Windows image; failures here should not stop deployment
Write-Host "Driver injection from folder succeeded." $driverInjectExitCode = Invoke-Process -FilePath dism.exe -ArgumentList "/image:W:\ /Add-Driver /Driver:$shortDriverPath /Recurse" -IgnoreExitCode -PassThruExitCode
if ($driverInjectExitCode -ne 0) {
$warningMessage = "Warning: One or more drivers failed to inject from folder. ExitCode = $driverInjectExitCode. Continuing deployment."
WriteLog $warningMessage
Write-Host $warningMessage -ForegroundColor Yellow
# Copy setupapi.offline.log to the USB drive when driver injection fails
$setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log'
if (Test-Path -Path $setupApiLogPath) {
try {
Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y"
}
catch {
WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive. "
}
}
else {
WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath"
}
}
else {
WriteLog "Driver injection from folder succeeded."
Write-Host "Driver injection from folder succeeded."
}
} }
catch { catch {
WriteLog "An error occurred during folder driver installation: $_" $warningMessage = "Warning: An error occurred during folder driver installation. Continuing deployment."
Invoke-Process xcopy.exe "X:\Windows\logs\dism\dism.log $USBDrive /Y" WriteLog $warningMessage
throw $_ Write-Host $warningMessage -ForegroundColor Yellow
# Copy troubleshooting logs to the USB drive when driver installation fails
try {
Invoke-Process xcopy.exe "X:\Windows\logs\dism\dism.log $USBDrive /Y"
}
catch {
WriteLog "Warning: Failed to copy dism.log to $USBDrive."
}
$setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log'
if (Test-Path -Path $setupApiLogPath) {
try {
Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y"
}
catch {
WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive."
}
}
else {
WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath"
}
} }
finally { finally {
# Always attempt to remove SUBST mapping; failures here should not stop deployment
if ($null -ne $substMapping) { if ($null -ne $substMapping) {
Remove-DriverSubstMapping -DriveLetter $substMapping.DriveLetter try {
Remove-DriverSubstMapping -DriveLetter $substMapping.DriveLetter
}
catch {
$warningMessage = "Warning: Failed to remove SUBST mapping $($substMapping.DriveLetter). Continuing deployment."
WriteLog $warningMessage
Write-Host $warningMessage -ForegroundColor Yellow
}
} }
} }
} }