-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUpdate.NET.ps1
More file actions
262 lines (229 loc) · 10.4 KB
/
Copy pathUpdate.NET.ps1
File metadata and controls
262 lines (229 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
param(
[switch]$DryRun
)
$ErrorActionPreference = 'Stop'
# Root folder containing manifest.json and payloads
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$root = Join-Path $scriptRoot 'KBs\NET'
$manifest = Join-Path $root 'manifest.json'
if (-not (Test-Path $manifest)) {
Write-Error "manifest.json not found at $manifest"
}
Write-Host ">>> Loading manifest database..." -ForegroundColor Cyan
$items = Get-Content $manifest -Raw | ConvertFrom-Json
# Detect installed runtimes and SDKs
Write-Host ">>> Scanning local system for installed .NET environments..." -ForegroundColor Cyan
$runtimesX64 = @()
$runtimesX86 = @()
$sdksX64 = @()
$sdksX86 = @()
$hasHosting = $false
try { $runtimesX64 = & dotnet.exe --list-runtimes 2>$null } catch {}
try { $sdksX64 = & dotnet.exe --list-sdks 2>$null } catch {}
$dotnetX86 = 'C:\Program Files (x86)\dotnet\dotnet.exe'
if (Test-Path $dotnetX86) {
try { $runtimesX86 = & $dotnetX86 --list-runtimes 2>$null } catch {}
try { $sdksX86 = & $dotnetX86 --list-sdks 2>$null } catch {}
}
# Hosting bundle detection
$hostingKey = 'HKLM:\SOFTWARE\Microsoft\IIS Extensions\IIS AspNetCore Module V2'
if (Test-Path $hostingKey) {
$hasHosting = $true
Write-Host " Found IIS ASP.NET Core Module V2 registration." -ForegroundColor Gray
}
# Helper to check runtime presence and get maximum installed version
function Get-MaxInstalledRuntimeVersion {
param(
[string]$Flavor,
[string]$Arch
)
$src = if ($Arch -eq 'x86') { $runtimesX86 } else { $runtimesX64 }
if (-not $src) { return $null }
# Parse out versions matching the flavor (e.g., "Microsoft.NETCore.App [8.0.4]")
$versions = $src | Where-Object { $_ -match [regex]::Escape($Flavor) } | ForEach-Object {
if ($_ -match '(\d+\.\d+\.\d+)') { [version]$Matches[1] }
}
if (-not $versions) { return $null }
return ($versions | Measure-Object -Maximum).Maximum
}
# Helper to get maximum installed SDK major/minor matching version
function Get-MaxInstalledSdkVersion {
param(
[string]$Arch
)
$src = if ($Arch -eq 'x86') { $sdksX86 } else { $sdksX64 }
if (-not $src) { return $null }
$versions = $src | ForEach-Object {
if ($_ -match '^(\d+\.\d+\.\d+)') { [version]$Matches[1] }
}
if (-not $versions) { return $null }
return ($versions | Measure-Object -Maximum).Maximum
}
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Yellow
Write-Host " Evaluating applicable .NET updates..." -ForegroundColor Yellow
Write-Host "=========================================================" -ForegroundColor Yellow
Write-Host ""
$applied = @()
foreach ($item in $items) {
$fileName = $item.FileName
if (-not $fileName) { continue }
$fullPath = Join-Path $root $fileName
if (-not (Test-Path $fullPath)) {
Write-Warning "File listed in manifest but not found in storage directory: $fileName"
continue
}
$name = $fileName.ToLowerInvariant()
$shouldInstall = $false
$skipReason = ""
# Pull target file metadata version for smart comparison if it's an executable
$fileVersion = $null
if ($name.EndsWith('.exe')) {
try {
$fileVersionInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($fullPath)
if ($fileVersionInfo.ProductVersion -match '^(\d+\.\d+\.\d+)') {
$fileVersion = [version]$Matches[1]
}
} catch {
Write-Verbose "Could not extract version from $fileName"
}
}
# Determine Architecture context
$archContext = if ($name -like '*-x86*' -or $name -like '*-win-x86*') { 'x86' } else { 'x64' }
# 1. Desktop Runtime
if ($name -like '*windowsdesktop-runtime*') {
$maxLocal = Get-MaxInstalledRuntimeVersion 'Microsoft.WindowsDesktop.App' $archContext
if ($null -ne $maxLocal) {
if ($fileVersion -and $maxLocal -ge $fileVersion) {
$skipReason = "Current or newer version ($maxLocal) already installed on system"
} else { $shouldInstall = $true }
} else {
$skipReason = "Base WindowsDesktop Runtime ($archContext) is not active on this machine"
}
}
# 2. Core Runtime
elseif ($name -like '*dotnet-runtime*') {
$maxLocal = Get-MaxInstalledRuntimeVersion 'Microsoft.NETCore.App' $archContext
if ($null -ne $maxLocal) {
if ($fileVersion -and $maxLocal -ge $fileVersion) {
$skipReason = "Current or newer version ($maxLocal) already installed on system"
} else { $shouldInstall = $true }
} else {
$skipReason = "Base .NET Core Runtime ($archContext) is not active on this machine"
}
}
# 3. ASP.NET Core Runtime
elseif ($name -like '*aspnetcore-runtime*') {
$maxLocal = Get-MaxInstalledRuntimeVersion 'Microsoft.AspNetCore.App' $archContext
if ($null -ne $maxLocal) {
if ($fileVersion -and $maxLocal -ge $fileVersion) {
$skipReason = "Current or newer version ($maxLocal) already installed on system"
} else { $shouldInstall = $true }
} else {
$skipReason = "Base ASP.NET Core Runtime ($archContext) is not active on this machine"
}
}
# 4. SDKs
elseif ($name -like '*dotnet-sdk*') {
$maxLocal = Get-MaxInstalledSdkVersion $archContext
if ($null -ne $maxLocal) {
# Check major/minor version boundary alignment
if ($fileVersion) {
if ($maxLocal -ge $fileVersion) {
$skipReason = "Current or newer SDK version ($maxLocal) already installed"
} elseif ($maxLocal.Major -eq $fileVersion.Major) {
# Same major band but older system patch, clear for upgrade
$shouldInstall = $true
} else {
$skipReason = "SDK version belongs to a different generation band"
}
} else { $shouldInstall = $true }
} else {
$skipReason = "No existing .NET SDK installation detected for architecture $archContext"
}
}
# 5. Hosting Bundle
elseif ($name -like '*dotnet-hosting*') {
if ($hasHosting) {
# Compare using the ASP.NET Core engine version bundled inside the installer
$maxLocal = Get-MaxInstalledRuntimeVersion 'Microsoft.AspNetCore.App' 'x64'
if ($fileVersion -and $maxLocal -and $maxLocal -ge $fileVersion) {
$skipReason = "Hosting bundle components are already current ($maxLocal)"
} else { $shouldInstall = $true }
} else {
$skipReason = "IIS Environment / AspNetCore Module V2 is not enabled on host machine"
}
}
# 6. .NET Framework MSU (Windows 11 Servicing)
elseif ($name -like '*ndp481*') {
# Check if KB payload is already tracked inside WUSA hotfix catalog
if ($name -match 'kb(\d+)') {
$kbId = $Matches[1]
if (Get-HotFix -Id "KB$kbId" -ErrorAction SilentlyContinue) {
$skipReason = "Windows Hotfix KB$kbId is already active on this operating system"
} else { $shouldInstall = $true }
} else {
$shouldInstall = $true
}
}
# Process Actions
if ($shouldInstall) {
if ($DryRun) {
Write-Host "[DryRun] Would upgrade/install target: $fileName" -ForegroundColor DarkYellow
}
else {
Write-Host ">>> Processing patch execution for: $fileName" -ForegroundColor White
if ($name.EndsWith('.msu')) {
Write-Host " Executing Windows Update Standalone Installer..." -ForegroundColor Gray
Start-Process wusa.exe -ArgumentList "`"$fullPath`" /quiet /norestart" -Wait
}
elseif ($name.EndsWith('.cab')) {
$isDriver = $false
$tempCab = Join-Path $env:TEMP "cabtest_$([guid]::NewGuid().ToString())"
New-Item -ItemType Directory -Path $tempCab -Force | Out-Null
try {
expand.exe "$fullPath" -F:* "$tempCab" 2>$null | Out-Null
if (Get-ChildItem "$tempCab" -Filter *.inf -ErrorAction SilentlyContinue) {
$isDriver = $true
}
} catch {}
Remove-Item $tempCab -Recurse -Force -ErrorAction SilentlyContinue
if ($isDriver) {
Write-Host " Injecting Driver package components..." -ForegroundColor Gray
Start-Process pnputil.exe -ArgumentList "/add-driver `"$fullPath`" /install" -Wait
}
else {
Write-Host " Injecting Operating System Update package via DISM deployment engine..." -ForegroundColor Gray
Start-Process dism.exe -ArgumentList "/online /add-package /packagepath:`"$fullPath`" /quiet /norestart" -Wait
}
}
else {
Write-Host " Spawning background silent package deployment thread..." -ForegroundColor Gray
# Using Start-Process instead of direct invocation to protect execution pipelines from hanging on background mutexes
Start-Process -FilePath $fullPath -ArgumentList "/quiet /norestart" -Wait -NoNewWindow
}
Write-Host " Done processing: $fileName" -ForegroundColor Green
}
$applied += $fileName
} else {
if ($skipReason) {
Write-Host "[-] Skipping: $fileName" -ForegroundColor Gray
Write-Host " Reason: $skipReason" -ForegroundColor DarkGray
}
}
}
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Yellow
Write-Host " Processing Summary" -ForegroundColor Yellow
Write-Host "=========================================================" -ForegroundColor Yellow
if (-not $applied) {
Write-Host "All components match or exceed manifest targets. No operations needed." -ForegroundColor Green
} else {
if ($DryRun) {
Write-Host "DryRun evaluation phase finalized. The following assets would be updated:" -ForegroundColor Yellow
} else {
Write-Host "Successfully analyzed and processed the following changes:" -ForegroundColor Green
}
$applied | ForEach-Object { Write-Host " [+] $_" -ForegroundColor Cyan }
}
Write-Host ""