diff --git a/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml b/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml index c5dec4a2..c16357da 100644 --- a/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml +++ b/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml @@ -128,6 +128,16 @@ jobs: ![Install counter](https://img.shields.io/github/downloads/Romanitho/Winget-AutoUpdate/v${{ steps.release_version.outputs.NextSemVer }}/WAU_InstallCounter?style=flat-square&label=Total%20reported%20installations%20for%20this%20release&color=blue) + sync: + name: Sync develop with main + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout code + uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + # Step 5: Configure Git for merge back to develop - name: Configure Git shell: bash diff --git a/.github/workflows/GitFlow_Nightly-builds.yml b/.github/workflows/GitFlow_Nightly-builds.yml index c919ad26..8cd95ac9 100644 --- a/.github/workflows/GitFlow_Nightly-builds.yml +++ b/.github/workflows/GitFlow_Nightly-builds.yml @@ -38,9 +38,9 @@ jobs: id: check_prs shell: powershell run: | - # Find the latest tag of any type - $LATEST_TAG = git for-each-ref --sort=-creatordate --format '%(refname:short)' refs/tags | Select-Object -First 1 - Write-Host "Latest tag: $LATEST_TAG" + # Get the latest release of any type + $LATEST_TAG = git tag -l --sort=-version:refname | Select-Object -First 1 + Write-Host "Latest release: $LATEST_TAG" # Get merged PRs since last tag using Git directly $MERGED_PRS = git log --merges --grep="Merge pull request" --oneline "$LATEST_TAG..${{ env.BRANCH }}" diff --git a/README.md b/README.md index 1ee1451b..353d0bb5 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,10 @@ Read more in the [Policies section](https://github.com/Romanitho/Winget-AutoUpda This script executes **if the network is active/any version of Winget is installed/WAU is running as SYSTEM**.
If **ExitCode** is **1** from `_WAU-mods.ps1` then **Re-run WAU**. +In addition to this legacy handling, a new action-based system is now supported.
+This system lets you define multiple actions and conditions directly in your mod scripts, enabling more advanced automation and control over the WAU process.
+With actions, you can execute different scripts, check results, and control the WAU flow with greater flexibility and improved logging compared to relying solely on **Exit Code**. + Likewise `_WAU-mods-postsys.ps1` can be used to do things at the end of the **SYSTEM context WAU** process before the user run. ## Custom scripts (Mods feature for Apps) diff --git a/Sources/Winget-AutoUpdate/Winget-Upgrade.ps1 b/Sources/Winget-AutoUpdate/Winget-Upgrade.ps1 index ad344eb6..998aa901 100644 --- a/Sources/Winget-AutoUpdate/Winget-Upgrade.ps1 +++ b/Sources/Winget-AutoUpdate/Winget-Upgrade.ps1 @@ -210,7 +210,7 @@ if (Test-Network) { #Compare if ((Compare-SemVer -Version1 $WAUCurrentVersion -Version2 $WAUAvailableVersion) -lt 0) { #If new version is available, update it - Write-ToLog "WAU Available version: $WAUAvailableVersion" "Yellow"; + Write-ToLog "WAU Available version: $WAUAvailableVersion" "DarkYellow"; Update-WAU; } else { @@ -246,10 +246,10 @@ if (Test-Network) { } if ($NewList) { if ($AlwaysDownloaded) { - Write-ToLog "List downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))" "Yellow" + Write-ToLog "List downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))" "DarkYellow" } else { - Write-ToLog "Newer List downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))" "Yellow" + Write-ToLog "Newer List downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))" "DarkYellow" } $Script:AlwaysDownloaded = $False } @@ -284,14 +284,14 @@ if (Test-Network) { $Script:ReachNoPath = $False } if ($NewMods -gt 0) { - Write-ToLog "$NewMods newer Mods downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))\mods" "Yellow" + Write-ToLog "$NewMods newer Mods downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))\mods" "DarkYellow" } else { if (Test-Path "$WorkingDir\mods\*.ps1") { Write-ToLog "Mods are up to date." "Green" } else { - Write-ToLog "No Mods are implemented..." "Yellow" + Write-ToLog "No Mods are implemented..." "DarkYellow" } } if ($DeletedMods -gt 0) { @@ -299,18 +299,11 @@ if (Test-Network) { } } - #Test if _WAU-mods.ps1 exist: Mods for WAU (if Network is active/any Winget is installed/running as SYSTEM) + # Test if _WAU-mods.ps1 exist: Mods for WAU (if Network is active/any Winget is installed/running as SYSTEM) $Mods = "$WorkingDir\mods" if (Test-Path "$Mods\_WAU-mods.ps1") { - Write-ToLog "Running Mods for WAU..." "Yellow" - & "$Mods\_WAU-mods.ps1" - $ModsExitCode = $LASTEXITCODE - #If _WAU-mods.ps1 has ExitCode 1 - Re-run WAU - if ($ModsExitCode -eq 1) { - Write-ToLog "Re-run WAU" - Start-Process powershell -ArgumentList "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$WorkingDir\winget-upgrade.ps1`"" - Exit - } + Write-ToLog "Running Mods for WAU..." "DarkYellow" + Test-WAUMods -WorkingDir $WorkingDir -WAUConfig $WAUConfig -GitHub_Repo $GitHub_Repo } } @@ -351,7 +344,7 @@ if (Test-Network) { } #Get outdated Winget packages - Write-ToLog "Checking application updates on Winget Repository named '$($Script:WingetSourceCustom)' .." "yellow" + Write-ToLog "Checking application updates on Winget Repository named '$($Script:WingetSourceCustom)' .." "DarkYellow" $outdated = Get-WingetOutdatedApps -src $Script:WingetSourceCustom; #If something unusual happened or no update found @@ -433,10 +426,10 @@ if (Test-Network) { Write-ToLog "No new update." "Green" } - #Test if _WAU-mods-postsys.ps1 exists: Mods for WAU (postsys) - if Network is active/any Winget is installed/running as SYSTEM _after_ SYSTEM updates + # Test if _WAU-mods-postsys.ps1 exists: Mods for WAU (postsys) - if Network is active/any Winget is installed/running as SYSTEM _after_ SYSTEM updates if ($true -eq $IsSystem) { if (Test-Path "$Mods\_WAU-mods-postsys.ps1") { - Write-ToLog "Running Mods (postsys) for WAU..." "Yellow" + Write-ToLog "Running Mods (postsys) for WAU..." "DarkYellow" & "$Mods\_WAU-mods-postsys.ps1" } } diff --git a/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1 b/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1 new file mode 100644 index 00000000..aaf34691 --- /dev/null +++ b/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1 @@ -0,0 +1,382 @@ +function Test-WAUMods { + param ( + [Parameter(Mandatory=$true)] + [string]$WorkingDir, + + [Parameter(Mandatory=$true)] + [PSCustomObject]$WAUConfig, + + [Parameter(Mandatory=$false)] + [string]$GitHub_Repo = "Winget-AutoUpdate" + ) + + # Define Mods path + $Mods = "$WorkingDir\mods" + + # Capture both output and exit code + $ModsOutput = & "$Mods\_WAU-mods.ps1" 2>&1 | Out-String + $ModsExitCode = $LASTEXITCODE + + # Handle legacy exit code behavior first (backward compatibility) + if ($ModsExitCode -eq 1) { + Write-ToLog "Legacy exit code 1 detected - Re-running WAU" + Start-Process powershell -ArgumentList "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$WorkingDir\Winget-Upgrade.ps1`"" + Exit + } + + # Try to parse JSON output for new action-based system + if ($ModsOutput -and $ModsOutput.Trim()) { + try { + # Remove any non-JSON content (like debug output) and find JSON + $jsonMatch = $ModsOutput | Select-String -Pattern '\{.*\}' | Select-Object -First 1 + + if ($jsonMatch) { + $ModsResult = $jsonMatch.Matches[0].Value | ConvertFrom-Json + + # Log message if provided + if ($ModsResult.Message) { + $logLevel = if ($ModsResult.LogLevel) { $ModsResult.LogLevel } else { "White" } + Write-ToLog $ModsResult.Message $logLevel + } + + # Execute action based on returned instruction + switch ($ModsResult.Action) { + "Rerun" { + Write-ToLog "Mods requested a WAU re-run" + Start-Process powershell -ArgumentList "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$WorkingDir\Winget-Upgrade.ps1`"" + $exitCode = if ($ModsResult.ExitCode) { [int]$ModsResult.ExitCode } else { 0 } + Exit $exitCode + } + "Abort" { + Write-ToLog "Mods requested WAU to abort" + $exitCode = if ($ModsResult.ExitCode) { [int]$ModsResult.ExitCode } else { 1602 } # Default to "User cancelled" + Exit $exitCode + } + "Postpone" { + Write-ToLog "Mods requested a postpone of WAU" + # Check if a postponed task already exists + $existingTask = Get-ScheduledTask -TaskPath "\WAU\" -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -like "Postponed-$($GitHub_Repo)*" } + if ($existingTask) { + Write-ToLog "A postponed task for $($GitHub_Repo) already exists, not creating another." "Yellow" + } + else { + # Get configurable duration, default to 1 hour + $postponeDuration = if ($ModsResult.PostponeDuration) { + try { + [double]$parsedDuration = [double]$ModsResult.PostponeDuration + # Ensure minimum duration of 0.1 hours (6 minutes) + if ($parsedDuration -lt 0.1) { + Write-ToLog "PostponeDuration adjusted to minimum 0.1 hours (6 minutes)" "Yellow" + 0.1 + } else { + $parsedDuration + } + } + catch { + Write-ToLog "Invalid PostponeDuration value '$($ModsResult.PostponeDuration)', using default 1 hour" "Yellow" + 1 + } + } else { + 1 + } + + # Create a postponed temporary scheduled task to try again later + $uniqueTaskName = "Postponed-$($GitHub_Repo)_$(Get-Random)" + $taskPath = "\WAU\" + $copyAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($WAUConfig.InstallLocation)Winget-Upgrade.ps1`"" + $copyTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddHours($postponeDuration) + # Set EndBoundary to make DeleteExpiredTaskAfter work + $copyTrigger.EndBoundary = (Get-Date).AddHours($postponeDuration).AddMinutes(1).ToString("yyyy-MM-ddTHH:mm:ss") + $copySettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -DeleteExpiredTaskAfter (New-TimeSpan -Seconds 0) + $copyPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest + Register-ScheduledTask -TaskName $uniqueTaskName -TaskPath $taskPath -Action $copyAction -Trigger $copyTrigger -Settings $copySettings -Principal $copyPrincipal -Description "Postponed copy of $($GitHub_Repo)" | Out-Null + Write-ToLog "WAU will try again in $postponeDuration hours" "Yellow" + } + $exitCode = if ($ModsResult.ExitCode) { [int]$ModsResult.ExitCode } else { 1602 } # Default to "User cancelled" + Exit $exitCode + } + "Reboot" { + Write-ToLog "Mods requested a system reboot" + # Get configurable delay, default to 5 minutes + $rebootDelay = if ($ModsResult.RebootDelay) { + try { + [double]$parsedDelay = [double]$ModsResult.RebootDelay + # Ensure minimum delay of 1 minute for safety + if ($parsedDelay -lt 1) { + Write-ToLog "RebootDelay adjusted to minimum 1 minute" "Yellow" + 1 + } else { + $parsedDelay + } + } + catch { + Write-ToLog "Invalid RebootDelay value '$($ModsResult.RebootDelay)', using default 5 minutes" "Yellow" + 5 + } + } else { + 5 + } + + $shutdownMessage = if ($ModsResult.Message) { $ModsResult.Message } else { "WAU Mods requested a system reboot in $rebootDelay minutes" } + $rebootHandler = if ($ModsResult.RebootHandler) { $ModsResult.RebootHandler } else { "Windows" } + + # Check if SCCM client is available for managed restart (user controlled) + $sccmClient = Get-CimInstance -Namespace "root\ccm" -ClassName "SMS_Client" -ErrorAction SilentlyContinue + + if ($sccmClient -and ($rebootHandler -eq "SCCM")) { + Write-ToLog "SCCM client detected - using managed restart (user controlled)" "Green" + + $ccmRestartPath = "$env:windir\CCM\CcmRestart.exe" + $regPath = 'HKLM:\SOFTWARE\Microsoft\SMS\Mobile Client\Reboot Management\RebootData' + + try { + # Check if SCCM restart registry values already exist + $existingRebootBy = $null + $existingRebootValues = $false + + if (Test-Path $regPath) { + $existingRebootBy = Get-ItemProperty -Path $regPath -Name 'RebootBy' -ErrorAction SilentlyContinue + $existingNotifyUI = Get-ItemProperty -Path $regPath -Name 'NotifyUI' -ErrorAction SilentlyContinue + $existingSetTime = Get-ItemProperty -Path $regPath -Name 'SetTime' -ErrorAction SilentlyContinue + + # Check if we have the key registry values indicating a restart is already scheduled + if ($existingRebootBy -and $existingNotifyUI -and $existingSetTime -and $existingRebootBy.PSObject.Properties['RebootBy']) { + $existingRebootValues = $true + $existingRestartTime = [DateTimeOffset]::FromUnixTimeSeconds([int64]$existingRebootBy.RebootBy).LocalDateTime + Write-ToLog "SCCM restart already scheduled for: $existingRestartTime" "Yellow" + } + } + + if ($existingRebootValues) { + # Try CcmRestart.exe for notification + if (Test-Path $ccmRestartPath) { + Write-ToLog "Triggering SCCM restart notification via CcmRestart.exe" "Cyan" + Start-Process -FilePath $ccmRestartPath -NoNewWindow -Wait -ErrorAction SilentlyContinue + } else { + Write-ToLog "CcmRestart.exe not found, restarting ccmexec service" "Yellow" + Restart-Service ccmexec -Force -ErrorAction SilentlyContinue + } + } else { + # No existing restart scheduled - create new SCCM managed restart (user controlled) + Write-ToLog "Setting up new SCCM managed restart schedule" "Green" + + # Ensure registry path exists + if (-not (Test-Path $regPath)) { + New-Item -Path $regPath -Force | Out-Null + } + + # Check the intended exit code to determine restart type + $intendedExitCode = if ($ModsResult.ExitCode) { $ModsResult.ExitCode } else { 3010 } + + if ($intendedExitCode -eq 1641) { + # HARD/MANDATORY REBOOT in SCCM registry (show UI to user, doesn't execute automatically!) + $restartTime = [DateTimeOffset]::Now.AddMinutes($rebootDelay).ToUnixTimeSeconds() + + # CRITICAL: Both RebootBy and OverrideRebootWindowTime must be set to the same value + New-ItemProperty -Path $regPath -Name 'RebootBy' -Value ([Int64]$restartTime) -PropertyType QWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'OverrideRebootWindowTime' -Value ([Int64]$restartTime) -PropertyType QWord -Force | Out-Null + + # Mandatory reboot settings + New-ItemProperty -Path $regPath -Name 'PreferredRebootWindowTypes' -Value @("3") -PropertyType MultiString -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'OverrideRebootWindow' -Value 1 -PropertyType DWord -Force | Out-Null + + # Ignore service window settings + New-ItemProperty -Path $regPath -Name 'OverrideServiceWindows' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'RebootOutsideOfServiceWindow' -Value 1 -PropertyType DWord -Force | Out-Null + + # Hard reboot settings + New-ItemProperty -Path $regPath -Name 'HardReboot' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'NotifyUI' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'RebootValueInUTC' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'SetTime' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'GraceSeconds' -Value 0 -PropertyType DWord -Force | Out-Null + + # HARD/MANDATORY REBOOT via Task Scheduler (for execution, unless user executed it manually via UI) + $taskName = "WAU_MandatoryRestart" + $taskPath = "\WAU\" + + # Create a self destroying scheduled task for mandatory restart + Write-ToLog "Creating scheduled task for mandatory restart in $rebootDelay minutes" "Yellow" + + # Escape the shutdown message properly before using in here-string + $escapedShutdownMessage = $shutdownMessage -replace '"', '\"' -replace '`', '``' + + # Create PowerShell script with enhanced logging + $scriptContent = @" +`$regPath = 'HKLM:\SOFTWARE\Microsoft\SMS\Mobile Client\Reboot Management\RebootData' +`$ccmRestartPath = "`$env:windir\CCM\CcmRestart.exe" +`$logPath = "$WorkingDir\logs\mandatory_restart.log" +`$rebootDelay = $rebootDelay +`$shutdownMessage = "$escapedShutdownMessage" + +# Function to write to log +function Write-RestartLog { + param([string]`$Message) + `$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + "`$timestamp - `$Message" | Out-File -FilePath `$logPath -Append -Encoding UTF8 +} + +Write-RestartLog "Mandatory restart task started" + +# Only run if RebootBy and OverrideRebootWindowTime exists under the key (if not: the user has already restarted the client) +`$regProps = Get-ItemProperty -Path `$regPath -ErrorAction SilentlyContinue +if (`$regProps.PSObject.Properties.Name -contains 'RebootBy' -and `$regProps.PSObject.Properties.Name -contains 'OverrideRebootWindowTime') { + Write-RestartLog "SCCM restart registry values found, proceeding with restart" + + # Grace period is over, system will now restart in 2 minutes + Write-RestartLog "Grace period: `$rebootDelay minutes is over (`$shutdownMessage). System will now restart in 2 minutes." "Yellow" + + # Cancels any pending system shutdown or restart using 'shutdown /a'. + `$null = & shutdown /a 2>&1 + + `$result = & shutdown /r /t 120 /c "Grace period: `$rebootDelay minutes is over (`$shutdownMessage). System will now restart in 2 minutes." 2>&1 + if (`$LASTEXITCODE -eq 0) { + Write-RestartLog "System restart scheduled in 2 minutes" "Yellow" + + # Remove all values under the registry key + `$key = Get-Item -Path `$regPath -ErrorAction SilentlyContinue + if (`$key) { + `$key.GetValueNames() | ForEach-Object { Remove-ItemProperty -Path `$regPath -Name `$_ -ErrorAction SilentlyContinue } + Write-RestartLog "All registry values under `$regPath have been deleted" + } else { + Write-RestartLog "Registry key `$regPath not found, nothing to delete" + } + } elseif (`$LASTEXITCODE -eq 1190) { + Write-RestartLog "A system shutdown already exists: `$result" "Yellow" + + # Remove all values under the registry key + `$key = Get-Item -Path `$regPath -ErrorAction SilentlyContinue + if (`$key) { + `$key.GetValueNames() | ForEach-Object { Remove-ItemProperty -Path `$regPath -Name `$_ -ErrorAction SilentlyContinue } + Write-RestartLog "All registry values under `$regPath have been deleted" + } else { + Write-RestartLog "Registry key `$regPath not found, nothing to delete" + } + } else { + Write-RestartLog "A system shutdown failed: `$result" "Yellow" + + 'RebootBy','OverrideRebootWindowTime' | ForEach-Object { + New-ItemProperty -Path `$regPath -Name `$_ -Value ([Int64]-1) -PropertyType QWord -Force + } + 'PreferredRebootWindowTypes' | ForEach-Object { + New-ItemProperty -Path `$regPath -Name `$_ -Value @('3') -PropertyType MultiString -Force + } + 'OverrideRebootWindow','HardReboot','NotifyUI','RebootValueInUTC','SetTime','OverrideServiceWindows','RebootOutsideOfServiceWindow' | ForEach-Object { + New-ItemProperty -Path `$regPath -Name `$_ -Value 1 -PropertyType DWord -Force + } + New-ItemProperty -Path `$regPath -Name 'GraceSeconds' -Value 0 -PropertyType DWord -Force + + Write-RestartLog "Registry values updated for SCCM mandatory restart" + + # Check if CcmRestart.exe exists and use it, otherwise restart the service + if (Test-Path `$ccmRestartPath) { + Write-RestartLog "Executing CcmRestart.exe" + Start-Process -FilePath `$ccmRestartPath -NoNewWindow -Wait -ErrorAction SilentlyContinue + Write-RestartLog "CcmRestart.exe execution completed" + } else { + Write-RestartLog "CcmRestart.exe not found, restarting ccmexec service" + Restart-Service ccmexec -Force -ErrorAction SilentlyContinue + Write-RestartLog "ccmexec service restart completed" + } + } +} else { + Write-RestartLog "No SCCM restart registry values found, task completed without action" +} + +Write-RestartLog "Mandatory restart task completed" +"@ + # Encode to Base64 + $encodedScript = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($scriptContent)) + + # Create action with encoded command + $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -WindowStyle Hidden -EncodedCommand $encodedScript" + + # Create a scheduled task trigger to run (rebootDelay - 2) minutes from now, but at least 2 minutes delay. + $triggerDelay = [math]::Max(($rebootDelay - 2), 2) + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes($triggerDelay) + # Set EndBoundary to make DeleteExpiredTaskAfter work + $trigger.EndBoundary = (Get-Date).AddMinutes($triggerDelay).AddMinutes(1).ToString("yyyy-MM-ddTHH:mm:ss") + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -DeleteExpiredTaskAfter (New-TimeSpan -Seconds 0) + $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest + Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description "Mandatory SCCM Restart" -Force | Out-Null + + # Add a standard shutdown command + $result = & shutdown /r /t ([int]($rebootDelay * 60)) /c $shutdownMessage 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-ToLog "System restart scheduled in $rebootDelay minutes" "Yellow" + } else { + Write-ToLog "A system shutdown has already been scheduled or failed: $result" "Yellow" + } + } else { + # SOFT/NON-MANDATORY REBOOT + Write-ToLog "Using soft reboot (non-mandatory) for SCCM restart" "Cyan" + + # For non-mandatory, RebootBy should be 0 to show dialog immediately + New-ItemProperty -Path $regPath -Name 'RebootBy' -Value 0 -PropertyType QWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'OverrideRebootWindowTime' -Value 0 -PropertyType QWord -Force | Out-Null + + # Set as non-mandatory reboot + New-ItemProperty -Path $regPath -Name 'PreferredRebootWindowTypes' -Value @("4") -PropertyType MultiString -Force | Out-Null + + # Soft reboot settings + New-ItemProperty -Path $regPath -Name 'HardReboot' -Value 0 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'NotifyUI' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'RebootValueInUTC' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'SetTime' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'GraceSeconds' -Value 300 -PropertyType DWord -Force | Out-Null # Default grace period of 5 minutes + } + + # Try CcmRestart.exe first for notification + if (Test-Path $ccmRestartPath) { + Write-ToLog "Triggering SCCM restart notification via CcmRestart.exe" "Cyan" + Start-Process -FilePath $ccmRestartPath -NoNewWindow -Wait -ErrorAction SilentlyContinue + } else { + Write-ToLog "CcmRestart.exe not found, restarting ccmexec service" "Yellow" + Restart-Service ccmexec -Force -ErrorAction SilentlyContinue + } + + if ($intendedExitCode -eq 1641) { + Write-ToLog "MANDATORY restart via scheduled task: In $rebootDelay minutes" "Green" + } else { + Write-ToLog "Non-mandatory restart dialog triggered" "Green" + } + } + } + catch { + Write-ToLog "Failed to set SCCM restart: $($_.Exception.Message). Falling back to standard restart." "Yellow" + # Fallback to standard shutdown + $result = & shutdown /r /t ([int]($rebootDelay * 60)) /c $shutdownMessage 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-ToLog "System restart scheduled in $rebootDelay minutes (fallback)" "Yellow" + } else { + Write-ToLog "A system shutdown has already been scheduled or failed: $result" "Yellow" + } + } + } else { + # Standard shutdown when SCCM is not available (or reboot handler is not "SCCM") + $result = & shutdown /r /t ([int]($rebootDelay * 60)) /c $shutdownMessage 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-ToLog "System restart scheduled in $rebootDelay minutes" "Yellow" + } else { + Write-ToLog "A system shutdown has already been scheduled or failed: $result" "Yellow" + } + } + $exitCode = if ($ModsResult.ExitCode) { [int]$ModsResult.ExitCode } else { 3010 } # Default to "Restart required" + Exit $exitCode + } + "Continue" { + Write-ToLog "Mods allows WAU to continue normally" + } + default { + Write-ToLog "Unknown action '$($ModsResult.Action)' from mods, continuing normally" "Cyan" + } + } + } + } + catch { + Write-ToLog "Failed to parse mods JSON output: $($_.Exception.Message)" "Red" + Write-ToLog "Continuing with normal WAU execution" "Cyan" + } + } + +} \ No newline at end of file diff --git a/Sources/Winget-AutoUpdate/mods/_WAU-mods-template.ps1 b/Sources/Winget-AutoUpdate/mods/_WAU-mods-template.ps1 index c85e6707..558f5af6 100644 --- a/Sources/Winget-AutoUpdate/mods/_WAU-mods-template.ps1 +++ b/Sources/Winget-AutoUpdate/mods/_WAU-mods-template.ps1 @@ -1,11 +1,98 @@ -<# Mods for WAU (if Network is active/any Winget is installed/running as SYSTEM) -Winget-Upgrade.ps1 calls this script with the code: -[Write-ToLog "Running Mods for WAU..." "Yellow" -& "$Mods\_WAU-mods.ps1"] -Make sure your Functions have unique names! -Exit 1 to Re-run WAU from this script (beware of loops)! +<# +.SYNOPSIS + Custom modifications for Winget-AutoUpdate (WAU) + Runs if Network is active/any Winget is installed/running as SYSTEM + + If mods\_WAU-mods.ps1 exist: Winget-Upgrade.ps1 calls this script with the code: + [Write-ToLog "Running Mods for WAU..." "Cyan" + + # Capture both output and exit code + $ModsOutput = & "$Mods\_WAU-mods.ps1" 2>&1 | Out-String + $ModsExitCode = $LASTEXITCODE] + +.DESCRIPTION + This script runs before the main WAU process and can control WAU execution + by returning a JSON object with action instructions. + + The script should output a JSON object with the following structure: + { + "Action": "string", // Required: Action for WAU to perform + "Message": "string", // Optional: Message to write to WAU log + "LogLevel": "string", // Optional: Log level for the message + "ExitCode": number, // Optional: Windows installer exit code for reference + "PostponeDuration": number, // Optional: Postpone duration in hours before running WAU again (default 1 hour) + "RebootDelay": number, // Optional: Delay in minutes before rebooting (default 5 minutes) + "RebootHandler": string // Optional: "SCCM" or "Windows" (default "Windows") to specify reboot handler + } + + Available Actions: + - "Continue" : Continue with normal WAU execution (default behavior) + - "Abort" : Abort WAU execution completely + - "Postpone" : Postpone WAU execution temporarily with 'PostponeDuration' hours + - "Rerun" : Re-run WAU (equivalent to legacy exit code 1) + - "Reboot" : Restart the system with delay and notification to end user + + Available LogLevels: + - "White" : Default/normal message + - "Green" : Success message + - "Yellow" : Warning message + - "Red" : Error message + - "Cyan" : Information message + - "Magenta" : Debug message + + Standard Windows Installer Exit Codes (for reference): + - 0 : Success + - 1602 : User cancelled installation + - 1618 : Another installation is in progress + - 3010 : Restart required (SCCM Soft Reboot) + - 1641 : Restart initiated by installer (SCCM Hard Reboot) + + Examples: + + # Example 1: Abort on specific day + $result = @{ + Action = "Abort" + Message = "WAU disabled on maintenance day" + LogLevel = "Yellow" + ExitCode = 1602 + } | ConvertTo-Json -Compress + + # Example 2: Postpone WAU execution + $result = @{ + Action = "Postpone" + Message = "WAU postponed due to maintenance schedule" + LogLevel = "Yellow" + ExitCode = 1602 + PostponeDuration = 2 # Optional: Postpone WAU execution for 2 hours (default is 1 hour) + } | ConvertTo-Json -Compress + + # Example 3: Continue normally + $result = @{ + Action = "Continue" + Message = "All checks passed, proceeding with updates" + LogLevel = "Green" + } | ConvertTo-Json -Compress + + # Example 4: Request reboot after checks + $result = @{ + Action = "Reboot" + Message = "The system needs to reboot within 15 minutes before WAU updates can be performed." + LogLevel = "Red" + ExitCode = 1641 # Optional: Use 1641 for SCCM Hard Reboot (default is 3010 for Soft Reboot) + RebootDelay = 15 # Optional: Delay before rebooting (default is 5 minutes) + RebootHandler = "SCCM" # Optional: Specify reboot handler (default is "Windows") + } | ConvertTo-Json -Compress + +.NOTES + - This script must always exit with code 0 when using JSON output + - Legacy exit code 1 is still supported for backward compatibility + - Only the first valid JSON object in output will be processed + - If JSON parsing fails, WAU will continue normally + - Make sure your Functions have unique names to avoid conflicts + - Beware of logic loops or long-running operations that may loop indefinitely or block WAU execution! #> + <# FUNCTIONS #> . $PSScriptRoot\_Mods-Functions.ps1 @@ -14,7 +101,115 @@ Exit 1 to Re-run WAU from this script (beware of loops)! <# MAIN #> +# Add your custom logic here + +<# +# Example implementation: Second Tuesday of month check +$today = Get-Date +$firstDayOfMonth = [DateTime]::new($today.Year, $today.Month, 1) +$firstTuesday = $firstDayOfMonth.AddDays((2 - [int]$firstDayOfMonth.DayOfWeek + 7) % 7) +$secondTuesday = $firstTuesday.AddDays(7) + +if ($today.Date -ne $secondTuesday.Date) { + # Not second Tuesday - abort WAU execution + $result = @{ + Action = "Abort" + Message = "Today is not the second Tuesday of the month. WAU execution aborted." + LogLevel = "Yellow" + ExitCode = 1602 # User cancelled + } | ConvertTo-Json -Compress + + Write-Output $result + Exit 0 +} + +# Example: Check if maintenance window is active +$maintenanceStart = Get-Date "02:00" +$maintenanceEnd = Get-Date "04:00" +$currentTime = Get-Date +if ($currentTime -ge $maintenanceStart -and $currentTime -le $maintenanceEnd) { + $result = @{ + Action = "Abort" + Message = "WAU aborted during maintenance window ($($maintenanceStart.ToString('HH:mm')) - $($maintenanceEnd.ToString('HH:mm')))" + LogLevel = "Yellow" + ExitCode = 1602 + } | ConvertTo-Json -Compress + + Write-Output $result + Exit 0 +} -Write-ToLog "...nothing to do!" "Green" +# Example: Check available disk space +$systemDrive = Get-PSDrive -Name ($env:SystemDrive.TrimEnd(':')) +$freeSpaceGB = [math]::Round($systemDrive.Free / 1GB, 2) +$minimumSpaceGB = 5 + +if ($freeSpaceGB -lt $minimumSpaceGB) { + $result = @{ + Action = "Abort" + Message = "Insufficient disk space: ${freeSpaceGB}GB available, ${minimumSpaceGB}GB required" + LogLevel = "Red" + ExitCode = 1618 # Another installation is in progress (or system busy) + } | ConvertTo-Json -Compress + + Write-Output $result + Exit 0 +} + +# Example: Check Windows Update registry keys for installation status +$wuInProgress = $false +$wuKeys = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Install", + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending" +) + +foreach ($key in $wuKeys) { + if (Test-Path $key) { + $lastInstall = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue + if ($lastInstall -and $lastInstall.PSObject.Properties.Name -contains "LastSuccessTime" -and $lastInstall.LastSuccessTime) { + try { + $lastSuccessTime = [DateTime]$lastInstall.LastSuccessTime + # If the last successful install was within the last 30 minutes, consider WU in progress + if ((Get-Date).AddMinutes(-30) -lt $lastSuccessTime) { + $wuInProgress = $true + break + } + } + catch { + # Failed to parse date, skip this check + continue + } + } + } +} + +# Check if Windows Update service is running +$wuInProgress = $wuInProgress -or (Get-Service -Name "wuauserv" -ErrorAction SilentlyContinue).Status -eq "Running" + +# Check for specific Windows Update processes (TiWorker and TrustedInstaller are strong indicators) +$wuInProgress = $wuInProgress -or (Get-Process -Name "TiWorker","TrustedInstaller" -ErrorAction SilentlyContinue).Count -gt 0 + +if ($wuInProgress) { + $result = @{ + Action = "Postpone" + Message = "Windows Update is currently installing. WAU postponed for 2 hours." + LogLevel = "Yellow" + ExitCode = 1618 + PostponeDuration = 2 + } | ConvertTo-Json -Compress + + Write-Output $result + Exit 0 +} + +# All checks passed - continue with normal WAU execution +$result = @{ + Action = "Continue" + Message = "Second Tuesday check passed. No maintenance window. Sufficient disk space (${freeSpaceGB}GB). No Windows Update in progress. Continuing with WAU execution." + LogLevel = "Green" +} | ConvertTo-Json -Compress + +Write-Output $result Exit 0 +#>