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:

+ 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
+#>