Private/New-StandaloneInstallShim.ps1
|
<#
.SYNOPSIS Generates a production-grade, self-healing Install.ps1 script to bundle inside an Intune Win32 package. .DESCRIPTION Creates an installation script that handles NT AUTHORITY\SYSTEM AppX registration, environment variable preparation (%APPDATA%, %LOCALAPPDATA%, %USERPROFILE%), Default User HKCU Hive mounting for per-user settings propagation, Global\_MSIExecute mutex lock detection, process tree monitoring, and clean taskkill termination. #> function New-StandaloneInstallShim { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$PackageId, [Parameter()] [string]$CustomArgs = '', [Parameter()] [string]$Scope = 'machine', [Parameter()] [int[]]$SuccessCodes = @(0, 3010, 1641) ) $scriptTemplate = @' <# .SYNOPSIS Standalone Intune Win32 App Installer for __PACKAGE_ID__ Generated by WingetIntune with Process Tree Watchdog, Mutex Protection, & Default User HKCU Mount #> [CmdletBinding()] param() $packageId = '__PACKAGE_ID__' $scope = '__SCOPE__' $customArgs = '__CUSTOM_ARGS__' # 1. Initialize SYSTEM Environment Directories (Prevents Missing %APPDATA% / %LOCALAPPDATA% crashes) $systemAppdataLocal = "C:\Windows\System32\config\systemprofile\AppData\Local" $systemAppdataRoaming = "C:\Windows\System32\config\systemprofile\AppData\Roaming" $systemTemp = "C:\Windows\Temp" foreach ($dir in @($systemAppdataLocal, $systemAppdataRoaming, $systemTemp)) { if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } } if (-not $env:LOCALAPPDATA) { $env:LOCALAPPDATA = $systemAppdataLocal } if (-not $env:APPDATA) { $env:APPDATA = $systemAppdataRoaming } if (-not $env:TEMP) { $env:TEMP = $systemTemp } if (-not $env:TMP) { $env:TMP = $systemTemp } # 2. Mount Default User Hive (Enables HKCU registry write propagation for new users) $defaultUserDat = "C:\Users\Default\NTUSER.DAT" $hiveLoaded = $false if (Test-Path $defaultUserDat) { try { reg.exe load "HKU\DefaultUser" "$defaultUserDat" 2>$null | Out-Null $hiveLoaded = $true } catch { } } # 3. Initialize Logging $logDir = "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs" if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } $logFile = Join-Path $logDir "WingetIntune-$packageId-Install.log" Start-Transcript -Path $logFile -Append -Force Write-Output "[$((Get-Date).ToString('o'))] Starting installation for $packageId (Scope: $scope)..." # 4. Wait for Global\_MSIExecute Mutex (Avoid Error 1618 Collisions) $mutexTimeoutSec = 120 $sw = [System.Diagnostics.Stopwatch]::StartNew() while ($sw.Elapsed.TotalSeconds -lt $mutexTimeoutSec) { $mutex = $null $isLocked = $false try { if ([System.Threading.Mutex]::TryOpenExisting("Global\_MSIExecute", [ref]$mutex)) { $isLocked = $true if ($mutex) { $mutex.Dispose() } } } catch { } if (-not $isLocked) { break } Write-Output "Windows Installer mutex (Global\_MSIExecute) is busy. Waiting for release..." Start-Sleep -Seconds 5 } $sw.Stop() # 5. Locate winget.exe Engine $wingetPath = $null if (Get-Command 'winget.exe' -ErrorAction SilentlyContinue) { $wingetPath = (Get-Command 'winget.exe').Source } if (-not $wingetPath) { $appxPaths = @(Get-ChildItem -Path 'C:\Program Files\WindowsApps' -Directory -Filter 'Microsoft.DesktopAppInstaller_*_x64__8wekyb3d8bbwe' -ErrorAction SilentlyContinue) if ($appxPaths.Count -gt 0) { $candidate = Join-Path $appxPaths[-1].FullName 'winget.exe' if (Test-Path $candidate) { $wingetPath = $candidate } } } if (-not $wingetPath) { $userCandidate = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\winget.exe' if (Test-Path $userCandidate) { $wingetPath = $userCandidate } } if (-not $wingetPath) { Write-Error "Winget executable not found on system. Aborting installation." if ($hiveLoaded) { [gc]::Collect(); reg.exe unload "HKU\DefaultUser" 2>$null | Out-Null } Stop-Transcript exit 1603 } # 6. Launch Installer with Process Tree Watchdog (Prevents Fork-and-Exit Trap) $params = @( 'install', '--exact', '--id', $packageId, '--source', 'winget', '--accept-package-agreements', '--accept-source-agreements', '--scope', $scope, '--disable-interactivity' ) if ($customArgs) { $params += $customArgs.Split(' ') } Write-Output "Executing: $wingetPath $($params -join ' ')" $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $wingetPath $psi.Arguments = ($params -join ' ') $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $process = [System.Diagnostics.Process]::Start($psi) $parentPid = $process.Id # Active Process Tree Watchdog $maxWaitMinutes = 15 $watchdogSw = [System.Diagnostics.Stopwatch]::StartNew() $timedOut = $false while (-not $process.HasExited) { if ($watchdogSw.Elapsed.TotalMinutes -ge $maxWaitMinutes) { $timedOut = $true break } Start-Sleep -Seconds 3 } # Ensure all child/grandchild installer processes have completed before exiting if (-not $timedOut) { $childWaitSec = 60 $childSw = [System.Diagnostics.Stopwatch]::StartNew() while ($childSw.Elapsed.TotalSeconds -lt $childWaitSec) { $childProcesses = @(Get-CimInstance -ClassName Win32_Process -Filter "ParentProcessId = $parentPid" -ErrorAction SilentlyContinue) if ($childProcesses.Count -eq 0) { break } Write-Output "Waiting for descendant installer process ($($childProcesses[0].ProcessId)) to finish..." Start-Sleep -Seconds 3 } } # 7. Unload Default User Hive if ($hiveLoaded) { [gc]::Collect() reg.exe unload "HKU\DefaultUser" 2>$null | Out-Null } if ($timedOut) { Write-Error "Installation timed out after $maxWaitMinutes minutes in Session 0. Terminating process tree..." taskkill.exe /F /T /PID $parentPid 2>$null | Out-Null Stop-Transcript exit 1603 } $exitCode = $process.ExitCode Write-Output "Winget installation completed with Exit Code: $exitCode" Stop-Transcript switch ($exitCode) { 0 { exit 0 } 3010 { exit 3010 } 1641 { exit 1641 } Default { exit $exitCode } } '@ $content = $scriptTemplate.Replace('__PACKAGE_ID__', $PackageId).Replace('__SCOPE__', $Scope).Replace('__CUSTOM_ARGS__', $CustomArgs) return $content } |