diff --git a/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 b/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 index 22714c8..de75a5d 100644 --- a/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 @@ -184,22 +184,39 @@ foreach ($app in $config.Applications) { Write-PreInstallLog " InstallArgs: $($app.InstallArgs)" } + # Per-app verbose msiexec log path - written by /L*v on MSI installs and tailed + # into the runner log on failure so we don't need to dig through C:\Logs manually. + $safeName = $app.Name -replace '[^a-zA-Z0-9]','_' + $msiLog = Join-Path $logDir "msi-$safeName.log" + if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue } + try { - # Start without -Wait so we can poll. UDC_Setup.exe in particular hangs forever - # after install (it spawns UDC.exe and hides its main window without exiting), - # so we can't rely on the process exiting on its own. + # We use [System.Diagnostics.Process]::Start() directly instead of Start-Process + # because PowerShell 5.1's Start-Process -PassThru has a bug where the returned + # Process object's OS handle is disposed when control returns to the script, + # causing ExitCode to always read as $null even after WaitForExit() succeeds. + # Calling Process.Start() directly returns a Process object with a live handle + # that exposes ExitCode correctly. We poll HasExited so UDC_Setup.exe (which + # hangs forever after spawning a hidden WPF window) can be killed via the + # detection-during-install path. + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden + if ($app.Type -eq "MSI") { - $msiArgs = "/i `"$installerPath`"" + $psi.FileName = "msiexec.exe" + $psi.Arguments = "/i `"$installerPath`"" if ($app.InstallArgs) { - $msiArgs += " " + $app.InstallArgs + $psi.Arguments += " " + $app.InstallArgs } - $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList $msiArgs -PassThru -NoNewWindow + $psi.Arguments += " /L*v `"$msiLog`"" + Write-PreInstallLog " msiexec verbose log: $msiLog" } elseif ($app.Type -eq "EXE") { + $psi.FileName = $installerPath if ($app.InstallArgs) { - $proc = Start-Process -FilePath $installerPath -ArgumentList $app.InstallArgs -PassThru -NoNewWindow - } else { - $proc = Start-Process -FilePath $installerPath -PassThru -NoNewWindow + $psi.Arguments = $app.InstallArgs } } else { @@ -208,6 +225,8 @@ foreach ($app in $config.Applications) { continue } + $proc = [System.Diagnostics.Process]::Start($psi) + # Poll for completion: process exit OR detection success (whichever happens first) $timeoutSec = 600 # 10 min hard cap per app $pollInterval = 5 @@ -256,16 +275,59 @@ foreach ($app in $config.Applications) { Write-PreInstallLog " SUCCESS (verified via detection during install)" $installed++ } - elseif ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010) { - Write-PreInstallLog " Exit code $($proc.ExitCode) after $elapsed s - SUCCESS" - if ($proc.ExitCode -eq 3010) { - Write-PreInstallLog " (Reboot required for $($app.Name))" - } - $installed++ - } else { - Write-PreInstallLog " Exit code $($proc.ExitCode) - FAILED" "ERROR" - $failed++ + # Start-Process -PassThru returns a Process object whose ExitCode is null + # until WaitForExit() is called - even if HasExited is already true. Without + # this, every install that goes through the exit-code branch gets reported + # as "Exit code - FAILED" (empty) regardless of the real result. + try { $proc.WaitForExit() } catch { } + $exitCode = $proc.ExitCode + + if ($exitCode -eq 0 -or $exitCode -eq 3010) { + Write-PreInstallLog " Exit code $exitCode after $elapsed s - SUCCESS" + if ($exitCode -eq 3010) { + Write-PreInstallLog " (Reboot required for $($app.Name))" + } + $installed++ + } + else { + Write-PreInstallLog " Exit code $exitCode - FAILED" "ERROR" + # Surface the *meaningful* lines from the verbose msiexec log (not just + # the tail, which is rollback/cleanup noise). The actual root-cause lines + # are: MSI error notes (Note: 1: ), failed action returns (return + # value 3), errors logged by custom actions, and any line containing the + # word "Error". Grep these out so the runner log shows the smoking gun. + if ($app.Type -eq "MSI" -and (Test-Path $msiLog)) { + Write-PreInstallLog " --- meaningful lines from $msiLog ---" + $patterns = @( + 'Note: 1: ', + 'return value 3', + 'Error \d+\.', + 'CustomAction .* returned actual error', + 'Failed to ', + 'Installation failed', + 'Calling custom action', + '1: 2262', # Database not found + '1: 2203', # Cannot open database + '1: 2330' # Insufficient permissions + ) + $regex = ($patterns -join '|') + $matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue | + Select-Object -First 30 + if ($matches) { + foreach ($m in $matches) { + Write-PreInstallLog " $($m.Line.Trim())" + } + } else { + Write-PreInstallLog " (no error patterns matched - falling back to last 25 lines)" + Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object { + Write-PreInstallLog " $_" + } + } + Write-PreInstallLog " --- end MSI log scan ---" + } + $failed++ + } } } catch { Write-PreInstallLog " Install threw: $_" "ERROR"