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
}