Refactor MSI extraction for improved reliability

Implements a dedicated mutex to serialize MSI extraction operations. This prevents race conditions when multiple driver packages are processed in parallel by different tasks.

Adds a post-extraction verification step to ensure the target directory is not empty. This guards against silent failures where `msiexec` exits successfully but extracts no files, triggering a retry if necessary.
This commit is contained in:
rbalsleyMSFT
2025-06-24 13:01:23 -07:00
parent 54dad486a4
commit 4f5445a833
@@ -232,39 +232,48 @@ function Save-MicrosoftDriversTask {
### EXTRACT ### EXTRACT
if ($fileExtension -eq ".msi") { if ($fileExtension -eq ".msi") {
$status = "Extracting MSI..." # Set initial status $status = "Waiting for MSI lock..." # Set initial status
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
# Loop indefinitely to wait for mutex and handle MSIExec exit codes by catching errors # Use a named mutex to ensure only one MSI extraction happens at a time across all parallel tasks
$msiMutexName = "Global\FFUDevelopmentMSIExtractionMutex"
$msiMutex = New-Object System.Threading.Mutex($false, $msiMutexName)
try {
WriteLog "Waiting to acquire global MSI extraction lock for '$modelName'..."
$msiMutex.WaitOne() | Out-Null
WriteLog "Acquired global MSI extraction lock for '$modelName'."
# Loop indefinitely to wait for system mutex and handle MSIExec exit codes
while ($true) { while ($true) {
$mutexClear = $false $mutexClear = $false
# 1. Check Mutex # 1. Check System-level MSI Mutex
try { try {
$Mutex = [System.Threading.Mutex]::OpenExisting("Global\_MSIExecute") $sysMutex = [System.Threading.Mutex]::OpenExisting("Global\_MSIExecute")
$Mutex.Dispose() $sysMutex.Dispose()
$status = "Waiting for MSIExec..." $status = "Waiting for MSIExec..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
WriteLog "Another MSIExec installer is running (Mutex Held). Waiting 5 seconds before rechecking for $modelName..." WriteLog "Another MSIExec installer is running (System Mutex Held). Waiting 5 seconds before rechecking for $modelName..."
Start-Sleep -Seconds 5 Start-Sleep -Seconds 5
continue # Go back to start of while loop to re-check mutex continue # Go back to start of while loop to re-check mutex
} }
catch [System.Threading.WaitHandleCannotBeOpenedException] { catch [System.Threading.WaitHandleCannotBeOpenedException] {
# Mutex is clear, proceed to extraction attempt # Mutex is clear, proceed to extraction attempt
WriteLog "Mutex clear. Proceeding with MSI extraction attempt for $modelName." WriteLog "System MSI mutex clear. Proceeding with MSI extraction attempt for $modelName."
$status = "Extracting MSI..." $status = "Extracting MSI..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$mutexClear = $true $mutexClear = $true
} }
catch { catch {
# Handle other potential errors when checking the mutex # Handle other potential errors when checking the mutex
WriteLog "Warning: Error checking MSIExec mutex for $($modelName): $_. Proceeding with caution." WriteLog "Warning: Error checking system MSI mutex for $($modelName): $_. Proceeding with caution."
$status = "Extracting MSI (Mutex Error)..." $status = "Extracting MSI (Mutex Error)..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status } if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
$mutexClear = $true # Proceed despite mutex error $mutexClear = $true # Proceed despite mutex error
} }
# 2. Attempt Extraction (only if mutex was clear or error occurred during check) # 2. Attempt Extraction (only if mutex was clear)
if ($mutexClear) { if ($mutexClear) {
WriteLog "Extracting MSI file to $modelPath" WriteLog "Extracting MSI file to $modelPath"
$arguments = "/a `"$($filePath)`" /qn TARGETDIR=`"$($modelPath)`"" $arguments = "/a `"$($filePath)`" /qn TARGETDIR=`"$($modelPath)`""
@@ -274,6 +283,18 @@ function Save-MicrosoftDriversTask {
# If Invoke-Process succeeded (didn't throw), extraction is complete. # If Invoke-Process succeeded (didn't throw), extraction is complete.
WriteLog "Extraction complete for $modelName (Exit Code 0)." WriteLog "Extraction complete for $modelName (Exit Code 0)."
# Verification Step: Ensure the target folder is not empty.
$itemsInDest = Get-ChildItem -Path $modelPath -Recurse
if ($itemsInDest.Count -eq 0) {
WriteLog "VERIFICATION FAILED: MSI extraction for '$modelName' produced an empty folder. Retrying..."
$status = "Retrying (Empty Folder)"
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status $status }
Start-Sleep -Seconds 5
continue # Retry the whole process
}
WriteLog "VERIFICATION PASSED: Target folder for '$modelName' is not empty."
break # Success, exit the while loop break # Success, exit the while loop
} }
catch { catch {
@@ -294,7 +315,15 @@ function Save-MicrosoftDriversTask {
} }
} }
} # End if ($mutexClear) } # End if ($mutexClear)
} # End while ($true) - Loop runs until break or throw } # End while ($true)
}
finally {
if ($null -ne $msiMutex) {
$msiMutex.ReleaseMutex()
$msiMutex.Dispose()
WriteLog "Released global MSI extraction lock for '$modelName'."
}
}
} }
elseif ($fileExtension -eq ".zip") { elseif ($fileExtension -eq ".zip") {
$status = "Extracting ZIP..." # Set status before extraction $status = "Extracting ZIP..." # Set status before extraction