649 lines
24 KiB
PowerShell
649 lines
24 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Windows Update Manager with Rich Notifications
|
|
.DESCRIPTION
|
|
Automated Windows Update manager with Windows 10/11 Action Center notifications
|
|
and detailed update popup windows
|
|
.NOTES
|
|
Author: oxmc
|
|
Version: 2.5
|
|
Requires: PowerShell 5.1+, Administrator privileges
|
|
#>
|
|
|
|
#Requires -RunAsAdministrator
|
|
|
|
param(
|
|
[switch]$Silent,
|
|
[switch]$AutoInstall,
|
|
[switch]$CheckOnly
|
|
)
|
|
|
|
# Import notification module
|
|
Import-Module "$PSScriptRoot\UpdateNotifications.psm1" -Force
|
|
|
|
$script:DataDir = "$env:ProgramData\WindowsUpdateManager"
|
|
$script:LogPath = "$script:DataDir\WindowsUpdate.log"
|
|
$script:ConfigPath = "$PSScriptRoot\UpdateConfig.json"
|
|
|
|
#region Configuration
|
|
|
|
function Get-UpdateConfiguration {
|
|
if (Test-Path $script:ConfigPath) {
|
|
try {
|
|
$config = Get-Content $script:ConfigPath -Raw | ConvertFrom-Json
|
|
Write-Log "Configuration loaded from $script:ConfigPath" -Level "INFO"
|
|
return $config
|
|
}
|
|
catch {
|
|
Write-Log "Failed to load config, using defaults" -Level "WARNING"
|
|
}
|
|
}
|
|
|
|
# Create default config
|
|
$defaultConfig = @{
|
|
AutoInstall = $false
|
|
AutoReboot = $false
|
|
IncludeDrivers = $false
|
|
IncludeOptional = $false
|
|
ShowNotifications = $true
|
|
CheckIntervalHours = 12
|
|
LogPath = "C:\Logs\WindowsUpdate.log"
|
|
}
|
|
|
|
# Save default config to file
|
|
try {
|
|
$defaultConfig | ConvertTo-Json -Depth 5 | Out-File $script:ConfigPath -Encoding UTF8
|
|
Write-Log "Created default configuration at $script:ConfigPath" -Level "SUCCESS"
|
|
}
|
|
catch {
|
|
Write-Log "Could not create config file: $($_.Exception.Message)" -Level "WARNING"
|
|
}
|
|
|
|
return $defaultConfig
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Logging
|
|
|
|
function Write-Log {
|
|
param(
|
|
[string]$Message,
|
|
[ValidateSet("INFO", "WARNING", "ERROR", "SUCCESS")]
|
|
[string]$Level = "INFO"
|
|
)
|
|
|
|
$logDir = Split-Path $script:LogPath -Parent
|
|
if (!(Test-Path $logDir)) {
|
|
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
|
}
|
|
|
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
$logEntry = "[$timestamp] [$Level] $Message"
|
|
Add-Content -Path $script:LogPath -Value $logEntry
|
|
|
|
$color = switch ($Level) {
|
|
"ERROR" { "Red" }
|
|
"WARNING" { "Yellow" }
|
|
"SUCCESS" { "Green" }
|
|
default { "White" }
|
|
}
|
|
Write-Host $logEntry -ForegroundColor $color
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Update Functions
|
|
|
|
function Start-UpdateService {
|
|
param([string]$Name)
|
|
$svc = Get-Service -Name $Name -ErrorAction SilentlyContinue
|
|
if (-not $svc) { Write-Log "Service $Name not found" "ERROR"; return $false }
|
|
if ($svc.Status -eq 'Running') { return $true }
|
|
try {
|
|
Start-Service -Name $Name -ErrorAction Stop
|
|
return $true
|
|
} catch {
|
|
Write-Log "Could not start $Name`: $($_.Exception.Message)" "ERROR"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Test-UpdateServices {
|
|
Write-Log "Ensuring Windows Update services are available..." -Level "INFO"
|
|
|
|
$ok = $true
|
|
foreach ($svc in @("wuauserv", "BITS", "cryptsvc")) {
|
|
if (-not (Start-UpdateService $svc)) { $ok = $false }
|
|
}
|
|
return $ok
|
|
}
|
|
|
|
function Repair-UpdateServices {
|
|
Write-Log "Repair: stopping services and clearing corrupt cache..." -Level "WARNING"
|
|
|
|
foreach ($svc in @("BITS", "wuauserv", "cryptsvc")) {
|
|
Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
if (Test-Path "C:\Windows\SoftwareDistribution\Download") {
|
|
Remove-Item "C:\Windows\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-Log "Cleared SoftwareDistribution download cache" -Level "SUCCESS"
|
|
}
|
|
|
|
$ok = $true
|
|
foreach ($svc in @("BITS", "wuauserv", "cryptsvc")) {
|
|
if (-not (Start-UpdateService $svc)) { $ok = $false }
|
|
}
|
|
return $ok
|
|
}
|
|
|
|
function Get-AvailableUpdates {
|
|
param([PSCustomObject]$Config)
|
|
|
|
Write-Log "Searching for available updates..." -Level "INFO"
|
|
|
|
try {
|
|
$session = New-Object -ComObject "Microsoft.Update.Session"
|
|
$searcher = $session.CreateUpdateSearcher()
|
|
|
|
$criteria = "IsInstalled=0"
|
|
Write-Log "Search criteria: $criteria" -Level "INFO"
|
|
|
|
$searchResult = $searcher.Search($criteria)
|
|
Write-Log "Found $($searchResult.Updates.Count) total updates" -Level "INFO"
|
|
|
|
if ($searchResult.Updates.Count -eq 0) {
|
|
return @()
|
|
}
|
|
|
|
# Filter updates based on config
|
|
$filteredUpdates = @()
|
|
foreach ($update in $searchResult.Updates) {
|
|
$shouldInclude = $true
|
|
|
|
# Debug: Show update type
|
|
Write-Log "Evaluating: $($update.Title) - Type: $($update.Type)" -Level "INFO"
|
|
|
|
# Filter drivers
|
|
if (-not $Config.IncludeDrivers -and $update.Type -eq 2) {
|
|
Write-Log "FILTERED: $($update.Title) - Driver update excluded by config" -Level "WARNING"
|
|
$shouldInclude = $false
|
|
}
|
|
|
|
# Filter optional updates
|
|
if ($shouldInclude -and -not $Config.IncludeOptional -and -not $update.IsMandatory) {
|
|
# Check if it's a critical/security/important update
|
|
$isImportant = $false
|
|
foreach ($category in $update.Categories) {
|
|
if ($category.Name -match "Critical|Security|Important|Definition") {
|
|
$isImportant = $true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (-not $isImportant) {
|
|
Write-Log "FILTERED: $($update.Title) - Optional update excluded by config" -Level "WARNING"
|
|
$shouldInclude = $false
|
|
}
|
|
}
|
|
|
|
if ($shouldInclude) {
|
|
Write-Log "INCLUDED: $($update.Title)" -Level "SUCCESS"
|
|
$filteredUpdates += $update
|
|
}
|
|
}
|
|
|
|
Write-Log "After filtering: $($filteredUpdates.Count) updates remaining" -Level "SUCCESS"
|
|
return $filteredUpdates
|
|
}
|
|
catch {
|
|
Write-Log "Error searching for updates: $($_.Exception.Message)" -Level "ERROR"
|
|
return @()
|
|
}
|
|
}
|
|
|
|
function Send-PipeMessage {
|
|
param([System.IO.StreamWriter]$Writer, [hashtable]$Msg)
|
|
try { $Writer.WriteLine(($Msg | ConvertTo-Json -Compress -Depth 4)) } catch {}
|
|
}
|
|
|
|
function Install-WindowsUpdates {
|
|
param(
|
|
[array]$Updates,
|
|
[scriptblock]$OnMessage = $null # if set, progress goes to pipe instead of WinForms
|
|
)
|
|
|
|
if (@($Updates).Count -eq 0) {
|
|
Write-Log "No updates to install" -Level "INFO"
|
|
return [PSCustomObject]@{ Succeeded = 0; Failed = 0; RebootRequired = $false }
|
|
}
|
|
|
|
Write-Log "Installing $($Updates.Count) update(s)..." -Level "INFO"
|
|
|
|
$useWinForms = ($null -eq $OnMessage)
|
|
$progressForm = $null
|
|
|
|
if ($useWinForms) {
|
|
Show-UpdateInstallingToast -UpdateCount $Updates.Count
|
|
$progressForm = Show-InstallationProgressWindow -Updates $Updates
|
|
}
|
|
|
|
$emit = {
|
|
param($msg)
|
|
if ($OnMessage) { & $OnMessage $msg }
|
|
}
|
|
|
|
try {
|
|
$session = New-Object -ComObject "Microsoft.Update.Session"
|
|
$collection = New-Object -ComObject "Microsoft.Update.UpdateColl"
|
|
|
|
$i = 1
|
|
foreach ($u in $Updates) {
|
|
Write-Log "Queuing: $($u.Title)" -Level "INFO"
|
|
if ($useWinForms) { Update-InstallationProgress -Form $progressForm -Current $i -Total $Updates.Count -CurrentUpdate $u.Title -Status "Preparing" }
|
|
& $emit @{ cmd = "progress"; phase = "Preparing"; index = $i; total = $Updates.Count; title = $u.Title; percent = 0 }
|
|
$collection.Add($u) | Out-Null
|
|
$i++
|
|
}
|
|
|
|
# Download
|
|
Write-Log "Downloading..." -Level "INFO"
|
|
if ($useWinForms) { Update-InstallationProgress -Form $progressForm -Current 0 -Total $Updates.Count -CurrentUpdate "Downloading..." -Status "Downloading" }
|
|
& $emit @{ cmd = "progress"; phase = "Downloading"; index = 0; total = $Updates.Count; title = "Downloading updates..."; percent = 0.1 }
|
|
|
|
$downloader = $session.CreateUpdateDownloader()
|
|
$downloader.Updates = $collection
|
|
$dlResult = $downloader.Download()
|
|
|
|
if ($dlResult.ResultCode -ne 2) {
|
|
Write-Log "Download failed (code $($dlResult.ResultCode))" -Level "ERROR"
|
|
& $emit @{ cmd = "progress"; phase = "Error"; index = 0; total = $Updates.Count; title = "Download failed"; percent = 0 }
|
|
if ($useWinForms) { if ($progressForm -and -not $progressForm.IsDisposed) { $progressForm.Close(); $progressForm.Dispose() } }
|
|
return [PSCustomObject]@{ Succeeded = 0; Failed = $Updates.Count; RebootRequired = $false }
|
|
}
|
|
|
|
Write-Log "Download complete. Installing..." -Level "SUCCESS"
|
|
if ($useWinForms) { Update-InstallationProgressBar -Form $progressForm -PercentComplete 50 -Status "Installing..." }
|
|
& $emit @{ cmd = "progress"; phase = "Installing"; index = 0; total = $Updates.Count; title = "Installing updates..."; percent = 0.5 }
|
|
|
|
$installer = $session.CreateUpdateInstaller()
|
|
$installer.Updates = $collection
|
|
$instResult = $installer.Install()
|
|
|
|
if ($useWinForms -and $progressForm -and -not $progressForm.IsDisposed) { $progressForm.Close(); $progressForm.Dispose() }
|
|
|
|
Write-Log "Install result code: $($instResult.ResultCode)" -Level "INFO"
|
|
|
|
$succeeded = 0; $failed = 0
|
|
for ($j = 0; $j -lt $Updates.Count; $j++) {
|
|
$ur = $instResult.GetUpdateResult($j)
|
|
$u = $Updates[$j]
|
|
$ok = $ur.ResultCode -in @(2, 3)
|
|
if ($ok) { $succeeded++ } else { $failed++ }
|
|
Write-Log "$(if ($ok) { 'OK' } else { 'FAIL' }): $($u.Title)" -Level $(if ($ok) { "SUCCESS" } else { "ERROR" })
|
|
& $emit @{
|
|
cmd = "progress"
|
|
phase = if ($ok) { "Installed" } else { "Failed" }
|
|
index = $j + 1
|
|
total = $Updates.Count
|
|
title = $u.Title
|
|
percent = ($j + 1) / $Updates.Count
|
|
}
|
|
}
|
|
|
|
if ($instResult.RebootRequired) { Write-Log "Reboot required." -Level "WARNING" }
|
|
|
|
return [PSCustomObject]@{ Succeeded = $succeeded; Failed = $failed; RebootRequired = $instResult.RebootRequired }
|
|
}
|
|
catch {
|
|
if ($useWinForms -and $progressForm -and -not $progressForm.IsDisposed) { $progressForm.Close(); $progressForm.Dispose() }
|
|
Write-Log "Installation error: $($_.Exception.Message)" -Level "ERROR"
|
|
return [PSCustomObject]@{ Succeeded = 0; Failed = $Updates.Count; RebootRequired = $false }
|
|
}
|
|
}
|
|
|
|
function Get-UpdateErrorMessage {
|
|
param([int]$HResult)
|
|
|
|
$errorMessages = @{
|
|
0x80240001 = "WU_E_NO_SERVICE - Windows Update Agent was unable to provide the service"
|
|
0x80240002 = "WU_E_MAX_CAPACITY_REACHED - The maximum capacity of the service was exceeded"
|
|
0x80240016 = "WU_E_INSTALL_NOT_ALLOWED - Operation tried to install while another installation was in progress"
|
|
0x80240017 = "WU_E_NOT_APPLICABLE - Operation was not performed because there are no applicable updates"
|
|
0x80240022 = "WU_E_REBOOT_REQUIRED - The operation could not be completed because a reboot is required"
|
|
0x80246007 = "WU_E_DM_NOTDOWNLOADED - The update has not been downloaded (driver updates may require manual download)"
|
|
0x80070BC9 = "ERROR_FAIL_REBOOT_REQUIRED - The requested operation failed. A system reboot is required"
|
|
0x80070643 = "ERROR_INSTALL_FAILURE - Fatal error during installation"
|
|
0x80070490 = "ERROR_NOT_FOUND - Element not found"
|
|
0x800F0922 = "CBS_E_INSTALLERS_FAILED - Processing advanced installers failed"
|
|
0x80242014 = "WU_E_UH_POSTREBOOTSTILLPENDING - The post-reboot operation for the update is still in progress"
|
|
0x80242006 = "WU_E_UH_INVALIDMETADATA - Update contains invalid metadata"
|
|
}
|
|
|
|
if ($errorMessages.ContainsKey($HResult)) {
|
|
return $errorMessages[$HResult]
|
|
}
|
|
else {
|
|
$hexCode = "0x{0:X8}" -f $HResult
|
|
return "Unknown error ($hexCode)"
|
|
}
|
|
}
|
|
|
|
function Invoke-RebootRequest {
|
|
param([bool]$RebootRequired)
|
|
|
|
if (-not $RebootRequired) { return }
|
|
|
|
$config = Get-UpdateConfiguration
|
|
|
|
if ($config.AutoReboot -or $AutoInstall) {
|
|
Write-Log "Auto-reboot enabled. System will restart in 5 minutes..." -Level "WARNING"
|
|
|
|
# Show 5 minute warning
|
|
Show-RebootRequiredToast -MinutesUntilReboot 5
|
|
|
|
Show-BalloonNotification -Title "Restart Required" `
|
|
-Message "Your computer will restart in 5 minutes to complete update installation" `
|
|
-Icon "Warning"
|
|
|
|
Start-Sleep -Seconds 300
|
|
Restart-Computer -Force
|
|
}
|
|
else {
|
|
# Show reboot notification without auto-restart
|
|
Show-RebootRequiredToast -MinutesUntilReboot 0
|
|
|
|
Show-BalloonNotification -Title "Restart Required" `
|
|
-Message "Please restart your computer to complete update installation" `
|
|
-Icon "Warning"
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Main Logic
|
|
|
|
function Start-UpdateCheck {
|
|
Write-Log "=== Windows Update Manager v2.5 Starting ===" -Level "INFO"
|
|
|
|
# Check services
|
|
if (-not (Test-UpdateServices)) {
|
|
Write-Log "Update services not running. Attempting repair..." -Level "WARNING"
|
|
|
|
if (-not (Repair-UpdateServices)) {
|
|
Write-Log "Failed to repair services. Exiting." -Level "ERROR"
|
|
|
|
Show-BalloonNotification -Title "Service Error" `
|
|
-Message "Windows Update services could not be started" `
|
|
-Icon "Error"
|
|
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Load configuration
|
|
$config = Get-UpdateConfiguration
|
|
|
|
# Get available updates - force PS array so .Count is always reliable
|
|
$updates = @(Get-AvailableUpdates -Config $config)
|
|
|
|
if (@($updates).Count -eq 0) {
|
|
Write-Log "No updates available" -Level "SUCCESS"
|
|
|
|
if (-not $Silent) {
|
|
Show-BalloonNotification -Title "Windows Update" `
|
|
-Message "Your system is up to date. No updates available." `
|
|
-Icon "Info"
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
Write-Log "Found $($updates.Count) available updates" -Level "SUCCESS"
|
|
|
|
if ($CheckOnly) {
|
|
Write-Log "Check-only mode. Exiting." -Level "INFO"
|
|
return
|
|
}
|
|
|
|
$config = Get-UpdateConfiguration
|
|
$pipeName = "WUM_IPC_$(Get-Date -Format 'yyyyMMddHHmmss')"
|
|
|
|
if ($AutoInstall -or $config.AutoInstall) {
|
|
$userLoggedIn = [bool](Get-Process explorer -ErrorAction SilentlyContinue)
|
|
if ($userLoggedIn) {
|
|
Write-Log "Auto-install: notifying user and installing via pipe..." -Level "INFO"
|
|
Invoke-UpdatePipe -Updates $updates -PipeName $pipeName -AutoInstall
|
|
} else {
|
|
Write-Log "Auto-install: no user session, installing silently..." -Level "INFO"
|
|
$result = Install-WindowsUpdates -Updates $updates
|
|
if ($result.RebootRequired) { Invoke-RebootRequest -RebootRequired $true }
|
|
}
|
|
} else {
|
|
Write-Log "Waiting for user approval via pipe..." -Level "INFO"
|
|
Invoke-UpdatePipe -Updates $updates -PipeName $pipeName
|
|
}
|
|
}
|
|
|
|
function Invoke-UpdatePipe {
|
|
param(
|
|
[array]$Updates,
|
|
[string]$PipeName,
|
|
[switch]$AutoInstall # skip approval, go straight to install+progress
|
|
)
|
|
|
|
$pipeSecurity = [System.IO.Pipes.PipeSecurity]::new()
|
|
$sid = [System.Security.Principal.SecurityIdentifier]::new(
|
|
[System.Security.Principal.WellKnownSidType]::AuthenticatedUserSid, $null)
|
|
$rule = [System.IO.Pipes.PipeAccessRule]::new(
|
|
$sid, [System.IO.Pipes.PipeAccessRights]::ReadWrite,
|
|
[System.Security.AccessControl.AccessControlType]::Allow)
|
|
$pipeSecurity.AddAccessRule($rule)
|
|
|
|
$pipe = [System.IO.Pipes.NamedPipeServerStream]::new(
|
|
$PipeName,
|
|
[System.IO.Pipes.PipeDirection]::InOut, 1,
|
|
[System.IO.Pipes.PipeTransmissionMode]::Byte,
|
|
[System.IO.Pipes.PipeOptions]::None,
|
|
65536, 65536, $pipeSecurity)
|
|
|
|
$reader = $null
|
|
$writer = $null
|
|
|
|
try {
|
|
$spawned = Invoke-UserInteractiveSession -PipeName $PipeName
|
|
if (-not $spawned) { return }
|
|
|
|
Write-Log "Pipe server listening on $PipeName ..."
|
|
$cts = [System.Threading.CancellationTokenSource]::new([System.TimeSpan]::FromHours(2))
|
|
$task = $pipe.WaitForConnectionAsync($cts.Token)
|
|
while (-not $task.IsCompleted) { Start-Sleep -Milliseconds 200 }
|
|
if ($task.IsFaulted -or $task.IsCanceled) { Write-Log "Pipe wait cancelled." "WARN"; return }
|
|
Write-Log "UI connected."
|
|
|
|
$enc = [System.Text.UTF8Encoding]::new($false)
|
|
$writer = [System.IO.StreamWriter]::new($pipe, $enc, 65536, $true)
|
|
$reader = [System.IO.StreamReader]::new($pipe, $enc, $false, 65536, $true)
|
|
$writer.AutoFlush = $true
|
|
|
|
# Build the update metadata payload (IDs + titles for toast)
|
|
$meta = @($Updates | ForEach-Object {
|
|
[PSCustomObject]@{ UpdateID = $_.Identity.UpdateID; Title = $_.Title }
|
|
})
|
|
|
|
$toInstall = $Updates
|
|
|
|
if ($AutoInstall) {
|
|
# Tell UI to show "installing" notification - no approval needed
|
|
Send-PipeMessage $writer @{ cmd = "auto_install"; count = $Updates.Count; updates = @($meta | ForEach-Object { $_.Title }) }
|
|
} else {
|
|
# Send for user approval
|
|
$writer.WriteLine(($meta | ConvertTo-Json -Compress))
|
|
Write-Log "Waiting for user approval..."
|
|
|
|
$response = $reader.ReadLine()
|
|
Write-Log "UI response received."
|
|
|
|
if ([string]::IsNullOrEmpty($response) -or $response -eq "[]") {
|
|
Write-Log "User dismissed - nothing to install."
|
|
return
|
|
}
|
|
|
|
$approvedIds = @($response | ConvertFrom-Json)
|
|
Write-Log "User approved $($approvedIds.Count) update(s). Re-querying WU..."
|
|
|
|
$wuSession = New-Object -ComObject "Microsoft.Update.Session"
|
|
$wuSearcher = $wuSession.CreateUpdateSearcher()
|
|
$wuResult = $wuSearcher.Search("IsInstalled=0")
|
|
$toInstall = @($wuResult.Updates | Where-Object { $_.Identity.UpdateID -in @($approvedIds) })
|
|
|
|
if ($toInstall.Count -eq 0) { Write-Log "Approved updates no longer in WU." "WARN"; return }
|
|
}
|
|
|
|
# Notify UI that install is starting
|
|
Send-PipeMessage $writer @{ cmd = "install_start"; count = $toInstall.Count }
|
|
|
|
# Install with progress pushed back to UI via pipe
|
|
$progressCb = {
|
|
param($msg)
|
|
Send-PipeMessage $writer $msg
|
|
}
|
|
|
|
$result = Install-WindowsUpdates -Updates $toInstall -OnMessage $progressCb
|
|
|
|
# Send completion
|
|
Send-PipeMessage $writer @{
|
|
cmd = "complete"
|
|
succeeded = $result.Succeeded
|
|
failed = $result.Failed
|
|
rebootRequired = $result.RebootRequired
|
|
}
|
|
|
|
Write-Log "Done. Succeeded: $($result.Succeeded) Failed: $($result.Failed) Reboot: $($result.RebootRequired)"
|
|
|
|
if ($result.RebootRequired) { Invoke-RebootRequest -RebootRequired $true }
|
|
}
|
|
catch {
|
|
Write-Log "Pipe error: $($_.Exception.Message)" "ERROR"
|
|
try { Send-PipeMessage $writer @{ cmd = "error"; message = $_.Exception.Message } } catch {}
|
|
}
|
|
finally {
|
|
if ($reader) { try { $reader.Dispose() } catch {} }
|
|
if ($writer) { try { $writer.Dispose() } catch {} }
|
|
try { $pipe.Close(); $pipe.Dispose() } catch {}
|
|
}
|
|
}
|
|
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
public class WumSession {
|
|
[DllImport("wtsapi32.dll", SetLastError=true)]
|
|
static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token);
|
|
|
|
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
|
|
static extern bool CreateProcessAsUser(
|
|
IntPtr hToken, string app, string cmdLine,
|
|
IntPtr procAttr, IntPtr threadAttr, bool inherit,
|
|
uint flags, IntPtr env, string dir,
|
|
ref STARTUPINFO si, out PROCESS_INFORMATION pi);
|
|
|
|
[DllImport("userenv.dll", SetLastError=true)]
|
|
static extern bool CreateEnvironmentBlock(out IntPtr env, IntPtr token, bool inherit);
|
|
|
|
[DllImport("userenv.dll", SetLastError=true)]
|
|
static extern bool DestroyEnvironmentBlock(IntPtr env);
|
|
|
|
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
|
|
struct STARTUPINFO {
|
|
public int cb, _r1; public string lpReserved, lpDesktop, lpTitle;
|
|
public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars;
|
|
public int dwFillAttribute, dwFlags; public short wShowWindow, cbReserved2;
|
|
public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
struct PROCESS_INFORMATION {
|
|
public IntPtr hProcess, hThread;
|
|
public int dwProcessId, dwThreadId;
|
|
}
|
|
|
|
public static bool SpawnInUserSession(uint sessionId, string commandLine) {
|
|
IntPtr token = IntPtr.Zero, env = IntPtr.Zero;
|
|
if (!WTSQueryUserToken(sessionId, out token)) return false;
|
|
try {
|
|
CreateEnvironmentBlock(out env, token, false);
|
|
var si = new STARTUPINFO();
|
|
si.cb = Marshal.SizeOf(si);
|
|
si.lpDesktop = "winsta0\\default";
|
|
PROCESS_INFORMATION pi;
|
|
// CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE
|
|
bool ok = CreateProcessAsUser(token, null, commandLine,
|
|
IntPtr.Zero, IntPtr.Zero, false, 0x410, env, null, ref si, out pi);
|
|
if (ok) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
|
|
return ok;
|
|
} finally {
|
|
if (env != IntPtr.Zero) DestroyEnvironmentBlock(env);
|
|
CloseHandle(token);
|
|
}
|
|
}
|
|
}
|
|
'@
|
|
|
|
function Invoke-UserInteractiveSession {
|
|
param([string]$PipeName)
|
|
|
|
$uiScript = Join-Path $PSScriptRoot "WindowsUpdateManager-UI.ps1"
|
|
if (-not (Test-Path $uiScript)) {
|
|
Write-Log "UI script not found: $uiScript" "ERROR"
|
|
return $false
|
|
}
|
|
|
|
$cmdLine = "PowerShell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$uiScript`" -PipeName `"$PipeName`""
|
|
|
|
$isSystem = [System.Security.Principal.WindowsIdentity]::GetCurrent().IsSystem
|
|
|
|
if ($isSystem) {
|
|
# Running as SYSTEM (scheduled task) - spawn directly into the user's desktop session
|
|
$explorerProc = Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if (-not $explorerProc) {
|
|
Write-Log "No interactive user session found - updates will be offered at next scheduled run." "WARN"
|
|
return $false
|
|
}
|
|
$sessionId = [uint32]$explorerProc.SessionId
|
|
$ok = [WumSession]::SpawnInUserSession($sessionId, $cmdLine)
|
|
if ($ok) {
|
|
Write-Log "UI process spawned in session $sessionId (pipe: $PipeName)"
|
|
} else {
|
|
Write-Log "SpawnInUserSession failed (error $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error()))" "ERROR"
|
|
}
|
|
return $ok
|
|
} else {
|
|
# Running as interactive admin (dev/test) - keep window open so errors are visible
|
|
Write-Log "Not SYSTEM - spawning UI in current session for testing (pipe: $PipeName)" "INFO"
|
|
Start-Process -FilePath "PowerShell.exe" `
|
|
-ArgumentList "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$uiScript`" -PipeName `"$PipeName`""
|
|
return $true
|
|
}
|
|
}
|
|
|
|
# Main entry point
|
|
try {
|
|
Start-UpdateCheck
|
|
Write-Log "=== Windows Update Manager Completed ===" -Level "SUCCESS"
|
|
}
|
|
catch {
|
|
Write-Log "Fatal error: $($_.Exception.Message)" -Level "ERROR"
|
|
|
|
Show-BalloonNotification -Title "Update Manager Error" `
|
|
-Message "An error occurred: $($_.Exception.Message)" `
|
|
-Icon "Error"
|
|
|
|
exit 1
|
|
}
|
|
|
|
#endregion |