Auto-commit: 2025-10-31 08:55:43
This commit is contained in:
72
install-msu-web-download/installmsu.ps1
Normal file
72
install-msu-web-download/installmsu.ps1
Normal file
@@ -0,0 +1,72 @@
|
||||
# MSU Prerequisite Check and Install Script
|
||||
|
||||
$LogPath = "C:\Windows\Temp\MSU_Install.log"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message)
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
Add-Content -Path $LogPath -Value "$timestamp $Message"
|
||||
Write-Host $Message
|
||||
}
|
||||
|
||||
# Check 1: Reboot pending
|
||||
$RebootRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
|
||||
if (Test-Path $RebootRegKey) {
|
||||
Write-Log "Pending reboot detected."
|
||||
exit 101
|
||||
} else {
|
||||
Write-Log "No pending reboot."
|
||||
}
|
||||
|
||||
# Check 3: Free space on C:
|
||||
$minFreeGB = 5
|
||||
$drive = Get-PSDrive -Name C
|
||||
if ($null -eq $drive -or ($drive.Free/1GB) -lt $minFreeGB) {
|
||||
Write-Log "Insufficient free space on C:. Required: ${minFreeGB}GB"
|
||||
exit 103
|
||||
} else {
|
||||
Write-Log ("Sufficient free space on C:. Free: {0:N2}GB" -f ($drive.Free/1GB))
|
||||
}
|
||||
|
||||
# MSU download info
|
||||
$MSUFile = "windows10.0-kb5060531-x64.msu"
|
||||
$MSUUrl = "https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/secu/2025/06/windows10.0-kb5060531-x64_83789c3b9350e10e207370622c4ef54dd685ee02.msu"
|
||||
$ScriptDir = "c:\windows\temp"
|
||||
$MSUPath = Join-Path $ScriptDir $MSUFile
|
||||
|
||||
# Always delete the MSU file before download
|
||||
if (Test-Path $MSUPath) {
|
||||
Write-Log "Deleting existing MSU file: $MSUPath"
|
||||
Remove-Item -Path $MSUPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Log "Downloading MSU from $MSUUrl using System.Net.WebClient"
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$wc = New-Object System.Net.WebClient
|
||||
$wc.DownloadFile($MSUUrl, $MSUPath)
|
||||
$sw.Stop()
|
||||
$downloadTime = "{0:N2}" -f $sw.Elapsed.TotalSeconds
|
||||
|
||||
if (-not (Test-Path $MSUPath)) {
|
||||
Write-Log "Failed to download MSU (file not found after download)"
|
||||
exit 104
|
||||
} else {
|
||||
Write-Log "Download completed: $MSUPath"
|
||||
Write-Log "Download time: $downloadTime seconds"
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Failed to download MSU with WebClient: $_"
|
||||
exit 104
|
||||
}
|
||||
|
||||
Write-Log "Starting installation of $MSUPath"
|
||||
|
||||
# Install the MSU silently, no restart
|
||||
$process = Start-Process -FilePath "wusa.exe" -ArgumentList "`"$MSUPath`" /quiet /norestart" -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
Write-Log "wusa.exe exit code: $exitCode"
|
||||
|
||||
# Return the exit code as the script's own exit code
|
||||
exit $exitCode
|
||||
250
install-msu-web-download/installmsuv2.ps1
Normal file
250
install-msu-web-download/installmsuv2.ps1
Normal file
@@ -0,0 +1,250 @@
|
||||
#requires -version 5.1
|
||||
<#
|
||||
MSU Prereq + Install + WS logging + STRICT selection by FULL BUILD (major.UBR)
|
||||
- Mapping is downloaded from https://nas.wuibaille.fr/WS/patchs/msu.json
|
||||
- Supports multiple packages per build (e.g., SSU then LCU)
|
||||
- JSON needs only: { "<build>": { "packages": [ { "url": "...", "type": "LCU", "order": 20 }, ... ] } }
|
||||
- No defaults: if build key missing -> exit 111
|
||||
- If JSON cannot be downloaded and no cache -> exit 112
|
||||
- If cached JSON invalid -> exit 113
|
||||
#>
|
||||
|
||||
# -------- Settings --------
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$ComputerName = $env:COMPUTERNAME
|
||||
$LogPath = "C:\Windows\Temp\MSU_Install.log"
|
||||
$ApiUrl = "https://nas.wuibaille.fr/WS/message.php"
|
||||
$SendLogsToWS = $true
|
||||
|
||||
# Remote JSON map + local cache
|
||||
$MsuMapUrl = "https://nas.wuibaille.fr/WS/patchs/msu.json"
|
||||
$MsuMapCache = "C:\Windows\Temp\msu.json"
|
||||
|
||||
# Optional: force a build key like '17763.7786'
|
||||
$OverrideBuildFull = $null
|
||||
|
||||
# -------- Helpers --------
|
||||
function Write-Log {
|
||||
param([Parameter(Mandatory)][string]$Message)
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$fullMessage = "[$ComputerName] $Message"
|
||||
Add-Content -Path $LogPath -Value "$timestamp $fullMessage" -Encoding UTF8
|
||||
Write-Host $fullMessage
|
||||
if ($SendLogsToWS) {
|
||||
try {
|
||||
$body = @{ message = $fullMessage } | ConvertTo-Json -Depth 2
|
||||
$null = Invoke-RestMethod -Uri $ApiUrl -Method POST -Body $body -ContentType "application/json" -TimeoutSec 10 -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Warning ("[$ComputerName] Webservice log failed: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-BuildFull {
|
||||
# Returns PSCustomObject with BuildMajor, UBR, BuildFull="major.UBR"
|
||||
$cvPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
|
||||
$cv = Get-ItemProperty -LiteralPath $cvPath
|
||||
|
||||
$buildStr = [string]$cv.CurrentBuild
|
||||
if ([string]::IsNullOrWhiteSpace($buildStr)) { $buildStr = [string]$cv.CurrentBuildNumber }
|
||||
$buildMajor = [int]$buildStr
|
||||
|
||||
$ubr = 0; try { $ubr = [int]$cv.UBR } catch {}
|
||||
|
||||
[PSCustomObject]@{
|
||||
BuildMajor = $buildMajor
|
||||
UBR = $ubr
|
||||
BuildFull = ('{0}.{1}' -f $buildMajor, $ubr)
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MSUMapFromJson {
|
||||
# Downloads JSON (and caches it). On failure, tries cache; else exits.
|
||||
Write-Log ("Fetching MSU map: {0}" -f $MsuMapUrl)
|
||||
try {
|
||||
$json = Invoke-RestMethod -Uri $MsuMapUrl -Method GET -TimeoutSec 15 -ErrorAction Stop
|
||||
try { ($json | ConvertTo-Json -Depth 10) | Out-File -FilePath $MsuMapCache -Encoding UTF8 -Force } catch {}
|
||||
return $json
|
||||
} catch {
|
||||
Write-Log ("Online map fetch failed: {0}" -f $_.Exception.Message)
|
||||
if (Test-Path -LiteralPath $MsuMapCache) {
|
||||
Write-Log ("Using cached map: {0}" -f $MsuMapCache)
|
||||
try {
|
||||
$raw = Get-Content -LiteralPath $MsuMapCache -Raw -ErrorAction Stop
|
||||
$json = $raw | ConvertFrom-Json -ErrorAction Stop
|
||||
return $json
|
||||
} catch {
|
||||
Write-Log ("Cached map is invalid: {0}. Exiting 113." -f $_.Exception.Message)
|
||||
exit 113
|
||||
}
|
||||
} else {
|
||||
Write-Log "No cached map available. Exiting 112."
|
||||
exit 112
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-PackagesArray {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Entry,
|
||||
[Parameter(Mandatory)][string]$KeyForErrors
|
||||
)
|
||||
<#
|
||||
Accepts:
|
||||
- { "packages": [ { "url": "...", "type": "LCU", "order": 20 }, ... ] }
|
||||
- { "url": "..." } (single package)
|
||||
- "https://..." (single package as string)
|
||||
Returns: array of package objects with at least .url
|
||||
#>
|
||||
if ($Entry -is [string]) {
|
||||
if ($Entry -notmatch '^https?://') { throw "Invalid entry for build '$KeyForErrors': expected URL string." }
|
||||
return @(@{ url = $Entry; type = $null; order = $null })
|
||||
}
|
||||
|
||||
$props = @($Entry.PSObject.Properties.Name)
|
||||
if ($props -contains 'packages') {
|
||||
$arr = @($Entry.packages)
|
||||
if (-not $arr -or $arr.Count -eq 0) { throw "Entry for build '$KeyForErrors' has empty 'packages'." }
|
||||
foreach ($p in $arr) {
|
||||
if (-not $p.url) { throw "One package in build '$KeyForErrors' is missing 'url'." }
|
||||
}
|
||||
return $arr
|
||||
}
|
||||
|
||||
if ($props -contains 'url') {
|
||||
return @(@{ url = [string]$Entry.url; type = $Entry.type; order = $Entry.order })
|
||||
}
|
||||
|
||||
throw "Invalid entry format for build '$KeyForErrors'. Expected 'packages' array or 'url' string/property."
|
||||
}
|
||||
|
||||
function Sort-Packages {
|
||||
param([Parameter(Mandatory)][array]$Packages)
|
||||
# Priority: explicit 'order' asc, else by 'type' (SSU=10, LCU=20, NET=30, other=50)
|
||||
$prio = @{ 'SSU' = 10; 'LCU' = 20; 'NET' = 30 }
|
||||
return $Packages | Sort-Object `
|
||||
@{ Expression = { if ($_.order -ne $null -and "$($_.order)" -match '^\d+$') { [int]$_.order } else { 999 } } },
|
||||
@{ Expression = { $t = ("" + $_.type).ToUpper(); if ($prio.ContainsKey($t)) { $prio[$t] } else { 50 } } }
|
||||
}
|
||||
|
||||
function Resolve-PackagesForBuild {
|
||||
param(
|
||||
[Parameter(Mandatory)]$BuildInfo,
|
||||
[Parameter(Mandatory)]$JsonMap,
|
||||
[string]$OverrideBuildFull
|
||||
)
|
||||
$key = if ([string]::IsNullOrWhiteSpace($OverrideBuildFull)) { $BuildInfo.BuildFull } else { $OverrideBuildFull }
|
||||
|
||||
# Convert root object to hashtable keyed by build strings
|
||||
$map = @{}
|
||||
foreach ($p in $JsonMap.PSObject.Properties) { $map[$p.Name] = $p.Value }
|
||||
|
||||
if (-not $map.ContainsKey($key)) { throw ("No MSU mapping defined for full build '{0}'." -f $key) }
|
||||
|
||||
$entry = $map[$key]
|
||||
$packages = Ensure-PackagesArray -Entry $entry -KeyForErrors $key
|
||||
$packages = Sort-Packages -Packages $packages
|
||||
return @{ Key=$key; Packages=$packages }
|
||||
}
|
||||
|
||||
function Test-IsAdmin {
|
||||
$wi = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$wp = [Security.Principal.WindowsPrincipal]::new($wi)
|
||||
return $wp.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Filename-FromUrl {
|
||||
param([Parameter(Mandatory)][string]$Url)
|
||||
try {
|
||||
$u = [Uri]$Url
|
||||
$name = [IO.Path]::GetFileName($u.AbsolutePath)
|
||||
if ([string]::IsNullOrWhiteSpace($name)) { $name = "update.msu" }
|
||||
} catch { $name = "update.msu" }
|
||||
if ($name -notmatch '\.msu$') { $name = "$name.msu" }
|
||||
return $name
|
||||
}
|
||||
|
||||
# -------- Script start --------
|
||||
if (-not (Test-IsAdmin)) { Write-Log "Warning: Script is not running elevated." }
|
||||
|
||||
# Build detection
|
||||
$b = Get-BuildFull
|
||||
Write-Log ("Detected OS: Build={0}" -f $b.BuildFull)
|
||||
|
||||
# Load mapping + resolve package list
|
||||
$msuJson = Get-MSUMapFromJson
|
||||
try {
|
||||
$sel = Resolve-PackagesForBuild -BuildInfo $b -JsonMap $msuJson -OverrideBuildFull $OverrideBuildFull
|
||||
Write-Log ("Selected {0} package(s) for Build={1}" -f $sel.Packages.Count, $sel.Key)
|
||||
} catch {
|
||||
Write-Log ("MSU selection error: {0}. Exiting 111." -f $_.Exception.Message)
|
||||
exit 111
|
||||
}
|
||||
|
||||
# Checks
|
||||
try {
|
||||
$RebootRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
|
||||
if (Test-Path $RebootRegKey) { Write-Log "Pending reboot detected. Exiting 101."; exit 101 }
|
||||
} catch { Write-Log ("Error while checking free space: {0}" -f $_.Exception.Message); exit 199 }
|
||||
|
||||
try {
|
||||
$minFreeGB = 5
|
||||
$drive = Get-PSDrive -Name C -ErrorAction Stop
|
||||
$freeGB = [math]::Round(($drive.Free/1GB),2)
|
||||
if ($freeGB -lt $minFreeGB) { Write-Log ("Insufficient free space: required {0}GB, available {1}GB. Exiting 103." -f $minFreeGB, $freeGB); exit 103 }
|
||||
} catch { Write-Log ("Error while checking free space: {0}" -f $_.Exception.Message); exit 198 }
|
||||
|
||||
# Download + install each package in order
|
||||
$ScriptDir = "C:\Windows\Temp"
|
||||
if (-not (Test-Path -LiteralPath $ScriptDir)) { New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null }
|
||||
|
||||
$anyNeedReboot = $false
|
||||
$anyInstalled = $false
|
||||
|
||||
foreach ($pkg in $sel.Packages) {
|
||||
$url = [string]$pkg.url
|
||||
if ([string]::IsNullOrWhiteSpace($url)) { Write-Log "Package without URL encountered. Aborting (bad JSON)."; exit 113 }
|
||||
$type = ("" + $pkg.type)
|
||||
$fileName = Filename-FromUrl -Url $url
|
||||
$destPath = Join-Path $ScriptDir $fileName
|
||||
|
||||
Write-Log ("Downloading {0}{1} from {2}" -f (if ($type) { "[$type] " } else { "" }), $fileName, $url)
|
||||
try {
|
||||
$wc = New-Object System.Net.WebClient
|
||||
try {
|
||||
$wc.Headers['User-Agent'] = ("MSU-Installer/1.0 ({0})" -f $ComputerName)
|
||||
if (Test-Path -LiteralPath $destPath) { Remove-Item -LiteralPath $destPath -Force -ErrorAction Stop }
|
||||
$wc.DownloadFile($url, $destPath)
|
||||
} finally { if ($wc) { $wc.Dispose() } }
|
||||
if (-not (Test-Path -LiteralPath $destPath)) { Write-Log "Download failed: file not found after download. Exiting 104."; exit 104 }
|
||||
} catch {
|
||||
Write-Log ("Failed to download package {0}: {1}. Exiting 104." -f $fileName, $_.Exception.Message)
|
||||
exit 104
|
||||
}
|
||||
|
||||
Write-Log ("Installing {0}{1}" -f (if ($type) { "[$type] " } else { "" }), $fileName)
|
||||
try {
|
||||
$proc = Start-Process -FilePath "wusa.exe" -ArgumentList "`"$destPath`" /quiet /norestart" -Wait -PassThru
|
||||
$code = $proc.ExitCode
|
||||
Write-Log ("wusa.exe exit code: {0} for {1}" -f $code, $fileName)
|
||||
|
||||
if ($code -eq 3010) { $anyNeedReboot = $true; $anyInstalled = $true }
|
||||
elseif ($code -eq 0) { $anyInstalled = $true }
|
||||
elseif ($code -eq 2359302) { } # already installed, continue
|
||||
else {
|
||||
Write-Log ("Installation failed for {0} with code {1}. Aborting." -f $fileName, $code)
|
||||
exit $code
|
||||
}
|
||||
} catch {
|
||||
Write-Log ("Installation failed for {0}: {1}" -f $fileName, $_.Exception.Message)
|
||||
exit 195
|
||||
}
|
||||
}
|
||||
|
||||
# Overall exit code policy
|
||||
if ($anyNeedReboot) { exit 3010 }
|
||||
elseif ($anyInstalled) { exit 0 }
|
||||
else { exit 2359302 }
|
||||
329
install-msu-web-download/installmsuv3.ps1
Normal file
329
install-msu-web-download/installmsuv3.ps1
Normal file
@@ -0,0 +1,329 @@
|
||||
#requires -version 5.1
|
||||
<#
|
||||
Auto-install latest OS cumulative updates using PSWindowsUpdate (no JSON)
|
||||
- Forces Windows Update public service (-WindowsUpdate)
|
||||
- Installs SSU (if present) first, then latest non-Preview LCU
|
||||
- ASCII-only logs; messages sanitized before sending to web service
|
||||
#>
|
||||
|
||||
# -------- Settings --------
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$ComputerName = $env:COMPUTERNAME
|
||||
$LogPath = "C:\Windows\Temp\MSU_Install.log"
|
||||
$ApiUrl = "https://nas.wuibaille.fr/WS/message.php"
|
||||
$SendLogsToWS = $true
|
||||
|
||||
# Optional WS retry tunables (defaults: 6 tries, 1s delay)
|
||||
$script:WsMaxRetries = 6
|
||||
$script:WsRetryDelaySec = 1
|
||||
|
||||
# Module install policy
|
||||
$AllowInstallPSWindowsUpdate = $true
|
||||
|
||||
# -------- Utilities --------
|
||||
function Sanitize-Ascii {
|
||||
param([string]$Text)
|
||||
if ($null -eq $Text) { return "" }
|
||||
|
||||
$t = $Text
|
||||
$t = $t -replace [char]0x2013, "-" # en dash
|
||||
$t = $t -replace [char]0x2014, "-" # em dash
|
||||
$t = $t -replace [char]0x2026, "..." # ellipsis
|
||||
$t = $t -replace [char]0x2018, "'" # left single quote
|
||||
$t = $t -replace [char]0x2019, "'" # right single quote
|
||||
$t = $t -replace [char]0x201C, '"' # left double quote
|
||||
$t = $t -replace [char]0x201D, '"' # right double quote
|
||||
$t = $t -replace [char]0x00A0, " " # nbsp
|
||||
$t = $t -replace [char]0x2022, "-" # bullet
|
||||
$t = [System.Text.RegularExpressions.Regex]::Replace($t, '[^\x09\x0A\x0D\x20-\x7E]', '')
|
||||
return $t
|
||||
}
|
||||
|
||||
# -------- Logging --------
|
||||
function Write-Log {
|
||||
param([Parameter(Mandatory)][string]$Message)
|
||||
|
||||
$msg = Sanitize-Ascii $Message
|
||||
$full = "[$env:COMPUTERNAME] $msg"
|
||||
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
Add-Content -Path $LogPath -Value "$ts $full" -Encoding UTF8
|
||||
Write-Host $full
|
||||
|
||||
if (-not $SendLogsToWS) { return }
|
||||
|
||||
$payload = @{ message = $full } | ConvertTo-Json -Compress
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
|
||||
|
||||
$max = if ($script:WsMaxRetries) { [int]$script:WsMaxRetries } else { 6 }
|
||||
$delay = if ($script:WsRetryDelaySec) { [int]$script:WsRetryDelaySec } else { 1 }
|
||||
|
||||
$ok = $false
|
||||
$lastErr = $null
|
||||
|
||||
for ($attempt = 1; $attempt -le $max -and -not $ok; $attempt++) {
|
||||
try {
|
||||
$null = Invoke-RestMethod -Uri $ApiUrl -Method POST -Body $bytes `
|
||||
-ContentType 'application/json; charset=utf-8' -TimeoutSec 10 -ErrorAction Stop
|
||||
$ok = $true
|
||||
} catch {
|
||||
$lastErr = $_.Exception.Message
|
||||
if ($attempt -lt $max) { Start-Sleep -Seconds $delay }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $ok) {
|
||||
Write-Warning "[$env:COMPUTERNAME] Webservice log failed after $max attempts: $lastErr"
|
||||
}
|
||||
}
|
||||
|
||||
# -------- Prereqs --------
|
||||
function Test-IsAdmin {
|
||||
$wi = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$wp = [Security.Principal.WindowsPrincipal]::new($wi)
|
||||
return $wp.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Get-BuildFull {
|
||||
$cv = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
|
||||
$buildStr = [string]$cv.CurrentBuild
|
||||
if ([string]::IsNullOrWhiteSpace($buildStr)) { $buildStr = [string]$cv.CurrentBuildNumber }
|
||||
$major = [int]$buildStr
|
||||
$ubr = 0; try { $ubr = [int]$cv.UBR } catch {}
|
||||
[PSCustomObject]@{ BuildMajor=$major; UBR=$ubr; BuildFull=("{0}.{1}" -f $major,$ubr) }
|
||||
}
|
||||
|
||||
# -------- PSWindowsUpdate bootstrap (silent) --------
|
||||
function Ensure-PSWindowsUpdate {
|
||||
try {
|
||||
Import-Module PSWindowsUpdate -ErrorAction Stop
|
||||
$m = Get-Module PSWindowsUpdate
|
||||
Write-Log ("PSWindowsUpdate module loaded (v{0})." -f $m.Version)
|
||||
return $true
|
||||
} catch {}
|
||||
|
||||
if (-not $AllowInstallPSWindowsUpdate) {
|
||||
Write-Log "PSWindowsUpdate module not found and auto-install is disabled. Exiting 120."
|
||||
exit 120
|
||||
}
|
||||
|
||||
$scope = if (Test-IsAdmin) { 'AllUsers' } else { 'CurrentUser' }
|
||||
|
||||
try {
|
||||
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue |
|
||||
Sort-Object Version -Descending | Select-Object -First 1
|
||||
if (-not $nuget -or ([Version]$nuget.Version -lt [Version]"2.8.5.201")) {
|
||||
Write-Log "Installing NuGet provider silently..."
|
||||
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope $scope -ErrorAction Stop
|
||||
}
|
||||
|
||||
$repo = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
|
||||
if (-not $repo) {
|
||||
Write-Log "Registering PSGallery repository..."
|
||||
try { Register-PSRepository -Default -ErrorAction Stop } catch {}
|
||||
$repo = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($repo -and $repo.InstallationPolicy -ne 'Trusted') {
|
||||
Write-Log "Setting PSGallery as Trusted..."
|
||||
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
try {
|
||||
$psget = Get-Module -ListAvailable PowerShellGet | Sort-Object Version -Descending | Select-Object -First 1
|
||||
if (-not $psget -or ([Version]$psget.Version -lt [Version]"2.2.5")) {
|
||||
Write-Log "Updating PowerShellGet silently..."
|
||||
Install-Module PowerShellGet -Scope $scope -Force -AllowClobber -ErrorAction Stop
|
||||
}
|
||||
} catch {
|
||||
Write-Log ("PowerShellGet update failed (continuing): {0}" -f $_.Exception.Message)
|
||||
}
|
||||
|
||||
Write-Log "Installing PSWindowsUpdate silently..."
|
||||
Install-Module PSWindowsUpdate -Scope $scope -Force -AllowClobber -ErrorAction Stop
|
||||
|
||||
Import-Module PSWindowsUpdate -ErrorAction Stop
|
||||
$m = Get-Module PSWindowsUpdate
|
||||
Write-Log ("PSWindowsUpdate installed and loaded (v{0})." -f $m.Version)
|
||||
return $true
|
||||
} catch {
|
||||
Write-Log ("Failed to install/load PSWindowsUpdate: {0}. Exiting 120." -f $_.Exception.Message)
|
||||
exit 120
|
||||
}
|
||||
}
|
||||
|
||||
# -------- Discovery & Install using PSWindowsUpdate --------
|
||||
function Get-AvailableOSUpdates {
|
||||
# Returns: @{ SSU = <update or $null>; LCU = <update or $null>; Raw = <array> }
|
||||
try {
|
||||
$raw = Get-WindowsUpdate -WindowsUpdate -UpdateType Software -IgnoreReboot -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Log ("Windows Update query failed: {0}" -f $_.Exception.Message)
|
||||
Write-Log "This system may be WSUS-managed or blocked by policy/network. Exiting 121."
|
||||
exit 121
|
||||
}
|
||||
|
||||
if (-not $raw) {
|
||||
return [PSCustomObject]@{ SSU=$null; LCU=$null; Raw=@() }
|
||||
}
|
||||
|
||||
$filtered = $raw | Where-Object {
|
||||
$_.Title -and
|
||||
($_.Title -notmatch 'Preview') -and
|
||||
($_.Title -notmatch 'for\s*\.NET') -and
|
||||
($_.Categories -notcontains 'Drivers') -and
|
||||
($_.Categories -notcontains 'Definition Updates')
|
||||
}
|
||||
|
||||
$norm = {
|
||||
param($s)
|
||||
if (-not $s) { return '' }
|
||||
$t = Sanitize-Ascii $s
|
||||
return $t.ToLowerInvariant()
|
||||
}
|
||||
|
||||
$ssu = $filtered | Where-Object {
|
||||
$t = & $norm $_.Title
|
||||
($t -match 'servicing stack update' -or $t -match 'pile de maintenance')
|
||||
} | Sort-Object -Property LastDeploymentChangeTime -Descending | Select-Object -First 1
|
||||
|
||||
$lcu = $filtered | Where-Object {
|
||||
$t = & $norm $_.Title
|
||||
($t -match 'cumulative update' -or $t -match 'mise a jour cumulative')
|
||||
} | Sort-Object -Property LastDeploymentChangeTime -Descending | Select-Object -First 1
|
||||
|
||||
[PSCustomObject]@{ SSU=$ssu; LCU=$lcu; Raw=$filtered }
|
||||
}
|
||||
|
||||
function Install-OneWU {
|
||||
param([Parameter(Mandatory)]$UpdateObject)
|
||||
|
||||
$updateId = $UpdateObject.UpdateID
|
||||
$title = Sanitize-Ascii $UpdateObject.Title
|
||||
$kb = $UpdateObject.KB
|
||||
|
||||
$kbPrefix = if ($kb) { "$kb " } else { "" }
|
||||
Write-Log ("Installing: {0}{1}" -f $kbPrefix, $title)
|
||||
|
||||
try {
|
||||
if ($updateId) {
|
||||
$res = Install-WindowsUpdate -WindowsUpdate -UpdateID $updateId -AcceptAll -IgnoreReboot -Confirm:$false -ErrorAction Stop
|
||||
} elseif ($kb) {
|
||||
$res = Install-WindowsUpdate -WindowsUpdate -KBArticleID $kb -AcceptAll -IgnoreReboot -Confirm:$false -ErrorAction Stop
|
||||
} else {
|
||||
$res = Install-WindowsUpdate -WindowsUpdate -AcceptAll -IgnoreReboot -Confirm:$false -ErrorAction Stop |
|
||||
Where-Object { (Sanitize-Ascii $_.Title) -eq $title }
|
||||
}
|
||||
} catch {
|
||||
Write-Log ("Install failed for {0}{1}: {2}" -f $kbPrefix, $title, $_.Exception.Message)
|
||||
return [PSCustomObject]@{
|
||||
Title = $title; KB = $kb; Result = 'Failed'; HResult = $null; RebootRequired = $false
|
||||
}
|
||||
}
|
||||
|
||||
# Build a clean result array without inline 'if'
|
||||
$out = @()
|
||||
foreach ($o in @($res)) {
|
||||
$rTitle = Sanitize-Ascii $o.Title
|
||||
|
||||
$rKB = $null
|
||||
if ($o.PSObject.Properties['KB'] -and $o.KB) { $rKB = $o.KB }
|
||||
elseif ($o.PSObject.Properties['KBArticleID'] -and $o.KBArticleID) { $rKB = $o.KBArticleID }
|
||||
|
||||
$rResult = $null
|
||||
if ($o.PSObject.Properties['Result'] -and $o.Result) { $rResult = $o.Result } else { $rResult = $o.ResultCode }
|
||||
|
||||
$rHResult = $null
|
||||
if ($o.PSObject.Properties['HResult']) { $rHResult = $o.HResult }
|
||||
|
||||
$rReboot = $false
|
||||
if ($o.PSObject.Properties['RebootRequired']) { $rReboot = [bool]$o.RebootRequired }
|
||||
|
||||
$out += [PSCustomObject]@{
|
||||
Title = $rTitle
|
||||
KB = $rKB
|
||||
Result = $rResult
|
||||
HResult = $rHResult
|
||||
RebootRequired = $rReboot
|
||||
}
|
||||
}
|
||||
return ,$out # ensure array even if single item
|
||||
}
|
||||
|
||||
# =================== Main ===================
|
||||
if (-not (Test-IsAdmin)) { Write-Log "Warning: script is not running elevated." }
|
||||
|
||||
try {
|
||||
$RebootRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
|
||||
if (Test-Path $RebootRegKey) { Write-Log "Pending reboot detected. Exiting 101."; exit 101 }
|
||||
} catch { Write-Log ("Error while checking pending reboot: {0}" -f $_.Exception.Message); exit 199 }
|
||||
|
||||
try {
|
||||
$minFreeGB = 5
|
||||
$drive = Get-PSDrive -Name C -ErrorAction Stop
|
||||
$freeGB = [math]::Round(($drive.Free/1GB),2)
|
||||
if ($freeGB -lt $minFreeGB) { Write-Log ("Insufficient free space: required {0}GB, available {1}GB. Exiting 103." -f $minFreeGB, $freeGB); exit 103 }
|
||||
} catch { Write-Log ("Error while checking free space: {0}" -f $_.Exception.Message); exit 198 }
|
||||
|
||||
$build = Get-BuildFull
|
||||
Write-Log ("Detected OS: Build={0}" -f $build.BuildFull)
|
||||
|
||||
Ensure-PSWindowsUpdate | Out-Null
|
||||
|
||||
$updates = Get-AvailableOSUpdates
|
||||
|
||||
# Plan (pre-calc strings to avoid inline if in -f)
|
||||
if ($updates.SSU) {
|
||||
$kbPrefix = if ($updates.SSU.KB) { "$($updates.SSU.KB) " } else { "" }
|
||||
$title = Sanitize-Ascii $updates.SSU.Title
|
||||
Write-Log ("Plan: SSU -> {0}{1}" -f $kbPrefix, $title)
|
||||
}
|
||||
if ($updates.LCU) {
|
||||
$kbPrefix = if ($updates.LCU.KB) { "$($updates.LCU.KB) " } else { "" }
|
||||
$title = Sanitize-Ascii $updates.LCU.Title
|
||||
Write-Log ("Plan: LCU -> {0}{1}" -f $kbPrefix, $title)
|
||||
}
|
||||
|
||||
if (-not $updates.SSU -and -not $updates.LCU) {
|
||||
Write-Log "No OS SSU/LCU updates available (system appears up-to-date)."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$needReboot = $false
|
||||
$installed = $false
|
||||
$failedAny = $false
|
||||
|
||||
if ($updates.SSU) {
|
||||
$r = Install-OneWU -UpdateObject $updates.SSU
|
||||
foreach ($i in @($r)) {
|
||||
$kbPrefix = if ($i.KB) { "$($i.KB) " } else { "" }
|
||||
$hrSuf = if ($i.HResult) { " (HResult=$($i.HResult))" } else { "" }
|
||||
$rbSuf = if ($i.RebootRequired) { " [RebootRequired]" } else { "" }
|
||||
Write-Log ("Result: {0}{1} => {2}{3}{4}" -f $kbPrefix, $i.Title, $i.Result, $hrSuf, $rbSuf)
|
||||
|
||||
if ($i.Result -match 'Succeeded') { $installed = $true }
|
||||
elseif ($i.Result -match 'Failed|Aborted|Error') { $failedAny = $true }
|
||||
if ($i.RebootRequired) { $needReboot = $true }
|
||||
}
|
||||
}
|
||||
|
||||
if ($updates.LCU) {
|
||||
$r = Install-OneWU -UpdateObject $updates.LCU
|
||||
foreach ($i in @($r)) {
|
||||
$kbPrefix = if ($i.KB) { "$($i.KB) " } else { "" }
|
||||
$hrSuf = if ($i.HResult) { " (HResult=$($i.HResult))" } else { "" }
|
||||
$rbSuf = if ($i.RebootRequired) { " [RebootRequired]" } else { "" }
|
||||
Write-Log ("Result: {0}{1} => {2}{3}{4}" -f $kbPrefix, $i.Title, $i.Result, $hrSuf, $rbSuf)
|
||||
|
||||
if ($i.Result -match 'Succeeded') { $installed = $true }
|
||||
elseif ($i.Result -match 'Failed|Aborted|Error') { $failedAny = $true }
|
||||
if ($i.RebootRequired) { $needReboot = $true }
|
||||
}
|
||||
}
|
||||
|
||||
if ($failedAny) { Write-Log "One or more updates failed."; exit 195 }
|
||||
if ($needReboot) { exit 3010 }
|
||||
if ($installed) { exit 0 }
|
||||
exit 0
|
||||
41
install-msu-web-download/readme.md
Normal file
41
install-msu-web-download/readme.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# MSU Direct Download & Silent Install (PowerShell)
|
||||
|
||||
Download a specific **MSU** from Microsoft Update Catalog, run **pre-checks**, install with `wusa.exe /quiet /norestart`, and return the installer’s exit code. Logs to `C:\Windows\Temp\MSU_Install.log`.
|
||||
|
||||
## What it checks
|
||||
- **Pending reboot** → exits **101**.
|
||||
- **Free space on C:** ≥ **5 GB** (default) → exits **103**.
|
||||
- **Download** success from the Catalog URL → exits **104** on failure.
|
||||
- Otherwise runs `wusa` and returns its native **exit code** (`0` OK, `3010` reboot required).
|
||||
|
||||
## Quick start
|
||||
1. Edit the variables at the top of the script:
|
||||
- `$MSUUrl` (direct Catalog URL)
|
||||
- `$MSUFile` (e.g. `windows10.0-kb5060531-x64.msu`)
|
||||
- Optional: `$minFreeGB`
|
||||
2. Run as Administrator:
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Install-MSU.ps1
|
||||
```
|
||||
|
||||
## Exit codes
|
||||
- **0** = Installed successfully
|
||||
- **3010** = Installed, reboot required (from `wusa`)
|
||||
- **101** = Pending reboot detected
|
||||
- **103** = Not enough free space on C:
|
||||
- **104** = Download failed
|
||||
- **others** = Native `wusa.exe` codes
|
||||
|
||||
## Requirements
|
||||
- Windows 10/11 or Server, **elevated** PowerShell.
|
||||
- Outbound access to the Microsoft Update Catalog URL.
|
||||
- `wusa.exe` available (built-in).
|
||||
|
||||
## Notes
|
||||
- The script deletes any existing MSU at the target path before downloading.
|
||||
- If proxy/TLS blocks `WebClient`, switch to `Invoke-WebRequest` and enforce TLS 1.2:
|
||||
```powershell
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
Invoke-WebRequest -Uri $MSUUrl -OutFile $MSUPath -UseBasicParsing
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user