Runtime/Invoke-OSDAppPreInstall.ps1
|
[CmdletBinding()] param( [Parameter(Mandatory)][string]$StagedPath, [ValidateRange(1,120)][int]$OfficeRefreshTimeoutMinutes = 20 ) $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Net.Http -ErrorAction Stop $manifestPath = Join-Path $StagedPath 'DeviceManifest.json' $logPath = [Environment]::ExpandEnvironmentVariables('%ProgramData%\OSDApps\Logs\Runtime.log') $logDirectory = Split-Path -Path $logPath -Parent $preInstallStopwatch = [System.Diagnostics.Stopwatch]::StartNew() function Write-PreInstallLog { param([string]$Event,[ValidateSet('Info','Warning','Error')][string]$Level='Info',[string]$Message,[hashtable]$Data) New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null $type = if ($Level -eq 'Warning') { 2 } elseif ($Level -eq 'Error') { 3 } else { 1 } $details = @() if ($Data) { foreach ($key in @($Data.Keys | Sort-Object)) { $value=$Data[$key]; if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) { $value=@($value)-join ',' }; $details += ('{0}={1}' -f $key,$value) } } $msg = if ($Message) { "[$Event] $Message" } else { "[$Event]" } if ($details.Count -gt 0) { $msg += ' | ' + ($details -join '; ') } $msg = $msg -replace '\]LOG\]!>', ']LOG]! >' $now=(Get-Date).ToUniversalTime(); $time=$now.ToString('HH:mm:ss.fff') + '+000'; $date=$now.ToString('MM-dd-yyyy'); $thread=[System.Threading.Thread]::CurrentThread.ManagedThreadId $line='<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="PreInstall" context="" type="{3}" thread="{4}" file="">' -f $msg,$time,$date,$type,$thread Add-Content -LiteralPath $logPath -Value $line -Encoding UTF8 } function Test-InternetEndpoint { param([Parameter(Mandatory)][string]$Uri,[int]$TimeoutSec=10) try { Invoke-WebRequest -Uri $Uri -Method Head -UseBasicParsing -TimeoutSec $TimeoutSec -ErrorAction Stop | Out-Null; return $true } catch { return $false } } function Get-OfficeCacheVersion { param([Parameter(Mandatory)][string]$OfficeRoot) $dataPath=Join-Path $OfficeRoot 'Office\Data' if (-not (Test-Path -LiteralPath $dataPath -PathType Container)) { return $null } $versions=@(Get-ChildItem -LiteralPath $dataPath -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | ForEach-Object { try { [pscustomobject]@{Name=$_.Name;Version=[version]$_.Name} } catch{} } | Sort-Object Version -Descending) if ($versions.Count -gt 0) { return $versions[0].Name } return $null } function Get-TeamsPackageVersion { param([Parameter(Mandatory)][string]$Path) Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue $archive=[System.IO.Compression.ZipFile]::OpenRead($Path) try { $entry=$archive.Entries | Where-Object { $_.FullName -eq 'AppxManifest.xml' } | Select-Object -First 1 if (-not $entry) { throw 'AppxManifest.xml was not found in the Teams MSIX.' } $reader=New-Object System.IO.StreamReader($entry.Open()) try { [xml]$m=$reader.ReadToEnd() } finally { $reader.Dispose() } return [string]$m.Package.Identity.Version } finally { $archive.Dispose() } } function Get-RemoteMetadata { param([Parameter(Mandatory)][string]$Uri,[int]$TimeoutSec=15) $handler=[System.Net.Http.HttpClientHandler]::new(); $handler.AllowAutoRedirect=$true $client=[System.Net.Http.HttpClient]::new($handler); $client.Timeout=[TimeSpan]::FromSeconds($TimeoutSec) try { $request=[System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Head,$Uri) $response=$client.SendAsync($request).GetAwaiter().GetResult() try { $response.EnsureSuccessStatusCode() | Out-Null [pscustomobject]@{ ETag=if($response.Headers.ETag){[string]$response.Headers.ETag.Tag}else{$null} LastModified=if($response.Content.Headers.LastModified){$response.Content.Headers.LastModified.UtcDateTime.ToString('o')}else{$null} ContentLength=if($response.Content.Headers.ContentLength){[int64]$response.Content.Headers.ContentLength}else{$null} FinalUri=[string]$response.RequestMessage.RequestUri.AbsoluteUri } } finally { $response.Dispose(); $request.Dispose() } } finally { $client.Dispose(); $handler.Dispose() } } function Save-PreInstallDownload { param( [Parameter(Mandatory)][string]$Uri, [Parameter(Mandatory)][string]$DestinationPath, [Parameter(Mandatory)][string]$Id, [Parameter(Mandatory)][string]$Description, [int]$TimeoutMinutes = 30 ) $directory = Split-Path -Path $DestinationPath -Parent if ($directory) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } $tempPath = "$DestinationPath.download" if (Test-Path -LiteralPath $tempPath) { Remove-Item -LiteralPath $tempPath -Force } $handler = [System.Net.Http.HttpClientHandler]::new() $handler.AllowAutoRedirect = $true $client = [System.Net.Http.HttpClient]::new($handler) $client.Timeout = [TimeSpan]::FromMinutes($TimeoutMinutes) $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() Write-PreInstallLog -Event 'DownloadStart' -Message "Downloading $Description." -Data @{ Id=$Id; Uri=$Uri; Destination=$DestinationPath } try { $response = $client.GetAsync($Uri, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult() try { $response.EnsureSuccessStatusCode() | Out-Null $totalBytes = $response.Content.Headers.ContentLength $source = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() $target = [System.IO.FileStream]::new( $tempPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None ) try { $buffer = New-Object byte[] (1024 * 1024) [int64]$downloaded = 0 while (($read = $source.Read($buffer, 0, $buffer.Length)) -gt 0) { $target.Write($buffer, 0, $read) $downloaded += $read } } finally { $target.Dispose() $source.Dispose() } } finally { $response.Dispose() } if (Test-Path -LiteralPath $DestinationPath) { Remove-Item -LiteralPath $DestinationPath -Force } Move-Item -LiteralPath $tempPath -Destination $DestinationPath -Force $stopwatch.Stop() $bytes = (Get-Item -LiteralPath $DestinationPath).Length $seconds = [math]::Max($stopwatch.Elapsed.TotalSeconds, 0.001) $averageMBps = [math]::Round(($bytes / 1MB) / $seconds, 1) Write-PreInstallLog -Event 'DownloadComplete' -Message "$Description download completed." -Data @{ Id=$Id Bytes=$bytes SizeMB=[math]::Round($bytes / 1MB, 1) DurationSeconds=[math]::Round($seconds, 1) AverageMBps=$averageMBps Destination=$DestinationPath } } catch { if (Test-Path -LiteralPath $tempPath) { Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue } throw } finally { if ($stopwatch.IsRunning) { $stopwatch.Stop() } $client.Dispose() $handler.Dispose() } } function Copy-DirectoryReplace { param([Parameter(Mandatory)][string]$Source,[Parameter(Mandatory)][string]$Destination) New-Item -ItemType Directory -Path (Split-Path $Destination -Parent) -Force | Out-Null $temp="$Destination.refresh" if(Test-Path -LiteralPath $temp){Remove-Item -LiteralPath $temp -Recurse -Force} Copy-Item -LiteralPath $Source -Destination $temp -Recurse -Force if(Test-Path -LiteralPath $Destination){Remove-Item -LiteralPath $Destination -Recurse -Force} Move-Item -LiteralPath $temp -Destination $Destination -Force } # Avoid PowerShell's self-copy error when the OSDCloud USB cache is absent. # In that case the acquisition directory is already the local staged directory. function Copy-OfficeConfigurationToCache { [CmdletBinding()] param( [Parameter(Mandatory)][string]$LocalConfig, [Parameter(Mandatory)][string]$AcquireConfig ) $sourcePath = [System.IO.Path]::GetFullPath($LocalConfig) $destinationPath = [System.IO.Path]::GetFullPath($AcquireConfig) if ([string]::Equals($sourcePath, $destinationPath, [System.StringComparison]::OrdinalIgnoreCase)) { return } Copy-Item -LiteralPath $LocalConfig -Destination $AcquireConfig -Force -ErrorAction Stop } function Test-LocalOfficeSource { param([string]$Root) return ((Test-Path -LiteralPath (Join-Path $Root 'setup.exe') -PathType Leaf) -and (Test-Path -LiteralPath (Join-Path $Root 'configuration.xml') -PathType Leaf) -and (Test-Path -LiteralPath (Join-Path $Root 'Office\Data') -PathType Container)) } function Test-LocalTeamsSource { param([string]$Root) return ((Test-Path -LiteralPath (Join-Path $Root 'teamsbootstrapper.exe') -PathType Leaf) -and (Test-Path -LiteralPath (Join-Path $Root 'teams.msix') -PathType Leaf)) } function Test-LocalAdobeAcrobatUnifiedSource { param([string]$Root) return (Test-Path -LiteralPath (Join-Path $Root 'Package.zip') -PathType Leaf) } function Test-LocalMsiSource { param([string]$Root) return (Test-Path -LiteralPath (Join-Path $Root 'Package.msi') -PathType Leaf) } function Sync-PreInstallVendorMsi { param( [Parameter(Mandatory)][psobject]$App, [Parameter(Mandatory)][string]$RelativeRoot, [Parameter(Mandatory)][string]$PackageUri, [Parameter(Mandatory)][string]$DisplayName ) $id = [string]$App.Id $localRoot = Join-Path $StagedPath $RelativeRoot New-Item -ItemType Directory -Path $localRoot -Force | Out-Null $acquireRoot = if ($usbRoot) { Join-Path $usbRoot $RelativeRoot } else { $localRoot } New-Item -ItemType Directory -Path $acquireRoot -Force | Out-Null if ($usbRoot -and -not (Test-LocalMsiSource -Root $localRoot) -and (Test-LocalMsiSource -Root $acquireRoot)) { Copy-Item -LiteralPath (Join-Path $acquireRoot 'Package.msi') -Destination (Join-Path $localRoot 'Package.msi') -Force if (Test-Path -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -PathType Leaf) { Copy-Item -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -Destination (Join-Path $localRoot 'CacheInfo.json') -Force } Write-PreInstallLog -Event 'BuiltInCacheRestaged' -Message "Existing $DisplayName USB cache was staged locally." -Data @{ Id=$id } } if (-not $networkAvailable) { if (Test-LocalMsiSource -Root $localRoot) { Write-PreInstallLog -Event 'BuiltInRefreshSkipped' -Level 'Warning' -Message "No network is available. Existing staged $DisplayName payload will be used." -Data @{ Id=$id } return } throw "$DisplayName has no staged or USB-cached payload and no network connection is available to acquire one." } try { $cacheInfoPath = Join-Path $acquireRoot 'CacheInfo.json' $cacheInfo = $null if (Test-Path -LiteralPath $cacheInfoPath -PathType Leaf) { try { $cacheInfo = Get-Content -LiteralPath $cacheInfoPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { } } $packagePath = Join-Path $acquireRoot 'Package.msi' Write-PreInstallLog -Event 'BuiltInRefreshStart' -Message "Checking $DisplayName cache for updates." -Data @{ Id=$id; Target=if($usbRoot){'USB'}else{'Local'} } $remote = Get-RemoteMetadata -Uri $PackageUri $matches = $false if ((Test-Path -LiteralPath $packagePath -PathType Leaf) -and $cacheInfo) { if ($remote.ETag -and $cacheInfo.RemoteETag) { $matches = $remote.ETag -eq [string]$cacheInfo.RemoteETag } elseif ($remote.LastModified -and $cacheInfo.RemoteLastModified -and $remote.ContentLength -and $cacheInfo.RemoteContentLength) { $matches = ( $remote.LastModified -eq [string]$cacheInfo.RemoteLastModified -and [int64]$remote.ContentLength -eq [int64]$cacheInfo.RemoteContentLength ) } } $updated = $false if (-not $matches) { $tempRoot = Join-Path $StagedPath (Join-Path 'Work\Refresh' $id) New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null $tempPackage = Join-Path $tempRoot 'Package.msi' Save-PreInstallDownload -Uri $PackageUri -DestinationPath $tempPackage -Id $id -Description "$DisplayName MSI" -TimeoutMinutes 15 Copy-Item -LiteralPath $tempPackage -Destination $packagePath -Force $updated = $true } $cacheRecord = [ordered]@{ Id = $id Cached = [bool]$usbRoot Version = 'Current' Architecture = $App.Architecture SourcePolicy = 'Evergreen' SyncMethod = 'HttpMetadata' Updated = $updated PackageUri = $PackageUri RemoteETag = $remote.ETag RemoteLastModified = $remote.LastModified RemoteContentLength = $remote.ContentLength RemoteFinalUri = $remote.FinalUri SyncedAt = (Get-Date).ToUniversalTime().ToString('o') } if ($App.PSObject.Properties.Name -contains 'Channel' -and $App.Channel) { $cacheRecord.Channel = [string]$App.Channel } if ($App.PSObject.Properties.Name -contains 'Language' -and $App.Language) { $cacheRecord.Language = [string]$App.Language } $cacheRecord | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8 if ($usbRoot) { Copy-Item -LiteralPath $packagePath -Destination (Join-Path $localRoot 'Package.msi') -Force Copy-Item -LiteralPath $cacheInfoPath -Destination (Join-Path $localRoot 'CacheInfo.json') -Force } Write-PreInstallLog -Event 'BuiltInRefreshComplete' -Message "$DisplayName content is ready for installation." -Data @{ Id=$id; PackageUpdated=$updated; CacheSynchronized=[bool]$usbRoot; SourcePolicy='Evergreen'; SyncMethod='HttpMetadata'; DurationSeconds=[math]::Round($builtInStopwatch.Elapsed.TotalSeconds,1) } } catch { if (Test-LocalMsiSource -Root $localRoot) { Write-PreInstallLog -Event 'BuiltInRefreshFailed' -Level 'Warning' -Message $_.Exception.Message -Data @{ Id=$id; Fallback='ExistingStagedPayload' } return } throw "$DisplayName acquisition failed and no staged fallback is available. $($_.Exception.Message)" } } try { if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "Device manifest not found: $manifestPath" } $manifest=Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json if (-not $manifest.Runtime) { throw 'Device manifest Runtime configuration is missing.' } if (-not $manifest.Runtime.CacheVolumeLabel) { throw 'Device manifest Runtime.CacheVolumeLabel is missing.' } if (-not $manifest.Runtime.LogPath) { throw 'Device manifest Runtime.LogPath is missing.' } $cacheVolumeLabel = [string]$manifest.Runtime.CacheVolumeLabel $logPath = [Environment]::ExpandEnvironmentVariables([string]$manifest.Runtime.LogPath) $logDirectory = Split-Path -Path $logPath -Parent $apps=if($manifest.PSObject.Properties.Name -contains 'Apps'){@($manifest.Apps)}else{@()} $builtInApps=@($apps | Where-Object { [string]$_.Source -eq 'BuiltIn' }) if($builtInApps.Count -eq 0){Write-PreInstallLog -Event 'RefreshSkipped' -Message 'No built-in applications are staged. Nothing to refresh.'; exit 0} $cacheVolume=Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.FileSystemLabel -eq $cacheVolumeLabel -and $_.DriveLetter } | Select-Object -First 1 $usbRoot=if($cacheVolume){"$($cacheVolume.DriveLetter):\OSDApps"}else{$null} Write-PreInstallLog -Event 'RefreshStart' -Message 'Starting built-in acquisition and refresh before installation.' -Data @{StagedPath=$StagedPath;BuiltInApps=@($builtInApps.Id);CacheAvailable=[bool]$usbRoot;CacheRoot=$usbRoot} if($usbRoot){New-Item -ItemType Directory -Path $usbRoot -Force | Out-Null; Write-PreInstallLog -Event 'CacheVolumeFound' -Message 'OSDCloud USB cache is available and will be used automatically.' -Data @{CacheRoot=$usbRoot}} else{Write-PreInstallLog -Event 'CacheVolumeNotFound' -Message 'No OSDCloud USB cache is connected. Built-ins will be acquired directly to the local Windows runtime.'} $networkAvailable=[System.Net.NetworkInformation.NetworkInterface]::GetIsNetworkAvailable() foreach($app in $builtInApps){ $builtInStopwatch = [System.Diagnostics.Stopwatch]::StartNew() switch($app.Id){ 'Microsoft365Apps' { $localRoot=Join-Path $StagedPath 'BuiltIn\Microsoft365Apps' New-Item -ItemType Directory -Path $localRoot -Force | Out-Null $acquireRoot=if($usbRoot){Join-Path $usbRoot 'BuiltIn\Microsoft365Apps'}else{$localRoot} New-Item -ItemType Directory -Path $acquireRoot -Force | Out-Null $localConfig=Join-Path $localRoot 'configuration.xml' if(-not (Test-Path -LiteralPath $localConfig -PathType Leaf)){throw 'Microsoft 365 Apps configuration.xml is missing from the staged deployment intent.'} if($usbRoot -and -not (Test-LocalOfficeSource -Root $localRoot) -and (Test-LocalOfficeSource -Root $acquireRoot)){ Copy-Item -LiteralPath (Join-Path $acquireRoot 'setup.exe') -Destination (Join-Path $localRoot 'setup.exe') -Force Copy-Item -LiteralPath (Join-Path $acquireRoot 'configuration.xml') -Destination $localConfig -Force if(Test-Path -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -PathType Leaf){Copy-Item -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -Destination (Join-Path $localRoot 'CacheInfo.json') -Force} Copy-DirectoryReplace -Source (Join-Path $acquireRoot 'Office') -Destination (Join-Path $localRoot 'Office') Write-PreInstallLog -Event 'BuiltInCacheRestaged' -Message 'Existing Microsoft 365 Apps USB cache was staged locally.' -Data @{Id='Microsoft365Apps'} } if(-not $networkAvailable){ if(Test-LocalOfficeSource -Root $localRoot){Write-PreInstallLog -Event 'BuiltInRefreshSkipped' -Level 'Warning' -Message 'No network is available. Existing staged Office payload will be used.' -Data @{Id='Microsoft365Apps'}; continue} throw 'Microsoft 365 Apps has no staged or USB-cached payload and no network connection is available to acquire one.' } try { $acquireConfig=Join-Path $acquireRoot 'configuration.xml' Copy-OfficeConfigurationToCache -LocalConfig $localConfig -AcquireConfig $acquireConfig $setupPath=Join-Path $acquireRoot 'setup.exe' $odtUri=if($app.OfficeDeploymentToolUri){[string]$app.OfficeDeploymentToolUri}else{'https://officecdn.microsoft.com/pr/wsus/setup.exe'} if(-not (Test-Path -LiteralPath $setupPath -PathType Leaf)){Save-PreInstallDownload -Uri $odtUri -DestinationPath $setupPath -Id 'Microsoft365Apps' -Description 'Office Deployment Tool' -TimeoutMinutes 5} if(-not (Test-InternetEndpoint -Uri $odtUri)){throw 'Microsoft 365 Apps CDN is not reachable.'} $previous=Get-OfficeCacheVersion -OfficeRoot $acquireRoot Write-PreInstallLog -Event 'BuiltInRefreshStart' -Message 'Synchronizing Microsoft 365 Apps content.' -Data @{Id='Microsoft365Apps';Target=if($usbRoot){'USB'}else{'Local'};PreviousVersion=$previous;TimeoutMinutes=$OfficeRefreshTimeoutMinutes} $process=Start-Process -FilePath $setupPath -ArgumentList @('/download',$acquireConfig) -WorkingDirectory $acquireRoot -PassThru $sw=[System.Diagnostics.Stopwatch]::StartNew() while(-not $process.HasExited){if($sw.Elapsed.TotalMinutes -ge $OfficeRefreshTimeoutMinutes){try{$process.Kill();$process.WaitForExit()}catch{};throw "Office Deployment Tool cache refresh exceeded the $OfficeRefreshTimeoutMinutes minute timeout."};Start-Sleep -Seconds 2;$process.Refresh()} $sw.Stop() if($process.ExitCode -ne 0){throw "Office Deployment Tool exited with code $($process.ExitCode)."} $version=Get-OfficeCacheVersion -OfficeRoot $acquireRoot if(-not $version){throw 'Office acquisition completed but Office\Data does not contain a resolvable version.'} $cacheInfo=[ordered]@{Id='Microsoft365Apps';Cached=[bool]$usbRoot;Version=$version;Architecture=$app.Architecture;Channel=$app.Channel;ProductId=$app.ProductId;Language=@($app.Language);SourcePolicy='Evergreen';SyncMethod='OfficeDeploymentTool';Updated=($previous -ne $version);SyncedAt=(Get-Date).ToUniversalTime().ToString('o')} $cacheInfoPath=Join-Path $acquireRoot 'CacheInfo.json'; $cacheInfo | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8 if($usbRoot){ Copy-Item -LiteralPath $setupPath -Destination (Join-Path $localRoot 'setup.exe') -Force Copy-Item -LiteralPath $acquireConfig -Destination $localConfig -Force Copy-Item -LiteralPath $cacheInfoPath -Destination (Join-Path $localRoot 'CacheInfo.json') -Force Copy-DirectoryReplace -Source (Join-Path $acquireRoot 'Office') -Destination (Join-Path $localRoot 'Office') } $versionChanged = ($previous -ne $version) Write-PreInstallLog -Event 'BuiltInRefreshComplete' -Message 'Microsoft 365 Apps content is ready for installation.' -Data @{Id='Microsoft365Apps';Version=$version;CacheSynchronized=[bool]$usbRoot;VersionChanged=$versionChanged;SourcePolicy='Evergreen';SyncMethod='OfficeDeploymentTool';DurationSeconds=[math]::Round($builtInStopwatch.Elapsed.TotalSeconds,1)} } catch { if(Test-LocalOfficeSource -Root $localRoot){Write-PreInstallLog -Event 'BuiltInRefreshFailed' -Level 'Warning' -Message $_.Exception.Message -Data @{Id='Microsoft365Apps';Fallback='ExistingStagedPayload'};continue} throw "Microsoft 365 Apps acquisition failed and no staged fallback is available. $($_.Exception.Message)" } } 'Teams' { $localRoot=Join-Path $StagedPath 'BuiltIn\Teams'; New-Item -ItemType Directory -Path $localRoot -Force | Out-Null $acquireRoot=if($usbRoot){Join-Path $usbRoot 'BuiltIn\Teams'}else{$localRoot}; New-Item -ItemType Directory -Path $acquireRoot -Force | Out-Null if($usbRoot -and -not (Test-LocalTeamsSource -Root $localRoot) -and (Test-LocalTeamsSource -Root $acquireRoot)){ Copy-Item -LiteralPath (Join-Path $acquireRoot 'teamsbootstrapper.exe') -Destination (Join-Path $localRoot 'teamsbootstrapper.exe') -Force Copy-Item -LiteralPath (Join-Path $acquireRoot 'teams.msix') -Destination (Join-Path $localRoot 'teams.msix') -Force if(Test-Path -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -PathType Leaf){Copy-Item -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -Destination (Join-Path $localRoot 'CacheInfo.json') -Force} Write-PreInstallLog -Event 'BuiltInCacheRestaged' -Message 'Existing Microsoft Teams USB cache was staged locally.' -Data @{Id='Teams'} } if(-not $networkAvailable){ if(Test-LocalTeamsSource -Root $localRoot){Write-PreInstallLog -Event 'BuiltInRefreshSkipped' -Level 'Warning' -Message 'No network is available. Existing staged Teams payload will be used.' -Data @{Id='Teams'};continue} throw 'Microsoft Teams has no staged or USB-cached payload and no network connection is available to acquire one.' } try { $architecture=if($app.Architecture){[string]$app.Architecture}elseif($env:PROCESSOR_ARCHITECTURE -eq 'ARM64'){'arm64'}else{'x64'} $msixUri=if($app.MsixUri){[string]$app.MsixUri}else{switch($architecture){'x86'{'https://go.microsoft.com/fwlink/?clcid=0x409&linkid=2196060'}'x64'{'https://go.microsoft.com/fwlink/?linkid=2196106'}'arm64'{'https://go.microsoft.com/fwlink/?clcid=0x409&linkid=2196207'}}} $bootstrapperUri=if($app.BootstrapperUri){[string]$app.BootstrapperUri}else{'https://go.microsoft.com/fwlink/?clcid=0x409&linkid=2243204'} $cacheInfoPath=Join-Path $acquireRoot 'CacheInfo.json'; $cacheInfo=$null if(Test-Path -LiteralPath $cacheInfoPath -PathType Leaf){try{$cacheInfo=Get-Content -LiteralPath $cacheInfoPath -Raw -Encoding UTF8 | ConvertFrom-Json}catch{}} $msixPath=Join-Path $acquireRoot 'teams.msix'; $bootstrapperPath=Join-Path $acquireRoot 'teamsbootstrapper.exe' $remote=Get-RemoteMetadata -Uri $msixUri; $matches=$false if((Test-Path -LiteralPath $msixPath -PathType Leaf) -and $cacheInfo){if($remote.ETag -and $cacheInfo.RemoteETag){$matches=$remote.ETag -eq [string]$cacheInfo.RemoteETag}elseif($remote.LastModified -and $cacheInfo.RemoteLastModified -and $remote.ContentLength -and $cacheInfo.RemoteContentLength){$matches=($remote.LastModified -eq [string]$cacheInfo.RemoteLastModified -and [int64]$remote.ContentLength -eq [int64]$cacheInfo.RemoteContentLength)}} $tempRoot=Join-Path $StagedPath 'Work\Refresh\Teams'; New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null $tempBootstrapper=Join-Path $tempRoot 'teamsbootstrapper.exe'; Save-PreInstallDownload -Uri $bootstrapperUri -DestinationPath $tempBootstrapper -Id 'Teams' -Description 'Microsoft Teams bootstrapper' -TimeoutMinutes 5; Copy-Item -LiteralPath $tempBootstrapper -Destination $bootstrapperPath -Force $updated=$false if(-not $matches){$tempMsix=Join-Path $tempRoot 'teams.msix';Save-PreInstallDownload -Uri $msixUri -DestinationPath $tempMsix -Id 'Teams' -Description "Microsoft Teams $architecture MSIX" -TimeoutMinutes 15;$version=Get-TeamsPackageVersion -Path $tempMsix;Copy-Item -LiteralPath $tempMsix -Destination $msixPath -Force;$updated=$true}else{$version=if($cacheInfo -and $cacheInfo.Version){[string]$cacheInfo.Version}else{Get-TeamsPackageVersion -Path $msixPath}} [ordered]@{Id='Teams';Cached=[bool]$usbRoot;Version=$version;Architecture=$architecture;SourcePolicy='Evergreen';SyncMethod='HttpMetadata';Updated=$updated;BootstrapperUri=$bootstrapperUri;RemoteETag=$remote.ETag;RemoteLastModified=$remote.LastModified;RemoteContentLength=$remote.ContentLength;RemoteFinalUri=$remote.FinalUri;SyncedAt=(Get-Date).ToUniversalTime().ToString('o')} | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8 if($usbRoot){Copy-Item -LiteralPath $bootstrapperPath -Destination (Join-Path $localRoot 'teamsbootstrapper.exe') -Force;Copy-Item -LiteralPath $msixPath -Destination (Join-Path $localRoot 'teams.msix') -Force;Copy-Item -LiteralPath $cacheInfoPath -Destination (Join-Path $localRoot 'CacheInfo.json') -Force} Write-PreInstallLog -Event 'BuiltInRefreshComplete' -Message 'Microsoft Teams content is ready for installation.' -Data @{Id='Teams';Version=$version;PackageUpdated=$updated;CacheSynchronized=[bool]$usbRoot;SourcePolicy='Evergreen';SyncMethod='HttpMetadata';DurationSeconds=[math]::Round($builtInStopwatch.Elapsed.TotalSeconds,1)} } catch { if(Test-LocalTeamsSource -Root $localRoot){Write-PreInstallLog -Event 'BuiltInRefreshFailed' -Level 'Warning' -Message $_.Exception.Message -Data @{Id='Teams';Fallback='ExistingStagedPayload'};continue} throw "Microsoft Teams acquisition failed and no staged fallback is available. $($_.Exception.Message)" } } 'AdobeAcrobatUnified' { $architecture=if($app.Architecture){[string]$app.Architecture}else{'x64'} $localRoot=Join-Path $StagedPath (Join-Path 'BuiltIn\AdobeAcrobatUnified' $architecture) New-Item -ItemType Directory -Path $localRoot -Force | Out-Null $acquireRoot=if($usbRoot){Join-Path $usbRoot (Join-Path 'BuiltIn\AdobeAcrobatUnified' $architecture)}else{$localRoot} New-Item -ItemType Directory -Path $acquireRoot -Force | Out-Null if($usbRoot -and -not (Test-LocalAdobeAcrobatUnifiedSource -Root $localRoot) -and (Test-LocalAdobeAcrobatUnifiedSource -Root $acquireRoot)){ Copy-Item -LiteralPath (Join-Path $acquireRoot 'Package.zip') -Destination (Join-Path $localRoot 'Package.zip') -Force if(Test-Path -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -PathType Leaf){Copy-Item -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -Destination (Join-Path $localRoot 'CacheInfo.json') -Force} Write-PreInstallLog -Event 'BuiltInCacheRestaged' -Message 'Existing Adobe Acrobat Unified USB cache was staged locally.' -Data @{Id='AdobeAcrobatUnified'} } if(-not $networkAvailable){ if(Test-LocalAdobeAcrobatUnifiedSource -Root $localRoot){ Write-PreInstallLog -Event 'BuiltInRefreshSkipped' -Level 'Warning' -Message 'No network is available. Existing staged Adobe Acrobat Unified payload will be used.' -Data @{Id='AdobeAcrobatUnified'} continue } throw 'Adobe Acrobat Unified has no staged or USB-cached payload and no network connection is available to acquire one.' } try { $packageUri=if($app.PackageUri){[string]$app.PackageUri}else{switch($architecture){'x86'{'https://trials.adobe.com/AdobeProducts/APRO/Acrobat_HelpX/win32/Acrobat_DC_Web_WWMUI.zip'}default{'https://trials.adobe.com/AdobeProducts/APRO/Acrobat_HelpX/win32/Acrobat_DC_Web_x64_WWMUI.zip'}}} $cacheInfoPath=Join-Path $acquireRoot 'CacheInfo.json' $cacheInfo=$null if(Test-Path -LiteralPath $cacheInfoPath -PathType Leaf){try{$cacheInfo=Get-Content -LiteralPath $cacheInfoPath -Raw -Encoding UTF8 | ConvertFrom-Json}catch{}} $packagePath=Join-Path $acquireRoot 'Package.zip' Write-PreInstallLog -Event 'BuiltInRefreshStart' -Message 'Checking Adobe Acrobat Unified cache for updates.' -Data @{Id='AdobeAcrobatUnified';Target=if($usbRoot){'USB'}else{'Local'}} $remote=Get-RemoteMetadata -Uri $packageUri $matches=$false if((Test-Path -LiteralPath $packagePath -PathType Leaf) -and $cacheInfo){ if($remote.ETag -and $cacheInfo.RemoteETag){ $matches=$remote.ETag -eq [string]$cacheInfo.RemoteETag } elseif($remote.LastModified -and $cacheInfo.RemoteLastModified -and $remote.ContentLength -and $cacheInfo.RemoteContentLength){ $matches=($remote.LastModified -eq [string]$cacheInfo.RemoteLastModified -and [int64]$remote.ContentLength -eq [int64]$cacheInfo.RemoteContentLength) } } $updated=$false if(-not $matches){ $tempRoot=Join-Path $StagedPath 'Work\Refresh\AdobeAcrobatUnified' New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null $tempPackage=Join-Path $tempRoot 'Package.zip' Save-PreInstallDownload -Uri $packageUri -DestinationPath $tempPackage -Id 'AdobeAcrobatUnified' -Description "Adobe Acrobat Unified $architecture package" -TimeoutMinutes 30 Copy-Item -LiteralPath $tempPackage -Destination $packagePath -Force $updated=$true } [ordered]@{ Id='AdobeAcrobatUnified' Cached=[bool]$usbRoot Version='Current' Architecture=$architecture SourcePolicy='Evergreen' SyncMethod='HttpMetadata' Updated=$updated PackageUri=$packageUri RemoteETag=$remote.ETag RemoteLastModified=$remote.LastModified RemoteContentLength=$remote.ContentLength RemoteFinalUri=$remote.FinalUri SyncedAt=(Get-Date).ToUniversalTime().ToString('o') } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8 if($usbRoot){ Copy-Item -LiteralPath $packagePath -Destination (Join-Path $localRoot 'Package.zip') -Force Copy-Item -LiteralPath $cacheInfoPath -Destination (Join-Path $localRoot 'CacheInfo.json') -Force } Write-PreInstallLog -Event 'BuiltInRefreshComplete' -Message 'Adobe Acrobat Unified content is ready for installation.' -Data @{Id='AdobeAcrobatUnified';PackageUpdated=$updated;CacheSynchronized=[bool]$usbRoot;SourcePolicy='Evergreen';SyncMethod='HttpMetadata';DurationSeconds=[math]::Round($builtInStopwatch.Elapsed.TotalSeconds,1)} } catch { if(Test-LocalAdobeAcrobatUnifiedSource -Root $localRoot){ Write-PreInstallLog -Event 'BuiltInRefreshFailed' -Level 'Warning' -Message $_.Exception.Message -Data @{Id='AdobeAcrobatUnified';Fallback='ExistingStagedPayload'} continue } throw "Adobe Acrobat Unified acquisition failed and no staged fallback is available. $($_.Exception.Message)" } } 'GoogleChromeEnterprise' { $architecture = if ($app.Architecture) { [string]$app.Architecture } else { 'x64' } $packageUri = if ($app.PackageUri) { [string]$app.PackageUri } else { switch ($architecture) { 'x86' { 'https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise.msi' } default { 'https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise64.msi' } } } $relativeRoot = Join-Path 'BuiltIn\GoogleChromeEnterprise' $architecture Sync-PreInstallVendorMsi -App $app -RelativeRoot $relativeRoot -PackageUri $packageUri -DisplayName 'Google Chrome Enterprise' } 'MozillaFirefoxEnterprise' { $channel = if ($app.Channel) { [string]$app.Channel } else { 'Rapid' } $architecture = if ($app.Architecture) { [string]$app.Architecture } else { 'x64' } $language = if ($app.Language) { [string]$app.Language } else { 'en-US' } $packageUri = if ($app.PackageUri) { [string]$app.PackageUri } else { $product = if ($channel -eq 'ESR') { 'firefox-esr-msi-latest-ssl' } else { 'firefox-msi-latest-ssl' } $os = if ($architecture -eq 'x64') { 'win64' } else { 'win' } "https://download.mozilla.org/?product=$product&os=$os&lang=$language" } $relativeRoot = Join-Path 'BuiltIn\MozillaFirefoxEnterprise' (Join-Path $channel (Join-Path $architecture $language)) Sync-PreInstallVendorMsi -App $app -RelativeRoot $relativeRoot -PackageUri $packageUri -DisplayName 'Mozilla Firefox Enterprise' } } } $preInstallStopwatch.Stop() Write-PreInstallLog -Event 'RefreshComplete' -Message 'Built-in acquisition and refresh completed. Installation will continue.' -Data @{CacheUsed=[bool]$usbRoot;DurationSeconds=[math]::Round($preInstallStopwatch.Elapsed.TotalSeconds,1)} Write-PreInstallLog -Event 'PreInstallComplete' -Message 'PreInstall completed successfully.' -Data @{BuiltInAppCount=@($builtInApps).Count;DurationSeconds=[math]::Round($preInstallStopwatch.Elapsed.TotalSeconds,1)} exit 0 } catch { if ($preInstallStopwatch.IsRunning) { $preInstallStopwatch.Stop() } $failureMessage = $_.Exception.Message $failureType = if ($failureMessage -match 'no network|not reachable') { 'NetworkUnavailable' } elseif ($failureMessage -match 'timeout|exceeded') { 'Timeout' } elseif ($failureMessage -match 'no staged|no .*cached|missing') { 'CacheOrSourceMissing' } elseif ($failureMessage -match 'metadata|HTTP|response|download') { 'RemoteAcquisition' } else { 'PreInstallError' } Write-PreInstallLog -Event 'RefreshFailed' -Level 'Error' -Message $failureMessage -Data @{FailureType=$failureType;DurationSeconds=[math]::Round($preInstallStopwatch.Elapsed.TotalSeconds,1)} exit 1 } |