diff --git a/.github/workflows/GA_Mega-linter.yml b/.github/workflows/GA_Mega-linter.yml index 42fd86c4a..79f4d1ef9 100644 --- a/.github/workflows/GA_Mega-linter.yml +++ b/.github/workflows/GA_Mega-linter.yml @@ -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@5a91fb06c83d0e69fbd23756d47438aa723b4a5a # v8.7.0 + uses: oxsecurity/megalinter@e08c2b05e3dbc40af4c23f41172ef1e068a7d651 # v8.8.0 env: # All available variables are described in documentation # https://megalinter.github.io/configuration/ @@ -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@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0 + uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1 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_Make-Release-and-Sync-to-Dev.yml b/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml index c16357da8..200dca977 100644 --- a/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml +++ b/.github/workflows/GitFlow_Make-Release-and-Sync-to-Dev.yml @@ -24,14 +24,15 @@ jobs: # Run only when PR is merged (not just closed) and source branch is release/* or hotfix/* if: github.event.pull_request.merged == true && (startsWith(github.event.pull_request.head.ref, 'release/') || startsWith(github.event.pull_request.head.ref, 'hotfix/')) runs-on: windows-latest + outputs: + next_semver: ${{ steps.release_version.outputs.NextSemVer }} steps: # Step 1: Checkout the code from the main branch after merge - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4 with: lfs: "true" fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} # Step 2: Extract version from branch name and calculate build number - name: Get final Release Version @@ -112,7 +113,7 @@ jobs: # Step 4: Create stable GitHub release with all artifacts - name: Create release - uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0 + uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1.18.0 with: tag: v${{ steps.release_version.outputs.NextSemVer }} prerelease: false # This is a stable release @@ -134,10 +135,11 @@ jobs: needs: build steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4 with: fetch-depth: 0 - + token: ${{ secrets.GH_PAT_SYNC }} + # Step 5: Configure Git for merge back to develop - name: Configure Git shell: bash @@ -152,9 +154,8 @@ jobs: # Checkout develop branch git checkout develop git pull origin develop - # Merge main into develop with no fast-forward to preserve history - git merge --no-ff origin/main -m "Merge main into develop after the creation of release v${{ steps.release_version.outputs.NextSemVer }}" + git merge --no-ff origin/main -m "Merge main into develop after the creation of release v${{ needs.build.outputs.next_semver }}" # Push changes to develop branch git push origin develop diff --git a/README.md b/README.md index 1ee1451bf..5a644ba53 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ List and Mods folder content will be copied to WAU install location: ### Notification Level -You can choose which notification will be displayed: `Full`, `Success only` or `None`. +You can choose which notification will be displayed: `Full`, `Success only`, `Errors only` or `None`. ### Notification language You can easily translate toast notifications by creating your locale xml config file (and share it with us :) ). @@ -147,7 +147,7 @@ Set `DESKTOPSHORTCUT=1` to create a shortcut for user interaction on the Desktop Set `STARTMENUSHORTCUT=1` to create shortcuts for user interaction in the Start Menu to run task `Winget-AutoUpdate` and open Logs. ### NOTIFICATIONLEVEL -Specify the Notification level: Full (Default, displays all notification), SuccessOnly (Only displays notification for success) or None (Does not show any popup). +Specify the Notification level: Full (Default, displays all notification), SuccessOnly (Only displays notification for success), ErrorsOnly (Only displays notification for errors) or None (Does not show any popup). ### UPDATESATLOGON Default value 1. Set `UPDATESATLOGON=0` to disable WAU from running at user logon. @@ -158,6 +158,11 @@ Default value Never. Specify the update frequency: Daily, BiDaily, Weekly, BiWee ### UPDATESATTIME Default value 6AM (06:00:00). Specify the time of the update interval execution time. Example `UPDATESATTIME="11:00:00"` +### UPDATESATTIMEDELAY +Default value is none (00:00). This setting specifies the delay for the scheduled task. +A scheduled task random delay adds a random amount of wait time (up to the specified maximum) before the task starts. +This helps prevent many devices from running the task at the exact same time. This is not applicable to "on logon" triggers. + ### DONOTRUNONMETERED Default value 1. Set `DONOTRUNONMETERED=0` to force WAU to run on metered connections. May add cellular data costs on shared connexion from smartphone for example. @@ -175,10 +180,25 @@ Default is 1048576 = 1 MB (ca. 7500 lines) Specify Winget-AutoUpdate installation location. Default: `C:\Program Files\Winget-AutoUpdate` (Recommended to leave default). ### Deploy with Intune -You can use [Winget-Install](https://github.com/Romanitho/Winget-AutoUpdate/blob/main/Sources/Winget-AutoUpdate/Winget-Install.ps1) to deploy the package for example in Intune: +You can use [Winget-Install](https://github.com/Romanitho/Winget-AutoUpdate/blob/main/Sources/Winget-AutoUpdate/Winget-Install.ps1) to deploy the package (this example with an override of parameters): +```batch +"%systemroot%\sysnative\WindowsPowerShell\v1.0\powershell.exe" -noprofile -executionpolicy bypass -file "C:\Program Files\Winget-AutoUpdate\Winget-Install.ps1" -AppIDs "Adobe.Acrobat.Reader.64-bit --scope machine --override \"-sfx_nu /sAll /rs /msi EULA_ACCEPT=YES DISABLEDESKTOPSHORTCUT=1"" +``` +### Deploy with SCCM +You can also use [Winget-Install](https://github.com/Romanitho/Winget-AutoUpdate/blob/main/Sources/Winget-AutoUpdate/Winget-Install.ps1) to deploy the same package in **SCCM**: ```batch -"%systemroot%\sysnative\WindowsPowerShell\v1.0\powershell.exe" -noprofile -executionpolicy bypass -file "C:\Program Files\Winget-AutoUpdate\Winget-Install.ps1" -AppIDs "Romanitho.Winget-AutoUpdate --scope machine --override \"/qn RUN_WAU=YES USERCONTEXT=1 STARTMENUSHORTCUT=1 NOTIFICATIONLEVEL=SuccessOnly UPDATESINTERVAL=Daily"" +powershell.exe -noprofile -executionpolicy bypass -file "C:\Program Files\Winget-AutoUpdate\Winget-Install.ps1" -AppIDs "Adobe.Acrobat.Reader.64-bit --scope machine --override \"-sfx_nu /sAll /rs /msi EULA_ACCEPT=YES DISABLEDESKTOPSHORTCUT=1"" ``` +Instead of including the override parameters in the install string you can use a **Mod** (**mods\Adobe.Acrobat.Reader.64-bit-override.txt**) with the content: +```batch +"-sfx_nu /sAll /rs /msi EULA_ACCEPT=YES DISABLEDESKTOPSHORTCUT=1" +``` +* A standard single installation: **-AppIDs Notepad++.Notepad++** +* Multiple installations: **-AppIDs "7zip.7zip, Notepad++.Notepad++"** + +As a custom detection script you can download/edit [winget-detect.ps1](Sources/Tools/Detection/winget-detect.ps1) (change app to detect [**Application ID**]) in **Intune**/**SCCM** + +A nice feature is if you're already using the deprecated standalone script **winget-install.ps1** from the [old repo](https://github.com/Romanitho/Winget-Install) and have placed it somwhere locally on all clients you can make a **SymLink** in its place and keep using the old path (avoiding a lot of work) in your deployed applications (**Winget-Install.ps1** takes care of the SymLink logic). ## GPO / Intune Management Read more in the [Policies section](https://github.com/Romanitho/Winget-AutoUpdate/tree/main/Sources/Policies). @@ -189,8 +209,14 @@ Read more in the [Policies section](https://github.com/Romanitho/Winget-AutoUpda This script executes **if the network is active/any version of Winget is installed/WAU is running as SYSTEM**.
If **ExitCode** is **1** from `_WAU-mods.ps1` then **Re-run WAU**. +In addition to this legacy handling, a new action-based system is now supported.
+This system lets you define multiple actions and conditions directly in your mod scripts, enabling more advanced automation and control over the WAU process.
+With actions, you can execute different scripts, check results, and control the WAU flow with greater flexibility and improved logging compared to relying solely on **Exit Code**. + Likewise `_WAU-mods-postsys.ps1` can be used to do things at the end of the **SYSTEM context WAU** process before the user run. +You can find more information in [README Mods for WAU](Sources/Winget-AutoUpdate/mods/README.md) + ## Custom scripts (Mods feature for Apps) The Mods feature allows you to run additional scripts when upgrading or installing an app. Just put the scripts in question with the **AppID** followed by the `-preinstall`, `-upgrade`, `-install`, `-installed` or `-notinstalled` suffix in the **mods** folder. @@ -207,8 +233,7 @@ The **-install** mod will be used for upgrades too if **-upgrade** doesn't exist > Example:
If you want to run a script that removes the shortcut from **%PUBLIC%\Desktop** (we don't want to fill the desktop with shortcuts our users can't delete) just after installing **Acrobat Reader DC** (32-bit), prepare a powershell script that removes the Public Desktop shortcut **Acrobat Reader DC.lnk** and name your script like this: `Adobe.Acrobat.Reader.32-bit-installed.ps1` and put it in the **mods** folder. -You can find more information on [Winget-Install Repo](https://github.com/Romanitho/Winget-AutoUpdate?tab=readme-ov-file#custom-script-mods-for-wau), as it's a related feature.
-Read more in the `README.md` under the directory **mods**. +You can find more information in [README Mods for WAU](Sources/Winget-AutoUpdate/mods/README.md), as it's a related feature. Share your mods with the community:
@@ -216,9 +241,16 @@ 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. > Example:
-**Canneverbe.CDBurnerXP-override.txt** with the content `ADDLOCAL=All REMOVE=Desktop_Shortcut /qn` +**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. + +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*). +> 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 will use the **content** of the text file as a native **winget --override** parameter when upgrading (as proposed by [JonNesovic](https://github.com/JonNesovic) in [Mod for --override argument #244](https://github.com/Romanitho/Winget-AutoUpdate/discussions/244#discussion-4637666)). ## Known issues * As reported by [soredake](https://github.com/soredake), Powershell from MsStore is not supported with WAU in system context. See diff --git a/Sources/Policies/ADMX/WAU.admx b/Sources/Policies/ADMX/WAU.admx index 6ab2d2e8d..0f76dddc5 100644 --- a/Sources/Policies/ADMX/WAU.admx +++ b/Sources/Policies/ADMX/WAU.admx @@ -1,6 +1,6 @@ @@ -10,6 +10,7 @@ + @@ -163,6 +164,11 @@ SuccessOnly + + + ErrorsOnly + + None @@ -366,31 +372,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -420,5 +401,16 @@ + + + + + + + - \ No newline at end of file + diff --git a/Sources/Policies/ADMX/en-US/WAU.adml b/Sources/Policies/ADMX/en-US/WAU.adml index 0a85f7dbf..3403823a4 100644 --- a/Sources/Policies/ADMX/en-US/WAU.adml +++ b/Sources/Policies/ADMX/en-US/WAU.adml @@ -1,6 +1,6 @@ WinGet-AutoUpdate WinGet-AutoUpdate GPO Management @@ -10,114 +10,95 @@ Experimental Winget-AutoUpdate version 1.16.0 or later Winget-AutoUpdate version 1.16.5 or later - Winget-AutoUpdate Experimental, subject to change, do - not use on PROD - Activate WAU GPO Management - This policy setting is an overriding - toggle for GPO Management of Winget-AutoUpdate. + Winget-AutoUpdate version 2.6.0 or later + Winget-AutoUpdate Experimental, subject to change, do not use on PROD + Activate WAU GPO Management [DEPRECATED] + This policy setting is deprecated and will be removed in a future version. The GPO Management is now always enabled when GPO policies are configured. + +This policy previously controlled whether GPO Management of Winget-AutoUpdate was active or not. Bypass Black/White list for User - This policy setting specifies whether to - Bypass Black/White list when run in user context or not. + This policy setting specifies whether to Bypass Black/White list when run in user context or not. - If this policy is disabled or not configured, the default is No. +If this policy is disabled or not configured, the default is No. Disable WAU AutoUpdate - This policy setting specifies whether to - Disable WAU AutoUpdate or not: - By default, WAU AutoUpdate is enabled. - It will not overwrite the configurations, icons (if personalised), - excluded_apps list... + This policy setting specifies whether to Disable WAU AutoUpdate or not: +By default, WAU AutoUpdate is enabled. +It will not overwrite the configurations, icons (if personalised), +excluded_apps list... - If this policy is disabled or not configured, the default is No. +If this policy is disabled or not configured, the default is No. Run WAU on metered connection - This policy setting specifies whether to - Run WAU on metered connection or not. + This policy setting specifies whether to Run WAU on metered connection or not. - If this policy is disabled or not configured, the default is No. +If this policy is disabled or not configured, the default is No. Update WAU to PreRelease versions - This policy setting specifies whether to - update WAU to PreRelease versions or not (via WAU AutoUpdate). + This policy setting specifies whether to update WAU to PreRelease versions or not (via WAU AutoUpdate). - If this policy is disabled or not configured, the default is No. +If this policy is disabled or not configured, the default is No. Application GPO Blacklist - Provide the WinGet IDs of applications you want to - exclude. + Provide the WinGet IDs of applications you want to exclude. - If this policy is disabled or not configured, GPO Blacklist is not used. +If this policy is disabled or not configured, GPO Blacklist is not used. Application GPO Whitelist - Provide the WinGet IDs of applications you want to - include. + Provide the WinGet IDs of applications you want to include. - If this policy is disabled or not configured, GPO Whitelist is not used. +If this policy is disabled or not configured, GPO Whitelist is not used. Use WhiteList instead of BlackList - This policy setting specifies whether to use a - Whitelist or not. - - If this policy is disabled or not configured, the default is No. - Get Black/White List from external Path - (URL/UNC/GPO/Local) - If this policy is enabled, you can set a - (URL/UNC/GPO/Local) Path to external lists other than the default. - If "Application GPO Blacklist/Whitelist" is set in this GPO the Path can be: - GPO - - If this policy is disabled or not configured, the default ListPath is used - (WAU InstallLocation). + This policy setting specifies whether to use a Whitelist or not. + +If this policy is disabled or not configured, the default is No. + Get Black/White List from external Path (URL/UNC/GPO/Local) + If this policy is enabled, you can set a (URL/UNC/GPO/Local) Path to external lists other than the default. +If "Application GPO Blacklist/Whitelist" is set in this GPO the Path MUST be: GPO + +If this policy is disabled or not configured, the default ListPath is used (WAU InstallLocation). Get Mods from external Path (URL/UNC/Local/AzureBlob) - If this policy is enabled, you can set a - (URL/UNC/Local/AzureBlob) Path to external mods other than the default. + If this policy is enabled, you can set a (URL/UNC/Local/AzureBlob) Path to external mods other than the default. - If this policy is disabled or not configured, the default ModsPath is used - (WAU InstallLocation). +If this policy is disabled or not configured, the default ModsPath is used (WAU InstallLocation). - Note: When set to 'AzureBlob', ensure you also configure 'Set Azure Blob URL - with SAS token'. +Note: When set to 'AzureBlob', ensure you also configure 'Set Azure Blob URL with SAS token'. Set Azure Blob URL with SAS Token - If this policy is enabled, you can set an Azure - Storage Blob URL with SAS token for use with the 'Mods' feature. The URL - must include the SAS token and have 'read' and 'list' permissions. + If this policy is enabled, you can set an Azure Storage Blob URL with SAS token for use with the 'Mods' feature. The URL must include the SAS token and have 'read' and 'list' permissions. - If this policy is disabled or not configured, the value is blank and Azure - Blob storage will NOT work. +If this policy is disabled or not configured, the value is blank and Azure Blob storage will NOT work. Notification Level - If this policy is enabled, you can - configure the Notification Level: - 1. Full (Default) - 2. SuccessOnly - 3. None + If this policy is enabled, you can configure the Notification Level: + 1. Full (Default) + 2. SuccessOnly + 3. ErrorsOnly + 4. None - If this policy is not configured or disabled, Notification Level: (1. Full). +If this policy is not configured or disabled, Notification Level: (1. Full). 1. Full (Default) 2. SuccessOnly - 3. None + 3. ErrorsOnly + 4. None Updates Interval - If this policy is enabled, you can configure - the Updates Interval: - 1. Daily (Default) - 2. BiDaily - 3. Weekly - 4. BiWeekly - 5. Monthly - 6. Never (e.g. in combination with 'Updates at Logon') - - If this policy is not configured or disabled, Updates Interval: (1. Daily). + If this policy is enabled, you can configure the Updates Interval: + 1. Daily (Default) + 2. BiDaily + 3. Weekly + 4. BiWeekly + 5. Monthly + 6. Never (e.g. in combination with 'Updates at Logon') + +If this policy is not configured or disabled, Updates Interval: (1. Daily). 1. Daily (Default) 2. BiDaily 3. Weekly 4. BiWeekly 5. Monthly - 6. Never (e.g. in combination with 'Updates - Interval') + 6. Never (e.g. in combination with 'Updates Interval') Updates at Logon - This policy setting specifies whether to set - WAU to run at user logon or not. + This policy setting specifies whether to set WAU to run at user logon or not. - If this policy is disabled or not configured, the default is No. +If this policy is disabled or not configured, the default is No. Updates at Time - If this policy is enabled, you can configure - the Sheduled Task Update time: - From 01:00 to 24:00 (Military/24 Hour Time) + If this policy is enabled, you can configure the Sheduled Task Update time: + From 01:00 to 24:00 (Military/24 Hour Time) - If this policy is not configured or disabled, Updates at Time: (06:00 AM). +If this policy is not configured or disabled, Updates at Time: (06:00 AM). 01:00 AM 02:00 03:00 @@ -143,48 +124,35 @@ 23:00 24:00 User context execution - This policy setting specifies whether to enable - User context execution or not. - - If this policy is disabled or not configured, the default is No. - Enable Deskop Shortcut [DEPRECATED] - This policy setting specifies whether to - enable a Desktop Shortcut or not: - WAU - Check for updated Apps - - If this policy is disabled or not configured, the default is No. - Enable Start Menu Shortcuts [DEPRECATED] - This policy setting specifies whether to - enable the Start Menu Shortcuts or not: - WAU - Check for updated Apps - WAU - Open logs - WAU - Web Help - - If this policy is disabled or not configured, the default is No. + This policy setting specifies whether to enable User context execution or not. + +If this policy is disabled or not configured, the default is No. Log: Number of allowed log files - If this policy is enabled, you can set a number - of allowed log files: - Setting MaxLogFiles to 0 don't delete any old archived log files, 1 keeps - the original one and just let it grow. - Default number is 3 (0-99) + If this policy is enabled, you can set a number of allowed log files: +Setting MaxLogFiles to 0 don't delete any old archived log files, 1 keeps the original one and just let it grow. +Default number is 3 (0-99) - If this policy is disabled or not configured, the default number is used. +If this policy is disabled or not configured, the default number is used. Log: Size of the log file in bytes before rotating - If this policy is enabled, you can set the size of - the log file in bytes before rotating. - Default size is 1048576 = 1 MB + If this policy is enabled, you can set the size of the log file in bytes before rotating. +Default size is 1048576 = 1 MB - If this policy is disabled or not configured, the default size is used. +If this policy is disabled or not configured, the default size is used. Use custom Winget Source repository - This policy setting specifies whether to - use winget tool with custom repository or not: - (WAU - Check for updated Apps) + This policy setting specifies whether to use winget tool with custom repository or not: +(WAU - Check for updated Apps) - If this policy is enabled, WAU will TRY to use a custom repo instead of the - built-in/default one called winget. +If this policy is enabled, WAU will TRY to use a custom repo instead of the built-in/default one called winget. - If this policy is disabled or not configured, the default is always the - built-in winget. +If this policy is disabled or not configured, the default is always the built-in winget. + Random delay for scheduled task triggers + This policy setting specifies the delay for the scheduled task. +A scheduled task random delay adds a random amount of wait time (up to the specified maximum) before the task starts. +This helps prevent many devices from running the task at the exact same time. This is not applicable to "on logon" triggers. + +If this policy is enabled, the scheduled task will have a random delay set based on the time inputted. + +If this policy is disabled or not configured, the default no delay. @@ -232,6 +200,11 @@ + + + + + \ No newline at end of file diff --git a/Sources/Tools/Detection/winget-detect.ps1 b/Sources/Tools/Detection/winget-detect.ps1 new file mode 100644 index 000000000..6c83d7d07 --- /dev/null +++ b/Sources/Tools/Detection/winget-detect.ps1 @@ -0,0 +1,62 @@ +<# +.SYNOPSIS +Helper script to use as detection method with Intune or SCCM. + +.DESCRIPTION +This script uses `winget export` to detect if a specific application is installed. +Intended for use as a detection rule script in Intune or SCCM deployments. +#> + +#Change app to detect [Application ID] +$AppToDetect = "Notepad++.Notepad++" + + +<# FUNCTIONS #> + +Function Get-WingetCmd { + + $WingetCmd = $null + + #Get WinGet Path + try { + #Get Admin Context Winget Location + $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 + } + catch { + #Get User context Winget Location + if (Test-Path "$env:LocalAppData\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe") { + $WingetCmd = "$env:LocalAppData\Microsoft\WindowsApps\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\winget.exe" + } + } + + return $WingetCmd +} + +<# MAIN #> + +#Get WinGet Location Function +$winget = Get-WingetCmd + +#Set json export file +$JsonFile = "$env:TEMP\InstalledApps.json" + +#Get installed apps and version in json file +& $Winget export -o $JsonFile --accept-source-agreements | Out-Null + +#Get json content +$Json = Get-Content $JsonFile -Raw | ConvertFrom-Json + +#Get apps and version in hashtable +$Packages = $Json.Sources.Packages + +#Remove json file +Remove-Item $JsonFile -Force + +# Search for specific app and version +$Apps = $Packages | Where-Object { $_.PackageIdentifier -eq $AppToDetect } + +if ($Apps) { + return "Installed!" +} diff --git a/Sources/Winget-AutoUpdate/WAU-Policies.ps1 b/Sources/Winget-AutoUpdate/WAU-Policies.ps1 index e1276ddf7..af5e17d3a 100644 --- a/Sources/Winget-AutoUpdate/WAU-Policies.ps1 +++ b/Sources/Winget-AutoUpdate/WAU-Policies.ps1 @@ -9,18 +9,13 @@ Daily update settings from policies #Import functions . "$PSScriptRoot\functions\Get-WAUConfig.ps1" -#Check if GPO Management is enabled -$ActivateGPOManagement = Get-ItemPropertyValue "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate" -Name "WAU_ActivateGPOManagement" -ErrorAction SilentlyContinue -if ($ActivateGPOManagement -eq 1) { - #Add (or update) tag to activate WAU-Policies Management - New-ItemProperty "HKLM:\SOFTWARE\Romanitho\Winget-AutoUpdate" -Name WAU_RunGPOManagement -Value 1 -Force | Out-Null -} +#Check if GPO Management is detected +$GPOManagementDetected = Get-ItemProperty "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate" -ErrorAction SilentlyContinue -#Get WAU settings -$WAUConfig = Get-WAUConfig +if ($GPOManagementDetected) { -#Check if GPO got applied from Get-WAUConfig (tag) -if ($WAUConfig.WAU_RunGPOManagement -eq 1) { + #Get WAU settings + $WAUConfig = Get-WAUConfig #Log init $GPOLogDirectory = Join-Path -Path $WAUConfig.InstallLocation -ChildPath "logs" @@ -30,16 +25,6 @@ if ($WAUConfig.WAU_RunGPOManagement -eq 1) { $GPOLogFile = Join-Path -Path $GPOLogDirectory -ChildPath "LatestAppliedSettings.txt" Set-Content -Path $GPOLogFile -Value "### POLICY CYCLE - $(Get-Date) ###`n" - #Reset WAU_RunGPOManagement if not GPO managed anymore (This is used to run this job one last time and reset initial settings) - if ($($WAUConfig.WAU_ActivateGPOManagement -eq 1)) { - Add-Content -Path $GPOLogFile -Value "GPO Management Enabled. Policies updated." - } - else { - New-ItemProperty "HKLM:\SOFTWARE\Romanitho\Winget-AutoUpdate" -Name WAU_RunGPOManagement -Value 0 -Force | Out-Null - $WAUConfig.WAU_RunGPOManagement = 0 - Add-Content -Path $GPOLogFile -Value "GPO Management Disabled. Policies removed." - } - #Get Winget-AutoUpdate scheduled task $WAUTask = Get-ScheduledTask -TaskName 'Winget-AutoUpdate' -ErrorAction SilentlyContinue @@ -49,7 +34,7 @@ if ($WAUConfig.WAU_RunGPOManagement -eq 1) { #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 + if (($WAUConfig.WAU_UpdatesAtLogon -eq 1 -and -not $hasLogonTrigger) -or ($WAUConfig.WAU_UpdatesAtLogon -ne 1 -and $hasLogonTrigger)) { $configChanged = $true } @@ -87,9 +72,20 @@ if ($WAUConfig.WAU_RunGPOManagement -eq 1) { $configChanged = $true } + #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)?$') { + $hours = if ($matches[1]) { [int]$matches[1] } else { 0 } + $minutes = if ($matches[2]) { [int]$matches[2] } else { 0 } + $existingRandomDelay = New-TimeSpan -Hours $hours -Minutes $minutes + } + if ($existingRandomDelay -ne $randomDelay) { + $configChanged = $true + } + #Check if schedule time has changed if ($currentIntervalType -ne "None" -and $currentIntervalType -ne "Never") { - $timeTrigger = $currentTriggers | Where-Object { $_.CimClass.CimClassName -ne "MSFT_TaskLogonTrigger" } | Select-Object -First 1 if ($timeTrigger) { $currentTime = [DateTime]::Parse($timeTrigger.StartBoundary).ToString("HH:mm:ss") if ($currentTime -ne $WAUConfig.WAU_UpdatesAtTime) { @@ -105,21 +101,21 @@ if ($WAUConfig.WAU_RunGPOManagement -eq 1) { $tasktriggers += New-ScheduledTaskTrigger -AtLogOn } if ($WAUConfig.WAU_UpdatesInterval -eq "Daily") { - $tasktriggers += New-ScheduledTaskTrigger -Daily -At $WAUConfig.WAU_UpdatesAtTime + $tasktriggers += New-ScheduledTaskTrigger -Daily -At $WAUConfig.WAU_UpdatesAtTime -RandomDelay $randomDelay } elseif ($WAUConfig.WAU_UpdatesInterval -eq "BiDaily") { - $tasktriggers += New-ScheduledTaskTrigger -Daily -At $WAUConfig.WAU_UpdatesAtTime -DaysInterval 2 + $tasktriggers += New-ScheduledTaskTrigger -Daily -At $WAUConfig.WAU_UpdatesAtTime -DaysInterval 2 -RandomDelay $randomDelay } elseif ($WAUConfig.WAU_UpdatesInterval -eq "Weekly") { - $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUConfig.WAU_UpdatesAtTime -DaysOfWeek 2 + $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUConfig.WAU_UpdatesAtTime -DaysOfWeek 2 -RandomDelay $randomDelay } elseif ($WAUConfig.WAU_UpdatesInterval -eq "BiWeekly") { - $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUConfig.WAU_UpdatesAtTime -DaysOfWeek 2 -WeeksInterval 2 + $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUConfig.WAU_UpdatesAtTime -DaysOfWeek 2 -WeeksInterval 2 -RandomDelay $randomDelay } elseif ($WAUConfig.WAU_UpdatesInterval -eq "Monthly") { - $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUConfig.WAU_UpdatesAtTime -DaysOfWeek 2 -WeeksInterval 4 + $tasktriggers += New-ScheduledTaskTrigger -Weekly -At $WAUConfig.WAU_UpdatesAtTime -DaysOfWeek 2 -WeeksInterval 4 -RandomDelay $randomDelay } - + #If trigger(s) set if ($taskTriggers) { #Edit scheduled task @@ -132,10 +128,11 @@ if ($WAUConfig.WAU_RunGPOManagement -eq 1) { Set-ScheduledTask -TaskPath $WAUTask.TaskPath -TaskName $WAUTask.TaskName -Trigger $tasktriggers | Out-Null } } - + #Log latest applied config 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 + } Exit 0 diff --git a/Sources/Winget-AutoUpdate/Winget-Install.ps1 b/Sources/Winget-AutoUpdate/Winget-Install.ps1 index 84d34324b..02bdb672d 100644 --- a/Sources/Winget-AutoUpdate/Winget-Install.ps1 +++ b/Sources/Winget-AutoUpdate/Winget-Install.ps1 @@ -1,11 +1,11 @@ <# .SYNOPSIS Install apps with Winget through Intune or SCCM. -Can be used standalone. +(Can be used standalone.) - Deprecated in favor of Winget-AutoUpdate. .DESCRIPTION Allow to run Winget in System Context to install your apps. -https://github.com/Romanitho/Winget-Install +(https://github.com/Romanitho/Winget-Install) - Deprecated in favor of Winget-AutoUpdate. .PARAMETER AppIDs Forward Winget App ID to install. For multiple apps, separate with ",". Case sensitive. @@ -90,59 +90,39 @@ function Confirm-Exist ($AppID) { #Check if install modifications exist in "mods" directory function Test-ModsInstall ($AppID) { - #Check current location - if (Test-Path ".\mods\$AppID-preinstall.ps1") { - $ModsPreInstall = ".\mods\$AppID-preinstall.ps1" - } - #Else, check in WAU mods - elseif (Test-Path "$WAUModsLocation\$AppID-preinstall.ps1") { - $ModsPreInstall = "$WAUModsLocation\$AppID-preinstall.ps1" - } - - if (Test-Path ".\mods\$AppID-install.ps1") { - $ModsInstall = ".\mods\$AppID-install.ps1" - } - elseif (Test-Path "$WAUModsLocation\$AppID-install.ps1") { - $ModsInstall = "$WAUModsLocation\$AppID-install.ps1" - } - - if (Test-Path ".\mods\$AppID-installed-once.ps1") { - $ModsInstalledOnce = ".\mods\$AppID-installed-once.ps1" - } - - if (Test-Path ".\mods\$AppID-installed.ps1") { - $ModsInstalled = ".\mods\$AppID-installed.ps1" - } - elseif (Test-Path "$WAUModsLocation\$AppID-installed.ps1") { - $ModsInstalled = "$WAUModsLocation\$AppID-installed.ps1" + if (Test-Path "$Mods\$AppID-*") { + if (Test-Path "$Mods\$AppID-preinstall.ps1") { + $ModsPreInstall = "$Mods\$AppID-preinstall.ps1" + } + if (Test-Path "$Mods\$AppID-override.txt") { + $ModsOverride = (Get-Content "$Mods\$AppID-override.txt" -Raw).Trim() + } + if (Test-Path "$Mods\$AppID-custom.txt") { + $ModsCustom = (Get-Content "$Mods\$AppID-custom.txt" -Raw).Trim() + } + if (Test-Path "$Mods\$AppID-install.ps1") { + $ModsInstall = "$Mods\$AppID-install.ps1" + } + if (Test-Path "$Mods\$AppID-installed.ps1") { + $ModsInstalled = "$Mods\$AppID-installed.ps1" + } } - return $ModsPreInstall, $ModsInstall, $ModsInstalledOnce, $ModsInstalled + return $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsInstall, $ModsInstalled } #Check if uninstall modifications exist in "mods" directory function Test-ModsUninstall ($AppID) { - #Check current location - if (Test-Path ".\mods\$AppID-preuninstall.ps1") { - $ModsPreUninstall = ".\mods\$AppID-preuninstall.ps1" - } - #Else, check in WAU mods - elseif (Test-Path "$WAUModsLocation\$AppID-preuninstall.ps1") { - $ModsPreUninstall = "$WAUModsLocation\$AppID-preuninstall.ps1" - } - - if (Test-Path ".\mods\$AppID-uninstall.ps1") { - $ModsUninstall = ".\mods\$AppID-uninstall.ps1" - } - elseif (Test-Path "$WAUModsLocation\$AppID-uninstall.ps1") { - $ModsUninstall = "$WAUModsLocation\$AppID-uninstall.ps1" - } - - if (Test-Path ".\mods\$AppID-uninstalled.ps1") { - $ModsUninstalled = ".\mods\$AppID-uninstalled.ps1" - } - elseif (Test-Path "$WAUModsLocation\$AppID-uninstalled.ps1") { - $ModsUninstalled = "$WAUModsLocation\$AppID-uninstalled.ps1" + if (Test-Path "$Mods\$AppID-*") { + if (Test-Path "$Mods\$AppID-preuninstall.ps1") { + $ModsPreUninstall = "$Mods\$AppID-preuninstall.ps1" + } + if (Test-Path "$Mods\$AppID-uninstall.ps1") { + $ModsUninstall = "$Mods\$AppID-uninstall.ps1" + } + if (Test-Path "$Mods\$AppID-uninstalled.ps1") { + $ModsUninstalled = "$Mods\$AppID-uninstalled.ps1" + } } return $ModsPreUninstall, $ModsUninstall, $ModsUninstalled @@ -152,23 +132,38 @@ 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/install/installedonce/installed - $ModsPreInstall, $ModsInstall, $ModsInstalledOnce, $ModsInstalled = Test-ModsInstall $($AppID) + #Check if mods exist (or already exist) for preinstall/override/custom/install/installed + $ModsPreInstall, $ModsOverride, $ModsCustom, $ModsInstall, $ModsInstalled = Test-ModsInstall $($AppID) #If PreInstall script exist if ($ModsPreInstall) { - Write-ToLog "-> Modifications for $AppID before install are being applied..." "Yellow" - & "$ModsPreInstall" + Write-ToLog "Modifications for $AppID before install are being applied..." "DarkYellow" + $preInstallResult = & "$ModsPreInstall" + if ($preInstallResult -eq $false) { + Write-ToLog "PreInstall script for $AppID requested to skip this installation" "Yellow" + return # Exit the function early + } } #Install App - Write-ToLog "-> Installing $AppID..." "Yellow" - $WingetArgs = "install --id $AppID -e --accept-package-agreements --accept-source-agreements -s winget -h $AppArgs" -split " " + Write-ToLog "-> Installing $AppID..." "DarkYellow" + if ($ModsOverride) { + Write-ToLog "-> Arguments (overriding default): $ModsOverride" # Without -h (user overrides default) + $WingetArgs = "install --id $AppID -e --accept-package-agreements --accept-source-agreements -s winget --override $ModsOverride" -split " " + } + elseif ($ModsCustom) { + 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 " " + } + else { + $WingetArgs = "install --id $AppID -e --accept-package-agreements --accept-source-agreements -s winget -h $AppArgs" -split " " + } + Write-ToLog "-> Running: `"$Winget`" $WingetArgs" & "$Winget" $WingetArgs | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append if ($ModsInstall) { - Write-ToLog "-> Modifications for $AppID during install are being applied..." "Yellow" + Write-ToLog "-> Modifications for $AppID during install are being applied..." "DarkYellow" & "$ModsInstall" } @@ -177,26 +172,11 @@ function Install-App ($AppID, $AppArgs) { if ($IsInstalled) { Write-ToLog "-> $AppID successfully installed." "Green" - if ($ModsInstalledOnce) { - Write-ToLog "-> Modifications for $AppID after install (one time) are being applied..." "Yellow" - & "$ModsInstalledOnce" - } - elseif ($ModsInstalled) { - Write-ToLog "-> Modifications for $AppID after install are being applied..." "Yellow" + if ($ModsInstalled) { + Write-ToLog "-> Modifications for $AppID after install are being applied..." "DarkYellow" & "$ModsInstalled" } - #Add mods if deployed from Winget-Install - if (Test-Path ".\mods\$AppID-*") { - #Check if WAU default install path exists - $Mods = "$WAUModsLocation" - if (Test-Path $Mods) { - #Add mods - Write-ToLog "-> Add modifications for $AppID to WAU 'mods'" - Copy-Item ".\mods\$AppID-*" -Destination "$Mods" -Exclude "*installed-once*", "*uninstall*" -Force - } - } - #Add to WAU White List if set if ($WAUWhiteList) { Add-WAUWhiteList $AppID @@ -220,18 +200,22 @@ function Uninstall-App ($AppID, $AppArgs) { #If PreUninstall script exist if ($ModsPreUninstall) { - Write-ToLog "-> Modifications for $AppID before uninstall are being applied..." "Yellow" - & "$ModsPreUninstall" + Write-ToLog "Modifications for $AppID before uninstall are being applied..." "DarkYellow" + $preUnInstallResult = & "$ModsPreUnInstall" + if ($preUnInstallResult -eq $false) { + Write-ToLog "PreUnInstall script for $AppID requested to skip this uninstallation" "Yellow" + return # Exit the function early + } } #Uninstall App - Write-ToLog "-> Uninstalling $AppID..." "Yellow" + Write-ToLog "-> Uninstalling $AppID..." "DarkYellow" $WingetArgs = "uninstall --id $AppID -e --accept-source-agreements -h $AppArgs" -split " " Write-ToLog "-> Running: `"$Winget`" $WingetArgs" & "$Winget" $WingetArgs | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append if ($ModsUninstall) { - Write-ToLog "-> Modifications for $AppID during uninstall are being applied..." "Yellow" + Write-ToLog "-> Modifications for $AppID during uninstall are being applied..." "DarkYellow" & "$ModsUninstall" } @@ -240,21 +224,10 @@ function Uninstall-App ($AppID, $AppArgs) { if (!($IsInstalled)) { Write-ToLog "-> $AppID successfully uninstalled." "Green" if ($ModsUninstalled) { - Write-ToLog "-> Modifications for $AppID after uninstall are being applied..." "Yellow" + Write-ToLog "-> Modifications for $AppID after uninstall are being applied..." "DarkYellow" & "$ModsUninstalled" } - #Remove mods if deployed from Winget-Install - if (Test-Path ".\mods\$AppID-*") { - #Check if WAU default install path exists - $Mods = "$WAUModsLocation" - if (Test-Path "$Mods\$AppID*") { - Write-ToLog "-> Remove $AppID modifications from WAU 'mods'" - #Remove mods - Remove-Item -Path "$Mods\$AppID-*" -Exclude "*uninstall*" -Force - } - } - #Remove from WAU White List if set if ($WAUWhiteList) { Remove-WAUWhiteList $AppID @@ -309,9 +282,18 @@ if ("$env:PROCESSOR_ARCHITEW6432" -ne "ARM64") { } } -#Config console output encoding -$null = cmd /c '' #Tip for ISE +# Workaround for ISE: Force UTF-8 output encoding by briefly invoking cmd.exe +if ($psISE) { + try { + $null = Start-Process "cmd.exe" -ArgumentList "/c """ -NoNewWindow -Wait -WindowStyle Hidden + } + catch { + Write-ToLog "-> Unable to execute cmd.exe - skipping ISE output encoding workaround." "Red" + } +} +# Set UTF-8 encoding for all console output (e.g., Write-Output, Write-Host, etc.) [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# Suppress progress bars (used by some cmdlets like Invoke-WebRequest) $Script:ProgressPreference = 'SilentlyContinue' #Check if current process is elevated (System or admin user) @@ -321,7 +303,9 @@ $Script:IsElevated = $CurrentPrincipal.IsInRole([Security.Principal.WindowsBuilt #Get WAU Installed location $WAURegKey = "HKLM:\SOFTWARE\Romanitho\Winget-AutoUpdate\" $Script:WAUInstallLocation = Get-ItemProperty $WAURegKey -ErrorAction SilentlyContinue | Select-Object -ExpandProperty InstallLocation -$Script:WAUModsLocation = Join-Path -Path $WAUInstallLocation -ChildPath "mods" + +# Use the Working Dir (even if it is from a symlink) +$Mods = "$realPath\mods" #Log file & LogPath initialization if ($IsElevated) { diff --git a/Sources/Winget-AutoUpdate/Winget-Upgrade.ps1 b/Sources/Winget-AutoUpdate/Winget-Upgrade.ps1 index ad344eb60..935f9ce5d 100644 --- a/Sources/Winget-AutoUpdate/Winget-Upgrade.ps1 +++ b/Sources/Winget-AutoUpdate/Winget-Upgrade.ps1 @@ -22,10 +22,6 @@ $Script:ProgressPreference = [System.Management.Automation.ActionPreference]::Si #region Get settings and Domain/Local Policies (GPO) if activated. Write-ToLog "Reading WAUConfig"; $Script:WAUConfig = Get-WAUConfig; - -if ($WAUConfig.WAU_ActivateGPOManagement -eq 1) { - Write-ToLog "WAU Policies management Enabled."; -} #endregion Get settings and Domain/Local Policies (GPO) if activated. # Default name of winget repository used within this script @@ -35,12 +31,10 @@ if ($WAUConfig.WAU_ActivateGPOManagement -eq 1) { # Defining a custom source even if not used below (failsafe suggested by github/sebneus mentioned in issues/823) [string]$Script:WingetSourceCustom = $DefaultWingetRepoName; -# Defining custom repository for winget tool (only if GPO management is active) -if ($Script:WAUConfig.WAU_ActivateGPOManagement) { - if ($null -ne $Script:WAUConfig.WAU_WingetSourceCustom) { - $Script:WingetSourceCustom = $Script:WAUConfig.WAU_WingetSourceCustom.Trim(); - Write-ToLog "Selecting winget repository named '$($Script:WingetSourceCustom)'"; - } +# Defining custom repository for winget tool +if ($null -ne $Script:WAUConfig.WAU_WingetSourceCustom) { + $Script:WingetSourceCustom = $Script:WAUConfig.WAU_WingetSourceCustom.Trim(); + Write-ToLog "Selecting winget repository named '$($Script:WingetSourceCustom)'"; } #endregion Winget Source Custom @@ -210,7 +204,7 @@ if (Test-Network) { #Compare if ((Compare-SemVer -Version1 $WAUCurrentVersion -Version2 $WAUAvailableVersion) -lt 0) { #If new version is available, update it - Write-ToLog "WAU Available version: $WAUAvailableVersion" "Yellow"; + Write-ToLog "WAU Available version: $WAUAvailableVersion" "DarkYellow"; Update-WAU; } else { @@ -246,10 +240,10 @@ if (Test-Network) { } if ($NewList) { if ($AlwaysDownloaded) { - Write-ToLog "List downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))" "Yellow" + Write-ToLog "List downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))" "DarkYellow" } else { - Write-ToLog "Newer List downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))" "Yellow" + Write-ToLog "Newer List downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))" "DarkYellow" } $Script:AlwaysDownloaded = $False } @@ -284,14 +278,14 @@ if (Test-Network) { $Script:ReachNoPath = $False } if ($NewMods -gt 0) { - Write-ToLog "$NewMods newer Mods downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))\mods" "Yellow" + Write-ToLog "$NewMods newer Mods downloaded/copied to local path: $($WAUConfig.InstallLocation.TrimEnd(" ", "\"))\mods" "DarkYellow" } else { if (Test-Path "$WorkingDir\mods\*.ps1") { Write-ToLog "Mods are up to date." "Green" } else { - Write-ToLog "No Mods are implemented..." "Yellow" + Write-ToLog "No Mods are implemented..." "DarkYellow" } } if ($DeletedMods -gt 0) { @@ -299,18 +293,11 @@ if (Test-Network) { } } - #Test if _WAU-mods.ps1 exist: Mods for WAU (if Network is active/any Winget is installed/running as SYSTEM) + # Test if _WAU-mods.ps1 exist: Mods for WAU (if Network is active/any Winget is installed/running as SYSTEM) $Mods = "$WorkingDir\mods" if (Test-Path "$Mods\_WAU-mods.ps1") { - Write-ToLog "Running Mods for WAU..." "Yellow" - & "$Mods\_WAU-mods.ps1" - $ModsExitCode = $LASTEXITCODE - #If _WAU-mods.ps1 has ExitCode 1 - Re-run WAU - if ($ModsExitCode -eq 1) { - Write-ToLog "Re-run WAU" - Start-Process powershell -ArgumentList "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$WorkingDir\winget-upgrade.ps1`"" - Exit - } + Write-ToLog "Running Mods for WAU..." "DarkYellow" + Test-WAUMods -WorkingDir $WorkingDir -WAUConfig $WAUConfig -GitHub_Repo $GitHub_Repo } } @@ -351,7 +338,7 @@ if (Test-Network) { } #Get outdated Winget packages - Write-ToLog "Checking application updates on Winget Repository named '$($Script:WingetSourceCustom)' .." "yellow" + Write-ToLog "Checking application updates on Winget Repository named '$($Script:WingetSourceCustom)' .." "DarkYellow" $outdated = Get-WingetOutdatedApps -src $Script:WingetSourceCustom; #If something unusual happened or no update found @@ -433,10 +420,10 @@ if (Test-Network) { Write-ToLog "No new update." "Green" } - #Test if _WAU-mods-postsys.ps1 exists: Mods for WAU (postsys) - if Network is active/any Winget is installed/running as SYSTEM _after_ SYSTEM updates + # Test if _WAU-mods-postsys.ps1 exists: Mods for WAU (postsys) - if Network is active/any Winget is installed/running as SYSTEM _after_ SYSTEM updates if ($true -eq $IsSystem) { if (Test-Path "$Mods\_WAU-mods-postsys.ps1") { - Write-ToLog "Running Mods (postsys) for WAU..." "Yellow" + Write-ToLog "Running Mods (postsys) for WAU..." "DarkYellow" & "$Mods\_WAU-mods-postsys.ps1" } } diff --git a/Sources/Winget-AutoUpdate/functions/Get-WAUConfig.ps1 b/Sources/Winget-AutoUpdate/functions/Get-WAUConfig.ps1 index a202d55be..8481d223d 100644 --- a/Sources/Winget-AutoUpdate/functions/Get-WAUConfig.ps1 +++ b/Sources/Winget-AutoUpdate/functions/Get-WAUConfig.ps1 @@ -6,20 +6,17 @@ Function Get-WAUConfig { $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] - #Check if GPO Management is enabled - $ActivateGPOManagement = Get-ItemPropertyValue "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate" -Name "WAU_ActivateGPOManagement" -ErrorAction SilentlyContinue + #Check if GPO policies exist + $WAUPolicies = Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate" -ErrorAction SilentlyContinue - #If GPO Management is enabled, replace settings - if ($ActivateGPOManagement -eq 1) { - - #Get all WAU Policies - $WAUPolicies = Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Romanitho\Winget-AutoUpdate" -ErrorAction SilentlyContinue + #If GPO policies exist, apply them (regardless of ActivateGPOManagement value) + if ($WAUPolicies) { + Write-ToLog "GPO policies detected - applying GPO configuration" "Yellow" #Replace loaded configurations by ones from Policies $WAUPolicies.PSObject.Properties | ForEach-Object { $WAUConfig.PSObject.Properties.add($_) } - } #Return config diff --git a/Sources/Winget-AutoUpdate/functions/Start-NotifTask.ps1 b/Sources/Winget-AutoUpdate/functions/Start-NotifTask.ps1 index 02c9d0469..6e8312ca3 100644 --- a/Sources/Winget-AutoUpdate/functions/Start-NotifTask.ps1 +++ b/Sources/Winget-AutoUpdate/functions/Start-NotifTask.ps1 @@ -15,7 +15,7 @@ function Start-NotifTask { [Switch]$UserRun = $false ) - if (($WAUConfig.WAU_NotificationLevel -eq "Full") -or ($WAUConfig.WAU_NotificationLevel -eq "SuccessOnly" -and $MessageType -eq "Success") -or ($UserRun)) { + 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 [xml]$ToastTemplate = New-Object system.Xml.XmlDocument diff --git a/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1 b/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1 new file mode 100644 index 000000000..aaf346919 --- /dev/null +++ b/Sources/Winget-AutoUpdate/functions/Test-WAUMods.ps1 @@ -0,0 +1,382 @@ +function Test-WAUMods { + param ( + [Parameter(Mandatory=$true)] + [string]$WorkingDir, + + [Parameter(Mandatory=$true)] + [PSCustomObject]$WAUConfig, + + [Parameter(Mandatory=$false)] + [string]$GitHub_Repo = "Winget-AutoUpdate" + ) + + # Define Mods path + $Mods = "$WorkingDir\mods" + + # Capture both output and exit code + $ModsOutput = & "$Mods\_WAU-mods.ps1" 2>&1 | Out-String + $ModsExitCode = $LASTEXITCODE + + # Handle legacy exit code behavior first (backward compatibility) + if ($ModsExitCode -eq 1) { + Write-ToLog "Legacy exit code 1 detected - Re-running WAU" + Start-Process powershell -ArgumentList "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$WorkingDir\Winget-Upgrade.ps1`"" + Exit + } + + # Try to parse JSON output for new action-based system + if ($ModsOutput -and $ModsOutput.Trim()) { + try { + # Remove any non-JSON content (like debug output) and find JSON + $jsonMatch = $ModsOutput | Select-String -Pattern '\{.*\}' | Select-Object -First 1 + + if ($jsonMatch) { + $ModsResult = $jsonMatch.Matches[0].Value | ConvertFrom-Json + + # Log message if provided + if ($ModsResult.Message) { + $logLevel = if ($ModsResult.LogLevel) { $ModsResult.LogLevel } else { "White" } + Write-ToLog $ModsResult.Message $logLevel + } + + # Execute action based on returned instruction + switch ($ModsResult.Action) { + "Rerun" { + Write-ToLog "Mods requested a WAU re-run" + Start-Process powershell -ArgumentList "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$WorkingDir\Winget-Upgrade.ps1`"" + $exitCode = if ($ModsResult.ExitCode) { [int]$ModsResult.ExitCode } else { 0 } + Exit $exitCode + } + "Abort" { + Write-ToLog "Mods requested WAU to abort" + $exitCode = if ($ModsResult.ExitCode) { [int]$ModsResult.ExitCode } else { 1602 } # Default to "User cancelled" + Exit $exitCode + } + "Postpone" { + Write-ToLog "Mods requested a postpone of WAU" + # Check if a postponed task already exists + $existingTask = Get-ScheduledTask -TaskPath "\WAU\" -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -like "Postponed-$($GitHub_Repo)*" } + if ($existingTask) { + Write-ToLog "A postponed task for $($GitHub_Repo) already exists, not creating another." "Yellow" + } + else { + # Get configurable duration, default to 1 hour + $postponeDuration = if ($ModsResult.PostponeDuration) { + try { + [double]$parsedDuration = [double]$ModsResult.PostponeDuration + # Ensure minimum duration of 0.1 hours (6 minutes) + if ($parsedDuration -lt 0.1) { + Write-ToLog "PostponeDuration adjusted to minimum 0.1 hours (6 minutes)" "Yellow" + 0.1 + } else { + $parsedDuration + } + } + catch { + Write-ToLog "Invalid PostponeDuration value '$($ModsResult.PostponeDuration)', using default 1 hour" "Yellow" + 1 + } + } else { + 1 + } + + # Create a postponed temporary scheduled task to try again later + $uniqueTaskName = "Postponed-$($GitHub_Repo)_$(Get-Random)" + $taskPath = "\WAU\" + $copyAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($WAUConfig.InstallLocation)Winget-Upgrade.ps1`"" + $copyTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddHours($postponeDuration) + # Set EndBoundary to make DeleteExpiredTaskAfter work + $copyTrigger.EndBoundary = (Get-Date).AddHours($postponeDuration).AddMinutes(1).ToString("yyyy-MM-ddTHH:mm:ss") + $copySettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -DeleteExpiredTaskAfter (New-TimeSpan -Seconds 0) + $copyPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest + Register-ScheduledTask -TaskName $uniqueTaskName -TaskPath $taskPath -Action $copyAction -Trigger $copyTrigger -Settings $copySettings -Principal $copyPrincipal -Description "Postponed copy of $($GitHub_Repo)" | Out-Null + Write-ToLog "WAU will try again in $postponeDuration hours" "Yellow" + } + $exitCode = if ($ModsResult.ExitCode) { [int]$ModsResult.ExitCode } else { 1602 } # Default to "User cancelled" + Exit $exitCode + } + "Reboot" { + Write-ToLog "Mods requested a system reboot" + # Get configurable delay, default to 5 minutes + $rebootDelay = if ($ModsResult.RebootDelay) { + try { + [double]$parsedDelay = [double]$ModsResult.RebootDelay + # Ensure minimum delay of 1 minute for safety + if ($parsedDelay -lt 1) { + Write-ToLog "RebootDelay adjusted to minimum 1 minute" "Yellow" + 1 + } else { + $parsedDelay + } + } + catch { + Write-ToLog "Invalid RebootDelay value '$($ModsResult.RebootDelay)', using default 5 minutes" "Yellow" + 5 + } + } else { + 5 + } + + $shutdownMessage = if ($ModsResult.Message) { $ModsResult.Message } else { "WAU Mods requested a system reboot in $rebootDelay minutes" } + $rebootHandler = if ($ModsResult.RebootHandler) { $ModsResult.RebootHandler } else { "Windows" } + + # Check if SCCM client is available for managed restart (user controlled) + $sccmClient = Get-CimInstance -Namespace "root\ccm" -ClassName "SMS_Client" -ErrorAction SilentlyContinue + + if ($sccmClient -and ($rebootHandler -eq "SCCM")) { + Write-ToLog "SCCM client detected - using managed restart (user controlled)" "Green" + + $ccmRestartPath = "$env:windir\CCM\CcmRestart.exe" + $regPath = 'HKLM:\SOFTWARE\Microsoft\SMS\Mobile Client\Reboot Management\RebootData' + + try { + # Check if SCCM restart registry values already exist + $existingRebootBy = $null + $existingRebootValues = $false + + if (Test-Path $regPath) { + $existingRebootBy = Get-ItemProperty -Path $regPath -Name 'RebootBy' -ErrorAction SilentlyContinue + $existingNotifyUI = Get-ItemProperty -Path $regPath -Name 'NotifyUI' -ErrorAction SilentlyContinue + $existingSetTime = Get-ItemProperty -Path $regPath -Name 'SetTime' -ErrorAction SilentlyContinue + + # Check if we have the key registry values indicating a restart is already scheduled + if ($existingRebootBy -and $existingNotifyUI -and $existingSetTime -and $existingRebootBy.PSObject.Properties['RebootBy']) { + $existingRebootValues = $true + $existingRestartTime = [DateTimeOffset]::FromUnixTimeSeconds([int64]$existingRebootBy.RebootBy).LocalDateTime + Write-ToLog "SCCM restart already scheduled for: $existingRestartTime" "Yellow" + } + } + + if ($existingRebootValues) { + # Try CcmRestart.exe for notification + if (Test-Path $ccmRestartPath) { + Write-ToLog "Triggering SCCM restart notification via CcmRestart.exe" "Cyan" + Start-Process -FilePath $ccmRestartPath -NoNewWindow -Wait -ErrorAction SilentlyContinue + } else { + Write-ToLog "CcmRestart.exe not found, restarting ccmexec service" "Yellow" + Restart-Service ccmexec -Force -ErrorAction SilentlyContinue + } + } else { + # No existing restart scheduled - create new SCCM managed restart (user controlled) + Write-ToLog "Setting up new SCCM managed restart schedule" "Green" + + # Ensure registry path exists + if (-not (Test-Path $regPath)) { + New-Item -Path $regPath -Force | Out-Null + } + + # Check the intended exit code to determine restart type + $intendedExitCode = if ($ModsResult.ExitCode) { $ModsResult.ExitCode } else { 3010 } + + if ($intendedExitCode -eq 1641) { + # HARD/MANDATORY REBOOT in SCCM registry (show UI to user, doesn't execute automatically!) + $restartTime = [DateTimeOffset]::Now.AddMinutes($rebootDelay).ToUnixTimeSeconds() + + # CRITICAL: Both RebootBy and OverrideRebootWindowTime must be set to the same value + New-ItemProperty -Path $regPath -Name 'RebootBy' -Value ([Int64]$restartTime) -PropertyType QWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'OverrideRebootWindowTime' -Value ([Int64]$restartTime) -PropertyType QWord -Force | Out-Null + + # Mandatory reboot settings + New-ItemProperty -Path $regPath -Name 'PreferredRebootWindowTypes' -Value @("3") -PropertyType MultiString -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'OverrideRebootWindow' -Value 1 -PropertyType DWord -Force | Out-Null + + # Ignore service window settings + New-ItemProperty -Path $regPath -Name 'OverrideServiceWindows' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'RebootOutsideOfServiceWindow' -Value 1 -PropertyType DWord -Force | Out-Null + + # Hard reboot settings + New-ItemProperty -Path $regPath -Name 'HardReboot' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'NotifyUI' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'RebootValueInUTC' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'SetTime' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'GraceSeconds' -Value 0 -PropertyType DWord -Force | Out-Null + + # HARD/MANDATORY REBOOT via Task Scheduler (for execution, unless user executed it manually via UI) + $taskName = "WAU_MandatoryRestart" + $taskPath = "\WAU\" + + # Create a self destroying scheduled task for mandatory restart + Write-ToLog "Creating scheduled task for mandatory restart in $rebootDelay minutes" "Yellow" + + # Escape the shutdown message properly before using in here-string + $escapedShutdownMessage = $shutdownMessage -replace '"', '\"' -replace '`', '``' + + # Create PowerShell script with enhanced logging + $scriptContent = @" +`$regPath = 'HKLM:\SOFTWARE\Microsoft\SMS\Mobile Client\Reboot Management\RebootData' +`$ccmRestartPath = "`$env:windir\CCM\CcmRestart.exe" +`$logPath = "$WorkingDir\logs\mandatory_restart.log" +`$rebootDelay = $rebootDelay +`$shutdownMessage = "$escapedShutdownMessage" + +# Function to write to log +function Write-RestartLog { + param([string]`$Message) + `$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + "`$timestamp - `$Message" | Out-File -FilePath `$logPath -Append -Encoding UTF8 +} + +Write-RestartLog "Mandatory restart task started" + +# Only run if RebootBy and OverrideRebootWindowTime exists under the key (if not: the user has already restarted the client) +`$regProps = Get-ItemProperty -Path `$regPath -ErrorAction SilentlyContinue +if (`$regProps.PSObject.Properties.Name -contains 'RebootBy' -and `$regProps.PSObject.Properties.Name -contains 'OverrideRebootWindowTime') { + Write-RestartLog "SCCM restart registry values found, proceeding with restart" + + # Grace period is over, system will now restart in 2 minutes + Write-RestartLog "Grace period: `$rebootDelay minutes is over (`$shutdownMessage). System will now restart in 2 minutes." "Yellow" + + # Cancels any pending system shutdown or restart using 'shutdown /a'. + `$null = & shutdown /a 2>&1 + + `$result = & shutdown /r /t 120 /c "Grace period: `$rebootDelay minutes is over (`$shutdownMessage). System will now restart in 2 minutes." 2>&1 + if (`$LASTEXITCODE -eq 0) { + Write-RestartLog "System restart scheduled in 2 minutes" "Yellow" + + # Remove all values under the registry key + `$key = Get-Item -Path `$regPath -ErrorAction SilentlyContinue + if (`$key) { + `$key.GetValueNames() | ForEach-Object { Remove-ItemProperty -Path `$regPath -Name `$_ -ErrorAction SilentlyContinue } + Write-RestartLog "All registry values under `$regPath have been deleted" + } else { + Write-RestartLog "Registry key `$regPath not found, nothing to delete" + } + } elseif (`$LASTEXITCODE -eq 1190) { + Write-RestartLog "A system shutdown already exists: `$result" "Yellow" + + # Remove all values under the registry key + `$key = Get-Item -Path `$regPath -ErrorAction SilentlyContinue + if (`$key) { + `$key.GetValueNames() | ForEach-Object { Remove-ItemProperty -Path `$regPath -Name `$_ -ErrorAction SilentlyContinue } + Write-RestartLog "All registry values under `$regPath have been deleted" + } else { + Write-RestartLog "Registry key `$regPath not found, nothing to delete" + } + } else { + Write-RestartLog "A system shutdown failed: `$result" "Yellow" + + 'RebootBy','OverrideRebootWindowTime' | ForEach-Object { + New-ItemProperty -Path `$regPath -Name `$_ -Value ([Int64]-1) -PropertyType QWord -Force + } + 'PreferredRebootWindowTypes' | ForEach-Object { + New-ItemProperty -Path `$regPath -Name `$_ -Value @('3') -PropertyType MultiString -Force + } + 'OverrideRebootWindow','HardReboot','NotifyUI','RebootValueInUTC','SetTime','OverrideServiceWindows','RebootOutsideOfServiceWindow' | ForEach-Object { + New-ItemProperty -Path `$regPath -Name `$_ -Value 1 -PropertyType DWord -Force + } + New-ItemProperty -Path `$regPath -Name 'GraceSeconds' -Value 0 -PropertyType DWord -Force + + Write-RestartLog "Registry values updated for SCCM mandatory restart" + + # Check if CcmRestart.exe exists and use it, otherwise restart the service + if (Test-Path `$ccmRestartPath) { + Write-RestartLog "Executing CcmRestart.exe" + Start-Process -FilePath `$ccmRestartPath -NoNewWindow -Wait -ErrorAction SilentlyContinue + Write-RestartLog "CcmRestart.exe execution completed" + } else { + Write-RestartLog "CcmRestart.exe not found, restarting ccmexec service" + Restart-Service ccmexec -Force -ErrorAction SilentlyContinue + Write-RestartLog "ccmexec service restart completed" + } + } +} else { + Write-RestartLog "No SCCM restart registry values found, task completed without action" +} + +Write-RestartLog "Mandatory restart task completed" +"@ + # Encode to Base64 + $encodedScript = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($scriptContent)) + + # Create action with encoded command + $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -WindowStyle Hidden -EncodedCommand $encodedScript" + + # Create a scheduled task trigger to run (rebootDelay - 2) minutes from now, but at least 2 minutes delay. + $triggerDelay = [math]::Max(($rebootDelay - 2), 2) + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes($triggerDelay) + # Set EndBoundary to make DeleteExpiredTaskAfter work + $trigger.EndBoundary = (Get-Date).AddMinutes($triggerDelay).AddMinutes(1).ToString("yyyy-MM-ddTHH:mm:ss") + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 60) -DeleteExpiredTaskAfter (New-TimeSpan -Seconds 0) + $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest + Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description "Mandatory SCCM Restart" -Force | Out-Null + + # Add a standard shutdown command + $result = & shutdown /r /t ([int]($rebootDelay * 60)) /c $shutdownMessage 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-ToLog "System restart scheduled in $rebootDelay minutes" "Yellow" + } else { + Write-ToLog "A system shutdown has already been scheduled or failed: $result" "Yellow" + } + } else { + # SOFT/NON-MANDATORY REBOOT + Write-ToLog "Using soft reboot (non-mandatory) for SCCM restart" "Cyan" + + # For non-mandatory, RebootBy should be 0 to show dialog immediately + New-ItemProperty -Path $regPath -Name 'RebootBy' -Value 0 -PropertyType QWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'OverrideRebootWindowTime' -Value 0 -PropertyType QWord -Force | Out-Null + + # Set as non-mandatory reboot + New-ItemProperty -Path $regPath -Name 'PreferredRebootWindowTypes' -Value @("4") -PropertyType MultiString -Force | Out-Null + + # Soft reboot settings + New-ItemProperty -Path $regPath -Name 'HardReboot' -Value 0 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'NotifyUI' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'RebootValueInUTC' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'SetTime' -Value 1 -PropertyType DWord -Force | Out-Null + New-ItemProperty -Path $regPath -Name 'GraceSeconds' -Value 300 -PropertyType DWord -Force | Out-Null # Default grace period of 5 minutes + } + + # Try CcmRestart.exe first for notification + if (Test-Path $ccmRestartPath) { + Write-ToLog "Triggering SCCM restart notification via CcmRestart.exe" "Cyan" + Start-Process -FilePath $ccmRestartPath -NoNewWindow -Wait -ErrorAction SilentlyContinue + } else { + Write-ToLog "CcmRestart.exe not found, restarting ccmexec service" "Yellow" + Restart-Service ccmexec -Force -ErrorAction SilentlyContinue + } + + if ($intendedExitCode -eq 1641) { + Write-ToLog "MANDATORY restart via scheduled task: In $rebootDelay minutes" "Green" + } else { + Write-ToLog "Non-mandatory restart dialog triggered" "Green" + } + } + } + catch { + Write-ToLog "Failed to set SCCM restart: $($_.Exception.Message). Falling back to standard restart." "Yellow" + # Fallback to standard shutdown + $result = & shutdown /r /t ([int]($rebootDelay * 60)) /c $shutdownMessage 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-ToLog "System restart scheduled in $rebootDelay minutes (fallback)" "Yellow" + } else { + Write-ToLog "A system shutdown has already been scheduled or failed: $result" "Yellow" + } + } + } else { + # Standard shutdown when SCCM is not available (or reboot handler is not "SCCM") + $result = & shutdown /r /t ([int]($rebootDelay * 60)) /c $shutdownMessage 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-ToLog "System restart scheduled in $rebootDelay minutes" "Yellow" + } else { + Write-ToLog "A system shutdown has already been scheduled or failed: $result" "Yellow" + } + } + $exitCode = if ($ModsResult.ExitCode) { [int]$ModsResult.ExitCode } else { 3010 } # Default to "Restart required" + Exit $exitCode + } + "Continue" { + Write-ToLog "Mods allows WAU to continue normally" + } + default { + Write-ToLog "Unknown action '$($ModsResult.Action)' from mods, continuing normally" "Cyan" + } + } + } + } + catch { + Write-ToLog "Failed to parse mods JSON output: $($_.Exception.Message)" "Red" + Write-ToLog "Continuing with normal WAU execution" "Cyan" + } + } + +} \ No newline at end of file diff --git a/Sources/Winget-AutoUpdate/functions/Update-App.ps1 b/Sources/Winget-AutoUpdate/functions/Update-App.ps1 index ff174ccf6..d3e48580c 100644 --- a/Sources/Winget-AutoUpdate/functions/Update-App.ps1 +++ b/Sources/Winget-AutoUpdate/functions/Update-App.ps1 @@ -24,26 +24,56 @@ Function Update-App ($app) { #If PreInstall script exist if ($ModsPreInstall) { - Write-ToLog "Modifications for $($app.Id) before upgrade are being applied..." "Yellow" - & "$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 + } } - #Run Winget Upgrade command - if ($ModsOverride) { - Write-ToLog "-> Running (overriding default): Winget upgrade --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget --override $ModsOverride" - & $Winget upgrade --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget --override $ModsOverride | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append - } - elseif ($ModsCustom) { - Write-ToLog "-> Running (customizing default): Winget upgrade --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget -h --custom $ModsCustom" - & $Winget upgrade --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget -h --custom $ModsCustom | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append - } - else { - Write-ToLog "-> Running: Winget upgrade --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget -h" - & $Winget upgrade --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget -h | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append - } + # 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 if ($ModsUpgrade) { - Write-ToLog "Modifications for $($app.Id) during upgrade are being applied..." "Yellow" + Write-ToLog "Modifications for $($app.Id) during upgrade are being applied..." "DarkYellow" & "$ModsUpgrade" } @@ -55,31 +85,61 @@ Function Update-App ($app) { #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-> ...an install for $($app.Name) is NOT executed!" "Red" - continue + 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 } - - #If app failed to upgrade, run Install command (2 tries max - some apps get uninstalled after single "Install" command.) - $retry = 1 + 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)" "Yellow" + 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) { - Write-ToLog "-> Running (overriding default): Winget install --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget --force --override $ModsOverride" - & $Winget install --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget --force --override $ModsOverride | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append - } + $allParams = $baseParams + @("--override", "$ModsOverride") + $logPrefix = "Running (overriding default):" + $logSuffix = "--override $ModsOverride" + } elseif ($ModsCustom) { - Write-ToLog "-> Running (customizing default): Winget install --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget -h --force --custom $ModsCustom" - & $Winget install --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget -h --force --custom $ModsCustom | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append - } + $allParams = $baseParams + @("-h", "--custom", "$ModsCustom") + $logPrefix = "Running (customizing default):" + $logSuffix = "-h --custom $ModsCustom" + } else { - Write-ToLog "-> Running: Winget install --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget -h --force" - & $Winget install --id $($app.Id) -e --accept-package-agreements --accept-source-agreements -s winget -h --force | Where-Object { $_ -notlike " *" } | Tee-Object -file $LogFile -Append + $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 + if ($ModsInstall) { - Write-ToLog "Modifications for $($app.Id) during install are being applied..." "Yellow" + Write-ToLog "Modifications for $($app.Id) during install are being applied..." "DarkYellow" & "$ModsInstall" } @@ -93,14 +153,14 @@ Function Update-App ($app) { # Upgrade/install was successful $true { if ($ModsInstalled) { - Write-ToLog "Modifications for $($app.Id) after upgrade/install are being applied..." "Yellow" + 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..." "Yellow" + Write-ToLog "Modifications for $($app.Id) after a failed upgrade/install are being applied..." "DarkYellow" & "$ModsNotInstalled" } } diff --git a/Sources/Winget-AutoUpdate/mods/README.md b/Sources/Winget-AutoUpdate/mods/README.md index e58b8ddfe..f5d83f5ce 100644 --- a/Sources/Winget-AutoUpdate/mods/README.md +++ b/Sources/Winget-AutoUpdate/mods/README.md @@ -24,13 +24,15 @@ The **-install** mod will be used for upgrades too if **-upgrade** doesn't exist `AppID-install.ps1` is recommended because it's used in **both** scenarios. +If **AppID**`-preinstall.ps1`/`-preuninstall.ps1` returns `$false`, the install/update/uninstall for that **AppID** is skipped (checking if an App is running, etc...). + A script **Template** for an all-purpose mod (`_WAU-notinstalled-template.ps1`) is included in which actions can be taken if an upgrade/install fails for any **AppID** (any individual `AppID-notinstalled.ps1` overrides this global one) Name it `_WAU-notinstalled.ps1` for activation ### Winget native parameters: Another finess is the **AppID** followed by the `-override` or `-custom` suffix as a **text file** (**.txt**). > Example: -> **Canneverbe.CDBurnerXP-override.txt** with the content `ADDLOCAL=All REMOVE=Desktop_Shortcut /qn` +> **Adobe.Acrobat.Reader.64-bit-override.txt** with the content `"-sfx_nu /sAll /rs /msi EULA_ACCEPT=YES DISABLEDESKTOPSHORTCUT=1"` > Example: > **ShareX.ShareX-custom.txt** with the content `/MERGETASKS=!CreateDesktopIcon` diff --git a/Sources/Winget-AutoUpdate/mods/_AppID-template.ps1 b/Sources/Winget-AutoUpdate/mods/_AppID-template.ps1 index 265f87c45..4aec7f8d3 100644 --- a/Sources/Winget-AutoUpdate/mods/_AppID-template.ps1 +++ b/Sources/Winget-AutoUpdate/mods/_AppID-template.ps1 @@ -20,6 +20,10 @@ $RunSystem = "" $RunSwitch = "" $RunWait = $True +# Beginning of Process Name to check for if running - optional wildcard (*) after, without .exe, multiple: "proc1*","proc2" +# If found, it will return $False (to $preInstall-/UninstallResult), stop this script and request for the main script to skip the app. +$SkipApp = @("") + # Beginning of Process Name to Stop - optional wildcard (*) after, without .exe, multiple: "proc1*","proc2" $Proc = @("") @@ -100,6 +104,10 @@ $User = $True if ($RunSystem) { Invoke-ModsApp $RunSystem $RunSwitch $RunWait "" } +if ($SkipApp) { + $result = Skip-ModsProc $SkipApp + if ($result -eq $true) { return $false } +} if ($Proc) { Stop-ModsProc $Proc } diff --git a/Sources/Winget-AutoUpdate/mods/_Mods-Functions.ps1 b/Sources/Winget-AutoUpdate/mods/_Mods-Functions.ps1 index 430b27752..e532dc931 100644 --- a/Sources/Winget-AutoUpdate/mods/_Mods-Functions.ps1 +++ b/Sources/Winget-AutoUpdate/mods/_Mods-Functions.ps1 @@ -18,6 +18,15 @@ function Invoke-ModsApp ($Run, $RunSwitch, $RunWait, $User) { Return } +function Skip-ModsProc ($SkipApp) { + foreach ($process in $SkipApp) { + $running = Get-Process -Name $process -ErrorAction SilentlyContinue + if ($running) { + Return $true + } + } + Return +} function Stop-ModsProc ($Proc) { foreach ($process in $Proc) { @@ -25,6 +34,7 @@ function Stop-ModsProc ($Proc) { } Return } + function Stop-ModsSvc ($Svc) { foreach ($service in $Svc) { Stop-Service -Name $service -Force -ErrorAction SilentlyContinue | Out-Null diff --git a/Sources/Winget-AutoUpdate/mods/_WAU-mods-template.ps1 b/Sources/Winget-AutoUpdate/mods/_WAU-mods-template.ps1 index c85e67070..558f5af66 100644 --- a/Sources/Winget-AutoUpdate/mods/_WAU-mods-template.ps1 +++ b/Sources/Winget-AutoUpdate/mods/_WAU-mods-template.ps1 @@ -1,11 +1,98 @@ -<# Mods for WAU (if Network is active/any Winget is installed/running as SYSTEM) -Winget-Upgrade.ps1 calls this script with the code: -[Write-ToLog "Running Mods for WAU..." "Yellow" -& "$Mods\_WAU-mods.ps1"] -Make sure your Functions have unique names! -Exit 1 to Re-run WAU from this script (beware of loops)! +<# +.SYNOPSIS + Custom modifications for Winget-AutoUpdate (WAU) + Runs if Network is active/any Winget is installed/running as SYSTEM + + If mods\_WAU-mods.ps1 exist: Winget-Upgrade.ps1 calls this script with the code: + [Write-ToLog "Running Mods for WAU..." "Cyan" + + # Capture both output and exit code + $ModsOutput = & "$Mods\_WAU-mods.ps1" 2>&1 | Out-String + $ModsExitCode = $LASTEXITCODE] + +.DESCRIPTION + This script runs before the main WAU process and can control WAU execution + by returning a JSON object with action instructions. + + The script should output a JSON object with the following structure: + { + "Action": "string", // Required: Action for WAU to perform + "Message": "string", // Optional: Message to write to WAU log + "LogLevel": "string", // Optional: Log level for the message + "ExitCode": number, // Optional: Windows installer exit code for reference + "PostponeDuration": number, // Optional: Postpone duration in hours before running WAU again (default 1 hour) + "RebootDelay": number, // Optional: Delay in minutes before rebooting (default 5 minutes) + "RebootHandler": string // Optional: "SCCM" or "Windows" (default "Windows") to specify reboot handler + } + + Available Actions: + - "Continue" : Continue with normal WAU execution (default behavior) + - "Abort" : Abort WAU execution completely + - "Postpone" : Postpone WAU execution temporarily with 'PostponeDuration' hours + - "Rerun" : Re-run WAU (equivalent to legacy exit code 1) + - "Reboot" : Restart the system with delay and notification to end user + + Available LogLevels: + - "White" : Default/normal message + - "Green" : Success message + - "Yellow" : Warning message + - "Red" : Error message + - "Cyan" : Information message + - "Magenta" : Debug message + + Standard Windows Installer Exit Codes (for reference): + - 0 : Success + - 1602 : User cancelled installation + - 1618 : Another installation is in progress + - 3010 : Restart required (SCCM Soft Reboot) + - 1641 : Restart initiated by installer (SCCM Hard Reboot) + + Examples: + + # Example 1: Abort on specific day + $result = @{ + Action = "Abort" + Message = "WAU disabled on maintenance day" + LogLevel = "Yellow" + ExitCode = 1602 + } | ConvertTo-Json -Compress + + # Example 2: Postpone WAU execution + $result = @{ + Action = "Postpone" + Message = "WAU postponed due to maintenance schedule" + LogLevel = "Yellow" + ExitCode = 1602 + PostponeDuration = 2 # Optional: Postpone WAU execution for 2 hours (default is 1 hour) + } | ConvertTo-Json -Compress + + # Example 3: Continue normally + $result = @{ + Action = "Continue" + Message = "All checks passed, proceeding with updates" + LogLevel = "Green" + } | ConvertTo-Json -Compress + + # Example 4: Request reboot after checks + $result = @{ + Action = "Reboot" + Message = "The system needs to reboot within 15 minutes before WAU updates can be performed." + LogLevel = "Red" + ExitCode = 1641 # Optional: Use 1641 for SCCM Hard Reboot (default is 3010 for Soft Reboot) + RebootDelay = 15 # Optional: Delay before rebooting (default is 5 minutes) + RebootHandler = "SCCM" # Optional: Specify reboot handler (default is "Windows") + } | ConvertTo-Json -Compress + +.NOTES + - This script must always exit with code 0 when using JSON output + - Legacy exit code 1 is still supported for backward compatibility + - Only the first valid JSON object in output will be processed + - If JSON parsing fails, WAU will continue normally + - Make sure your Functions have unique names to avoid conflicts + - Beware of logic loops or long-running operations that may loop indefinitely or block WAU execution! #> + <# FUNCTIONS #> . $PSScriptRoot\_Mods-Functions.ps1 @@ -14,7 +101,115 @@ Exit 1 to Re-run WAU from this script (beware of loops)! <# MAIN #> +# Add your custom logic here + +<# +# Example implementation: Second Tuesday of month check +$today = Get-Date +$firstDayOfMonth = [DateTime]::new($today.Year, $today.Month, 1) +$firstTuesday = $firstDayOfMonth.AddDays((2 - [int]$firstDayOfMonth.DayOfWeek + 7) % 7) +$secondTuesday = $firstTuesday.AddDays(7) + +if ($today.Date -ne $secondTuesday.Date) { + # Not second Tuesday - abort WAU execution + $result = @{ + Action = "Abort" + Message = "Today is not the second Tuesday of the month. WAU execution aborted." + LogLevel = "Yellow" + ExitCode = 1602 # User cancelled + } | ConvertTo-Json -Compress + + Write-Output $result + Exit 0 +} + +# Example: Check if maintenance window is active +$maintenanceStart = Get-Date "02:00" +$maintenanceEnd = Get-Date "04:00" +$currentTime = Get-Date +if ($currentTime -ge $maintenanceStart -and $currentTime -le $maintenanceEnd) { + $result = @{ + Action = "Abort" + Message = "WAU aborted during maintenance window ($($maintenanceStart.ToString('HH:mm')) - $($maintenanceEnd.ToString('HH:mm')))" + LogLevel = "Yellow" + ExitCode = 1602 + } | ConvertTo-Json -Compress + + Write-Output $result + Exit 0 +} -Write-ToLog "...nothing to do!" "Green" +# Example: Check available disk space +$systemDrive = Get-PSDrive -Name ($env:SystemDrive.TrimEnd(':')) +$freeSpaceGB = [math]::Round($systemDrive.Free / 1GB, 2) +$minimumSpaceGB = 5 + +if ($freeSpaceGB -lt $minimumSpaceGB) { + $result = @{ + Action = "Abort" + Message = "Insufficient disk space: ${freeSpaceGB}GB available, ${minimumSpaceGB}GB required" + LogLevel = "Red" + ExitCode = 1618 # Another installation is in progress (or system busy) + } | ConvertTo-Json -Compress + + Write-Output $result + Exit 0 +} + +# Example: Check Windows Update registry keys for installation status +$wuInProgress = $false +$wuKeys = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Install", + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending" +) + +foreach ($key in $wuKeys) { + if (Test-Path $key) { + $lastInstall = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue + if ($lastInstall -and $lastInstall.PSObject.Properties.Name -contains "LastSuccessTime" -and $lastInstall.LastSuccessTime) { + try { + $lastSuccessTime = [DateTime]$lastInstall.LastSuccessTime + # If the last successful install was within the last 30 minutes, consider WU in progress + if ((Get-Date).AddMinutes(-30) -lt $lastSuccessTime) { + $wuInProgress = $true + break + } + } + catch { + # Failed to parse date, skip this check + continue + } + } + } +} + +# Check if Windows Update service is running +$wuInProgress = $wuInProgress -or (Get-Service -Name "wuauserv" -ErrorAction SilentlyContinue).Status -eq "Running" + +# Check for specific Windows Update processes (TiWorker and TrustedInstaller are strong indicators) +$wuInProgress = $wuInProgress -or (Get-Process -Name "TiWorker","TrustedInstaller" -ErrorAction SilentlyContinue).Count -gt 0 + +if ($wuInProgress) { + $result = @{ + Action = "Postpone" + Message = "Windows Update is currently installing. WAU postponed for 2 hours." + LogLevel = "Yellow" + ExitCode = 1618 + PostponeDuration = 2 + } | ConvertTo-Json -Compress + + Write-Output $result + Exit 0 +} + +# All checks passed - continue with normal WAU execution +$result = @{ + Action = "Continue" + Message = "Second Tuesday check passed. No maintenance window. Sufficient disk space (${freeSpaceGB}GB). No Windows Update in progress. Continuing with WAU execution." + LogLevel = "Green" +} | ConvertTo-Json -Compress + +Write-Output $result Exit 0 +#> diff --git a/Sources/Wix/build.wxs b/Sources/Wix/build.wxs index bb0f964e7..04a652f71 100644 --- a/Sources/Wix/build.wxs +++ b/Sources/Wix/build.wxs @@ -83,6 +83,10 @@ + + + + @@ -196,6 +200,7 @@ + @@ -257,6 +262,7 @@ + @@ -330,6 +336,9 @@ + + +