Update-FSLogix.ps1
<#PSScriptInfo
.VERSION 0.1.1 .GUID 37311878-913e-4dd0-bc2f-a9400438f589 .AUTHOR Jörg Brors .COMPANYNAME .COPYRIGHT (c) 2025 Jörg Brors. All rights reserved. .TAGS FSLogix Update GoldenImage ZipCompare OnlineCompare .LICENSEURI https://opensource.org/licenses/MIT .PROJECTURI https://github.com/joergbrors/Update-FSLogix .DESCRIPTION Update-FSLogix checks, downloads, and updates Microsoft FSLogix to the latest available release (PowerShell 5.1 compatible). .RELEASENOTES 0.1.1 – PS 5.1 hardening: remove null-coalescing, avoid ProxyUseDefaultCredentials, implement NoProxy via DefaultWebProxy, keep only 5.1-safe params. 0.1.0 – Add -Update path, support -InstallerPath (ZIP/EXE), robust version compare, TLS 1.2, admin check, summary. 0.0.1 – Initial release. #> <# .SYNOPSIS Checks, downloads, and updates Microsoft FSLogix to the latest available release. .PARAMETER AcceptEula Required for -Update. .PARAMETER Update Perform silent in-place upgrade if newer. .PARAMETER ResolveOnly Resolve final aka.ms target URL and print it. .PARAMETER OnlineCompare Show best-effort version hint from URL. .PARAMETER ZipCompare Compare package FileVersion vs installed (no install). .PARAMETER InstallerPath Local ZIP or EXE to use instead of downloading. .PARAMETER DownloadUrl Defaults to https://aka.ms/fslogix_download. .PARAMETER UseBits Use BITS for download. .PARAMETER NoProxy Bypass system proxy for this process (via .NET DefaultWebProxy). .PARAMETER KeepTemp Keep temp files. .PARAMETER LogPath Log directory (default C:\ProgramData\FSLogix\Update). #> #Requires -Version 5.1 [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param( [switch]$AcceptEula, [switch]$Update, [switch]$ResolveOnly, [switch]$OnlineCompare, [switch]$ZipCompare, [string]$InstallerPath = "", [string]$DownloadUrl = "https://aka.ms/fslogix_download", [switch]$UseBits, [switch]$NoProxy, [switch]$KeepTemp, [string]$LogPath = "C:\ProgramData\FSLogix\Update" ) # --- Pre-flight: TLS & Admin --- try { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 } catch { } function Test-Admin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($id) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } if (-not (Test-Admin)) { Write-Warning "Please run elevated (Administrator). Some actions will fail otherwise." } # Handle NoProxy for the whole process (PS 5.1-safe) $originalProxy = [System.Net.WebRequest]::DefaultWebProxy if ($NoProxy) { try { [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy # direct } catch { } # also clear env proxies for child operations $env:http_proxy = $null $env:https_proxy = $null $env:HTTP_PROXY = $null $env:HTTPS_PROXY = $null } # --- Logging --- function Write-Log { param( [Parameter(Mandatory)][string]$Message, [ValidateSet("INFO","WARN","ERROR","DEBUG")] [string]$Level = "INFO" ) $line = "[{0}] [{1}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Level, $Message Write-Output $line if ($script:LogFile) { Add-Content -Path $script:LogFile -Value $line } } if (-not (Test-Path $LogPath)) { New-Item -Path $LogPath -ItemType Directory -Force | Out-Null } $script:LogFile = Join-Path $LogPath ("fslogix_update_{0}.log" -f (Get-Date -Format "yyyyMMdd_HHmmss")) Write-Log "Log file: $script:LogFile" # --- Helpers --- function Get-FSLogixInstalledVersion { $reg = "HKLM:\SOFTWARE\FSLogix\Apps" $result = [ordered]@{ Installed = $false Running = $false FileVersion = $null RegistryVersion = $null ExePath = "C:\Program Files\FSLogix\Apps\frx.exe" } if (Test-Path $reg) { try { $val = Get-ItemProperty -Path $reg -ErrorAction Stop $result.RegistryVersion = $val.Version } catch { Write-Log "Registry read failed: $_" "WARN" } } if (Test-Path $result.ExePath) { try { $fv = (Get-Item $result.ExePath).VersionInfo.FileVersion $result.FileVersion = $fv $result.Installed = $true } catch { Write-Log "Failed to read frx.exe FileVersion: $_" "WARN" } } try { $svc = Get-Service -Name "frxsvc" -ErrorAction SilentlyContinue if ($svc -and $svc.Status -eq "Running") { $result.Running = $true } } catch { } [pscustomobject]$result } function Resolve-FslogixUrl { param([Parameter(Mandatory)][string]$Url) # Use HttpWebRequest to catch Location header without auto-redirect (PS 5.1-safe) try { $req = [System.Net.HttpWebRequest]::Create($Url) $req.AllowAutoRedirect = $false $req.Method = "HEAD" $resp = $req.GetResponse() try { $loc = $resp.Headers["Location"] if ([string]::IsNullOrWhiteSpace($loc)) { return $Url } else { return $loc } } finally { $resp.Close() } } catch { # If it threw due to 3xx, try to read Location from the response try { $resp = $_.Exception.Response if ($resp -and $resp.Headers) { $loc = $resp.Headers["Location"] if ($loc) { return $loc } } } catch { } Write-Log "Failed to resolve URL ($Url): $($_.Exception.Message)" "ERROR" return $null } } function Download-File { param( [Parameter(Mandatory)][string]$Url, [Parameter(Mandatory)][string]$Destination ) if (Test-Path $Destination) { Remove-Item -Path $Destination -Force -ErrorAction SilentlyContinue } if ($UseBits) { Write-Log "Using BITS transfer..." Start-BitsTransfer -Source $Url -Destination $Destination -DisplayName "FSLogix Download" return } try { Write-Log "Using Invoke-WebRequest for download..." Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -ErrorAction Stop } catch { Write-Log "Download failed: $($_.Exception.Message)" "ERROR" throw } } function Expand-Zip { param( [Parameter(Mandatory)][string]$ZipPath, [Parameter(Mandatory)][string]$Destination ) if (-not (Test-Path $Destination)) { New-Item -Path $Destination -ItemType Directory -Force | Out-Null } Expand-Archive -Path $ZipPath -DestinationPath $Destination -Force } function Try-ParseVersion { param([string]$s) try { $parts = ($s -split '[^\d]+' | Where-Object { $_ -ne '' }) if ($parts.Count -ge 3) { $padded = @($parts[0], $parts[1], $parts[2], ($(if ($parts.Count -ge 4) { $parts[3] } else { '0' }))) $norm = ($padded[0..3]) -join '.' return [version]$norm } } catch { } return $null } function Get-FileVersionVersionObj { param([string]$FilePath) $fv = (Get-Item $FilePath).VersionInfo.FileVersion $vObj = Try-ParseVersion -s $fv [pscustomobject]@{ Raw = $fv; Version = $vObj } } function Find-SetupInFolder { param([string]$Root) $setup = Get-ChildItem -Path $Root -Recurse -Filter "FSLogixAppsSetup.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 if ($setup) { return $setup.FullName } else { return $null } } function Stop-Start-FrxSvc { param([switch]$StopOnly) $svc = Get-Service -Name "frxsvc" -ErrorAction SilentlyContinue if ($svc -and $svc.Status -eq "Running") { Write-Log "Stopping service frxsvc..." Stop-Service -Name frxsvc -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 } if (-not $StopOnly) { Write-Log "Starting service frxsvc..." Start-Service -Name frxsvc -ErrorAction SilentlyContinue } } function Install-FSLogix { param( [Parameter(Mandatory)][string]$SetupExe, [Parameter(Mandatory)][string]$LogDir ) if (-not (Test-Path $LogDir)) { New-Item -Path $LogDir -ItemType Directory -Force | Out-Null } $setupLog = Join-Path $LogDir ("FSLogixSetup_{0}.log" -f (Get-Date -Format "yyyyMMdd_HHmmss")) # Assumption: silent flags supported by FSLogix EXE $args = '/install /quiet /norestart /log "{0}"' -f $setupLog Write-Log "Running: `"$SetupExe`" $args" if ($PSCmdlet.ShouldProcess($SetupExe, "Install FSLogix")) { Stop-Start-FrxSvc -StopOnly $p = Start-Process -FilePath $SetupExe -ArgumentList $args -Wait -PassThru Write-Log "Installer exit code: $($p.ExitCode)" Start-Sleep -Seconds 2 Stop-Start-FrxSvc return $p.ExitCode } return 0 } # --- State --- $installed = Get-FSLogixInstalledVersion Write-Log "Installed: Installed=$($installed.Installed), Running=$($installed.Running), FileVersion=$($installed.FileVersion), RegistryVersion=$($installed.RegistryVersion)" # --- Work folders --- $workRoot = Join-Path ([IO.Path]::GetTempPath()) ("fslogix_{0}" -f ([guid]::NewGuid().ToString('N'))) $newPaths = [ordered]@{ Root = $workRoot Zip = Join-Path $workRoot "fslogix.zip" Extract = Join-Path $workRoot "extract" SetupExe = $null Source = $null # 'URL' | 'InstallerPathZIP' | 'InstallerPathEXE' ResolvedUrl = $null } New-Item -Path $workRoot -ItemType Directory -Force | Out-Null # Ensure cleanup of proxy on exit $cleanupProxyScriptBlock = { param($originalProxyRef, $noProxyFlag) if ($noProxyFlag) { try { [System.Net.WebRequest]::DefaultWebProxy = $originalProxyRef } catch { } } } try { if ($InstallerPath) { if (-not (Test-Path $InstallerPath)) { throw "InstallerPath not found: $InstallerPath" } $ext = [IO.Path]::GetExtension($InstallerPath).ToLowerInvariant() if ($ext -eq ".zip") { Write-Log "Using local ZIP: $InstallerPath" $newPaths.Source = 'InstallerPathZIP' Expand-Zip -ZipPath $InstallerPath -Destination $newPaths.Extract $setupExe = Find-SetupInFolder -Root $newPaths.Extract if (-not $setupExe) { throw "FSLogixAppsSetup.exe not found in ZIP." } $newPaths.SetupExe = $setupExe } elseif ($ext -eq ".exe") { Write-Log "Using local EXE: $InstallerPath" $newPaths.Source = 'InstallerPathEXE' $newPaths.SetupExe = $InstallerPath } else { throw "Unsupported file extension for InstallerPath: $ext" } } else { $resolved = Resolve-FslogixUrl -Url $DownloadUrl if (-not $resolved) { throw "Could not resolve download URL." } $newPaths.ResolvedUrl = $resolved Write-Log "Resolved final download URL: $resolved" Write-Log "Downloading package to $($newPaths.Zip)" Download-File -Url $resolved -Destination $newPaths.Zip Unblock-File -Path $newPaths.Zip -ErrorAction SilentlyContinue Expand-Zip -ZipPath $newPaths.Zip -Destination $newPaths.Extract $setupExe = Find-SetupInFolder -Root $newPaths.Extract if (-not $setupExe) { throw "FSLogixAppsSetup.exe not found after extraction." } $newPaths.SetupExe = $setupExe $newPaths.Source = 'URL' } # --- Version from package --- $pkgVersionInfo = Get-FileVersionVersionObj -FilePath $newPaths.SetupExe Write-Log "Package FSLogixAppsSetup.exe FileVersion (raw): $($pkgVersionInfo.Raw)" if (-not $pkgVersionInfo.Version) { Write-Log "Could not parse package version for robust comparison." "WARN" } # --- Installed version --- $installedVersionObj = $null if ($installed.FileVersion) { $installedVersionObj = Try-ParseVersion -s $installed.FileVersion Write-Log "Installed frx.exe FileVersion (raw): $($installed.FileVersion); parsed=$installedVersionObj" } else { Write-Log "FSLogix appears not installed or frx.exe missing." "WARN" } # --- Decide upgrade --- $isUpgradeAvailable = $false if ($pkgVersionInfo.Version -and $installedVersionObj) { $isUpgradeAvailable = ($installedVersionObj -lt $pkgVersionInfo.Version) Write-Log ("Comparison: Installed={0} Package={1} UpgradeAvailable={2}" -f $installedVersionObj, $pkgVersionInfo.Version, $isUpgradeAvailable) } elseif ($pkgVersionInfo.Version -and -not $installedVersionObj) { $isUpgradeAvailable = $true Write-Log "Treating as upgrade: installed version unknown, package has version." "WARN" } # --- Modes --- if ($ResolveOnly) { if ($newPaths.ResolvedUrl) { Write-Log "ResolveOnly: final URL = $($newPaths.ResolvedUrl)" } else { Write-Log "ResolveOnly: using local installer (no URL)." } return } if ($OnlineCompare) { if ($newPaths.ResolvedUrl) { $m = [regex]::Match($newPaths.ResolvedUrl, '(?<v>\d{4}\.\d{1,2}|\d{1,2}\.\d{1,2}|\d+\.\d+\.\d+\.\d+)') if ($m.Success) { Write-Log "Info: version hint in URL: $($m.Groups['v'].Value)" } else { Write-Log "No clear version hint in URL." "WARN" } } else { Write-Log "OnlineCompare requested, but using local InstallerPath. Skipping." "WARN" } return } if ($ZipCompare -and -not $Update) { Write-Log "ZipCompare: comparison complete. No install performed." return } if ($Update) { if (-not $AcceptEula) { Write-Log "You must specify -AcceptEula to proceed with installation." "ERROR" throw "EULA not accepted." } if (-not $isUpgradeAvailable -and $installed.Installed) { Write-Log "Installed version is up-to-date or newer. No installation performed." } else { $exit = Install-FSLogix -SetupExe $newPaths.SetupExe -LogDir $LogPath if ($exit -ne 0) { Write-Log "Installer returned non-zero exit code: $exit" "ERROR" throw "FSLogix installation failed with exit code $exit" } else { Write-Log "FSLogix installation completed successfully." } } } } finally { # restore default proxy if changed & $cleanupProxyScriptBlock $originalProxy $NoProxy | Out-Null if (-not $KeepTemp -and (Test-Path $workRoot)) { try { Remove-Item -Path $workRoot -Recurse -Force -ErrorAction SilentlyContinue } catch { } } else { Write-Log "Keeping temp folder: $workRoot" } } # --- Summary --- $after = Get-FSLogixInstalledVersion $result = [pscustomobject]@{ LogFile = $script:LogFile Source = $newPaths.Source ResolvedUrl = $newPaths.ResolvedUrl PackageSetupExe = $newPaths.SetupExe Installed_Before = $installed Installed_After = $after Package_FileVersion_Raw = $pkgVersionInfo.Raw Package_FileVersion = $pkgVersionInfo.Version } $result | Format-List |