diff --git a/.github/workflows/GA_Close-Inactive-Issues.yml b/.github/workflows/GA_Close-Inactive-Issues.yml
index f5dcc776..c14bc042 100644
--- a/.github/workflows/GA_Close-Inactive-Issues.yml
+++ b/.github/workflows/GA_Close-Inactive-Issues.yml
@@ -13,7 +13,7 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@v9
+ - uses: actions/stale@v10
with:
days-before-issue-stale: 30
days-before-issue-close: 14
diff --git a/.github/workflows/GA_Mega-linter.yml b/.github/workflows/GA_Mega-linter.yml
index 4b31eb42..2176a156 100644
--- a/.github/workflows/GA_Mega-linter.yml
+++ b/.github/workflows/GA_Mega-linter.yml
@@ -33,7 +33,7 @@ jobs:
steps:
# Git Checkout
- name: Checkout Code
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
id: ml
# You can override MegaLinter flavor used to have faster performances
# More info at https://megalinter.github.io/flavors/
- uses: oxsecurity/megalinter@e08c2b05e3dbc40af4c23f41172ef1e068a7d651 # v8.8.0
+ uses: oxsecurity/megalinter@55a59b24a441e0e1943080d4a512d827710d4a9d # v9.2.0
env:
# All available variables are described in documentation
# https://megalinter.github.io/configuration/
@@ -59,7 +59,7 @@ jobs:
# Upload MegaLinter artifacts
- name: Archive production artifacts
if: ${{ success() }} || ${{ failure() }}
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: MegaLinter reports
path: |
@@ -71,7 +71,7 @@ jobs:
- name: Create Pull Request with applied fixes
id: cpr
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}
commit-message: "[MegaLinter] Apply linters automatic fixes"
@@ -90,7 +90,7 @@ jobs:
run: sudo chown -Rc $UID .git/
- name: Commit and push applied linter fixes
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
- uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1
+ uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
with:
branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
commit_message: "[MegaLinter] Apply linters fixes"
diff --git a/.github/workflows/GitFlow_Check-pull-request-source-branch.yml b/.github/workflows/GitFlow_Check-pull-request-source-branch.yml
index 495b3d50..7ae6fc3c 100644
--- a/.github/workflows/GitFlow_Check-pull-request-source-branch.yml
+++ b/.github/workflows/GitFlow_Check-pull-request-source-branch.yml
@@ -59,7 +59,7 @@ jobs:
# Step 2: If the branch pattern is invalid, add a label and comment to the PR
- name: Handle invalid branch (label + comment)
if: failure()
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -122,7 +122,7 @@ jobs:
# Step 3: If the branch pattern is corrected, remove label and comment
- name: Clean up if branch is corrected
if: success()
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/GitFlow_Create-Release-Branch-and-PR.yaml b/.github/workflows/GitFlow_Create-Release-Branch-and-PR.yaml
index 53010296..b4e4720b 100644
--- a/.github/workflows/GitFlow_Create-Release-Branch-and-PR.yaml
+++ b/.github/workflows/GitFlow_Create-Release-Branch-and-PR.yaml
@@ -35,7 +35,7 @@ jobs:
# Step 2: Checkout the develop branch which contains all approved features
- name: Checkout code
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: develop # Always create releases from develop branch
fetch-depth: 0
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 b1621aca..41ab9253 100644
--- a/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml
+++ b/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml
@@ -29,7 +29,7 @@ jobs:
steps:
# Step 1: Checkout the code from the main branch after merge
- name: Checkout code
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
token: ${{ secrets.GH_PAT_SYNC }}
lfs: "true"
@@ -121,7 +121,7 @@ jobs:
# Step 4: Create stable GitHub release with all artifacts
- name: Create release
- uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1.18.0
+ uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
with:
tag: v${{ steps.release_version.outputs.NextSemVer }}
prerelease: false # This is a stable release
@@ -141,7 +141,7 @@ jobs:
needs: build
steps:
- name: Checkout code
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GH_PAT_SYNC }}
diff --git a/.github/workflows/GitFlow_Nightly-builds.yml b/.github/workflows/GitFlow_Nightly-builds.yml
index 4a5d1dea..14923a7e 100644
--- a/.github/workflows/GitFlow_Nightly-builds.yml
+++ b/.github/workflows/GitFlow_Nightly-builds.yml
@@ -28,7 +28,7 @@ jobs:
steps:
# Step 1: Checkout the develop branch for nightly builds
- name: Checkout code
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
lfs: "true"
fetch-depth: 0
@@ -180,7 +180,7 @@ jobs:
# Step 6: Create GitHub release with all artifacts
- name: Create release
- uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1.18.0
+ uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
if: steps.check_prs.outputs.BUILD_NEEDED == 'true'
with:
tag: v${{ steps.format_version.outputs.NextSemVer }}
diff --git a/README.md b/README.md
index 36132841..7e200754 100644
--- a/README.md
+++ b/README.md
@@ -39,8 +39,8 @@ You can update only pre-selected apps. To do so, create an "included_apps.txt" w
> The lists can contain Wildcard (*). For instance ```Mozilla.Firefox*``` will take care of all Firefox channels.
-List and Mods folder content will be copied to WAU install location:
-
+List and Mods folder content will be copied to WAU install location:
+
### Notification Level
@@ -242,17 +242,63 @@ Share your mods with the community:
### Winget native parameters
-Another finess is the **AppID** followed by the `-override` suffix as a **text file** (.**txt**) that you can place under the **mods** folder.
+You can customize winget behavior per-app using **text files** (.**txt**) placed in the **mods** folder:
+
+#### Override (Full installer control)
+Use **AppID**`-override.txt` to replace ALL installer arguments (without `-h` silent mode).
> Example:
**Adobe.Acrobat.Reader.64-bit-override.txt** with the content `"-sfx_nu /sAll /rs /msi EULA_ACCEPT=YES DISABLEDESKTOPSHORTCUT=1"`
-This will use the **content** of the text file as a native **winget --override** parameter when upgrading.
+This uses the **content** as a native **winget --override** parameter when upgrading.
-Likewise you can use the **AppID** followed by the `-custom` suffix as a **text file** (.**txt**) that you can place under the **mods** folder (*Arguments to be passed on to the installer in addition to the defaults*).
+#### Custom (Add installer arguments)
+Use **AppID**`-custom.txt` to add extra arguments to the installer (with `-h` silent mode).
> Example:
**Adobe.Acrobat.Reader.64-bit-custom.txt** with the content `"DISABLEDESKTOPSHORTCUT=1"`
-This will use the **content** of the text file as a native **winget --custom** parameter when upgrading.
+This uses the **content** as a native **winget --custom** parameter when upgrading.
+
+#### Arguments (Winget-level parameters) ⭐ NEW
+Use **AppID**`-arguments.txt` to pass **winget parameters** (not installer arguments, with `-h` silent mode).
+
+💡 **Locale Tip:** Many applications revert to English or system default language during WAU upgrades because winget doesn't remember the original installation locale. To prevent this:
+- **Best solution:** Use locale-specific package IDs in `included_apps.txt` (e.g., `Mozilla.Firefox.sv-SE` instead of `Mozilla.Firefox`)
+- **Alternative:** Create `{AppID}-arguments.txt` with `--locale` parameter to force language on every upgrade
+
+> Example for language control ([#1073](https://github.com/Romanitho/Winget-AutoUpdate/issues/1073)):
+**Mozilla.Firefox-arguments.txt** with the content `--locale pl`
+*This prevents Firefox from reverting to English after WAU upgrades.*
+*Better solution: Use `Mozilla.Firefox.pl` in included_apps.txt instead of `Mozilla.Firefox`.*
+
+> Example for dependency issues ([#1075](https://github.com/Romanitho/Winget-AutoUpdate/issues/1075)):
+**Cloudflare.Warp-arguments.txt** with the content `--skip-dependencies`
+
+> Example with multiple parameters:
+**Microsoft.VisualStudio.2022.Community-arguments.txt** with the content `--locale en-US --architecture x64`
+
+**Common use cases:**
+- `--locale ` - Force application language (e.g., `pl-PL`, `en-US`, `de-DE`)
+ - 💡 **Recommended alternative:** Use locale-specific package IDs when available (e.g., `Mozilla.Firefox.sv-SE`, `Mozilla.Firefox.de`, `Mozilla.Firefox.ESR.pl`) to get latest versions
+- `--skip-dependencies` - Skip dependency installations when they conflict
+- `--architecture ` - Force architecture (`x86`, `x64`, `arm64`)
+- `--version ` - Pin to specific version
+- `--ignore-security-hash` - Bypass hash verification
+- `--ignore-local-archive-malware-scan` - Skip AV scanning
+
+⚠️ **Important:** When combining `--locale` and `--version`, the specific version must have an installer available for that locale. Not all versions support all locales. Check available versions with `winget show --id --versions`.
+
+💡 **Locale Best Practice:** Search for locale-specific packages with `winget search ` to see if your language has a dedicated package ID (e.g., `Mozilla.Firefox.sv-SE` for Swedish Firefox). These packages are maintained with the latest versions in your preferred language.
+
+**Command-line usage:** You can also pass arguments when calling `Winget-Install.ps1`:
+```powershell
+.\winget-install.ps1 -AppIDs "Mozilla.Firefox --locale sv-SE"
+.\winget-install.ps1 -AppIDs "7zip.7zip, Notepad++.Notepad++"
+.\winget-install.ps1 -AppIDs "Adobe.Acrobat.Reader.64-bit --scope machine --override \"-sfx_nu /sAll /msi EULA_ACCEPT=YES\""
+```
+
+**Priority:** Override > Custom > Arguments (file) > Arguments (command-line) > Default
+
+See [_AppID-arguments-template.txt](Sources/Winget-AutoUpdate/mods/_AppID-arguments-template.txt) for more examples.
## Known issues
diff --git a/Sources/Winget-AutoUpdate/User-Run.ps1 b/Sources/Winget-AutoUpdate/User-Run.ps1
index 094e1325..6531ce87 100644
--- a/Sources/Winget-AutoUpdate/User-Run.ps1
+++ b/Sources/Winget-AutoUpdate/User-Run.ps1
@@ -1,73 +1,84 @@
<#
.SYNOPSIS
-Handle user interaction from shortcuts and show a Toast notification
+ Handles user-initiated WAU update checks via shortcut.
.DESCRIPTION
-Act on shortcut run
+ Provides a user-facing interface to manually trigger WAU update checks.
+ Displays toast notifications for status updates (starting, running,
+ completed, or error). Waits for the scheduled task to complete and
+ shows the result.
.EXAMPLE
-.\user-run.ps1
+ .\User-Run.ps1
+.NOTES
+ Triggered by desktop shortcut or Start menu entry.
+ Uses the Winget-AutoUpdate scheduled task.
+ Shows error details from logs\error.txt if update check fails.
#>
+# Check if WAU is currently running
function Test-WAUisRunning {
- If (((Get-ScheduledTask -TaskName 'Winget-AutoUpdate').State -eq 'Running') -or ((Get-ScheduledTask -TaskName 'Winget-AutoUpdate-UserContext').State -eq 'Running')) {
- Return $True
- }
+ If (((Get-ScheduledTask -TaskName 'Winget-AutoUpdate').State -eq 'Running') -or ((Get-ScheduledTask -TaskName 'Winget-AutoUpdate-UserContext').State -eq 'Running')) {
+ Return $True
+ }
}
<# MAIN #>
-#Get Working Dir
+# Set working directory
$Script:WorkingDir = $PSScriptRoot
-#Load external functions
+# Load required functions
. $WorkingDir\functions\Get-NotifLocale.ps1
. $WorkingDir\functions\Start-NotifTask.ps1
-#Get Toast Locale function
+# Load notification locale
Get-NotifLocale
-#Set common variables
+# Set common notification parameters
$OnClickAction = "$WorkingDir\logs\updates.log"
$Button1Text = $NotifLocale.local.outputs.output[11].message
try {
- #Check if WAU is currently running
- if (Test-WAUisRunning) {
- $Message = $NotifLocale.local.outputs.output[8].message
- $MessageType = "warning"
- Start-NotifTask -Message $Message -MessageType $MessageType -Button1Text $Button1Text -Button1Action $OnClickAction -ButtonDismiss -UserRun
- break
- }
- #Run scheduled task
- Get-ScheduledTask -TaskName "Winget-AutoUpdate" -ErrorAction Stop | Start-ScheduledTask -ErrorAction Stop
- #Starting check - Send notification
- $Message = $NotifLocale.local.outputs.output[6].message
- $MessageType = "info"
- Start-NotifTask -Message $Message -MessageType $MessageType -Button1Text $Button1Text -Button1Action $OnClickAction -ButtonDismiss -UserRun
- #Sleep until the task is done
- While (Test-WAUisRunning) {
- Start-Sleep 3
- }
+ # Check if WAU is already running
+ if (Test-WAUisRunning) {
+ $Message = $NotifLocale.local.outputs.output[8].message
+ $MessageType = "warning"
+ Start-NotifTask -Message $Message -MessageType $MessageType -Button1Text $Button1Text -Button1Action $OnClickAction -ButtonDismiss -UserRun
+ break
+ }
- #Test if there was a list_/winget_error
- if (Test-Path "$WorkingDir\logs\error.txt") {
- $MessageType = "error"
- $Critical = Get-Content "$WorkingDir\logs\error.txt" -Raw
- $Critical = $Critical.Trim()
- $Critical = $Critical.Substring(0, [Math]::Min($Critical.Length, 50))
- $Message = "Critical:`n$Critical..."
- }
- else {
- $MessageType = "success"
- $Message = $NotifLocale.local.outputs.output[9].message
- }
- Start-NotifTask -Message $Message -MessageType $MessageType -Button1Text $Button1Text -Button1Action $OnClickAction -ButtonDismiss -UserRun
+ # Start the WAU scheduled task
+ Get-ScheduledTask -TaskName "Winget-AutoUpdate" -ErrorAction Stop | Start-ScheduledTask -ErrorAction Stop
+
+ # Send "starting" notification
+ $Message = $NotifLocale.local.outputs.output[6].message
+ $MessageType = "info"
+ Start-NotifTask -Message $Message -MessageType $MessageType -Button1Text $Button1Text -Button1Action $OnClickAction -ButtonDismiss -UserRun
+
+ # Wait for task completion
+ While (Test-WAUisRunning) {
+ Start-Sleep 3
+ }
+
+ # Check for errors in the update process
+ if (Test-Path "$WorkingDir\logs\error.txt") {
+ $MessageType = "error"
+ $Critical = Get-Content "$WorkingDir\logs\error.txt" -Raw
+ $Critical = $Critical.Trim()
+ $Critical = $Critical.Substring(0, [Math]::Min($Critical.Length, 50))
+ $Message = "Critical:`n$Critical..."
+ }
+ else {
+ $MessageType = "success"
+ $Message = $NotifLocale.local.outputs.output[9].message
+ }
+ Start-NotifTask -Message $Message -MessageType $MessageType -Button1Text $Button1Text -Button1Action $OnClickAction -ButtonDismiss -UserRun
}
catch {
- #Check failed - Just send notification
- $Message = $NotifLocale.local.outputs.output[7].message
- $MessageType = "error"
- Start-NotifTask -Message $Message -MessageType $MessageType -Button1Text $Button1Text -Button1Action $OnClickAction -ButtonDismiss -UserRun
+ # Handle task start failure
+ $Message = $NotifLocale.local.outputs.output[7].message
+ $MessageType = "error"
+ Start-NotifTask -Message $Message -MessageType $MessageType -Button1Text $Button1Text -Button1Action $OnClickAction -ButtonDismiss -UserRun
}
diff --git a/Sources/Winget-AutoUpdate/WAU-Notify.ps1 b/Sources/Winget-AutoUpdate/WAU-Notify.ps1
index f34a4348..754b2a9f 100644
--- a/Sources/Winget-AutoUpdate/WAU-Notify.ps1
+++ b/Sources/Winget-AutoUpdate/WAU-Notify.ps1
@@ -1,24 +1,38 @@
-#Send Notify Script
+<#
+.SYNOPSIS
+ Displays a toast notification to the logged-in user.
-#get xml notif config
+.DESCRIPTION
+ Reads notification configuration from an XML file and displays
+ a Windows toast notification using the ToastNotificationManager API.
+ This script is called by a scheduled task when WAU runs in system context.
+
+.NOTES
+ Configuration file: config\notif.xml
+ Launcher ID: Windows.SystemToast.WAU.Notification
+#>
+
+# Get WAU installation path from registry
$WAUinstalledPath = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Romanitho\Winget-AutoUpdate\" -Name InstallLocation
+
+# Load notification XML configuration
[xml]$NotifConf = Get-Content "$WAUinstalledPath\config\notif.xml" -Encoding UTF8 -ErrorAction SilentlyContinue
if (!($NotifConf)) {
break
}
-#Load Assemblies
+# Load Windows notification assemblies
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
-#Prepare XML
+# Parse notification XML
$ToastXml = [Windows.Data.Xml.Dom.XmlDocument]::New()
$ToastXml.LoadXml($NotifConf.OuterXml)
-#Specify Launcher App ID
+# Specify toast launcher ID
$LauncherID = "Windows.SystemToast.WAU.Notification"
-#Prepare and Create Toast
+# Create and display the notification
$ToastMessage = [Windows.UI.Notifications.ToastNotification]::New($ToastXML)
$ToastMessage.Tag = $NotifConf.toast.tag
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($LauncherID).Show($ToastMessage)
diff --git a/Sources/Winget-AutoUpdate/WAU-Policies.ps1 b/Sources/Winget-AutoUpdate/WAU-Policies.ps1
index af5e17d3..411424d5 100644
--- a/Sources/Winget-AutoUpdate/WAU-Policies.ps1
+++ b/Sources/Winget-AutoUpdate/WAU-Policies.ps1
@@ -1,23 +1,31 @@
<#
.SYNOPSIS
-Handle GPO/Polices
+ Applies Group Policy settings to WAU scheduled tasks.
.DESCRIPTION
-Daily update settings from policies
+ Reads WAU configuration from GPO registry keys and updates the
+ Winget-AutoUpdate scheduled task triggers accordingly.
+ Handles daily, bi-daily, weekly, bi-weekly, and monthly schedules,
+ as well as logon triggers and time delays.
+
+.NOTES
+ This script is executed by the WAU-Policies scheduled task.
+ GPO registry path: HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate
+ Logs applied settings to: logs\LatestAppliedSettings.txt
#>
-#Import functions
+# Import configuration function
. "$PSScriptRoot\functions\Get-WAUConfig.ps1"
-#Check if GPO Management is detected
+# Check if GPO management is enabled
$GPOManagementDetected = Get-ItemProperty "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate" -ErrorAction SilentlyContinue
if ($GPOManagementDetected) {
- #Get WAU settings
+ # Load WAU configuration (with GPO overrides)
$WAUConfig = Get-WAUConfig
- #Log init
+ # Initialize logging
$GPOLogDirectory = Join-Path -Path $WAUConfig.InstallLocation -ChildPath "logs"
if (!(Test-Path -Path $GPOLogDirectory)) {
New-Item -ItemType Directory -Path $GPOLogDirectory -Force | Out-Null
@@ -25,21 +33,19 @@ if ($GPOManagementDetected) {
$GPOLogFile = Join-Path -Path $GPOLogDirectory -ChildPath "LatestAppliedSettings.txt"
Set-Content -Path $GPOLogFile -Value "### POLICY CYCLE - $(Get-Date) ###`n"
- #Get Winget-AutoUpdate scheduled task
+ # Get current scheduled task configuration
$WAUTask = Get-ScheduledTask -TaskName 'Winget-AutoUpdate' -ErrorAction SilentlyContinue
-
- #Update 'Winget-AutoUpdate' scheduled task settings
$currentTriggers = $WAUTask.Triggers
$configChanged = $false
- #Check if LogOn trigger setting has changed
+ # === Check if LogOn trigger setting has changed ===
$hasLogonTrigger = $currentTriggers | Where-Object { $_.CimClass.CimClassName -eq "MSFT_TaskLogonTrigger" }
if (($WAUConfig.WAU_UpdatesAtLogon -eq 1 -and -not $hasLogonTrigger) -or
($WAUConfig.WAU_UpdatesAtLogon -ne 1 -and $hasLogonTrigger)) {
$configChanged = $true
}
- #Check if schedule type has changed
+ # === Detect current schedule type ===
$currentIntervalType = "None"
foreach ($trigger in $currentTriggers) {
if ($trigger.CimClass.CimClassName -eq "MSFT_TaskDailyTrigger" -and $trigger.DaysInterval -eq 1) {
@@ -72,7 +78,7 @@ if ($GPOManagementDetected) {
$configChanged = $true
}
- #Check if delay has changed
+ # === Check if delay has changed ===
$randomDelay = [TimeSpan]::ParseExact($WAUConfig.WAU_UpdatesTimeDelay, "hh\:mm", $null)
$timeTrigger = $currentTriggers | Where-Object { $_.CimClass.CimClassName -ne "MSFT_TaskLogonTrigger" } | Select-Object -First 1
if ($timeTrigger.RandomDelay -match '^PT(?:(\d+)H)?(?:(\d+)M)?$') {
@@ -84,7 +90,7 @@ if ($GPOManagementDetected) {
$configChanged = $true
}
- #Check if schedule time has changed
+ # === Check if schedule time has changed ===
if ($currentIntervalType -ne "None" -and $currentIntervalType -ne "Never") {
if ($timeTrigger) {
$currentTime = [DateTime]::Parse($timeTrigger.StartBoundary).ToString("HH:mm:ss")
@@ -94,12 +100,16 @@ if ($GPOManagementDetected) {
}
}
- #Only update triggers if configuration has changed
+ # === Update triggers if configuration changed ===
if ($configChanged) {
$taskTriggers = @()
+
+ # Add logon trigger if enabled
if ($WAUConfig.WAU_UpdatesAtLogon -eq 1) {
$tasktriggers += New-ScheduledTaskTrigger -AtLogOn
}
+
+ # Add time-based trigger based on interval type
if ($WAUConfig.WAU_UpdatesInterval -eq "Daily") {
$tasktriggers += New-ScheduledTaskTrigger -Daily -At $WAUConfig.WAU_UpdatesAtTime -RandomDelay $randomDelay
}
@@ -116,20 +126,18 @@ if ($GPOManagementDetected) {
$tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUConfig.WAU_UpdatesAtTime -DaysOfWeek 2 -WeeksInterval 4 -RandomDelay $randomDelay
}
- #If trigger(s) set
+ # Apply new triggers or disable task
if ($taskTriggers) {
- #Edit scheduled task
Set-ScheduledTask -TaskPath $WAUTask.TaskPath -TaskName $WAUTask.TaskName -Trigger $taskTriggers | Out-Null
}
- #If not, remove trigger(s)
else {
- #Remove by setting past due date
+ # Disable by setting a past due date
$tasktriggers = New-ScheduledTaskTrigger -Once -At "01/01/1970"
Set-ScheduledTask -TaskPath $WAUTask.TaskPath -TaskName $WAUTask.TaskName -Trigger $tasktriggers | Out-Null
}
}
- #Log latest applied config
+ # Log applied configuration
Add-Content -Path $GPOLogFile -Value "`nLatest applied settings:"
$WAUConfig.PSObject.Properties | Where-Object { $_.Name -like "WAU_*" } | Select-Object Name, Value | Out-File -Encoding default -FilePath $GPOLogFile -Append
diff --git a/Sources/Winget-AutoUpdate/Winget-Install.ps1 b/Sources/Winget-AutoUpdate/Winget-Install.ps1
index 02bdb672..f769aad0 100644
--- a/Sources/Winget-AutoUpdate/Winget-Install.ps1
+++ b/Sources/Winget-AutoUpdate/Winget-Install.ps1
@@ -71,6 +71,7 @@ else {
. "$realPath\functions\Write-ToLog.ps1"
. "$realPath\functions\Confirm-Installation.ps1"
. "$realPath\functions\Compare-SemVer.ps1"
+. "$realPath\functions\ConvertTo-WingetArgumentArray.ps1"
#Check if App exists in Winget Repository
function Confirm-Exist ($AppID) {
@@ -100,6 +101,15 @@ function Test-ModsInstall ($AppID) {
if (Test-Path "$Mods\$AppID-custom.txt") {
$ModsCustom = (Get-Content "$Mods\$AppID-custom.txt" -Raw).Trim()
}
+ if (Test-Path "$Mods\$AppID-arguments.txt") {
+ # Read file and filter out comments and empty lines
+ $lines = Get-Content "$Mods\$AppID-arguments.txt" | Where-Object {
+ $_.Trim() -ne "" -and -not $_.TrimStart().StartsWith("#")
+ }
+ if ($lines) {
+ $ModsArguments = ($lines -join " ").Trim()
+ }
+ }
if (Test-Path "$Mods\$AppID-install.ps1") {
$ModsInstall = "$Mods\$AppID-install.ps1"
}
@@ -108,7 +118,7 @@ function Test-ModsInstall ($AppID) {
}
}
- return $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsInstall, $ModsInstalled
+ return $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsArguments, $ModsInstall, $ModsInstalled
}
#Check if uninstall modifications exist in "mods" directory
@@ -132,8 +142,8 @@ function Test-ModsUninstall ($AppID) {
function Install-App ($AppID, $AppArgs) {
$IsInstalled = Confirm-Installation $AppID
if (!($IsInstalled) -or $AllowUpgrade ) {
- #Check if mods exist (or already exist) for preinstall/override/custom/install/installed
- $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsInstall, $ModsInstalled = Test-ModsInstall $($AppID)
+ #Check if mods exist (or already exist) for preinstall/override/custom/arguments/install/installed
+ $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsArguments, $ModsInstall, $ModsInstalled = Test-ModsInstall $($AppID)
#If PreInstall script exist
if ($ModsPreInstall) {
@@ -155,8 +165,15 @@ function Install-App ($AppID, $AppArgs) {
Write-ToLog "-> Arguments (customizing default): $ModsCustom" # With -h (user customizes default)
$WingetArgs = "install --id $AppID -e --accept-package-agreements --accept-source-agreements -s winget -h --custom $ModsCustom" -split " "
}
+ elseif ($ModsArguments -or (-not [string]::IsNullOrWhiteSpace($AppArgs))) {
+ # Prioritize ModsArguments from file over AppArgs from command line
+ $finalArgs = if ($ModsArguments) { $ModsArguments } else { $AppArgs }
+ Write-ToLog "-> Arguments (winget-level): $finalArgs" # Winget parameters with -h
+ $argArray = ConvertTo-WingetArgumentArray $finalArgs
+ $WingetArgs = @("install", "--id", $AppID, "-e", "--accept-package-agreements", "--accept-source-agreements", "-s", "winget") + $argArray + @("-h")
+ }
else {
- $WingetArgs = "install --id $AppID -e --accept-package-agreements --accept-source-agreements -s winget -h $AppArgs" -split " "
+ $WingetArgs = "install --id $AppID -e --accept-package-agreements --accept-source-agreements -s winget -h" -split " "
}
Write-ToLog "-> Running: `"$Winget`" $WingetArgs"
@@ -210,7 +227,14 @@ function Uninstall-App ($AppID, $AppArgs) {
#Uninstall App
Write-ToLog "-> Uninstalling $AppID..." "DarkYellow"
- $WingetArgs = "uninstall --id $AppID -e --accept-source-agreements -h $AppArgs" -split " "
+ if (-not [string]::IsNullOrWhiteSpace($AppArgs)) {
+ Write-ToLog "-> Arguments (winget-level): $AppArgs"
+ $argArray = ConvertTo-WingetArgumentArray $AppArgs
+ $WingetArgs = @("uninstall", "--id", $AppID, "-e", "--accept-source-agreements") + $argArray + @("-h")
+ }
+ else {
+ $WingetArgs = "uninstall --id $AppID -e --accept-source-agreements -h" -split " "
+ }
Write-ToLog "-> Running: `"$Winget`" $WingetArgs"
& "$Winget" $WingetArgs | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append
diff --git a/Sources/Winget-AutoUpdate/config/WAU-MSI_Actions.ps1 b/Sources/Winget-AutoUpdate/config/WAU-MSI_Actions.ps1
index affaccdc..2f68e710 100644
--- a/Sources/Winget-AutoUpdate/config/WAU-MSI_Actions.ps1
+++ b/Sources/Winget-AutoUpdate/config/WAU-MSI_Actions.ps1
@@ -1,13 +1,36 @@
+<#
+.SYNOPSIS
+ MSI installer post-actions script for WAU.
+
+.DESCRIPTION
+ Handles installation and uninstallation tasks for the WAU MSI package.
+ Creates scheduled tasks and configures permissions.
+
+.PARAMETER AppListPath
+ Path to the app list file (excluded_apps.txt or included_apps.txt).
+
+.PARAMETER InstallPath
+ WAU installation directory.
+
+.PARAMETER CurrentDir
+ Current working directory for the installer.
+
+.PARAMETER Upgrade
+ Upgrade product code when performing an upgrade.
+
+.PARAMETER Uninstall
+ Switch to trigger uninstallation instead of installation.
+#>
[CmdletBinding()]
param(
- [Parameter(Mandatory = $false)] [string] $AppListPath,
- [Parameter(Mandatory = $false)] [string] $InstallPath,
- [Parameter(Mandatory = $false)] [string] $CurrentDir,
- [Parameter(Mandatory = $false)] [string] $Upgrade,
- [Parameter(Mandatory = $False)] [Switch] $Uninstall = $false
+ [string]$AppListPath,
+ [string]$InstallPath,
+ [string]$CurrentDir,
+ [string]$Upgrade,
+ [switch]$Uninstall
)
-#For troubleshooting
+# Debug output
Write-Output "AppListPath: $AppListPath"
Write-Output "InstallPath: $InstallPath"
Write-Output "CurrentDir: $CurrentDir"
@@ -32,179 +55,164 @@ function Add-ACLRule {
}
function Install-WingetAutoUpdate {
-
Write-Host "### Post install actions ###"
try {
-
- # Clean potential old v1 install
+ # Clean old v1 installation if present
$OldConfRegPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Winget-AutoUpdate"
$OldWAUConfig = Get-ItemProperty $OldConfRegPath -ErrorAction SilentlyContinue
if ($OldWAUConfig.InstallLocation) {
- Write-Host "-> Cleanning old v1 WAU version ($($OldWAUConfig.DisplayVersion))"
+ Write-Host "-> Cleaning old v1 WAU version ($($OldWAUConfig.DisplayVersion))"
Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -WindowStyle Hidden -File ""$($OldWAUConfig.InstallLocation)\WAU-Uninstall.ps1""" -Wait
}
- #Get WAU config
+ # Get WAU config from registry
$WAUconfig = Get-ItemProperty "HKLM:\SOFTWARE\Romanitho\Winget-AutoUpdate"
Write-Output "-> WAU Config:"
Write-Output $WAUconfig
- # Settings for the scheduled task for Updates (System)
+ # Create scheduled tasks
Write-Host "-> Installing WAU scheduled tasks"
- $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($InstallPath)winget-upgrade.ps1`""
+
+ # Main update task (System context)
+ $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"${InstallPath}winget-upgrade.ps1`""
$taskTriggers = @()
+
if ($WAUconfig.WAU_UpdatesAtLogon -eq 1) {
- $tasktriggers += New-ScheduledTaskTrigger -AtLogOn
- }
- if ($WAUconfig.WAU_UpdatesInterval -eq "Daily") {
- $tasktriggers += New-ScheduledTaskTrigger -Daily -At $WAUconfig.WAU_UpdatesAtTime -RandomDelay $WAUconfig.WAU_UpdatesTimeDelay
- }
- elseif ($WAUconfig.WAU_UpdatesInterval -eq "BiDaily") {
- $tasktriggers += New-ScheduledTaskTrigger -Daily -At $WAUconfig.WAU_UpdatesAtTime -DaysInterval 2 -RandomDelay $WAUconfig.WAU_UpdatesTimeDelay
- }
- elseif ($WAUconfig.WAU_UpdatesInterval -eq "Weekly") {
- $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUconfig.WAU_UpdatesAtTime -DaysOfWeek 2 -RandomDelay $WAUconfig.WAU_UpdatesTimeDelay
+ $taskTriggers += New-ScheduledTaskTrigger -AtLogOn
}
- elseif ($WAUconfig.WAU_UpdatesInterval -eq "BiWeekly") {
- $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUconfig.WAU_UpdatesAtTime -DaysOfWeek 2 -WeeksInterval 2 -RandomDelay $WAUconfig.WAU_UpdatesTimeDelay
- }
- elseif ($WAUconfig.WAU_UpdatesInterval -eq "Monthly") {
- $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUconfig.WAU_UpdatesAtTime -DaysOfWeek 2 -WeeksInterval 4 -RandomDelay $WAUconfig.WAU_UpdatesTimeDelay
+
+ # Interval-based trigger
+ $time = $WAUconfig.WAU_UpdatesAtTime
+ $delay = $WAUconfig.WAU_UpdatesTimeDelay
+ switch ($WAUconfig.WAU_UpdatesInterval) {
+ "Daily" { $taskTriggers += New-ScheduledTaskTrigger -Daily -At $time -RandomDelay $delay }
+ "BiDaily" { $taskTriggers += New-ScheduledTaskTrigger -Daily -At $time -DaysInterval 2 -RandomDelay $delay }
+ "Weekly" { $taskTriggers += New-ScheduledTaskTrigger -Weekly -At $time -DaysOfWeek 2 -RandomDelay $delay }
+ "BiWeekly" { $taskTriggers += New-ScheduledTaskTrigger -Weekly -At $time -DaysOfWeek 2 -WeeksInterval 2 -RandomDelay $delay }
+ "Monthly" { $taskTriggers += New-ScheduledTaskTrigger -Weekly -At $time -DaysOfWeek 2 -WeeksInterval 4 -RandomDelay $delay }
}
- $taskUserPrincipal = New-ScheduledTaskPrincipal -UserId S-1-5-18 -RunLevel Highest
+
+ $taskPrincipal = New-ScheduledTaskPrincipal -UserId S-1-5-18 -RunLevel Highest
$taskSettings = New-ScheduledTaskSettingsSet -Compatibility Win8 -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit 03:00:00
- # Set up the task, and register it
- if ($taskTriggers) {
- $task = New-ScheduledTask -Action $taskAction -Principal $taskUserPrincipal -Settings $taskSettings -Trigger $taskTriggers
- }
- else {
- $task = New-ScheduledTask -Action $taskAction -Principal $taskUserPrincipal -Settings $taskSettings
+
+ $taskParams = @{
+ Action = $taskAction
+ Principal = $taskPrincipal
+ Settings = $taskSettings
}
+ if ($taskTriggers) { $taskParams.Trigger = $taskTriggers }
+
+ $task = New-ScheduledTask @taskParams
Register-ScheduledTask -TaskName 'Winget-AutoUpdate' -TaskPath 'WAU' -InputObject $task -Force | Out-Null
- # Settings for the scheduled task in User context
+ # User context task
$taskAction = New-ScheduledTaskAction -Execute "conhost.exe" -Argument "--headless powershell.exe -NoProfile -ExecutionPolicy Bypass -File winget-upgrade.ps1" -WorkingDirectory $InstallPath
- $taskUserPrincipal = New-ScheduledTaskPrincipal -GroupId S-1-5-11
+ $taskPrincipal = New-ScheduledTaskPrincipal -GroupId S-1-5-11
$taskSettings = New-ScheduledTaskSettingsSet -Compatibility Win8 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit 03:00:00
- # Set up the task for user apps
- $task = New-ScheduledTask -Action $taskAction -Principal $taskUserPrincipal -Settings $taskSettings
+ $task = New-ScheduledTask -Action $taskAction -Principal $taskPrincipal -Settings $taskSettings
Register-ScheduledTask -TaskName 'Winget-AutoUpdate-UserContext' -TaskPath 'WAU' -InputObject $task -Force | Out-Null
- # Settings for the scheduled task for Notifications
+ # Notification task
$taskAction = New-ScheduledTaskAction -Execute "conhost.exe" -Argument "--headless powershell.exe -NoProfile -ExecutionPolicy Bypass -File WAU-Notify.ps1" -WorkingDirectory $InstallPath
- $taskUserPrincipal = New-ScheduledTaskPrincipal -GroupId S-1-5-11
$taskSettings = New-ScheduledTaskSettingsSet -Compatibility Win8 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit 00:05:00
- # Set up the task, and register it
- $task = New-ScheduledTask -Action $taskAction -Principal $taskUserPrincipal -Settings $taskSettings
+ $task = New-ScheduledTask -Action $taskAction -Principal $taskPrincipal -Settings $taskSettings
Register-ScheduledTask -TaskName 'Winget-AutoUpdate-Notify' -TaskPath 'WAU' -InputObject $task -Force | Out-Null
- # Settings for the GPO scheduled task
- $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($InstallPath)WAU-Policies.ps1`""
- $tasktrigger = New-ScheduledTaskTrigger -Daily -At 6am
- $taskUserPrincipal = New-ScheduledTaskPrincipal -UserId S-1-5-18 -RunLevel Highest
+ # GPO policies task
+ $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"${InstallPath}WAU-Policies.ps1`""
+ $taskTrigger = New-ScheduledTaskTrigger -Daily -At 6am
+ $taskPrincipal = New-ScheduledTaskPrincipal -UserId S-1-5-18 -RunLevel Highest
$taskSettings = New-ScheduledTaskSettingsSet -Compatibility Win8 -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit 00:05:00
- # Set up the task, and register it
- $task = New-ScheduledTask -Action $taskAction -Principal $taskUserPrincipal -Settings $taskSettings -Trigger $taskTrigger
+ $task = New-ScheduledTask -Action $taskAction -Principal $taskPrincipal -Settings $taskSettings -Trigger $taskTrigger
Register-ScheduledTask -TaskName 'Winget-AutoUpdate-Policies' -TaskPath 'WAU' -InputObject $task -Force | Out-Null
- #Set task readable/runnable for all users
+ # Set task permissions for all users
$scheduler = New-Object -ComObject "Schedule.Service"
$scheduler.Connect()
$task = $scheduler.GetFolder("WAU").GetTask("Winget-AutoUpdate")
- $sec = $task.GetSecurityDescriptor(0xF)
- $sec = $sec + '(A;;GRGX;;;AU)'
+ $sec = $task.GetSecurityDescriptor(0xF) + '(A;;GRGX;;;AU)'
$task.SetSecurityDescriptor($sec, 0)
- #Copy App list to install folder (exept on self update)
+ # Copy app list (except on self-update)
if ($AppListPath -and ($AppListPath -notlike "$InstallPath*")) {
Write-Output "-> Copying $AppListPath to $InstallPath"
Copy-Item -Path $AppListPath -Destination $InstallPath
}
- #Copy Mods to install folder
+ # Copy mods folder if present
$ModsFolder = Join-Path $CurrentDir "Mods"
if (Test-Path $ModsFolder) {
Write-Output "-> Copying $ModsFolder to $InstallPath"
- Copy-Item -Path $ModsFolder -Destination "$InstallPath" -Recurse
+ Copy-Item -Path $ModsFolder -Destination $InstallPath -Recurse
}
- #Secure folders if not installed to ProgramFiles
+ # Secure folders if not in Program Files
if ($InstallPath -notlike "$env:ProgramFiles*") {
-
Write-Output "-> Securing functions and mods folders"
- $directories = @("$InstallPath\functions", "$InstallPath\mods")
- foreach ($directory in $directories) {
+ foreach ($dir in @("$InstallPath\functions", "$InstallPath\mods")) {
try {
- #Get dir
- $dirPath = Get-Item -Path $directory
- #Get ACL
+ $dirPath = Get-Item -Path $dir
$acl = Get-Acl -Path $dirPath.FullName
- #Disable inheritance
- $acl.SetAccessRuleProtection($True, $True)
- #Remove any existing rules
+ $acl.SetAccessRuleProtection($true, $true)
$acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) }
- # Add new ACL rules
- Add-ACLRule -acl $acl -sid "S-1-5-18" -access "FullControl" # SYSTEM Full
- Add-ACLRule -acl $acl -sid "S-1-5-32-544" -access "FullControl" # Administrators Full
- Add-ACLRule -acl $acl -sid "S-1-5-32-545" -access "ReadAndExecute" # Local Users ReadAndExecute
- Add-ACLRule -acl $acl -sid "S-1-5-11" -access "ReadAndExecute" # Authenticated Users ReadAndExecute
+ # Add permissions: SYSTEM, Admins = Full; Users, Authenticated = ReadAndExecute
+ Add-ACLRule -acl $acl -sid "S-1-5-18" -access "FullControl"
+ Add-ACLRule -acl $acl -sid "S-1-5-32-544" -access "FullControl"
+ Add-ACLRule -acl $acl -sid "S-1-5-32-545" -access "ReadAndExecute"
+ Add-ACLRule -acl $acl -sid "S-1-5-11" -access "ReadAndExecute"
- # Save the updated ACL to the directory
Set-Acl -Path $dirPath.FullName -AclObject $acl
-
- Write-Host "Permissions for '$directory' have been updated successfully."
+ Write-Host "Permissions for '$dir' updated successfully."
}
catch {
- Write-Host "Error setting ACL for '$directory' : $($_.Exception.Message)"
+ Write-Host "Error setting ACL for '$dir': $($_.Exception.Message)"
}
}
-
}
Write-Host "### WAU MSI Post actions succeeded! ###"
-
}
catch {
- Write-Host "### WAU Installation failed! Error $_. ###"
- return $False
+ Write-Host "### WAU Installation failed! Error: $_. ###"
+ return $false
}
}
function Uninstall-WingetAutoUpdate {
-
Write-Host "### Uninstalling WAU started! ###"
+ # Remove scheduled tasks
Write-Host "-> Removing scheduled tasks."
- Get-ScheduledTask -TaskName "Winget-AutoUpdate" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$False
- Get-ScheduledTask -TaskName "Winget-AutoUpdate-Notify" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$False
- Get-ScheduledTask -TaskName "Winget-AutoUpdate-UserContext" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$False
- Get-ScheduledTask -TaskName "Winget-AutoUpdate-Policies" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$False
+ @("Winget-AutoUpdate", "Winget-AutoUpdate-Notify", "Winget-AutoUpdate-UserContext", "Winget-AutoUpdate-Policies") | ForEach-Object {
+ Get-ScheduledTask -TaskName $_ -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false
+ }
- #If upgrade, keep app list and mods. Else, remove.
+ # Keep app lists and mods on upgrade, remove on full uninstall
if ($Upgrade -like "#{*}") {
Write-Output "-> Upgrade detected. Keeping *.txt and mods app lists"
}
else {
- $AppLists = Get-Item (Join-Path "$InstallPath" "*_apps.txt")
+ $AppLists = Get-Item (Join-Path $InstallPath "*_apps.txt") -ErrorAction SilentlyContinue
if ($AppLists) {
Write-Output "-> Removing items: $AppLists"
Remove-Item $AppLists -Force
}
- Remove-Item "$InstallPath\mods" -Recurse -Force
+ Remove-Item "$InstallPath\mods" -Recurse -Force -ErrorAction SilentlyContinue
}
- $ConfFolder = Get-Item (Join-Path "$InstallPath" "config") -ErrorAction SilentlyContinue
+ # Remove config folder
+ $ConfFolder = Get-Item (Join-Path $InstallPath "config") -ErrorAction SilentlyContinue
if ($ConfFolder) {
Write-Output "-> Removing item: $ConfFolder"
Remove-Item $ConfFolder -Force -Recurse
}
Write-Host "### Uninstallation done! ###"
- Start-sleep 1
+ Start-Sleep 1
}
@@ -213,12 +221,9 @@ function Uninstall-WingetAutoUpdate {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$Script:ProgressPreference = 'SilentlyContinue'
-
-# Uninstall
if ($Uninstall) {
Uninstall-WingetAutoUpdate
}
-# Install
else {
Install-WingetAutoUpdate
}
diff --git a/Sources/Winget-AutoUpdate/functions/Add-ScopeMachine.ps1 b/Sources/Winget-AutoUpdate/functions/Add-ScopeMachine.ps1
index 5b659448..c19c98e8 100644
--- a/Sources/Winget-AutoUpdate/functions/Add-ScopeMachine.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Add-ScopeMachine.ps1
@@ -1,7 +1,23 @@
-#Function to configure the preferred scope option as Machine
+<#
+.SYNOPSIS
+ Configures WinGet to prefer machine-scope installations.
+
+.DESCRIPTION
+ Updates the WinGet settings file to set the installation scope
+ preference to "Machine". This ensures applications are installed
+ for all users when running in system context.
+
+.EXAMPLE
+ Add-ScopeMachine
+
+.NOTES
+ Settings file location varies by context:
+ - System: %WINDIR%\System32\config\systemprofile\AppData\Local\Microsoft\WinGet\Settings
+ - User: %LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState
+#>
function Add-ScopeMachine {
- #Get Settings path for system or current user
+ # Determine settings path based on execution context
if ([System.Security.Principal.WindowsIdentity]::GetCurrent().IsSystem) {
$SettingsPath = "$Env:windir\System32\config\systemprofile\AppData\Local\Microsoft\WinGet\Settings\defaultState\settings.json"
}
@@ -11,22 +27,26 @@ function Add-ScopeMachine {
$ConfigFile = @{}
- #Check if setting file exist, if not create it
+ # Load existing settings or create new file
if (Test-Path $SettingsPath) {
+ # Parse JSON, excluding comment lines
$ConfigFile = Get-Content -Path $SettingsPath | Where-Object { $_ -notmatch '//' } | ConvertFrom-Json
}
else {
New-Item -Path $SettingsPath -Force | Out-Null
}
+ # Add or update the installBehavior.preferences.scope setting
if ($ConfigFile.installBehavior.preferences) {
Add-Member -InputObject $ConfigFile.installBehavior.preferences -MemberType NoteProperty -Name "scope" -Value "Machine" -Force
}
else {
+ # Create the nested structure if it doesn't exist
$Scope = New-Object PSObject -Property $(@{scope = "Machine" })
$Preference = New-Object PSObject -Property $(@{preferences = $Scope })
Add-Member -InputObject $ConfigFile -MemberType NoteProperty -Name "installBehavior" -Value $Preference -Force
}
+ # Save the updated settings
$ConfigFile | ConvertTo-Json -Depth 100 | Out-File $SettingsPath -Encoding utf8 -Force
}
diff --git a/Sources/Winget-AutoUpdate/functions/Compare-SemVer.ps1 b/Sources/Winget-AutoUpdate/functions/Compare-SemVer.ps1
index eff441e0..a3235a78 100644
--- a/Sources/Winget-AutoUpdate/functions/Compare-SemVer.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Compare-SemVer.ps1
@@ -1,40 +1,75 @@
+<#
+.SYNOPSIS
+ Compares two semantic version strings.
+
+.DESCRIPTION
+ Compares two version strings following semantic versioning rules.
+ Handles pre-release versions (e.g., "1.0.0-beta1").
+ Pre-release versions are considered less than their release counterparts.
+
+.PARAMETER Version1
+ The first version string to compare.
+
+.PARAMETER Version2
+ The second version string to compare.
+
+.OUTPUTS
+ Integer: -1 if Version1 < Version2, 0 if equal, 1 if Version1 > Version2
+
+.EXAMPLE
+ Compare-SemVer -Version1 "1.0.0" -Version2 "1.1.0" # Returns -1
+
+.EXAMPLE
+ Compare-SemVer -Version1 "2.0.0" -Version2 "2.0.0-beta" # Returns 1
+#>
function Compare-SemVer {
param (
[string]$Version1,
[string]$Version2
)
-
- # Split version and pre-release parts
+
+ # Split version and pre-release parts (e.g., "1.0.0-beta1" -> ["1.0.0", "beta1"])
$v1Parts = $Version1 -split '-'
$v2Parts = $Version2 -split '-'
-
+
+ # Parse main version numbers
$v1 = [Version]$v1Parts[0]
$v2 = [Version]$v2Parts[0]
-
- # Compare main version parts
+
+ # Compare major version
if ($v1.Major -ne $v2.Major) {
return [Math]::Sign($v1.Major - $v2.Major)
}
+
+ # Compare minor version
if ($v1.Minor -ne $v2.Minor) {
return [Math]::Sign($v1.Minor - $v2.Minor)
}
+
+ # Compare build version
if ($v1.Build -ne $v2.Build) {
return [Math]::Sign($v1.Build - $v2.Build)
}
+
+ # Compare revision
if ($v1.Revision -ne $v2.Revision) {
return [Math]::Sign($v1.Revision - $v2.Revision)
}
-
- # Compare pre-release parts if they exist
+
+ # Handle pre-release comparison
if ($v1Parts.Length -eq 2 -and $v2Parts.Length -eq 2) {
+ # Both have pre-release tags, compare them lexically
return [String]::Compare($v1Parts[1], $v2Parts[1])
}
elseif ($v1Parts.Length -eq 2) {
+ # Version1 has pre-release, Version2 doesn't (pre-release < release)
return -1
}
elseif ($v2Parts.Length -eq 2) {
+ # Version2 has pre-release, Version1 doesn't (release > pre-release)
return 1
}
-
+
+ # Versions are equal
return 0
}
\ No newline at end of file
diff --git a/Sources/Winget-AutoUpdate/functions/Confirm-Installation.ps1 b/Sources/Winget-AutoUpdate/functions/Confirm-Installation.ps1
index 98ca81ba..0d195b82 100644
--- a/Sources/Winget-AutoUpdate/functions/Confirm-Installation.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Confirm-Installation.ps1
@@ -1,24 +1,23 @@
-Function Confirm-Installation ($AppName, $AppVer) {
+<#
+.SYNOPSIS
+ Verifies application installation at expected version.
- #Set json export file
- $JsonFile = "$env:TEMP\InstalledApps.json"
+.PARAMETER AppName
+ WinGet package identifier.
- #Get installed apps and version in json file
- & $Winget export -s winget -o $JsonFile --include-versions | Out-Null
+.PARAMETER AppVer
+ Expected version prefix.
- #Get json content
- $Json = Get-Content $JsonFile -Raw | ConvertFrom-Json
+.OUTPUTS
+ Boolean: True if installed at version.
+#>
+Function Confirm-Installation ($AppName, $AppVer) {
- #Get apps and version in hashtable
- $Packages = $Json.Sources.Packages
+ $JsonFile = "$env:TEMP\InstalledApps.json"
+ & $Winget export -s winget -o $JsonFile --include-versions | Out-Null
- # Search for specific app and version
- $Apps = $Packages | Where-Object { $_.PackageIdentifier -eq $AppName -and $_.Version -like "$AppVer*" }
+ $Packages = (Get-Content $JsonFile -Raw | ConvertFrom-Json).Sources.Packages
+ $match = $Packages | Where-Object { $_.PackageIdentifier -eq $AppName -and $_.Version -like "$AppVer*" }
- if ($Apps) {
- return $true
- }
- else {
- return $false
- }
+ return [bool]$match
}
diff --git a/Sources/Winget-AutoUpdate/functions/ConvertTo-WingetArgumentArray.ps1 b/Sources/Winget-AutoUpdate/functions/ConvertTo-WingetArgumentArray.ps1
new file mode 100644
index 00000000..dd862a3d
--- /dev/null
+++ b/Sources/Winget-AutoUpdate/functions/ConvertTo-WingetArgumentArray.ps1
@@ -0,0 +1,90 @@
+<#
+.SYNOPSIS
+ Parses a string of winget arguments respecting quotes and spaces.
+
+.DESCRIPTION
+ Splits a string of winget command-line arguments into an array,
+ properly handling both single and double quotes, multiple spaces,
+ and quoted values containing spaces.
+
+.PARAMETER ArgumentString
+ The raw argument string to parse (e.g., "--locale en-US --skip-dependencies")
+
+.OUTPUTS
+ Array of individual argument strings
+
+.EXAMPLE
+ ConvertTo-WingetArgumentArray "--skip-dependencies"
+ Returns: @("--skip-dependencies")
+
+.EXAMPLE
+ ConvertTo-WingetArgumentArray '--locale "en-US" --architecture x64'
+ Returns: @("--locale", "en-US", "--architecture", "x64")
+
+.EXAMPLE
+ ConvertTo-WingetArgumentArray "--override '-sfx_nu /sAll /msi EULA_ACCEPT=YES'"
+ Returns: @("--override", "-sfx_nu /sAll /msi EULA_ACCEPT=YES")
+#>
+function ConvertTo-WingetArgumentArray {
+ [CmdletBinding()]
+ [OutputType([System.Object[]])]
+ param(
+ [Parameter(Mandatory = $false)]
+ [string]$ArgumentString
+ )
+
+ # Return empty array if input is null or whitespace
+ if ([string]::IsNullOrWhiteSpace($ArgumentString)) {
+ return @()
+ }
+
+ $ArgumentString = $ArgumentString.Trim()
+
+ # Regex pattern that handles:
+ # - Double quoted strings: "value with spaces"
+ # - Single quoted strings: 'value with spaces'
+ # - Unquoted arguments: --flag or value
+ # Pattern explanation:
+ # "([^"]*)" - Captures content between double quotes (group 1)
+ # '([^']*)' - Captures content between single quotes (group 2)
+ # (\S+) - Captures non-whitespace sequences (group 3)
+ $pattern = '(?:"([^"]*)"|''([^'']*)''|(\S+))'
+
+ try {
+ # Use [regex]::Matches() to find all argument tokens
+ $regex = [regex]::new($pattern)
+ $regexMatches = $regex.Matches($ArgumentString)
+
+ $result = @()
+ foreach ($match in $regexMatches) {
+ # Get the captured value from whichever group matched
+ $value = if ($match.Groups[1].Success) {
+ # Double-quoted value
+ $match.Groups[1].Value
+ }
+ elseif ($match.Groups[2].Success) {
+ # Single-quoted value
+ $match.Groups[2].Value
+ }
+ else {
+ # Unquoted value
+ $match.Groups[3].Value
+ }
+
+ # Only add non-empty values
+ if (-not [string]::IsNullOrWhiteSpace($value)) {
+ $result += $value
+ }
+ }
+
+ return $result
+ }
+ catch {
+ Write-ToLog "Warning: Failed to parse arguments '$ArgumentString' - Error: $($_.Exception.Message)" "Yellow"
+ Write-ToLog "Falling back to simple space split" "Yellow"
+
+ # Fallback to simple split if regex parsing fails
+ $fallbackResult = $ArgumentString.Trim() -split '\s+'
+ return $fallbackResult
+ }
+}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-AZCopy.ps1 b/Sources/Winget-AutoUpdate/functions/Get-AZCopy.ps1
index c94b175f..4512715e 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-AZCopy.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-AZCopy.ps1
@@ -1,32 +1,57 @@
-#Function to get AZCopy, if it doesn't exist and update it, if it does
+<#
+.SYNOPSIS
+ Downloads or updates AzCopy for Azure Blob storage operations.
+.DESCRIPTION
+ Checks for the latest version of AzCopy and downloads/updates it
+ if a newer version is available. AzCopy is used for syncing mods
+ from Azure Blob Storage.
+
+.PARAMETER WingetUpdatePath
+ The WAU installation directory where azcopy.exe will be stored.
+
+.EXAMPLE
+ Get-AZCopy "C:\Program Files\Winget-AutoUpdate"
+
+.NOTES
+ Downloads from Microsoft's official AzCopy distribution.
+ Extracts and copies only the azcopy.exe executable.
+#>
Function Get-AZCopy ($WingetUpdatePath) {
+ # Get latest AzCopy version from Microsoft redirect
$AZCopyLink = (Invoke-WebRequest -Uri https://aka.ms/downloadazcopy-v10-windows -UseBasicParsing -MaximumRedirection 0 -ErrorAction SilentlyContinue).headers.location
$AZCopyVersionRegex = [regex]::new("(\d+\.\d+\.\d+)")
$AZCopyLatestVersion = $AZCopyVersionRegex.Match($AZCopyLink).Value
+ # Default to 0.0.0 if version detection fails
if ($null -eq $AZCopyLatestVersion -or "" -eq $AZCopyLatestVersion) {
$AZCopyLatestVersion = "0.0.0"
}
+ # Check current installed version
if (Test-Path -Path "$WingetUpdatePath\azcopy.exe" -PathType Leaf) {
$AZCopyCurrentVersion = & "$WingetUpdatePath\azcopy.exe" -v
$AZCopyCurrentVersion = $AZCopyVersionRegex.Match($AZCopyCurrentVersion).Value
- Write-ToLog "AZCopy version $AZCopyCurrentVersion found"
+ Write-ToLog "AZCopy version $AZCopyCurrentVersion found"
}
else {
- Write-ToLog "AZCopy not already installed"
+ Write-ToLog "AZCopy not already installed"
$AZCopyCurrentVersion = "0.0.0"
}
+ # Download and install if newer version available
if (([version] $AZCopyCurrentVersion) -lt ([version] $AZCopyLatestVersion)) {
- Write-ToLog "Installing version $AZCopyLatestVersion of AZCopy"
+ Write-ToLog "Installing version $AZCopyLatestVersion of AZCopy"
+
+ # Download AzCopy zip
Invoke-WebRequest -Uri $AZCopyLink -UseBasicParsing -OutFile "$WingetUpdatePath\azcopyv10.zip"
- Write-ToLog "Extracting AZCopy zip file"
+ Write-ToLog "Extracting AZCopy zip file"
+ # Extract archive
Expand-archive -Path "$WingetUpdatePath\azcopyv10.zip" -Destinationpath "$WingetUpdatePath" -Force
+ # Find extracted folder (handles version-specific folder names)
$AZCopyPathSearch = Resolve-Path -path "$WingetUpdatePath\azcopy_*"
if ($AZCopyPathSearch -is [array]) {
@@ -36,15 +61,18 @@ Function Get-AZCopy ($WingetUpdatePath) {
$AZCopyEXEPath = $AZCopyPathSearch
}
- Write-ToLog "Copying 'azcopy.exe' to main folder"
+ # Copy executable to main folder
+ Write-ToLog "Copying 'azcopy.exe' to main folder"
Copy-Item "$AZCopyEXEPath\azcopy.exe" -Destination "$WingetUpdatePath\"
- Write-ToLog "Removing temporary AZCopy files"
+ # Cleanup temporary files
+ Write-ToLog "Removing temporary AZCopy files"
Remove-Item -Path $AZCopyEXEPath -Recurse
Remove-Item -Path "$WingetUpdatePath\azcopyv10.zip"
+ # Verify installation
$AZCopyCurrentVersion = & "$WingetUpdatePath\azcopy.exe" -v
$AZCopyCurrentVersion = $AZCopyVersionRegex.Match($AZCopyCurrentVersion).Value
- Write-ToLog "AZCopy version $AZCopyCurrentVersion installed"
+ Write-ToLog "AZCopy version $AZCopyCurrentVersion installed"
}
}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-AppInfo.ps1 b/Sources/Winget-AutoUpdate/functions/Get-AppInfo.ps1
index 747a8188..c226725f 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-AppInfo.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-AppInfo.ps1
@@ -1,12 +1,30 @@
-#Get the winget App Information
+<#
+.SYNOPSIS
+ Retrieves application release notes URL from WinGet.
+.DESCRIPTION
+ Queries WinGet for application metadata and extracts the
+ release notes URL if available.
+
+.PARAMETER AppID
+ The WinGet package identifier to query.
+
+.OUTPUTS
+ String containing the release notes URL, or empty if not found.
+
+.EXAMPLE
+ $releaseUrl = Get-AppInfo "Microsoft.PowerShell"
+
+.NOTES
+ Uses WinGet show command with source agreements accepted.
+#>
Function Get-AppInfo ($AppID) {
- #Get AppID Info
+
+ # Query WinGet for application details
$String = & $winget show $AppID --accept-source-agreements -s winget | Out-String
- #Search for Release Note info
+ # Extract Release Notes URL using regex
$ReleaseNote = [regex]::match($String, "(?<=Release Notes Url: )(.*)(?=\n)").Groups[0].Value
- #Return Release Note
return $ReleaseNote
}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-ExcludedApps.ps1 b/Sources/Winget-AutoUpdate/functions/Get-ExcludedApps.ps1
index 15dbc7a3..690e4a4c 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-ExcludedApps.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-ExcludedApps.ps1
@@ -1,34 +1,37 @@
-#Function to get the Block List apps
+<#
+.SYNOPSIS
+ Retrieves the list of excluded (blacklisted) applications.
+.DESCRIPTION
+ Returns application IDs to exclude from automatic updates.
+ Priority: GPO registry > local file > default file.
+
+.OUTPUTS
+ Array of application IDs to exclude.
+#>
function Get-ExcludedApps {
- $AppIDs = @()
+ $GPOPath = "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate\BlackList"
+ $LocalFile = "$WorkingDir\excluded_apps.txt"
+ $DefaultFile = "$WorkingDir\config\default_excluded_apps.txt"
- #blacklist in Policies registry
- if (Test-Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate\BlackList") {
+ # GPO takes priority
+ if (Test-Path $GPOPath) {
Write-ToLog "-> Excluded apps from GPO is activated"
- $ValueNames = (Get-Item -Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate\BlackList").Property
- foreach ($ValueName in $ValueNames) {
- $AppIDs += (Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate\BlackList" -Name $ValueName).Trim()
- }
- foreach ($app in $AppIDs) {
- Write-ToLog "Exclude app $app"
+ $AppIDs = (Get-Item $GPOPath).Property | ForEach-Object {
+ $id = (Get-ItemPropertyValue $GPOPath -Name $_).Trim()
+ Write-ToLog "Exclude app $id"
+ $id
}
}
- #blacklist pulled from local file
- elseif (Test-Path "$WorkingDir\excluded_apps.txt") {
-
- $AppIDs = (Get-Content -Path "$WorkingDir\excluded_apps.txt").Trim()
- Write-ToLog "-> Successsfully loaded local excluded apps list."
-
+ elseif (Test-Path $LocalFile) {
+ Write-ToLog "-> Successfully loaded local excluded apps list."
+ $AppIDs = (Get-Content $LocalFile).Trim()
}
- #blacklist pulled from default file
- elseif (Test-Path "$WorkingDir\config\default_excluded_apps.txt") {
-
- $AppIDs = (Get-Content -Path "$WorkingDir\config\default_excluded_apps.txt").Trim()
- Write-ToLog "-> Successsfully loaded default excluded apps list."
-
+ elseif (Test-Path $DefaultFile) {
+ Write-ToLog "-> Successfully loaded default excluded apps list."
+ $AppIDs = (Get-Content $DefaultFile).Trim()
}
- return $AppIDs | Where-Object { $_.length -gt 0 }
+ return $AppIDs | Where-Object { $_ }
}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-IncludedApps.ps1 b/Sources/Winget-AutoUpdate/functions/Get-IncludedApps.ps1
index 4bb5f132..d297d411 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-IncludedApps.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-IncludedApps.ps1
@@ -1,28 +1,32 @@
-#Function to get the allow List apps
+<#
+.SYNOPSIS
+ Retrieves the list of included (whitelisted) applications.
+.DESCRIPTION
+ Returns application IDs to include in automatic updates (whitelist mode).
+ Priority: GPO registry > local file.
+
+.OUTPUTS
+ Array of application IDs to include.
+#>
function Get-IncludedApps {
- $AppIDs = @()
+ $GPOPath = "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate\WhiteList"
+ $LocalFile = "$WorkingDir\included_apps.txt"
- #whitelist in Policies registry
- if (Test-Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate\WhiteList") {
+ # GPO takes priority
+ if (Test-Path $GPOPath) {
Write-ToLog "-> Included apps from GPO is activated"
- $ValueNames = (Get-Item -Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate\WhiteList").Property
- foreach ($ValueName in $ValueNames) {
- $AppIDs += (Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate\WhiteList" -Name $ValueName).Trim()
- }
- foreach ($app in $AppIDs) {
- Write-ToLog "Include app $app"
+ $AppIDs = (Get-Item $GPOPath).Property | ForEach-Object {
+ $id = (Get-ItemPropertyValue $GPOPath -Name $_).Trim()
+ Write-ToLog "Include app $id"
+ $id
}
}
- #whitelist pulled from local file
- elseif (Test-Path "$WorkingDir\included_apps.txt") {
-
- $AppIDs = (Get-Content -Path "$WorkingDir\included_apps.txt").Trim()
- Write-ToLog "-> Successsfully loaded local included apps list."
-
+ elseif (Test-Path $LocalFile) {
+ Write-ToLog "-> Successfully loaded local included apps list."
+ $AppIDs = (Get-Content $LocalFile).Trim()
}
- return $AppIDs | Where-Object { $_.length -gt 0 }
-
+ return $AppIDs | Where-Object { $_ }
}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-NotifLocale.ps1 b/Sources/Winget-AutoUpdate/functions/Get-NotifLocale.ps1
index f0bd3a2d..a16067a0 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-NotifLocale.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-NotifLocale.ps1
@@ -1,28 +1,44 @@
-#Function to get the locale file for notifications
+<#
+.SYNOPSIS
+ Retrieves the notification locale file for the current OS language.
+.DESCRIPTION
+ Loads the appropriate XML locale file for toast notifications based
+ on the operating system's UI culture. Falls back to English if the
+ OS locale is not available.
+
+.OUTPUTS
+ String containing the display name of the selected locale.
+
+.EXAMPLE
+ $localeName = Get-NotifLocale
+
+.NOTES
+ Sets the script-scoped $NotifLocale variable with the loaded XML content.
+ Locale files are stored in the locale subfolder.
+#>
Function Get-NotifLocale {
- #Get OS locale
+ # Get the OS UI culture (parent culture for regional variants)
$OSLocale = (Get-UICulture).Parent
- #Test if OS locale notif file exists
+ # Build path to locale-specific notification file
$TestOSLocalPath = "$WorkingDir\locale\$($OSLocale.Name).xml"
- #Set OS Local if file exists
+ # Use OS locale if file exists, otherwise fall back to English
if (Test-Path $TestOSLocalPath) {
$LocaleDisplayName = $OSLocale.DisplayName
$LocaleFile = $TestOSLocalPath
}
- #Set English if file doesn't exist
else {
$LocaleDisplayName = "English"
$LocaleFile = "$WorkingDir\locale\en.xml"
}
- #Get locale XML file content
+ # Load the locale XML file into script scope for notification messages
[xml]$Script:NotifLocale = Get-Content $LocaleFile -Encoding UTF8 -ErrorAction SilentlyContinue
- #Rerturn langague display name
- Return $LocaleDisplayName
+ # Return the language display name
+ return $LocaleDisplayName
}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-WAUAvailableVersion.ps1 b/Sources/Winget-AutoUpdate/functions/Get-WAUAvailableVersion.ps1
index b07eac70..5229d866 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-WAUAvailableVersion.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-WAUAvailableVersion.ps1
@@ -1,19 +1,36 @@
-#Function to get the latest WAU available version on Github
+<#
+.SYNOPSIS
+ Retrieves the latest available version of WAU from GitHub.
+.DESCRIPTION
+ Queries the GitHub API to determine the latest available version
+ of Winget-AutoUpdate. Supports both stable releases and pre-releases
+ based on configuration.
+
+.OUTPUTS
+ String containing the version number (without 'v' prefix).
+
+.EXAMPLE
+ $version = Get-WAUAvailableVersion
+
+.NOTES
+ Requires WAUConfig with WAU_UpdatePrerelease setting.
+ Falls back to web scraping if API fails.
+#>
function Get-WAUAvailableVersion {
- #Get Github latest version
+ # Check if pre-release versions should be considered
if ($WAUConfig.WAU_UpdatePrerelease -eq 1) {
- #Log
Write-ToLog "WAU AutoUpdate Pre-release versions is Enabled" "Cyan"
try {
- #Get latest pre-release info
+ # Query GitHub API for all releases (pre-releases included)
$WAUurl = "https://api.github.com/repos/Romanitho/$($GitHub_Repo)/releases"
$WAUAvailableVersion = ((Invoke-WebRequest $WAUurl -UseBasicParsing | ConvertFrom-Json)[0].tag_name).Replace("v", "")
}
catch {
+ # Fallback: Parse version from GitHub releases page
$url = "https://github.com/Romanitho/$($GitHub_Repo)/releases"
$link = ((Invoke-WebRequest $url -UseBasicParsing).Links.href -match "/$($GitHub_Repo)/releases/tag/v*")[0]
$WAUAvailableVersion = $link.Trim().Split("v")[-1]
@@ -23,11 +40,12 @@ function Get-WAUAvailableVersion {
else {
try {
- #Get latest stable info
+ # Query GitHub API for latest stable release only
$WAUurl = "https://api.github.com/repos/Romanitho/$($GitHub_Repo)/releases/latest"
$WAUAvailableVersion = ((Invoke-WebRequest $WAUurl -UseBasicParsing | ConvertFrom-Json)[0].tag_name).Replace("v", "")
}
catch {
+ # Fallback: Parse version from GitHub releases page
$url = "https://github.com/Romanitho/$($GitHub_Repo)/releases/latest"
$link = ((Invoke-WebRequest $url -UseBasicParsing).Links.href -match "/$($GitHub_Repo)/releases/tag/v*")[0]
$WAUAvailableVersion = $link.Trim().Split("v")[-1]
@@ -35,7 +53,6 @@ function Get-WAUAvailableVersion {
}
- #Return version
return $WAUAvailableVersion
}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-WAUConfig.ps1 b/Sources/Winget-AutoUpdate/functions/Get-WAUConfig.ps1
index 8481d223..dba767ba 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-WAUConfig.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-WAUConfig.ps1
@@ -1,24 +1,26 @@
-#Function to get the WAU settings, including Domain/Local Policies (GPO)
+<#
+.SYNOPSIS
+ Gets WAU configuration including GPO overrides.
-Function Get-WAUConfig {
-
- #Get WAU Configurations from install config
- $WAUConfig_64_86 = Get-ItemProperty -Path "HKLM:\SOFTWARE\Romanitho\Winget-AutoUpdate*", "HKLM:\SOFTWARE\WOW6432Node\Romanitho\Winget-AutoUpdate*" -ErrorAction SilentlyContinue | Sort-Object { $_.ProductVersion } -Descending
- $WAUConfig = $WAUConfig_64_86[0]
+.DESCRIPTION
+ Reads settings from registry, applying GPO policies if present.
- #Check if GPO policies exist
- $WAUPolicies = Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate" -ErrorAction SilentlyContinue
+.OUTPUTS
+ PSCustomObject with WAU configuration properties.
+#>
+Function Get-WAUConfig {
- #If GPO policies exist, apply them (regardless of ActivateGPOManagement value)
- if ($WAUPolicies) {
- Write-ToLog "GPO policies detected - applying GPO configuration" "Yellow"
+ # Get base config (newest version from registry)
+ $WAUConfig = Get-ItemProperty "HKLM:\SOFTWARE\Romanitho\Winget-AutoUpdate*", "HKLM:\SOFTWARE\WOW6432Node\Romanitho\Winget-AutoUpdate*" -ErrorAction SilentlyContinue |
+ Sort-Object { $_.ProductVersion } -Descending |
+ Select-Object -First 1
- #Replace loaded configurations by ones from Policies
- $WAUPolicies.PSObject.Properties | ForEach-Object {
- $WAUConfig.PSObject.Properties.add($_)
- }
+ # Apply GPO overrides if present
+ $GPO = Get-ItemProperty "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate" -ErrorAction SilentlyContinue
+ if ($GPO) {
+ Write-ToLog "GPO policies detected - applying" "Yellow"
+ $GPO.PSObject.Properties | ForEach-Object { $WAUConfig.PSObject.Properties.add($_) }
}
- #Return config
return $WAUConfig
}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-WingetCmd.ps1 b/Sources/Winget-AutoUpdate/functions/Get-WingetCmd.ps1
index 2f4d41a1..e7b48218 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-WingetCmd.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-WingetCmd.ps1
@@ -1,30 +1,38 @@
-#Function to get the winget command regarding execution context (User, System...)
+<#
+.SYNOPSIS
+ Retrieves the path to the Winget executable.
-Function Get-WingetCmd
-{
- [OutputType([String])]
- $WingetCmd = [string]::Empty;
+.DESCRIPTION
+ Locates winget.exe from system context (WindowsApps) or user context.
+ Returns the most recent version when multiple exist.
- #Get WinGet Path
- # default winget path (in system context)
- [string]$ps = "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller_*_8wekyb3d8bbwe\winget.exe";
+.OUTPUTS
+ String: Full path to winget.exe, or empty if not found.
+#>
+Function Get-WingetCmd {
+ [OutputType([String])]
- #default winget path (in user context)
- [string]$pu = "$env:LocalAppData\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe";
+ $systemPath = "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller_*_8wekyb3d8bbwe\winget.exe"
+ $userPath = "$env:LocalAppData\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe"
- #Get Admin Context Winget Location
- $WingetInfo = (Get-Item -Path $ps -ErrorAction Stop).VersionInfo | Sort-Object -Property FileVersionRaw -Descending | Select-Object -First 1;
- #If multiple versions, pick most recent one
- $WingetCmd = $WingetInfo.FileName;
+ # Try system context first (newest version)
+ try {
+ $WingetInfo = (Get-Item $systemPath -ErrorAction Stop).VersionInfo |
+ Sort-Object FileVersionRaw -Descending |
+ Select-Object -First 1
- if ([String]::IsNullOrEmpty($WingetCmd))
- {
- #Get User context Winget Location
- if (Test-Path -Path $pu -PathType Leaf)
- {
- $WingetCmd = $pu;
+ if ($WingetInfo.FileName) {
+ return $WingetInfo.FileName
}
}
+ catch {
+ # System context not found, try user context
+ }
+
+ # Fall back to user context
+ if (Test-Path $userPath) {
+ return $userPath
+ }
- return $WingetCmd;
+ return [string]::Empty
}
\ No newline at end of file
diff --git a/Sources/Winget-AutoUpdate/functions/Get-WingetOutdatedApps.ps1 b/Sources/Winget-AutoUpdate/functions/Get-WingetOutdatedApps.ps1
index b4f9bba6..909aaf6f 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-WingetOutdatedApps.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-WingetOutdatedApps.ps1
@@ -1,5 +1,26 @@
-#Function to get the outdated app list, in formatted array
+<#
+.SYNOPSIS
+ Retrieves the list of applications with available updates from WinGet.
+.DESCRIPTION
+ Queries WinGet for applications with available updates and returns
+ a structured list. Parses WinGet's tabular output and handles
+ non-Latin characters and text formatting issues.
+
+.PARAMETER src
+ The WinGet source repository name to query (e.g., "winget").
+
+.OUTPUTS
+ Array of Software objects with Name, Id, Version, and AvailableVersion.
+ Returns descriptive string if no updates are found.
+
+.EXAMPLE
+ $outdated = Get-WingetOutdatedApps -src "winget"
+
+.NOTES
+ Excludes system-installed apps when running in user context.
+ Results are randomized to prevent update ordering bias.
+#>
function Get-WingetOutdatedApps {
Param(
@@ -7,6 +28,8 @@ function Get-WingetOutdatedApps {
[ValidateNotNullorEmpty()]
[string]$src
)
+
+ # Define class for structured output
class Software {
[string]$Name
[string]$Id
@@ -14,79 +37,78 @@ function Get-WingetOutdatedApps {
[string]$AvailableVersion
}
- #Get list of available upgrades on winget format
+ # Get upgrade list from WinGet
try {
$upgradeResult = & $Winget upgrade --source $src | Where-Object { $_ -notlike " *" } | Out-String
}
catch {
- Write-ToLog "Error while recieving winget upgrade list: $_" "Red"
+ Write-ToLog "Error while receiving winget upgrade list: $_" "Red"
$upgradeResult = $null
}
- #Start Conversion of winget format to an array. Check if "-----" exists (Winget Error Handling)
+ # Check if output contains valid data (header separator line)
if (!($upgradeResult -match "-----")) {
-
return "No update found. 'Winget upgrade' output:`n$upgradeResult"
-
}
else {
- #Split winget output to lines
+ # Split output into lines, removing empty lines
$lines = $upgradeResult.Split([Environment]::NewLine) | Where-Object { $_ }
- # Find the line that starts with "------"
+ # Find the separator line (starts with "-----")
$fl = 0
while (-not $lines[$fl].StartsWith("-----")) {
$fl++
}
- #Get header line
+ # Get header line (one line before separator)
$fl = $fl - 1
- #Get header titles [without remove separator]
+ # Split header into columns (preserving trailing spaces for positioning)
$index = $lines[$fl] -split '(?<=\s)(?!\s)'
- # Line $fl has the header, we can find char where we find ID and Version [and manage non latin characters]
+ # Calculate column positions (handle non-Latin characters by replacing with **)
$idStart = $($index[0] -replace '[\u4e00-\u9fa5]', '**').Length
$versionStart = $idStart + $($index[1] -replace '[\u4e00-\u9fa5]', '**').Length
$availableStart = $versionStart + $($index[2] -replace '[\u4e00-\u9fa5]', '**').Length
- # Now cycle in real package and split accordingly
+ # Parse each data line
$upgradeList = @()
For ($i = $fl + 2; $i -lt $lines.Length; $i++) {
- $line = $lines[$i] -replace "[\u2026]", " " #Fix "..." in long names
+ # Fix ellipsis character in long names
+ $line = $lines[$i] -replace "[\u2026]", " "
+
+ # Handle multiple tables (new header encountered)
if ($line.StartsWith("-----")) {
- #Get header line
$fl = $i - 1
-
- #Get header titles [without remove separator]
$index = $lines[$fl] -split '(?<=\s)(?!\s)'
-
- # Line $fl has the header, we can find char where we find ID and Version [and manage non latin characters]
$idStart = $($index[0] -replace '[\u4e00-\u9fa5]', '**').Length
$versionStart = $idStart + $($index[1] -replace '[\u4e00-\u9fa5]', '**').Length
$availableStart = $versionStart + $($index[2] -replace '[\u4e00-\u9fa5]', '**').Length
}
- #(Alphanumeric | Literal . | Alphanumeric) - the only unique thing in common for lines with applications
+
+ # Check if line contains an application entry (has format word.word)
if ($line -match "\w\.\w") {
$software = [Software]::new()
- #Manage non latin characters
+
+ # Calculate name declination for non-Latin character handling
$nameDeclination = $($line.Substring(0, $idStart) -replace '[\u4e00-\u9fa5]', '**').Length - $line.Substring(0, $idStart).Length
$software.Name = $line.Substring(0, $idStart - $nameDeclination).TrimEnd()
$software.Id = $line.Substring($idStart - $nameDeclination, $versionStart - $idStart).TrimEnd()
$software.Version = $line.Substring($versionStart - $nameDeclination, $availableStart - $versionStart).TrimEnd()
$software.AvailableVersion = $line.Substring($availableStart - $nameDeclination).TrimEnd()
- #add formatted soft to list
+
$upgradeList += $software
}
}
- #If current user is not system, remove system apps from list
+ # In user context, filter out system-installed apps
if ($IsSystem -eq $false) {
$SystemApps = Get-Content -Path "$WorkingDir\config\winget_system_apps.txt" -ErrorAction SilentlyContinue
$upgradeList = $upgradeList | Where-Object { $SystemApps -notcontains $_.Id }
}
+ # Return randomized list to prevent update ordering bias
return $upgradeList | Sort-Object { Get-Random }
}
diff --git a/Sources/Winget-AutoUpdate/functions/Get-WingetSystemApps.ps1 b/Sources/Winget-AutoUpdate/functions/Get-WingetSystemApps.ps1
index 953b01a8..3cd5e9b3 100644
--- a/Sources/Winget-AutoUpdate/functions/Get-WingetSystemApps.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Get-WingetSystemApps.ps1
@@ -1,22 +1,42 @@
+<#
+.SYNOPSIS
+ Exports a list of system-installed WinGet applications.
+
+.DESCRIPTION
+ Retrieves the list of applications installed in the system context
+ and saves their package identifiers to a configuration file. This
+ list is used to exclude system apps from user-context updates.
+
+.PARAMETER src
+ The WinGet source repository name to query.
+
+.EXAMPLE
+ Get-WingetSystemApps -src "winget"
+
+.NOTES
+ Output file: $WorkingDir\config\winget_system_apps.txt
+ Used by Get-WingetOutdatedApps to filter user-context updates.
+#>
function Get-WingetSystemApps {
-Param(
- [Parameter(Position=0,Mandatory=$True,HelpMessage="You MUST supply value for winget repo, we need it")]
- [ValidateNotNullorEmpty()]
- [string]$src
-)
- #Json File, where to export system installed apps
+ Param(
+ [Parameter(Position = 0, Mandatory = $True, HelpMessage = "You MUST supply value for winget repo, we need it")]
+ [ValidateNotNullorEmpty()]
+ [string]$src
+ )
+
+ # Output file for system apps list
$jsonFile = "$WorkingDir\config\winget_system_apps.txt"
- #Get list of installed Winget apps to json file
+ # Export installed apps from WinGet to JSON format
& $Winget export -o $jsonFile --accept-source-agreements -s $src | Out-Null
- #Convert json file to txt file with app ids
+ # Parse JSON and extract package identifiers
$InstalledApps = get-content $jsonFile | ConvertFrom-Json
- #Save app list
+ # Write app identifiers to text file
Set-Content $InstalledApps.Sources.Packages.PackageIdentifier -Path $jsonFile
- #Sort app list
+ # Sort the list for consistency
Get-Content $jsonFile | Sort-Object | Set-Content $jsonFile
}
diff --git a/Sources/Winget-AutoUpdate/functions/Install-Prerequisites.ps1 b/Sources/Winget-AutoUpdate/functions/Install-Prerequisites.ps1
index 84ee4fc1..57a064e2 100644
--- a/Sources/Winget-AutoUpdate/functions/Install-Prerequisites.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Install-Prerequisites.ps1
@@ -1,18 +1,38 @@
+<#
+.SYNOPSIS
+ Ensures all WinGet prerequisites are installed and up-to-date.
+
+.DESCRIPTION
+ Checks and installs the following prerequisites for WinGet:
+ - Microsoft Visual C++ 2015-2022 Redistributable
+ - Microsoft.VCLibs.140.00.UWPDesktop (UWP dependency)
+ - Microsoft.UI.Xaml.2.8 (UI framework dependency)
+ - WinGet CLI (App Installer) itself
+
+.EXAMPLE
+ Install-Prerequisites
+
+.NOTES
+ Must run with administrative privileges.
+ Downloads installers from Microsoft's official sources.
+ Falls back to Store update if WinGet installation fails.
+#>
function Install-Prerequisites {
try {
Write-ToLog "Checking prerequisites..." "Yellow"
- #Check if Visual C++ 2022 is installed
+ # === Check Visual C++ 2015-2022 Redistributable ===
$Visual2022 = "Microsoft Visual C++ 2015-2022 Redistributable*"
$VisualMinVer = "14.40.0.0"
$path = Get-Item HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*, HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object { $_.GetValue("DisplayName") -like $Visual2022 -and $_.GetValue("DisplayVersion") -gt $VisualMinVer }
+
if (!($path)) {
try {
Write-ToLog "MS Visual C++ 2015-2022 is not installed" "Red"
- #Get proc architecture
+ # Determine processor architecture for correct installer
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
$OSArch = "arm64"
}
@@ -23,7 +43,7 @@ function Install-Prerequisites {
$OSArch = "x86"
}
- #Download and install
+ # Download and install VC++ Redistributable
$SourceURL = "https://aka.ms/vs/17/release/VC_redist.$OSArch.exe"
$Installer = "$env:TEMP\VC_redist.$OSArch.exe"
Write-ToLog "-> Downloading $SourceURL..."
@@ -40,16 +60,18 @@ function Install-Prerequisites {
}
}
- #Check if Microsoft.VCLibs.140.00.UWPDesktop is installed
+ # === Check Microsoft.VCLibs.140.00.UWPDesktop ===
if (!(Get-AppxPackage -Name 'Microsoft.VCLibs.140.00.UWPDesktop' -AllUsers)) {
try {
Write-ToLog "Microsoft.VCLibs.140.00.UWPDesktop is not installed" "Red"
- #Download
+
+ # Download VCLibs package
$VCLibsUrl = "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx"
$VCLibsFile = "$env:TEMP\Microsoft.VCLibs.x64.14.00.Desktop.appx"
Write-ToLog "-> Downloading Microsoft.VCLibs.140.00.UWPDesktop..."
Invoke-WebRequest -Uri $VCLibsUrl -OutFile $VCLibsFile -UseBasicParsing
- #Install
+
+ # Install package
Write-ToLog "-> Installing Microsoft.VCLibs.140.00.UWPDesktop..."
Add-AppxProvisionedPackage -Online -PackagePath $VCLibsFile -SkipLicense | Out-Null
Write-ToLog "-> Microsoft.VCLibs.140.00.UWPDesktop installed successfully." "Green"
@@ -62,16 +84,18 @@ function Install-Prerequisites {
}
}
- #Check if Microsoft.UI.Xaml.2.8 is installed
+ # === Check Microsoft.UI.Xaml.2.8 ===
if (!(Get-AppxPackage -Name 'Microsoft.UI.Xaml.2.8' -AllUsers)) {
try {
Write-ToLog "Microsoft.UI.Xaml.2.8 is not installed" "Red"
- #Download
+
+ # Download UI.Xaml package
$UIXamlUrl = "https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx"
$UIXamlFile = "$env:TEMP\Microsoft.UI.Xaml.2.8.x64.appx"
Write-ToLog "-> Downloading Microsoft.UI.Xaml.2.8..."
Invoke-WebRequest -Uri $UIXamlUrl -OutFile $UIXamlFile -UseBasicParsing
- #Install
+
+ # Install package
Write-ToLog "-> Installing Microsoft.UI.Xaml.2.8..."
Add-AppxProvisionedPackage -Online -PackagePath $UIXamlFile -SkipLicense | Out-Null
Write-ToLog "-> Microsoft.UI.Xaml.2.8 installed successfully." "Green"
@@ -84,71 +108,68 @@ function Install-Prerequisites {
}
}
- #Check if Winget is installed (and up to date)
+ # === Check WinGet CLI ===
try {
- #Get latest WinGet info
+ # Get latest WinGet version from GitHub
$WinGeturl = 'https://api.github.com/repos/microsoft/winget-cli/releases/latest'
$WinGetAvailableVersion = ((Invoke-WebRequest $WinGeturl -UseBasicParsing | ConvertFrom-Json)[0].tag_name).TrimStart("v")
}
catch {
- #If fail set version to the latest version as of 2025-08-12
+ # Fallback to known version if API fails
$WinGetAvailableVersion = "1.11.430"
}
+
try {
- #Get Admin Context Winget Location
+ # Get currently installed WinGet version
$WingetInfo = (Get-Item "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller_*_8wekyb3d8bbwe\winget.exe").VersionInfo | Sort-Object -Property FileVersionRaw
- #If multiple versions, pick most recent one
$WingetCmd = $WingetInfo[-1].FileName
- #Get current Winget Version
$WingetInstalledVersion = (& $WingetCmd -v).Trim().TrimStart("v")
}
catch {
Write-ToLog "WinGet is not installed" "Red"
$WinGetInstalledVersion = "0.0.0"
}
+
Write-ToLog "WinGet installed version: $WinGetInstalledVersion | WinGet available version: $WinGetAvailableVersion"
- #Check if the currently installed version is less than the available version
+
+ # Install WinGet if outdated
if ((Compare-SemVer -Version1 $WinGetInstalledVersion -Version2 $WinGetAvailableVersion) -lt 0) {
- #Install WinGet MSIXBundle in SYSTEM context
try {
- #Download WinGet MSIXBundle
+ # Download WinGet MSIXBundle
Write-ToLog "-> Downloading WinGet MSIXBundle for App Installer..."
$WinGetURL = "https://github.com/microsoft/winget-cli/releases/download/v$WinGetAvailableVersion/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
$WingetInstaller = "$env:TEMP\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
Invoke-WebRequest -Uri $WinGetURL -OutFile $WingetInstaller -UseBasicParsing
- #Install
+ # Install WinGet
Write-ToLog "-> Installing WinGet MSIXBundle for App Installer..."
Add-AppxProvisionedPackage -Online -PackagePath $WingetInstaller -SkipLicense | Out-Null
Write-ToLog "-> WinGet MSIXBundle (v$WinGetAvailableVersion) for App Installer installed successfully!" "green"
- #Reset WinGet Sources
+ # Reset WinGet sources after installation
$WingetInfo = (Get-Item "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller_*_8wekyb3d8bbwe\winget.exe").VersionInfo | Sort-Object -Property FileVersionRaw
- #If multiple versions, pick most recent one
$WingetCmd = $WingetInfo[-1].FileName
& $WingetCmd source reset --force
Write-ToLog "-> WinGet sources reset." "green"
}
catch {
Write-ToLog "-> Failed to install WinGet MSIXBundle for App Installer..." "red"
- #Force Store Apps to update
+ # Try to update via Microsoft Store as fallback
Update-StoreApps
}
- #Remove WinGet MSIXBundle
+ # Cleanup installer
Remove-Item -Path $WingetInstaller -Force -ErrorAction SilentlyContinue
}
else {
Write-ToLog "-> WinGet is up to date." "Green"
}
+
Write-ToLog "Prerequisites checked. OK" "Green"
}
catch {
-
- Write-ToLog "Prerequisites checked failed" "Red"
-
+ Write-ToLog "Prerequisites check failed" "Red"
}
-
}
diff --git a/Sources/Winget-AutoUpdate/functions/Invoke-LogRotation.ps1 b/Sources/Winget-AutoUpdate/functions/Invoke-LogRotation.ps1
index 65bb2e4c..14a00a5e 100644
--- a/Sources/Winget-AutoUpdate/functions/Invoke-LogRotation.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Invoke-LogRotation.ps1
@@ -1,47 +1,73 @@
-#Function to rotate the logs
+<#
+.SYNOPSIS
+ Manages log file rotation and archival.
+.DESCRIPTION
+ Rotates log files when they exceed the maximum size, archiving old
+ logs with timestamps. Maintains a configurable maximum number of
+ archived log files by deleting the oldest when necessary.
+
+.PARAMETER LogFile
+ Full path to the log file to manage.
+
+.PARAMETER MaxLogFiles
+ Maximum number of log files to keep (0 = unlimited, 1 = no rotation).
+
+.PARAMETER MaxLogSize
+ Maximum log file size in bytes before rotation occurs.
+
+.OUTPUTS
+ Boolean: True on success, False if an error occurred.
+
+.EXAMPLE
+ Invoke-LogRotation "C:\logs\updates.log" 3 1048576
+
+.NOTES
+ Archived logs are named with timestamp: filename_yyyyMMddHHmmss.ext
+#>
function Invoke-LogRotation {
[OutputType([Bool])]
param(
- [string]$LogFile,
- [Int32]$MaxLogFiles,
+ [string]$LogFile,
+ [Int32]$MaxLogFiles,
[Int64]$MaxLogSize
)
- # if MaxLogFiles is 1 just keep the original one and let it grow
+ # If MaxLogFiles is 1, keep original file without rotation (let it grow)
if (-not($MaxLogFiles -eq 1)) {
try {
- # get current size of log file
+ # Get current log file size
$currentSize = (Get-Item $LogFile).Length
- # get log name
+ # Parse log file path components
$logFileName = Split-Path $LogFile -Leaf
$logFilePath = Split-Path $LogFile
$logFileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($logFileName)
$logFileNameExtension = [System.IO.Path]::GetExtension($logFileName)
+ # Check if rotation is needed
if ($currentSize -ge $MaxLogSize) {
- # construct name of archived log file
+ # Create archived filename with timestamp
$newLogFileName = $logFileNameWithoutExtension + (Get-Date -Format 'yyyyMMddHHmmss').ToString() + $logFileNameExtension
- # rename old log file
+
+ # Rename current log to archived name
Rename-Item -Path $LogFile -NewName $newLogFileName -Force -Confirm:$false
- # create new file
+ # Create new empty log file
Write-ToLog "New log file created"
- # if MaxLogFiles is 0 don't delete any old archived log files
+ # Clean up old archives if MaxLogFiles > 0
if (-not($MaxLogFiles -eq 0)) {
- # set filter to search for archived log files
+ # Build filter pattern for archived log files
$archivedLogFileFilter = $logFileNameWithoutExtension + '??????????????' + $logFileNameExtension
- # get archived log files
+ # Find all archived log files
$oldLogFiles = Get-Item -Path "$(Join-Path -Path $logFilePath -ChildPath $archivedLogFileFilter)"
if ([bool]$oldLogFiles) {
- # compare found log files to MaxLogFiles parameter of the log object, and delete oldest until we are
- # back to the correct number
+ # Delete oldest files if count exceeds maximum
if (($oldLogFiles.Count + 1) -gt $MaxLogFiles) {
[int]$numTooMany = (($oldLogFiles.Count) + 1) - $MaxLogFiles
$oldLogFiles | Sort-Object 'LastWriteTime' | Select-Object -First $numTooMany | Remove-Item
@@ -49,15 +75,15 @@ function Invoke-LogRotation {
}
}
- #Log Header
+ # Log rotation event
Write-ToLog -LogMsg "CHECK FOR APP UPDATES (System context)" -IsHeader
Write-ToLog -LogMsg "Max Log Size reached: $MaxLogSize bytes - Rotated Logs"
}
- # end of try block
- Return $true;
+
+ Return $true
}
catch {
- Return $false;
+ Return $false
}
}
}
diff --git a/Sources/Winget-AutoUpdate/functions/Start-NotifTask.ps1 b/Sources/Winget-AutoUpdate/functions/Start-NotifTask.ps1
index 6e8312ca..d4ec75dc 100644
--- a/Sources/Winget-AutoUpdate/functions/Start-NotifTask.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Start-NotifTask.ps1
@@ -1,5 +1,50 @@
-#Function to send the notifications to user
+<#
+.SYNOPSIS
+ Displays a Windows toast notification to the user.
+.DESCRIPTION
+ Creates and displays a Windows toast notification with customizable
+ title, message, icon, and action buttons. Handles both system context
+ (via scheduled task) and user context (direct notification).
+
+.PARAMETER Title
+ The notification title. Defaults to "Winget-AutoUpdate".
+
+.PARAMETER Message
+ The main notification message text.
+
+.PARAMETER MessageType
+ The notification type (info, success, warning, error).
+ Determines the icon displayed.
+
+.PARAMETER Balise
+ Unique tag for the notification (for replacement logic).
+
+.PARAMETER OnClickAction
+ URL or action to execute when notification is clicked.
+
+.PARAMETER Body
+ Optional body text displayed below the message.
+
+.PARAMETER Button1Text
+ Text for the primary action button.
+
+.PARAMETER Button1Action
+ URL or action for the primary button.
+
+.PARAMETER ButtonDismiss
+ When specified, adds a dismiss button.
+
+.PARAMETER UserRun
+ When specified, forces notification display regardless of notification level.
+
+.EXAMPLE
+ Start-NotifTask -Title "Update Available" -Message "Firefox update ready" -MessageType "info"
+
+.NOTES
+ Respects WAU_NotificationLevel setting (Full, SuccessOnly, ErrorsOnly, None).
+ In system context, saves notification to XML and triggers scheduled task.
+#>
function Start-NotifTask {
param(
@@ -15,25 +60,26 @@ function Start-NotifTask {
[Switch]$UserRun = $false
)
+ # Check if notification should be displayed based on notification level settings
if (($WAUConfig.WAU_NotificationLevel -eq "Full") -or ($WAUConfig.WAU_NotificationLevel -eq "SuccessOnly" -and $MessageType -eq "Success") -or ($WAUConfig.WAU_NotificationLevel -eq "ErrorsOnly" -and $MessageType -eq "Error") -or ($UserRun)) {
- # XML Toast template creation
+ # Create base XML toast template
[xml]$ToastTemplate = New-Object system.Xml.XmlDocument
$ToastTemplate.LoadXml("")
- # Creation of visual node
+ # Create visual container node
$XMLvisual = $ToastTemplate.CreateElement("visual")
- # Creation of a binding node
+ # Create binding node with generic template
$XMLbinding = $ToastTemplate.CreateElement("binding")
$XMLvisual.AppendChild($XMLbinding) | Out-Null
$XMLbindingAtt1 = ($ToastTemplate.CreateAttribute("template"))
$XMLbindingAtt1.Value = "ToastGeneric"
$XMLbinding.Attributes.Append($XMLbindingAtt1) | Out-Null
+ # Add icon image if available
$XMLimagepath = "$WorkingDir\icons\$MessageType.png"
if (Test-Path $XMLimagepath) {
- # Creation of an image node
$XMLimage = $ToastTemplate.CreateElement("image")
$XMLbinding.AppendChild($XMLimage) | Out-Null
$XMLimageAtt1 = $ToastTemplate.CreateAttribute("placement")
@@ -44,32 +90,30 @@ function Start-NotifTask {
$XMLimage.Attributes.Append($XMLimageAtt2) | Out-Null
}
+ # Add title text if provided
if ($Title) {
- # Creation of a text node
$XMLtitle = $ToastTemplate.CreateElement("text")
$XMLtitleText = $ToastTemplate.CreateTextNode($Title)
$XMLtitle.AppendChild($XMLtitleText) | Out-Null
$XMLbinding.AppendChild($XMLtitle) | Out-Null
}
+ # Add message text if provided
if ($Message) {
- # Creation of a text node
$XMLtext = $ToastTemplate.CreateElement("text")
$XMLtextText = $ToastTemplate.CreateTextNode($Message)
$XMLtext.AppendChild($XMLtextText) | Out-Null
$XMLbinding.AppendChild($XMLtext) | Out-Null
}
+ # Add body text in a group/subgroup structure if provided
if ($Body) {
- # Creation of a group node
$XMLgroup = $ToastTemplate.CreateElement("group")
$XMLbinding.AppendChild($XMLgroup) | Out-Null
- # Creation of a subgroup node
$XMLsubgroup = $ToastTemplate.CreateElement("subgroup")
$XMLgroup.AppendChild($XMLsubgroup) | Out-Null
- # Creation of a text node
$XMLcontent = $ToastTemplate.CreateElement("text")
$XMLcontentText = $ToastTemplate.CreateTextNode($Body)
$XMLcontent.AppendChild($XMLcontentText) | Out-Null
@@ -82,11 +126,11 @@ function Start-NotifTask {
$XMLcontent.Attributes.Append($XMLcontentAtt2) | Out-Null
}
- # Creation of actions node
+ # Create actions container for buttons
$XMLactions = $ToastTemplate.CreateElement("actions")
+ # Add primary button if text is provided
if ($Button1Text) {
- # Creation of action node
$XMLaction = $ToastTemplate.CreateElement("action")
$XMLactions.AppendChild($XMLaction) | Out-Null
$XMLactionAtt1 = $ToastTemplate.CreateAttribute("content")
@@ -102,8 +146,8 @@ function Start-NotifTask {
}
}
+ # Add dismiss button if requested
if ($ButtonDismiss) {
- # Creation of action node
$XMLaction = $ToastTemplate.CreateElement("action")
$XMLactions.AppendChild($XMLaction) | Out-Null
$XMLactionAtt1 = $ToastTemplate.CreateAttribute("content")
@@ -117,54 +161,47 @@ function Start-NotifTask {
$XMLaction.Attributes.Append($XMLactionAtt3) | Out-Null
}
- # Creation of tag node
+ # Add tag for notification identification/replacement
$XMLtag = $ToastTemplate.CreateElement("tag")
$XMLtagText = $ToastTemplate.CreateTextNode($Balise)
$XMLtag.AppendChild($XMLtagText) | Out-Null
- # Add the visual node to the xml
+ # Assemble the XML structure
$ToastTemplate.LastChild.AppendChild($XMLvisual) | Out-Null
$ToastTemplate.LastChild.AppendChild($XMLactions) | Out-Null
$ToastTemplate.LastChild.AppendChild($XMLtag) | Out-Null
+ # Add click action if provided
if ($OnClickAction) {
$ToastTemplate.toast.SetAttribute("activationType", "Protocol") | Out-Null
$ToastTemplate.toast.SetAttribute("launch", $OnClickAction) | Out-Null
}
- #if running as System, run Winget-AutoUpdate-Notify scheduled task
+ # Display notification based on execution context
if ($IsSystem) {
-
- #Save XML to File
+ # System context: Save XML and trigger notification task
$ToastTemplateLocation = "$($WAUConfig.InstallLocation)\config\"
$ToastTemplate.Save("$ToastTemplateLocation\notif.xml")
- #Run Notify scheduled task to notify conneted users
+ # Run scheduled task to display notification to logged-in users
Get-ScheduledTask -TaskName "Winget-AutoUpdate-Notify" -ErrorAction SilentlyContinue | Start-ScheduledTask -ErrorAction SilentlyContinue
-
}
- #else, run as connected user
else {
-
- #Load Assemblies
+ # User context: Display notification directly
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
- #Prepare XML
$ToastXml = [Windows.Data.Xml.Dom.XmlDocument]::New()
$ToastXml.LoadXml($ToastTemplate.OuterXml)
- #Specify Launcher App ID
$LauncherID = "Windows.SystemToast.WAU.Notification"
- #Prepare and Create Toast
$ToastMessage = [Windows.UI.Notifications.ToastNotification]::New($ToastXml)
$ToastMessage.Tag = $ToastTemplate.toast.tag
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($LauncherID).Show($ToastMessage)
-
}
- #Wait for notification to display
+ # Wait for notification to display
Start-Sleep 3
}
diff --git a/Sources/Winget-AutoUpdate/functions/Test-ListPath.ps1 b/Sources/Winget-AutoUpdate/functions/Test-ListPath.ps1
index 88482317..ff7682e8 100644
--- a/Sources/Winget-AutoUpdate/functions/Test-ListPath.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Test-ListPath.ps1
@@ -1,94 +1,76 @@
-#Function to check Block/Allow List External Path
+<#
+.SYNOPSIS
+ Syncs app list from external source.
-function Test-ListPath ($ListPath, $UseWhiteList, $WingetUpdatePath) {
- # URL, UNC or Local Path
- if ($UseWhiteList) {
- $ListType = "included_apps.txt"
- }
- else {
- $ListType = "excluded_apps.txt"
- }
+.DESCRIPTION
+ Downloads included/excluded apps list from URL, UNC, or local path if newer.
- # Get local and external list paths
- $LocalList = -join ($WingetUpdatePath, "\", $ListType)
- $ExternalList = -join ($ListPath, "\", $ListType)
+.PARAMETER ListPath
+ External path (URL, UNC, or local).
- # Check if a list exists
- if (Test-Path "$LocalList") {
- $dateLocal = (Get-Item "$LocalList").LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
- }
+.PARAMETER UseWhiteList
+ True for included_apps.txt, false for excluded_apps.txt.
- # If path is URL
- if ($ListPath -like "http*") {
- $ExternalList = -join ($ListPath, "/", $ListType)
+.PARAMETER WingetUpdatePath
+ Local WAU installation directory.
- # Test if $ListPath contains the character "?" (testing for SAS token)
- if ($Listpath -match "\?") {
- # Split the URL into two strings at the "?" substring
- $splitPath = $ListPath.Split("`?")
+.OUTPUTS
+ Boolean: True if updated, False otherwise.
+#>
+function Test-ListPath ($ListPath, $UseWhiteList, $WingetUpdatePath) {
- # Assign the first string (up to "?") to the variable $resourceURI
- $resourceURI = $splitPath[0]
+ $ListType = if ($UseWhiteList) { "included_apps.txt" } else { "excluded_apps.txt" }
+ $LocalList = Join-Path $WingetUpdatePath $ListType
+ $dateLocal = $null
+ if (Test-Path $LocalList) {
+ $dateLocal = (Get-Item $LocalList).LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
+ }
- # Assign the second string (after "?" to the end) to the variable $sasToken
- $sasToken = $splitPath[1]
+ # URL path
+ if ($ListPath -like "http*") {
+ $ExternalList = "$ListPath/$ListType"
- # Join the parts and add "/$ListType?" in between the parts
- $ExternalList = -join ($resourceURI, "/$ListType`?", $sasToken)
+ # Handle SAS token URLs
+ if ($ListPath -match "\?") {
+ $parts = $ListPath.Split("?")
+ $ExternalList = "$($parts[0])/$ListType`?$($parts[1])"
}
- $wc = New-Object System.Net.WebClient
try {
- $wc.OpenRead("$ExternalList").Close() | Out-Null
+ $wc = New-Object System.Net.WebClient
+ $wc.OpenRead($ExternalList).Close() | Out-Null
$dateExternal = ([DateTime]$wc.ResponseHeaders['Last-Modified']).ToString("yyyy-MM-dd HH:mm:ss")
- if ($dateExternal -and $dateExternal -gt $dateLocal) {
- try {
- $wc.DownloadFile($ExternalList, $LocalList)
- }
- catch {
- $Script:ReachNoPath = $True
- return $False
- }
+
+ if (-not $dateLocal -or $dateExternal -gt $dateLocal) {
+ $wc.DownloadFile($ExternalList, $LocalList)
return $true
}
}
catch {
try {
- $wc.OpenRead("$ExternalList").Close() | Out-Null
$wc.DownloadFile($ExternalList, $LocalList)
- $Script:AlwaysDownloaded = $True
+ $Script:AlwaysDownloaded = $true
return $true
}
catch {
- $Script:ReachNoPath = $True
- return $False
+ $Script:ReachNoPath = $true
+ return $false
}
}
}
- # If path is UNC or local
+ # UNC or local path
else {
- if (Test-Path -Path $ExternalList) {
- try {
- $dateExternal = (Get-Item "$ExternalList").LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
- }
- catch {
- $Script:ReachNoPath = $True
- return $False
- }
- if ($dateExternal -gt $dateLocal) {
- try {
- Copy-Item $ExternalList -Destination $LocalList -Force
- }
- catch {
- $Script:ReachNoPath = $True
- return $False
- }
- return $True
+ $ExternalList = Join-Path $ListPath $ListType
+ if (Test-Path $ExternalList) {
+ $dateExternal = (Get-Item $ExternalList).LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
+ if (-not $dateLocal -or $dateExternal -gt $dateLocal) {
+ Copy-Item $ExternalList -Destination $LocalList -Force
+ return $true
}
}
else {
- $Script:ReachNoPath = $True
- return $False
+ $Script:ReachNoPath = $true
+ return $false
}
}
}
diff --git a/Sources/Winget-AutoUpdate/functions/Test-Mods.ps1 b/Sources/Winget-AutoUpdate/functions/Test-Mods.ps1
index a3c6938e..bd8c93a9 100644
--- a/Sources/Winget-AutoUpdate/functions/Test-Mods.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Test-Mods.ps1
@@ -1,47 +1,58 @@
-#Function to check if modification exists within 'mods' directory
+<#
+.SYNOPSIS
+ Checks for application modification scripts in the mods folder.
-function Test-Mods ($app) {
+.DESCRIPTION
+ Searches for app-specific scripts that customize the install/upgrade process.
+ Hooks: preinstall, override, custom, arguments, upgrade, install, installed, notinstalled.
+
+.PARAMETER app
+ The WinGet application ID to check.
- #Takes care of a null situation
- $ModsPreInstall = $null
- $ModsOverride = $null
- $ModsCustom = $null
- $ModsUpgrade = $null
- $ModsInstall = $null
- $ModsInstalled = $null
- $ModsNotInstalled = $null
+.OUTPUTS
+ Array: [PreInstall, Override, Custom, Arguments, Upgrade, Install, Installed, NotInstalled]
+#>
+function Test-Mods ($app) {
$Mods = "$WorkingDir\mods"
+ $result = @{
+ PreInstall = $null
+ Override = $null
+ Custom = $null
+ Arguments = $null
+ Upgrade = $null
+ Install = $null
+ Installed = $null
+ NotInstalled = $null
+ }
+ # Global fallback for failed installs
if (Test-Path "$Mods\_WAU-notinstalled.ps1") {
- $ModsNotInstalled = "$Mods\_WAU-notinstalled.ps1"
+ $result.NotInstalled = "$Mods\_WAU-notinstalled.ps1"
}
+ # App-specific mods
if (Test-Path "$Mods\$app-*") {
- if (Test-Path "$Mods\$app-preinstall.ps1") {
- $ModsPreInstall = "$Mods\$app-preinstall.ps1"
- }
- if (Test-Path "$Mods\$app-override.txt") {
- $ModsOverride = (Get-Content "$Mods\$app-override.txt" -Raw).Trim()
- }
- if (Test-Path "$Mods\$app-custom.txt") {
- $ModsCustom = (Get-Content "$Mods\$app-custom.txt" -Raw).Trim()
+ if (Test-Path "$Mods\$app-preinstall.ps1") { $result.PreInstall = "$Mods\$app-preinstall.ps1" }
+ if (Test-Path "$Mods\$app-override.txt") { $result.Override = (Get-Content "$Mods\$app-override.txt" -Raw).Trim() }
+ if (Test-Path "$Mods\$app-custom.txt") { $result.Custom = (Get-Content "$Mods\$app-custom.txt" -Raw).Trim() }
+ if (Test-Path "$Mods\$app-arguments.txt") {
+ # Read file and filter out comments and empty lines
+ $lines = Get-Content "$Mods\$app-arguments.txt" | Where-Object {
+ $_.Trim() -ne "" -and -not $_.TrimStart().StartsWith("#")
+ }
+ if ($lines) {
+ $result.Arguments = ($lines -join " ").Trim()
+ }
}
if (Test-Path "$Mods\$app-install.ps1") {
- $ModsInstall = "$Mods\$app-install.ps1"
- $ModsUpgrade = "$Mods\$app-install.ps1"
- }
- if (Test-Path "$Mods\$app-upgrade.ps1") {
- $ModsUpgrade = "$Mods\$app-upgrade.ps1"
- }
- if (Test-Path "$Mods\$app-installed.ps1") {
- $ModsInstalled = "$Mods\$app-installed.ps1"
- }
- if (Test-Path "$Mods\$app-notinstalled.ps1") {
- $ModsNotInstalled = "$Mods\$app-notinstalled.ps1"
+ $result.Install = "$Mods\$app-install.ps1"
+ $result.Upgrade = "$Mods\$app-install.ps1"
}
+ if (Test-Path "$Mods\$app-upgrade.ps1") { $result.Upgrade = "$Mods\$app-upgrade.ps1" }
+ if (Test-Path "$Mods\$app-installed.ps1") { $result.Installed = "$Mods\$app-installed.ps1" }
+ if (Test-Path "$Mods\$app-notinstalled.ps1") { $result.NotInstalled = "$Mods\$app-notinstalled.ps1" }
}
- return $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsUpgrade, $ModsInstall, $ModsInstalled, $ModsNotInstalled
-
+ return $result.PreInstall, $result.Override, $result.Custom, $result.Arguments, $result.Upgrade, $result.Install, $result.Installed, $result.NotInstalled
}
diff --git a/Sources/Winget-AutoUpdate/functions/Test-ModsPath.ps1 b/Sources/Winget-AutoUpdate/functions/Test-ModsPath.ps1
index f917757b..4ce7b39f 100644
--- a/Sources/Winget-AutoUpdate/functions/Test-ModsPath.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Test-ModsPath.ps1
@@ -1,21 +1,52 @@
-#Function to check mods External Path
+<#
+.SYNOPSIS
+ Syncs modification scripts from an external source.
+.DESCRIPTION
+ Compares local mods folder with an external source and syncs changes.
+ Supports three source types:
+ - HTTP/HTTPS URLs (requires directory listing)
+ - Azure Blob Storage (uses AzCopy)
+ - UNC/local paths
+
+ Downloads newer files and removes local files that don't exist externally.
+
+.PARAMETER ModsPath
+ The external mods path (URL, "AzureBlob", UNC, or local path).
+
+.PARAMETER WingetUpdatePath
+ The local WAU installation directory.
+
+.PARAMETER AzureBlobSASURL
+ Optional Azure Blob Storage URL with SAS token for AzureBlob mode.
+
+.OUTPUTS
+ Array: [ModsUpdated count, DeletedMods count]
+
+.EXAMPLE
+ $result = Test-ModsPath "https://myserver.com/mods" "C:\Program Files\Winget-AutoUpdate"
+
+.NOTES
+ Sets script-scoped $ReachNoPath to True if external path is unreachable.
+ Handles both .ps1/.txt mod files and .exe binaries in bins subfolder.
+#>
function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
- # URL, UNC or Local Path
- # Get local and external Mods paths
+
+ # Build local and external paths
$LocalMods = -join ($WingetUpdatePath, "\", "mods")
$ExternalMods = "$ModsPath"
- #Get File Names Locally
+ # Get list of local mod files and binaries
$InternalModsNames = Get-ChildItem -Path $LocalMods -Name -Recurse -Include *.ps1, *.txt
$InternalBinsNames = Get-ChildItem -Path $LocalMods"\bins" -Name -Recurse -Include *.exe
- # If path is URL
+ # === Handle HTTP/HTTPS URLs ===
if ($ExternalMods -like "http*") {
- # ADD TLS 1.2 and TLS 1.1 to list of currently used protocols
- [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; #DevSkim: ignore DS440020,DS440020 Hard-coded SSL/TLS Protocol
- [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11; #DevSkim: ignore DS440020,DS440020 Hard-coded SSL/TLS Protocol
- #Get Index of $ExternalMods (or index page with href listing of all the Mods)
+ # Enable TLS 1.2 and TLS 1.1 for secure connections
+ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 #DevSkim: ignore DS440020,DS440020 Hard-coded SSL/TLS Protocol
+ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls11 #DevSkim: ignore DS440020,DS440020 Hard-coded SSL/TLS Protocol
+
+ # Get directory listing from web server
try {
$WebResponse = Invoke-WebRequest -Uri $ExternalMods -UseBasicParsing
}
@@ -24,15 +55,16 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
return $False
}
- #Check for bins, download if newer. Delete if not external
+ # --- Handle bins subfolder (executables) ---
$ExternalBins = "$ModsPath/bins"
if ($WebResponse -match "bins/") {
$BinResponse = Invoke-WebRequest -Uri $ExternalBins -UseBasicParsing
- # Collect the external list of href links
$BinLinks = $BinResponse.Links | Select-Object -ExpandProperty HREF
- #If there's a directory path in the HREF:s, delete it (IIS)
+
+ # Clean directory paths from HREFs (IIS compatibility)
$CleanBinLinks = $BinLinks -replace "/.*/", ""
- #Modify strings to HREF:s
+
+ # Build HREF strings for comparison
$index = 0
foreach ($Bin in $CleanBinLinks) {
if ($Bin) {
@@ -40,21 +72,19 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
}
$index++
}
- #Delete Local Bins that don't exist Externally
- $index = 0
+
+ # Delete local bins that don't exist externally
$CleanLinks = $BinLinks -replace "/.*/", ""
foreach ($Bin in $InternalBinsNames) {
If ($CleanLinks -notcontains "$Bin") {
Remove-Item $LocalMods\bins\$Bin -Force -ErrorAction SilentlyContinue | Out-Null
}
- $index++
}
+
+ # Download newer external bins
$CleanBinLinks = $BinLinks -replace "/.*/", ""
- $Bin = ""
- #Loop through all links
$wc = New-Object System.Net.WebClient
$CleanBinLinks | ForEach-Object {
- #Check for .exe in listing/HREF:s in an index page pointing to .exe
if ($_ -like "*.exe") {
$dateExternalBin = ""
$dateLocalBin = ""
@@ -71,13 +101,11 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
}
}
- # Collect the external list of href links
+ # --- Handle mod files (.ps1, .txt) ---
$ModLinks = $WebResponse.Links | Select-Object -ExpandProperty HREF
-
- #If there's a directory path in the HREF:s, delete it (IIS)
$CleanLinks = $ModLinks -replace "/.*/", ""
- #Modify strings to HREF:s
+ # Build HREF strings for comparison
$index = 0
foreach ($Mod in $CleanLinks) {
if ($Mod) {
@@ -86,24 +114,20 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
$index++
}
- #Delete Local Mods that don't exist Externally
+ # Delete local mods that don't exist externally
$DeletedMods = 0
- $index = 0
$CleanLinks = $ModLinks -replace "/.*/", ""
foreach ($Mod in $InternalModsNames) {
If ($CleanLinks -notcontains "$Mod") {
Remove-Item $LocalMods\$Mod -Force -ErrorAction SilentlyContinue | Out-Null
$DeletedMods++
}
- $index++
}
+ # Download newer external mods
$CleanLinks = $ModLinks -replace "/.*/", ""
-
- #Loop through all links
$wc = New-Object System.Net.WebClient
$CleanLinks | ForEach-Object {
- #Check for .ps1/.txt in listing/HREF:s in an index page pointing to .ps1/.txt
if (($_ -like "*.ps1") -or ($_ -like "*.txt")) {
try {
$dateExternalMod = ""
@@ -135,25 +159,28 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
}
return $ModsUpdated, $DeletedMods
}
- # If Path is Azure Blob
+ # === Handle Azure Blob Storage ===
elseif ($ExternalMods -like "AzureBlob") {
Write-ToLog "Azure Blob Storage set as mod source"
Write-ToLog "Checking AZCopy"
Get-AZCopy $WingetUpdatePath
- #Safety check to make sure we really do have azcopy.exe and a Blob URL
+
+ # Verify AzCopy and SAS URL are available
if ((Test-Path -Path "$WingetUpdatePath\azcopy.exe" -PathType Leaf) -and ($null -ne $AzureBlobSASURL)) {
Write-ToLog "Syncing Blob storage with local storage"
+ # Run AzCopy sync with delete option
$AZCopySyncOutput = & $WingetUpdatePath\azcopy.exe sync "$AzureBlobSASURL" "$LocalMods" --from-to BlobLocal --delete-destination=true
$AZCopyOutputLines = $AZCopySyncOutput.Split([Environment]::NewLine)
- foreach ( $_ in $AZCopyOutputLines) {
+ # Parse AzCopy output for statistics
+ foreach ($line in $AZCopyOutputLines) {
$AZCopySyncAdditionsRegex = [regex]::new("(?<=Number of Copy Transfers Completed:\s+)\d+")
$AZCopySyncDeletionsRegex = [regex]::new("(?<=Number of Deletions at Destination:\s+)\d+")
$AZCopySyncErrorRegex = [regex]::new("^Cannot perform sync due to error:")
- $AZCopyAdditions = [int] $AZCopySyncAdditionsRegex.Match($_).Value
- $AZCopyDeletions = [int] $AZCopySyncDeletionsRegex.Match($_).Value
+ $AZCopyAdditions = [int] $AZCopySyncAdditionsRegex.Match($line).Value
+ $AZCopyDeletions = [int] $AZCopySyncDeletionsRegex.Match($line).Value
if ($AZCopyAdditions -ne 0) {
$ModsUpdated = $AZCopyAdditions
@@ -163,8 +190,8 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
$DeletedMods = $AZCopyDeletions
}
- if ($AZCopySyncErrorRegex.Match($_).Value) {
- Write-ToLog "AZCopy Sync Error! $_"
+ if ($AZCopySyncErrorRegex.Match($line).Value) {
+ Write-ToLog "AZCopy Sync Error! $line"
}
}
}
@@ -174,18 +201,21 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
return $ModsUpdated, $DeletedMods
}
- # If path is UNC or local
+ # === Handle UNC or local paths ===
else {
+ # --- Handle bins subfolder ---
$ExternalBins = "$ModsPath\bins"
if (Test-Path -Path $ExternalBins"\*.exe") {
$ExternalBinsNames = Get-ChildItem -Path $ExternalBins -Name -Recurse -Include *.exe
- #Delete Local Bins that don't exist Externally
+
+ # Delete local bins that don't exist externally
foreach ($Bin in $InternalBinsNames) {
If ($Bin -notin $ExternalBinsNames ) {
Remove-Item $LocalMods\bins\$Bin -Force -ErrorAction SilentlyContinue | Out-Null
}
}
- #Copy newer external bins
+
+ # Copy newer external bins
foreach ($Bin in $ExternalBinsNames) {
$dateExternalBin = ""
$dateLocalBin = ""
@@ -199,11 +229,11 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
}
}
+ # --- Handle mod files ---
if ((Test-Path -Path $ExternalMods"\*.ps1") -or (Test-Path -Path $ExternalMods"\*.txt")) {
- #Get File Names Externally
$ExternalModsNames = Get-ChildItem -Path $ExternalMods -Name -Recurse -Include *.ps1, *.txt
- #Delete Local Mods that don't exist Externally
+ # Delete local mods that don't exist externally
$DeletedMods = 0
foreach ($Mod in $InternalModsNames) {
If ($Mod -notin $ExternalModsNames ) {
@@ -212,7 +242,7 @@ function Test-ModsPath ($ModsPath, $WingetUpdatePath, $AzureBlobSASURL) {
}
}
- #Copy newer external mods
+ # Copy newer external mods
foreach ($Mod in $ExternalModsNames) {
$dateExternalMod = ""
$dateLocalMod = ""
diff --git a/Sources/Winget-AutoUpdate/functions/Test-Network.ps1 b/Sources/Winget-AutoUpdate/functions/Test-Network.ps1
index 1321b4cc..2a22ff3a 100644
--- a/Sources/Winget-AutoUpdate/functions/Test-Network.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Test-Network.ps1
@@ -1,18 +1,24 @@
-#Function to check the connectivity
+<#
+.SYNOPSIS
+ Tests internet connectivity and metered connection status.
-function Test-Network {
+.DESCRIPTION
+ Verifies network using NCSI probes with 30-minute timeout.
+ Respects WAU_DoNotRunOnMetered setting.
- # Init
- $timeout = 0
+.OUTPUTS
+ Boolean: True if connected and allowed to proceed.
+#>
+function Test-Network {
- #Test connectivity during 30 min then timeout
Write-ToLog "Checking internet connection..." "Yellow"
+ # Get NCSI settings
try {
$NlaRegKey = "HKLM:\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet"
- $ncsiHost = Get-ItemPropertyValue -Path $NlaRegKey -Name ActiveWebProbeHost
- $ncsiPath = Get-ItemPropertyValue -Path $NlaRegKey -Name ActiveWebProbePath
- $ncsiContent = Get-ItemPropertyValue -Path $NlaRegKey -Name ActiveWebProbeContent
+ $ncsiHost = Get-ItemPropertyValue $NlaRegKey -Name ActiveWebProbeHost
+ $ncsiPath = Get-ItemPropertyValue $NlaRegKey -Name ActiveWebProbePath
+ $ncsiContent = Get-ItemPropertyValue $NlaRegKey -Name ActiveWebProbeContent
}
catch {
$ncsiHost = "www.msftconnecttest.com"
@@ -20,71 +26,53 @@ function Test-Network {
$ncsiContent = "Microsoft Connect Test"
}
- while ($timeout -lt 1800) {
+ # Test connectivity (30 min timeout)
+ for ($timeout = 0; $timeout -lt 1800; $timeout += 10) {
try {
- $ncsiResponse = Invoke-WebRequest -Uri "http://$($ncsiHost)/$($ncsiPath)" -UseBasicParsing -UserAgent ([Microsoft.PowerShell.Commands.PSUserAgent]::Chrome); # DevSkim: ignore DS137138 Insecure URL
- }
- catch {
- $ncsiResponse = $false
+ $response = Invoke-WebRequest -Uri "http://${ncsiHost}/${ncsiPath}" -UseBasicParsing -UserAgent ([Microsoft.PowerShell.Commands.PSUserAgent]::Chrome) # DevSkim: ignore DS137138
}
+ catch { $response = $null }
- if (($ncsiResponse) -and ($ncsiResponse.StatusCode -eq 200) -and ($ncsiResponse.content -eq $ncsiContent)) {
- Write-ToLog "Connected !" "Green"
+ if ($response -and $response.StatusCode -eq 200 -and $response.Content -eq $ncsiContent) {
+ Write-ToLog "Connected!" "Green"
- # Check for metered connection
+ # Check metered connection
try {
[void][Windows.Networking.Connectivity.NetworkInformation, Windows, ContentType = WindowsRuntime]
$cost = [Windows.Networking.Connectivity.NetworkInformation]::GetInternetConnectionProfile().GetConnectionCost()
-
- $networkCostTypeName = [Windows.Networking.Connectivity.NetworkCostType]::GetName(
- [Windows.Networking.Connectivity.NetworkCostType],
- $cost.NetworkCostType
- )
+ $networkCostTypeName = [Windows.Networking.Connectivity.NetworkCostType]::GetName([Windows.Networking.Connectivity.NetworkCostType], $cost.NetworkCostType)
}
catch {
- Write-ToLog "Could not evaluate metered connection status - skipping check." "Gray"
+ Write-ToLog "Could not check metered status - continuing" "Gray"
return $true
}
- if ($cost.ApproachingDataLimit -or $cost.OverDataLimit -or $cost.Roaming -or $cost.BackgroundDataUsageRestricted -or ($networkCostTypeName -ne "Unrestricted")) {
- Write-ToLog "Metered connection detected." "Yellow"
+ $isMetered = $cost.ApproachingDataLimit -or $cost.OverDataLimit -or $cost.Roaming -or $cost.BackgroundDataUsageRestricted -or ($networkCostTypeName -ne "Unrestricted" -and $networkCostTypeName -ne "Unknown")
+ if ($isMetered) {
+ Write-ToLog "Metered connection detected." "Yellow"
if ($WAUConfig.WAU_DoNotRunOnMetered -eq 1) {
- Write-ToLog "WAU is configured to bypass update checking on metered connection"
+ Write-ToLog "WAU configured to skip on metered connection"
return $false
}
- else {
- Write-ToLog "WAU is configured to force update checking on metered connection"
- return $true
- }
- }
- else {
- return $true
+ Write-ToLog "WAU configured to continue on metered connection"
}
+ return $true
}
- else {
- Start-Sleep 10
- $timeout += 10
- if ($timeout -eq 300) {
- Write-ToLog "Notify 'No connection' sent." "Yellow"
+ Start-Sleep 10
- $Title = $NotifLocale.local.outputs.output[0].title
- $Message = $NotifLocale.local.outputs.output[0].message
- $MessageType = "warning"
- $Balise = "Connection"
- Start-NotifTask -Title $Title -Message $Message -MessageType $MessageType -Balise $Balise
- }
+ # Notify after 5 minutes
+ if ($timeout -eq 300) {
+ Write-ToLog "No connection notification sent." "Yellow"
+ Start-NotifTask -Title $NotifLocale.local.outputs.output[0].title `
+ -Message $NotifLocale.local.outputs.output[0].message -MessageType "warning" -Balise "Connection"
}
}
- Write-ToLog "Timeout. No internet connection !" "Red"
-
- $Title = $NotifLocale.local.outputs.output[1].title
- $Message = $NotifLocale.local.outputs.output[1].message
- $MessageType = "error"
- $Balise = "Connection"
- Start-NotifTask -Title $Title -Message $Message -MessageType $MessageType -Balise $Balise
-
+ # Timeout
+ Write-ToLog "Timeout - No internet connection!" "Red"
+ Start-NotifTask -Title $NotifLocale.local.outputs.output[1].title `
+ -Message $NotifLocale.local.outputs.output[1].message -MessageType "error" -Balise "Connection"
return $false
-}
\ No newline at end of file
+}
diff --git a/Sources/Winget-AutoUpdate/functions/Test-PendingReboot.ps1 b/Sources/Winget-AutoUpdate/functions/Test-PendingReboot.ps1
index 962d30bd..34909d82 100644
--- a/Sources/Winget-AutoUpdate/functions/Test-PendingReboot.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Test-PendingReboot.ps1
@@ -1,25 +1,35 @@
-#Function to check if there is a Pending Reboot
+<#
+.SYNOPSIS
+ Checks for pending Windows reboot.
+.OUTPUTS
+ Boolean: True if reboot pending.
+#>
function Test-PendingReboot {
$Computer = $env:COMPUTERNAME
- $PendingReboot = $false
+ $HKLM = [UInt32]"0x80000002"
+ $WMI = [WMIClass]"\\$Computer\root\default:StdRegProv"
- $HKLM = [UInt32] "0x80000002"
- $WMI_Reg = [WMIClass] "\\$Computer\root\default:StdRegProv"
+ if (-not $WMI) { return $false }
- if ($WMI_Reg) {
- if (($WMI_Reg.EnumKey($HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\")).sNames -contains 'RebootPending') { $PendingReboot = $true }
- if (($WMI_Reg.EnumKey($HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\")).sNames -contains 'RebootRequired') { $PendingReboot = $true }
-
- #Checking for SCCM namespace (can't get it done with Get-CimInstance, using deprecated Get-WmiObject)
- $SCCM_Namespace = Get-WmiObject -Namespace ROOT\CCM\ClientSDK -List -ComputerName $Computer -ErrorAction Ignore
- if ($SCCM_Namespace) {
- if (([WmiClass]"\\$Computer\ROOT\CCM\ClientSDK:CCM_ClientUtilities").DetermineIfRebootPending().RebootPending -eq $true) { $PendingReboot = $true }
- }
+ # Check CBS
+ if (($WMI.EnumKey($HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\")).sNames -contains 'RebootPending') {
+ return $true
+ }
+ # Check Windows Update
+ if (($WMI.EnumKey($HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\")).sNames -contains 'RebootRequired') {
+ return $true
}
- return $PendingReboot
+ # Check SCCM
+ $SCCM = Get-WmiObject -Namespace ROOT\CCM\ClientSDK -List -ComputerName $Computer -ErrorAction Ignore
+ if ($SCCM) {
+ if (([WmiClass]"\\$Computer\ROOT\CCM\ClientSDK:CCM_ClientUtilities").DetermineIfRebootPending().RebootPending) {
+ return $true
+ }
+ }
+ return $false
}
diff --git a/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1 b/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1
index aaf34691..59e0eade 100644
--- a/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1
@@ -1,3 +1,35 @@
+<#
+.SYNOPSIS
+ Executes and processes WAU mods script with action-based responses.
+
+.DESCRIPTION
+ Runs the _WAU-mods.ps1 script and handles its output to control WAU behavior.
+ Supports two modes:
+ - Legacy: Exit code 1 triggers WAU re-run
+ - Action-based: JSON output with instructions (Rerun, Abort, Postpone, Reboot, Continue)
+
+.PARAMETER WorkingDir
+ The WAU installation directory.
+
+.PARAMETER WAUConfig
+ The WAU configuration object.
+
+.PARAMETER GitHub_Repo
+ The GitHub repository name for WAU. Defaults to "Winget-AutoUpdate".
+
+.EXAMPLE
+ Test-WAUMods -WorkingDir "C:\Program Files\Winget-AutoUpdate" -WAUConfig $config
+
+.NOTES
+ Supported JSON actions:
+ - Rerun: Restart WAU immediately
+ - Abort: Stop WAU execution
+ - Postpone: Create scheduled task to run WAU later
+ - Reboot: Trigger system reboot (supports SCCM integration)
+ - Continue: Continue normal WAU execution
+
+ JSON format: { "Action": "Rerun", "Message": "text", "ExitCode": 0 }
+#>
function Test-WAUMods {
param (
[Parameter(Mandatory=$true)]
diff --git a/Sources/Winget-AutoUpdate/functions/Update-App.ps1 b/Sources/Winget-AutoUpdate/functions/Update-App.ps1
index d3e48580..c6850c79 100644
--- a/Sources/Winget-AutoUpdate/functions/Update-App.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Update-App.ps1
@@ -1,201 +1,115 @@
-#Function to update an App
+<#
+.SYNOPSIS
+ Updates a single application using WinGet.
+.DESCRIPTION
+ Performs the complete update process: notification, pre-install mods,
+ WinGet upgrade/install with retry logic, post-install mods, result notification.
+
+.PARAMETER app
+ PSCustomObject with Name, Id, Version, AvailableVersion properties.
+#>
Function Update-App ($app) {
- #Get App Info
- $ReleaseNoteURL = Get-AppInfo $app.Id
- if ($ReleaseNoteURL) {
- $Button1Text = $NotifLocale.local.outputs.output[10].message
+ # Helper function to build winget command parameters
+ function Get-WingetParams ($Command, $ModsOverride, $ModsCustom, $ModsArguments) {
+ $params = @($Command, "--id", $app.Id, "-e", "--accept-package-agreements", "--accept-source-agreements", "-s", "winget")
+ if ($Command -eq "install") { $params += "--force" }
+
+ if ($ModsOverride) {
+ return @{ Params = $params + @("--override", $ModsOverride); Log = "$Command (override): $ModsOverride" }
+ }
+ elseif ($ModsCustom) {
+ return @{ Params = $params + @("-h", "--custom", $ModsCustom); Log = "$Command (custom): $ModsCustom" }
+ }
+ elseif ($ModsArguments) {
+ # Parse arguments respecting quotes and spaces
+ $argArray = ConvertTo-WingetArgumentArray $ModsArguments
+ return @{ Params = $params + $argArray + @("-h"); Log = "$Command (arguments): $ModsArguments" }
+ }
+ return @{ Params = $params + "-h"; Log = $Command }
}
- #Send available update notification
+ # Get release notes for notification button
+ $ReleaseNoteURL = Get-AppInfo $app.Id
+ $Button1Text = if ($ReleaseNoteURL) { $NotifLocale.local.outputs.output[10].message } else { $null }
+
+ # Send "updating" notification
Write-ToLog "Updating $($app.Name) from $($app.Version) to $($app.AvailableVersion)..." "Cyan"
- $Title = $NotifLocale.local.outputs.output[2].title -f $($app.Name)
- $Message = $NotifLocale.local.outputs.output[2].message -f $($app.Version), $($app.AvailableVersion)
- $MessageType = "info"
- $Balise = $($app.Name)
- Start-NotifTask -Title $Title -Message $Message -MessageType $MessageType -Balise $Balise -Button1Action $ReleaseNoteURL -Button1Text $Button1Text
+ Start-NotifTask -Title ($NotifLocale.local.outputs.output[2].title -f $app.Name) `
+ -Message ($NotifLocale.local.outputs.output[2].message -f $app.Version, $app.AvailableVersion) `
+ -MessageType "info" -Balise $app.Name -Button1Action $ReleaseNoteURL -Button1Text $Button1Text
- #Check if mods exist for preinstall/override/upgrade/install/installed/notinstalled
- $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsUpgrade, $ModsInstall, $ModsInstalled, $ModsNotInstalled = Test-Mods $($app.Id)
+ # Load mods
+ $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsArguments, $ModsUpgrade, $ModsInstall, $ModsInstalled, $ModsNotInstalled = Test-Mods $app.Id
- #Winget upgrade
- Write-ToLog "########## WINGET UPGRADE PROCESS STARTS FOR APPLICATION ID '$($App.Id)' ##########" "Gray"
+ Write-ToLog "########## WINGET UPGRADE: $($app.Id) ##########" "Gray"
- #If PreInstall script exist
+ # Pre-install mod
if ($ModsPreInstall) {
- Write-ToLog "Modifications for $($app.Id) before upgrade are being applied..." "DarkYellow"
- $preInstallResult = & "$ModsPreInstall"
- if ($preInstallResult -eq $false) {
- Write-ToLog "PreInstall script for $($app.Id) requested to skip this update" "Yellow"
- continue # Skip to next app in the parent loop
+ Write-ToLog "Running pre-install mod for $($app.Id)..." "DarkYellow"
+ if ((& $ModsPreInstall) -eq $false) {
+ Write-ToLog "Pre-install requested skip" "Yellow"
+ return
}
}
- # Define upgrade base parameters
- $baseParams = @(
- "upgrade",
- "--id", "$($app.Id)",
- "-e",
- "--accept-package-agreements",
- "--accept-source-agreements",
- "-s", "winget"
- )
-
- # Define base log message
- $baseLogMessage = "Winget upgrade --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget"
-
- # Determine which parameters and log message to use
- if ($ModsOverride) {
- $allParams = $baseParams + @("--override", "$ModsOverride")
- $logPrefix = "Running (overriding default):"
- $logSuffix = "--override $ModsOverride"
- }
- elseif ($ModsCustom) {
- $allParams = $baseParams + @("-h", "--custom", "$ModsCustom")
- $logPrefix = "Running (customizing default):"
- $logSuffix = "-h --custom $ModsCustom"
- }
- else {
- $allParams = $baseParams + @("-h")
- $logPrefix = "Running:"
- $logSuffix = "-h"
- }
-
- # Build the log message
- $logMessage = "$logPrefix $baseLogMessage $logSuffix"
-
- # Log the command
- Write-ToLog "-> $logMessage"
-
- # Execute command and log results
- & $Winget $allParams | Where-Object { $_ -notlike " *" } |
- Tee-Object -file $LogFile -Append
+ # Try upgrade first
+ $cmd = Get-WingetParams "upgrade" $ModsOverride $ModsCustom $ModsArguments
+ Write-ToLog "-> $($cmd.Log)"
+ & $Winget $cmd.Params | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append
if ($ModsUpgrade) {
- Write-ToLog "Modifications for $($app.Id) during upgrade are being applied..." "DarkYellow"
- & "$ModsUpgrade"
+ Write-ToLog "Running upgrade mod..." "DarkYellow"
+ & $ModsUpgrade
}
- #Check if application updated properly
- $ConfirmInstall = Confirm-Installation $($app.Id) $($app.AvailableVersion)
-
- if ($ConfirmInstall -ne $true) {
- #Upgrade failed!
- #Test for a Pending Reboot (Component Based Servicing/WindowsUpdate/CCM_ClientUtilities)
- $PendingReboot = Test-PendingReboot
- if ($PendingReboot -eq $true) {
- Write-ToLog "-> A Pending Reboot lingers and probably prohibited $($app.Name) from upgrading...`n-> ...limiting to 1 install attempt instead of 2" "Yellow"
- $retry = 2
- }
- else {
- #If app failed to upgrade, run Install command (2 tries max - some apps get uninstalled after single "Install" command.)
- $retry = 1
- }
-
- While (($ConfirmInstall -eq $false) -and ($retry -le 2)) {
-
- Write-ToLog "-> An upgrade for $($app.Name) failed, now trying an install instead... ($retry/2)" "DarkYellow"
-
- # Define install base parameters
- $baseParams = @(
- "install",
- "--id", "$($app.Id)",
- "-e",
- "--accept-package-agreements",
- "--accept-source-agreements",
- "-s", "winget",
- "--force"
- )
-
- # Define base log message
- $baseLogMessage = "Winget install --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget --force"
-
- # Determine which parameters and log message to use
- if ($ModsOverride) {
- $allParams = $baseParams + @("--override", "$ModsOverride")
- $logPrefix = "Running (overriding default):"
- $logSuffix = "--override $ModsOverride"
- }
- elseif ($ModsCustom) {
- $allParams = $baseParams + @("-h", "--custom", "$ModsCustom")
- $logPrefix = "Running (customizing default):"
- $logSuffix = "-h --custom $ModsCustom"
- }
- else {
- $allParams = $baseParams + @("-h")
- $logPrefix = "Running:"
- $logSuffix = "-h"
- }
+ $ConfirmInstall = Confirm-Installation $app.Id $app.AvailableVersion
- # Build the log message
- $logMessage = "$logPrefix $baseLogMessage $logSuffix"
+ # Fallback to install if upgrade failed
+ if (-not $ConfirmInstall) {
+ $maxRetry = if (Test-PendingReboot) { Write-ToLog "-> Pending reboot detected, limiting retries" "Yellow"; 1 } else { 2 }
- # Log the command
- Write-ToLog "-> $logMessage"
+ for ($retry = 1; $retry -le $maxRetry -and -not $ConfirmInstall; $retry++) {
+ Write-ToLog "-> Upgrade failed, trying install ($retry/$maxRetry)..." "DarkYellow"
- # Execute command and log results
- & $Winget $allParams | Where-Object { $_ -notlike " *" } |
- Tee-Object -file $LogFile -Append
+ $cmd = Get-WingetParams "install" $ModsOverride $ModsCustom $ModsArguments
+ Write-ToLog "-> $($cmd.Log)"
+ & $Winget $cmd.Params | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append
if ($ModsInstall) {
- Write-ToLog "Modifications for $($app.Id) during install are being applied..." "DarkYellow"
- & "$ModsInstall"
+ Write-ToLog "Running install mod..." "DarkYellow"
+ & $ModsInstall
}
- #Check if application installed properly
- $ConfirmInstall = Confirm-Installation $($app.Id) $($app.AvailableVersion)
- $retry += 1
+ $ConfirmInstall = Confirm-Installation $app.Id $app.AvailableVersion
}
}
- switch ($ConfirmInstall) {
- # Upgrade/install was successful
- $true {
- if ($ModsInstalled) {
- Write-ToLog "Modifications for $($app.Id) after upgrade/install are being applied..." "DarkYellow"
- & "$ModsInstalled"
- }
- }
- # Upgrade/install was unsuccessful
- $false {
- if ($ModsNotInstalled) {
- Write-ToLog "Modifications for $($app.Id) after a failed upgrade/install are being applied..." "DarkYellow"
- & "$ModsNotInstalled"
- }
- }
+ # Post-install mods
+ if ($ConfirmInstall -and $ModsInstalled) {
+ Write-ToLog "Running post-install mod..." "DarkYellow"
+ & $ModsInstalled
+ }
+ elseif (-not $ConfirmInstall -and $ModsNotInstalled) {
+ Write-ToLog "Running failure mod..." "DarkYellow"
+ & $ModsNotInstalled
}
- Write-ToLog "########## WINGET UPGRADE PROCESS FINISHED FOR APPLICATION ID '$($App.Id)' ##########" "Gray"
-
- #Notify installation
- if ($ConfirmInstall -eq $true) {
-
- #Send success updated app notification
- Write-ToLog "$($app.Name) updated to $($app.AvailableVersion) !" "Green"
-
- #Send Notif
- $Title = $NotifLocale.local.outputs.output[3].title -f $($app.Name)
- $Message = $NotifLocale.local.outputs.output[3].message -f $($app.AvailableVersion)
- $MessageType = "success"
- $Balise = $($app.Name)
- Start-NotifTask -Title $Title -Message $Message -MessageType $MessageType -Balise $Balise -Button1Action $ReleaseNoteURL -Button1Text $Button1Text
+ Write-ToLog "########## FINISHED: $($app.Id) ##########" "Gray"
+ # Result notification
+ if ($ConfirmInstall) {
+ Write-ToLog "$($app.Name) updated to $($app.AvailableVersion)!" "Green"
+ Start-NotifTask -Title ($NotifLocale.local.outputs.output[3].title -f $app.Name) `
+ -Message ($NotifLocale.local.outputs.output[3].message -f $app.AvailableVersion) `
+ -MessageType "success" -Balise $app.Name -Button1Action $ReleaseNoteURL -Button1Text $Button1Text
$Script:InstallOK += 1
-
}
else {
-
- #Send failed updated app notification
Write-ToLog "$($app.Name) update failed." "Red"
-
- #Send Notif
- $Title = $NotifLocale.local.outputs.output[4].title -f $($app.Name)
- $Message = $NotifLocale.local.outputs.output[4].message
- $MessageType = "error"
- $Balise = $($app.Name)
- Start-NotifTask -Title $Title -Message $Message -MessageType $MessageType -Balise $Balise -Button1Action $ReleaseNoteURL -Button1Text $Button1Text
-
+ Start-NotifTask -Title ($NotifLocale.local.outputs.output[4].title -f $app.Name) `
+ -Message $NotifLocale.local.outputs.output[4].message `
+ -MessageType "error" -Balise $app.Name -Button1Action $ReleaseNoteURL -Button1Text $Button1Text
}
-
}
diff --git a/Sources/Winget-AutoUpdate/functions/Update-StoreApps.ps1 b/Sources/Winget-AutoUpdate/functions/Update-StoreApps.ps1
index 1eae91ba..61b7f8a5 100644
--- a/Sources/Winget-AutoUpdate/functions/Update-StoreApps.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Update-StoreApps.ps1
@@ -1,36 +1,56 @@
-#Function to force an upgrade of Store Apps
+<#
+.SYNOPSIS
+ Triggers an update scan for Microsoft Store applications.
+.DESCRIPTION
+ Uses MDM (Mobile Device Management) API to force a scan for
+ Microsoft Store app updates. This is useful as a fallback when
+ WinGet MSIXBundle installation fails.
+
+.OUTPUTS
+ Boolean: True on success, False on failure.
+
+.EXAMPLE
+ Update-StoreApps
+
+.NOTES
+ Not available on Windows Server or Windows Sandbox (WSB).
+ Uses MDM_EnterpriseModernAppManagement WMI class.
+#>
Function Update-StoreApps {
- $info_string = "-> Forcing an upgrade of Store Apps..."
- $action_string = "-> ...this can take a minute!"
- $fail_string = "-> ...something went wrong!"
- $irrelevant_string = "-> ...WAU is running on WSB (Windows Sandbox) or Windows Server - Microsoft Store is not available!"
-
- Write-ToLog $info_string "yellow"
-
- #If not WSB or Server, upgrade Microsoft Store Apps!
- if (!(Test-Path "${env:SystemDrive}\Users\WDAGUtilityAccount") -and (Get-CimInstance Win32_OperatingSystem).Caption -notmatch "Windows Server") {
-
- try {
- $namespaceName = "root\cimv2\mdm\dmmap"
- $className = "MDM_EnterpriseModernAppManagement_AppManagement01"
- $methodName = 'UpdateScanMethod'
- $cimObj = Get-CimInstance -Namespace $namespaceName -ClassName $className -Shallow
- Write-ToLog $action_string "green"
- $result = $cimObj | Invoke-CimMethod -MethodName $methodName
- if ($result.ReturnValue -ne 0) {
- Write-ToLog $fail_string "red"
- return $false
- }
- return $true
- }
- catch {
- Write-ToLog $fail_string "red"
- return $false
- }
- }
- else {
- Write-ToLog $irrelevant_string "yellow"
- }
+ $info_string = "-> Forcing an upgrade of Store Apps..."
+ $action_string = "-> ...this can take a minute!"
+ $fail_string = "-> ...something went wrong!"
+ $irrelevant_string = "-> ...WAU is running on WSB (Windows Sandbox) or Windows Server - Microsoft Store is not available!"
+
+ Write-ToLog $info_string "yellow"
+
+ # Check if running on Windows Sandbox or Windows Server (Store not available)
+ if (!(Test-Path "${env:SystemDrive}\Users\WDAGUtilityAccount") -and (Get-CimInstance Win32_OperatingSystem).Caption -notmatch "Windows Server") {
+
+ try {
+ # Use MDM WMI class to trigger store app update scan
+ $namespaceName = "root\cimv2\mdm\dmmap"
+ $className = "MDM_EnterpriseModernAppManagement_AppManagement01"
+ $methodName = 'UpdateScanMethod'
+ $cimObj = Get-CimInstance -Namespace $namespaceName -ClassName $className -Shallow
+
+ Write-ToLog $action_string "green"
+ $result = $cimObj | Invoke-CimMethod -MethodName $methodName
+
+ if ($result.ReturnValue -ne 0) {
+ Write-ToLog $fail_string "red"
+ return $false
+ }
+ return $true
+ }
+ catch {
+ Write-ToLog $fail_string "red"
+ return $false
+ }
+ }
+ else {
+ Write-ToLog $irrelevant_string "yellow"
+ }
}
diff --git a/Sources/Winget-AutoUpdate/functions/Update-WAU.ps1 b/Sources/Winget-AutoUpdate/functions/Update-WAU.ps1
index 030e6096..27ab9465 100644
--- a/Sources/Winget-AutoUpdate/functions/Update-WAU.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Update-WAU.ps1
@@ -1,52 +1,67 @@
+<#
+.SYNOPSIS
+ Downloads the latest WAU release and performs self-update.
+
+.DESCRIPTION
+ Downloads the WAU MSI package from GitHub and installs it to update
+ WAU to the latest version. Sends notifications before and after
+ the update process.
+
+.EXAMPLE
+ Update-WAU
+
+.NOTES
+ Exits the script after update to allow the new version to run.
+ Uses MSI installer with silent installation parameters.
+#>
function Update-WAU {
+ # Setup notification action and button
$OnClickAction = "https://github.com/Romanitho/$($GitHub_Repo)/releases"
$Button1Text = $NotifLocale.local.outputs.output[10].message
- #Send available update notification
+ # Send "update available" notification
$Title = $NotifLocale.local.outputs.output[2].title -f "Winget-AutoUpdate"
$Message = $NotifLocale.local.outputs.output[2].message -f $WAUCurrentVersion, $WAUAvailableVersion
$MessageType = "info"
Start-NotifTask -Title $Title -Message $Message -MessageType $MessageType -Button1Action $OnClickAction -Button1Text $Button1Text
- #Run WAU update
+ # Download and install update
try {
Write-ToLog "Downloading the GitHub Repository version $WAUAvailableVersion" "Cyan"
- #Create an unpredictable temp folder for security reasons
+ # Create temporary folder with timestamp for security
$MsiFolder = "$env:temp\WAU_$(Get-Date -Format yyyyMMddHHmmss)"
New-Item -ItemType Directory -Path $MsiFolder
- #Download the msi
+ # Download the MSI package
$MsiFile = Join-Path $MsiFolder "WAU.msi"
Invoke-RestMethod -Uri "https://github.com/Romanitho/Winget-AutoUpdate/releases/download/v$($WAUAvailableVersion)/WAU.msi" -OutFile $MsiFile
- #Update WAU
+ # Install the update
Write-ToLog "Updating WAU..." "Yellow"
Start-Process msiexec.exe -ArgumentList "/i $MsiFile /qn /L*v ""$WorkingDir\logs\WAU-Installer.log"" RUN_WAU=YES INSTALLDIR=""$WorkingDir""" -Wait
- #Send success Notif
+ # Send success notification
Write-ToLog "WAU Update completed. Rerunning WAU..." "Green"
$Title = $NotifLocale.local.outputs.output[3].title -f "Winget-AutoUpdate"
$Message = $NotifLocale.local.outputs.output[3].message -f $WAUAvailableVersion
$MessageType = "success"
Start-NotifTask -Title $Title -Message $Message -MessageType $MessageType -Button1Action $OnClickAction -Button1Text $Button1Text
- #Remove temp folder and content
+ # Cleanup temporary files
Remove-Item $MsiFolder -Recurse -Force
exit 0
}
catch {
-
- #Send Error Notif
+ # Send error notification
$Title = $NotifLocale.local.outputs.output[4].title -f "Winget-AutoUpdate"
$Message = $NotifLocale.local.outputs.output[4].message
$MessageType = "error"
Start-NotifTask -Title $Title -Message $Message -MessageType $MessageType -Button1Action $OnClickAction -Button1Text $Button1Text
Write-ToLog "WAU Update failed" "Red"
-
}
}
\ No newline at end of file
diff --git a/Sources/Winget-AutoUpdate/functions/Write-ToLog.ps1 b/Sources/Winget-AutoUpdate/functions/Write-ToLog.ps1
index f84316df..66e6c711 100644
--- a/Sources/Winget-AutoUpdate/functions/Write-ToLog.ps1
+++ b/Sources/Winget-AutoUpdate/functions/Write-ToLog.ps1
@@ -1,41 +1,45 @@
-#Write to Log Function
+<#
+.SYNOPSIS
+ Writes a timestamped message to console and log file.
-function Write-ToLog {
+.PARAMETER LogMsg
+ Message to log.
+
+.PARAMETER LogColor
+ Console color (default: White).
+.PARAMETER IsHeader
+ Format as section header.
+#>
+function Write-ToLog {
[CmdletBinding()]
param(
- [Parameter()] [String] $LogMsg,
- [Parameter()] [String] $LogColor = "White",
- [Parameter()] [Switch] $IsHeader = $false
+ [String]$LogMsg,
+ [String]$LogColor = "White",
+ [Switch]$IsHeader
)
- #Create file if doesn't exist
+ # Create log file with proper ACL if needed
if (!(Test-Path $LogFile)) {
New-Item -ItemType File -Path $LogFile -Force | Out-Null
-
- #Set ACL for users on logfile
- $NewAcl = Get-Acl -Path $LogFile
- $identity = New-Object System.Security.Principal.SecurityIdentifier S-1-5-11
- $fileSystemRights = "Modify"
- $type = "Allow"
- $fileSystemAccessRuleArgumentList = $identity, $fileSystemRights, $type
- $fileSystemAccessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $fileSystemAccessRuleArgumentList
- $NewAcl.SetAccessRule($fileSystemAccessRule)
- Set-Acl -Path $LogFile -AclObject $NewAcl
+ $acl = Get-Acl $LogFile
+ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
+ (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-11")),
+ "Modify", "Allow"
+ )
+ $acl.SetAccessRule($rule)
+ Set-Acl $LogFile $acl
}
- #If header requested
- if ($IsHeader) {
- $Log = "#" * 65 + "`n# $(Get-Date -Format (Get-culture).DateTimeFormat.ShortDatePattern) - $LogMsg`n" + "#" * 65
+ # Format log entry
+ $Log = if ($IsHeader) {
+ $date = Get-Date -Format (Get-culture).DateTimeFormat.ShortDatePattern
+ "#" * 65 + "`n# $date - $LogMsg`n" + "#" * 65
}
else {
- $Log = "$(Get-Date -UFormat "%T") - $LogMsg"
+ "$(Get-Date -UFormat '%T') - $LogMsg"
}
- #Echo log
- $Log | Write-host -ForegroundColor $LogColor
-
- #Write log to file
+ Write-Host $Log -ForegroundColor $LogColor
$Log | Out-File -FilePath $LogFile -Append
-
}
diff --git a/Sources/Winget-AutoUpdate/locale/de.xml b/Sources/Winget-AutoUpdate/locale/de.xml
index 4b5a12c5..0bbd1a9b 100644
--- a/Sources/Winget-AutoUpdate/locale/de.xml
+++ b/Sources/Winget-AutoUpdate/locale/de.xml
@@ -2,13 +2,13 @@
@@ -28,7 +28,7 @@
{0} konnte nicht aktualisiert werden!
- Bitte wenden sie sich an den Support.
+ Bitte wenden Sie sich an den Support.