From b81371a504d3b02dc1e65729f067aab0659e18cf Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 09:09:49 -0400 Subject: [PATCH 1/9] feat: Add PowerShell script for Windows Update management --- Installer/scripts/ManageWindowsUpdate.ps1 | 193 ++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 Installer/scripts/ManageWindowsUpdate.ps1 diff --git a/Installer/scripts/ManageWindowsUpdate.ps1 b/Installer/scripts/ManageWindowsUpdate.ps1 new file mode 100644 index 0000000000..23b3f4397f --- /dev/null +++ b/Installer/scripts/ManageWindowsUpdate.ps1 @@ -0,0 +1,193 @@ +#Requires -Version 5.0 +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [string]$KBArticleID, + + [Parameter(Mandatory = $true)] + [ValidateSet("CheckInstalled", "TriggerInstall")] + [string]$Action +) + +function Write-LogOutput { + param ([string]$Message) + Write-Output "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') PS: $Message" +} + +function Test-KBInstalled { + param ([string]$KBID) + Write-LogOutput "Checking if $KBID is installed." + try { + $hotfix = Get-HotFix -Id $KBID -ErrorAction SilentlyContinue + if ($hotfix) { + Write-LogOutput "$KBID is installed." + Exit 0 # Installed + } + else { + Write-LogOutput "$KBID is not installed." + Exit 1 # Not Installed + } + } + catch { + Write-LogOutput "Error checking for $KBID using Get-HotFix: $($_.Exception.Message)" + # Fallback to WMI check if Get-HotFix fails (e.g. on older systems or if service is off) + try { + Write-LogOutput "Attempting WMI check for $KBID." + $wmiHotfix = Get-WmiObject -Class Win32_QuickFixEngineering -Filter "HotFixID = '$KBID'" -ErrorAction SilentlyContinue + if ($wmiHotfix) { + Write-LogOutput "$KBID found via WMI." + Exit 0 # Installed + } + else { + Write-LogOutput "$KBID not found via WMI." + Exit 1 # Not Installed + } + } + catch { + Write-LogOutput "Error checking for $KBID using WMI: $($_.Exception.Message)" + Exit 2 # Error during check + } + } +} + +function Invoke-KBInstall { + param ([string]$KBID) + Write-LogOutput "Attempting to trigger installation for $KBID via Windows Update service." + + try { + Write-LogOutput "Creating Update Session..." + $updateSession = New-Object -ComObject Microsoft.Update.Session + $updateSearcher = $updateSession.CreateUpdateSearcher() + + Write-LogOutput "Searching for update $KBID..." + # Search criteria for a specific KB article. + # May need refinement if the KBID isn't directly searchable this way, + # or if it's part of a cumulative update. + # Using a general search for the KBID in the title or description. + $searchCriteria = "IsInstalled=0 and Type='Software' and BrowseOnly=0 and SearchHidden=1" + # It's often better to search for "KBxxxxxxx" within the title or description if a direct ID search fails. + # $searchCriteria = "IsInstalled=0 and Type='Software' and Title like '%$KBID%'" + # For more specific targeting, one might need to know if it's a security update, critical update etc. + + $searchResult = $updateSearcher.Search($searchCriteria) + + if ($searchResult.Updates.Count -eq 0) { + Write-LogOutput "No applicable updates found for $KBID with current criteria. It might be already installed, superseded, or not applicable." + # Check if it's already installed as a fallback, as search criteria might miss it. + $hotfixCheck = Get-HotFix -Id $KBID -ErrorAction SilentlyContinue + if ($hotfixCheck) { + Write-LogOutput "$KBID appears to be already installed (found by Get-HotFix during trigger)." + Exit 0 # Already installed + } + Exit 3 # Update not found or not applicable + } + + $updatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl + $updateFound = $false + + foreach ($update in $searchResult.Updates) { + # Iterate through KBNames if available, or check title/description + $kbMatch = $false + if ($update.KBArticleIDs) { + foreach ($kb in $update.KBArticleIDs) { + if ($kb -eq $KBID.Replace("KB", "")) { $kbMatch = $true; break } + } + } + if (-not $kbMatch -and ($update.Title -like "*$KBID*" -or ($update.Description -and $update.Description -like "*$KBID*"))) { + $kbMatch = $true + } + + if ($kbMatch) { + Write-LogOutput "Found update: $($update.Title)" + if ($update.IsDownloaded) { + Write-LogOutput "Update '$($update.Title)' is already downloaded." + } + else { + Write-LogOutput "Adding update '$($update.Title)' to download list." + } + $updatesToDownload.Add($update) | Out-Null + $updateFound = $true + # Typically, we'd install one specific update if found. + # If multiple updates match a loose criteria, this logic might need adjustment. + # For a specific KB, we usually expect one primary match. + break + } + } + + if (-not $updateFound) { + Write-LogOutput "Specific update $KBID not found in search results, though other updates were listed." + Exit 3 # Update not found + } + + if ($updatesToDownload.Count -eq 0) { + Write-LogOutput "No updates matching $KBID require downloading or installation (already downloaded or not found)." + Exit 0 # Or appropriate code if it means already installed/not applicable + } + + $needsDownload = $false + foreach ($updateToInstall in $updatesToDownload) { + if (-not $updateToInstall.IsDownloaded) { + $needsDownload = $true + break + } + } + + if ($needsDownload) { + Write-LogOutput "Downloading updates..." + $downloader = $updateSession.CreateUpdateDownloader() + $downloader.Updates = $updatesToDownload + $downloader.Download() | Out-Null + Write-LogOutput "Download Result: $($downloader.GetProgress().ResultCode)" # Log download result + } + else { + Write-LogOutput "All required updates are already downloaded." + } + + Write-LogOutput "Installing updates..." + $installer = $updateSession.CreateUpdateInstaller() + $installer.Updates = $updatesToDownload + + # For non-interactive install, this should be fine. + # If user interaction is somehow triggered, this might hang in a hidden window. + # The ewNoWait in InnoSetup should help if this call blocks for too long. + $installationResult = $installer.Install() + + Write-LogOutput "Installation Result Code: $($installationResult.ResultCode)" + Write-LogOutput "Reboot Required: $($installationResult.RebootRequired)" + + if ($installationResult.ResultCode -eq 2) { + # or suSucceeded + Write-LogOutput "Installation of $KBID appears successful." + Exit 0 # Success + } + else { + Write-LogOutput "Installation of $KBID may have failed or requires attention. ResultCode: $($installationResult.ResultCode)" + Exit 4 # Installation failed or other issue + } + + } + catch { + $exceptionMessage = $_.Exception.Message + $scriptStackTraceFull = $_.ScriptStackTrace + Write-LogOutput ("Error during Windows Update operations for {0}: {1}" -f $KBID, $exceptionMessage) + Write-LogOutput ("ScriptStackTrace: {0}" -f $scriptStackTraceFull) + Exit 5 # Error during update process + } +} + + +# Main script logic +Write-LogOutput "ManageWindowsUpdate.ps1 called with Action: $Action, KBArticleID: $KBArticleID" + +switch ($Action) { + "CheckInstalled" { + Test-KBInstalled -KBID $KBArticleID + } + "TriggerInstall" { + Invoke-KBInstall -KBID $KBArticleID + } + default { + Write-LogOutput "Invalid action: $Action" + Exit 99 # Invalid action + } +} \ No newline at end of file From f65b43838cd22ea5121284175e0d903210ca5bda Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 09:10:01 -0400 Subject: [PATCH 2/9] feat: Integrate Windows Update check into InnoSetup installer --- Installer/scripts/windows_update_helper.iss | 172 ++++++++++++++++++++ Installer/setup.iss | 23 +++ 2 files changed, 195 insertions(+) create mode 100644 Installer/scripts/windows_update_helper.iss diff --git a/Installer/scripts/windows_update_helper.iss b/Installer/scripts/windows_update_helper.iss new file mode 100644 index 0000000000..ddef2b36d0 --- /dev/null +++ b/Installer/scripts/windows_update_helper.iss @@ -0,0 +1,172 @@ +#ifndef WINDOWS_UPDATE_HELPER_ISS +#define WINDOWS_UPDATE_HELPER_ISS + +[Code] +const + KBID_TO_CHECK = 'KB5053606'; + LTSC_PRODUCT_NAME_1 = 'Windows 10 Enterprise LTSC'; + LTSC_PRODUCT_NAME_2 = 'Windows 10 Enterprise LTSB'; // Older naming + +var + PowerShellExePath_WU: String; // Added _WU suffix to avoid potential global name clashes + +procedure LogMsg_WU(Msg: String); // Added _WU suffix +begin + Log(GetDateTimeString('yyyy-mm-dd hh:nn:ss', '-', ':') + ' [WUHelper]: ' + Msg); +end; + +function IsWin10LTSC_WU(): Boolean; // Added _WU suffix +var + ProductName: String; +begin + Result := False; // Default to False + if RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Windows NT\CurrentVersion', 'ProductName', ProductName) then + begin + LogMsg_WU('Detected OS ProductName: ' + ProductName); + if (Pos(LTSC_PRODUCT_NAME_1, ProductName) > 0) or (Pos(LTSC_PRODUCT_NAME_2, ProductName) > 0) then + begin + LogMsg_WU('System is Windows 10 LTSC.'); + Result := True; + end + else + begin + LogMsg_WU('System is not Windows 10 LTSC.'); + end; + end + else + begin + LogMsg_WU('Error: Could not read ProductName from registry.'); + end; +end; + +function FindPowerShell_WU(): String; // Added _WU suffix +var + SystemDir: String; + TestPath: String; +begin + Result := ''; + SystemDir := ExpandConstant('{sys}'); + TestPath := SystemDir + '\WindowsPowerShell\v1.0\powershell.exe'; + if FileExists(TestPath) then + begin + Result := TestPath; + LogMsg_WU('Found PowerShell at: ' + Result); + Exit; + end; + + // Fallback to PATH + LogMsg_WU('PowerShell not found in explicit system path, will rely on PATH for powershell.exe'); + Result := 'powershell.exe'; +end; + +function IsKBInstalledPS_WU(KBID: String): Boolean; // Added _WU suffix +var + ResultCode: Integer; + Cmd: String; +begin + Result := False; + if PowerShellExePath_WU = '' then + begin + LogMsg_WU('PowerShell executable path not determined. Cannot check KB.'); + Exit; + end; + + Cmd := AddQuotes(PowerShellExePath_WU) + ' -NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{tmp}') + '\ManageWindowsUpdate.ps1" -KBArticleID "' + KBID + '" -Action CheckInstalled'; + LogMsg_WU('Executing: ' + Cmd); + + if Exec(Cmd, '', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + begin + LogMsg_WU('PowerShell script (CheckInstalled) executed. Exit Code: ' + IntToStr(ResultCode)); + if ResultCode = 0 then // 0 means installed + begin + Result := True; + end + else if ResultCode = 1 then // 1 means not installed + begin + Result := False; + end + else + begin + LogMsg_WU('PowerShell script (CheckInstalled) for ' + KBID + ' returned an error or unexpected code: ' + IntToStr(ResultCode)); + Result := False; + end; + end + else + begin + LogMsg_WU('Failed to execute PowerShell script (CheckInstalled) for ' + KBID + '. Last Error Code (from Exec): ' + IntToStr(ResultCode)); + Result := False; + end; +end; + +procedure AttemptKBInstallPS_WU(KBID: String); // Added _WU suffix +var + ResultCode: Integer; + Cmd: String; +begin + if PowerShellExePath_WU = '' then + begin + LogMsg_WU('PowerShell executable path not determined. Cannot attempt KB install.'); + Exit; + end; + + Cmd := AddQuotes(PowerShellExePath_WU) + ' -NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{tmp}') + '\ManageWindowsUpdate.ps1" -KBArticleID "' + KBID + '" -Action TriggerInstall'; + LogMsg_WU('Attempting to trigger KB install (no wait): ' + Cmd); + + if Exec(Cmd, '', '', SW_HIDE, ewNoWait, ResultCode) then + begin + LogMsg_WU('PowerShell script (TriggerInstall) for ' + KBID + ' launched.'); + end + else + begin + LogMsg_WU('Failed to launch PowerShell script (TriggerInstall) for ' + KBID + '. Last Error Code (from Exec): ' + IntToStr(ResultCode)); + end; +end; + +// This InitializeSetup function will be called by Inno Setup. +// Ensure no other InitializeSetup exists or rename this one and call it from the main InitializeSetup. +// For this approach, we assume this IS the main InitializeSetup or it's called by it. +// If setup.iss already has an InitializeSetup, this needs to be integrated. +// For now, let's assume this will be the primary logic or called by the primary. + +function InitializeSetup_WindowsUpdatePrerequisites(): Boolean; +begin + PowerShellExePath_WU := FindPowerShell_WU(); + if PowerShellExePath_WU = 'powershell.exe' then // Relying on PATH + begin + LogMsg_WU('PowerShell.exe path set to "powershell.exe", relying on system PATH.'); + end + else if not FileExists(PowerShellExePath_WU) then // Explicit path not found + begin + LogMsg_WU('Critical: PowerShell.exe could not be definitively located via explicit path: ' + PowerShellExePath_WU + '. Update checks might fail.'); + PowerShellExePath_WU := ''; // Prevent attempts if not found explicitly + end; + + LogMsg_WU('Starting Windows Update prerequisite check.'); + if IsWin10LTSC_WU() then + begin + if PowerShellExePath_WU <> '' then // Only proceed if PowerShell is found + begin + if not IsKBInstalledPS_WU(KBID_TO_CHECK) then + begin + AttemptKBInstallPS_WU(KBID_TO_CHECK); + end + else + begin + LogMsg_WU(KBID_TO_CHECK + ' is already installed, or an error occurred during check (details above).'); + end; + end + else + begin + LogMsg_WU('Skipping KB check/install for ' + KBID_TO_CHECK + ' because PowerShell was not found.'); + end; + end + else + begin + LogMsg_WU('System is not Windows 10 LTSC. Skipping update check for ' + KBID_TO_CHECK + '.'); + end; + + LogMsg_WU('Windows Update prerequisite check finished.'); + Result := True; // Always allow main installation to proceed +end; + +#endif // WINDOWS_UPDATE_HELPER_ISS \ No newline at end of file diff --git a/Installer/setup.iss b/Installer/setup.iss index 678cd33d4d..4ca546743e 100644 --- a/Installer/setup.iss +++ b/Installer/setup.iss @@ -80,6 +80,7 @@ Name: deletefiles; Description: "{cm:ExistingSettings}"; GroupDescription: "{cm: Source: "{#ExeDir}SoundSwitch.exe"; DestDir: "{app}"; Flags: signonce ignoreversion Source: "{#ExeDir}SoundSwitch.CLI.exe"; DestDir: "{app}"; Flags: signonce ignoreversion Source: "{#ExeDir}*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion; +Source: "scripts\ManageWindowsUpdate.ps1"; DestDir: "{tmp}"; Flags: confirmoverwrite deleteafterinstall [Registry] Root: HKCU; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run\{#MyAppSetupName}"; Flags: uninsdeletekey @@ -120,5 +121,27 @@ Type: files; Name: {app}\Microsoft.WindowsAPICodePack.* #include "scripts\command_line_utils.iss" #include "scripts\setup_utils.iss" #include "scripts\uninstall_utils.iss" +#include "scripts\windows_update_helper.iss" + +[Code] +function InitializeSetup(): Boolean; +begin + // Call the prerequisite checker from the helper script + // InitializeSetup_WindowsUpdatePrerequisites is defined in windows_update_helper.iss + if InitializeSetup_WindowsUpdatePrerequisites() then + begin + // Prerequisite check passed (or silently continued as per its design) + // Add any other main InitializeSetup logic here if needed in the future. + // For now, just return True as the helper handles its own logic and always returns True. + Result := True; + end + else + begin + // This path should ideally not be taken if the helper function is designed to always return True. + // However, as a robust measure: + Log('Main InitializeSetup: Call to InitializeSetup_WindowsUpdatePrerequisites returned False. This is unexpected given the silent failure design.'); + Result := True; // Still proceed with installation as per overall "silent failure" requirement. + end; +end; From e184772b3a18051498639dd34058a3cdb2d6c32b Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 09:18:52 -0400 Subject: [PATCH 3/9] docs: Add Windows Update check for KB5053606 in InnoSetup installer --- .roo/win-10-ltsc-Win-Update-Plan.md | 105 ++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .roo/win-10-ltsc-Win-Update-Plan.md diff --git a/.roo/win-10-ltsc-Win-Update-Plan.md b/.roo/win-10-ltsc-Win-Update-Plan.md new file mode 100644 index 0000000000..1140cd2fba --- /dev/null +++ b/.roo/win-10-ltsc-Win-Update-Plan.md @@ -0,0 +1,105 @@ +# Plan: Integrate Windows Update Check (KB5053606) into SoundSwitch InnoSetup Installer + +**Overall Goal:** +The installer will first check if the operating system is Windows 10 LTSC. If it is, it will then use a PowerShell script to check if Windows Update KB5053606 is installed. If the update is not installed, the same PowerShell script will be invoked to attempt to trigger its installation via the Windows Update service. All operations and their outcomes will be logged to the InnoSetup log file. The main SoundSwitch installation will proceed regardless of the outcome of these update checks or installation attempts (silent failure with logging). + +**Detailed Plan:** + +1. **Helper Script (PowerShell):** + + - **Filename:** `ManageWindowsUpdate.ps1` + - **Location:** To be placed in `Installer/scripts/` relative to `setup.iss`. + - **Functionality:** + - Accepts parameters: + - `-KBArticleID ` (e.g., "KB5053606") + - `-Action ` (e.g., "CheckInstalled" or "TriggerInstall") + - **If `Action` is "CheckInstalled":** + - Uses `Get-HotFix -Id $KBArticleID` or queries WMI (`Get-WmiObject -Class Win32_QuickFixEngineering -Filter "HotFixID='$KBArticleID'"`) to check if the specified KB is installed. + - Exits with code `0` if found. + - Exits with code `1` if not found. + - Exits with other codes for errors (e.g., `2` if `Get-HotFix` fails or WMI query fails). + - Logs its findings (e.g., using `Write-Output` for InnoSetup to capture, or to a dedicated log if more detail is needed from the PS script). + - **If `Action` is "TriggerInstall":** + - Uses the Windows Update Agent API (e.g., `Microsoft.Update.Session`, `Microsoft.Update.Searcher`, `IUpdateDownloader`, `IUpdateInstaller2`) to search for the specific KB, download it if necessary, and initiate the installation. + - Logs its progress and outcome. + - This action should be designed to run without blocking the InnoSetup installer for too long (e.g., by initiating the update and exiting). + +2. **Inno Setup Script (`Installer/setup.iss`) Modifications:** + + - **`[Files]` Section:** + + - Add an entry to include `ManageWindowsUpdate.ps1`. It will be extracted to `{tmp}` during setup and deleted after installation. + + ```iss + [Files] + ; ... existing files ... + Source: "scripts\ManageWindowsUpdate.ps1"; DestDir: "{tmp}"; Flags: confirmoverwrite deleteafterinstall + ``` + + - **`[Code]` Section:** + - **Constants:** + ```pascal + const + KBID_TO_CHECK = 'KB5053606'; + LTSC_PRODUCT_NAME_1 = 'Windows 10 Enterprise LTSC'; + LTSC_PRODUCT_NAME_2 = 'Windows 10 Enterprise LTSB'; // Older naming + ``` + - **Logging Function:** + ```pascal + procedure LogMsg(Msg: String); + begin + Log(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ': ' + Msg); + end; + ``` + - **`IsWin10LTSC(): Boolean;` Function:** + - Reads `ProductName` from `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`. + - Checks if it contains `LTSC_PRODUCT_NAME_1` or `LTSC_PRODUCT_NAME_2`. + - Logs the detected OS and the result of the LTSC check. + - Returns `True` if LTSC, `False` otherwise. Logs an error and returns `False` if the registry key cannot be read. + - **`IsKBInstalledPS(KBID: String): Boolean;` Function:** + - Locates `powershell.exe`. + - Constructs the command: `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{tmp}\ManageWindowsUpdate.ps1" -KBArticleID "" -Action CheckInstalled`. + - Executes this command hidden using `Exec` and waits for it to terminate. + - Checks the `ExitCode`. `0` means installed (`True`). `1` means not installed (`False`). Any other code means an error occurred (treat as `False`, log the actual exit code). + - Logs the command executed and the outcome. + - **`AttemptKBInstallPS(KBID: String);` Procedure:** + - Locates `powershell.exe`. + - Constructs the command: `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{tmp}\ManageWindowsUpdate.ps1" -KBArticleID "" -Action TriggerInstall`. + - Executes this command hidden using `Exec` with the `ewNoWait` flag. + - Logs the attempt to trigger the installation. + - **`InitializeSetup(): Boolean;` Event Function:** + - This function is called when setup is initializing. + - Calls `LogMsg('Starting Windows Update prerequisite check.')`. + - Calls `IsWin10LTSC()`. + - If LTSC is detected: + - Calls `IsKBInstalledPS(KBID_TO_CHECK)`. + - If the KB is not installed, calls `AttemptKBInstallPS(KBID_TO_CHECK)`. + - Else (not LTSC): + - Calls `LogMsg('System is not Windows 10 LTSC. Skipping update check for ' + KBID_TO_CHECK + '.')`. + - All results and error conditions encountered by these functions will be logged. + - _Always_ returns `True` to allow the SoundSwitch installation to proceed. + - Calls `LogMsg('Windows Update prerequisite check finished. Proceeding with main installation.')`. + - Ensure `SetupLogging=yes` is present in the `[Setup]` section. + +**Mermaid Diagram of the Logic:** + +```mermaid +graph TD + A[Installer Starts] --> LogInit[Log: Begin Update Check]; + LogInit --> B{Call IsWin10LTSC()}; + B -- True --> LogLTSC[Log: LTSC Detected]; + LogLTSC --> C{Call IsKBInstalledPS('KB5053606') via ManageWindowsUpdate.ps1 -Action CheckInstalled}; + B -- False --> LogNotLTSC[Log: Not LTSC / Skip Update Check]; + LogNotLTSC --> Z[Proceed with SoundSwitch Installation]; + C -- Exit Code 0 (Installed) --> LogKBFound[Log: KB5053606 Installed]; + LogKBFound --> Z; + C -- Exit Code 1 (Not Installed) --> LogKBNotFound[Log: KB5053606 Not Installed]; + LogKBNotFound --> D[Call AttemptKBInstallPS('KB5053606') via ManageWindowsUpdate.ps1 -Action TriggerInstall]; + D --> LogAttempt[Log: Attempted to Trigger KB Install]; + LogAttempt --> Z; + C -- Other Exit Codes (Error) --> LogKBCheckError[Log: Error checking KB5053606 (Exit Code: )]; + LogKBCheckError --> Z; + B -- Error During LTSC Check --> LogLTSCError[Log: Error Checking LTSC]; + LogLTSCError --> Z; + Z --> LogFinal[Log: Update Check Finished. Proceeding with Main Install.] +``` From 12324fef97167209f87faaa6a54fcbbaf4385fbb Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 09:36:29 -0400 Subject: [PATCH 4/9] feat: add workflow for testing installer build without signing --- .github/workflows/test-installer-build.yml | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/test-installer-build.yml diff --git a/.github/workflows/test-installer-build.yml b/.github/workflows/test-installer-build.yml new file mode 100644 index 0000000000..c83f45b5f8 --- /dev/null +++ b/.github/workflows/test-installer-build.yml @@ -0,0 +1,29 @@ +name: Test Installer Build (No Sign) + +on: + push: + paths: + - "Installer/**" + pull_request: + paths: + - "Installer/**" + +jobs: + build-installer-no-sign: + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Inno Setup + run: winget install --id=JRSoftware.InnoSetup --exact --source winget --accept-package-agreements --accept-source-agreements + shell: pwsh + + - name: Modify SignTool in setup.iss + run: | + powershell -Command "(Get-Content -Path Installer/setup.iss) -replace 'SignTool=Certum', 'SignTool=none' | Set-Content -Path Installer/setup.iss" + shell: pwsh + + - name: Build Installer + run: .\\Installer\\Make-Installer.bat Nightly + shell: cmd From 4de3f3659574e0ad51ff05801bd5498dca06c214 Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 09:38:16 -0400 Subject: [PATCH 5/9] ci(installer): test installer fix: update runner version to windows-2025 in test installer build workflow fix: install Inno Setup with machine scope in workflow fix: use dummy script for SignTool in installer test workflow feat: pass SignTool override to Make-Installer.bat from workflow feat: update Make.bat and workflow for SignTool override and .NET 9 ci: enable dotnet cache in test installer workflow fix: add cache-dependency-path for .NET 9.0 setup in test installer workflow ci: add SoundSwitch application build step before installer build feat: add verbose output to Make-Installer.bat script fix: properly handle parameters with special characters in batch scripts fix: use direct parameter passing to avoid variable expansion issues in Make-Installer.bat fix: simplify parameter handling by constructing /SCertum= in Make-Installer.bat ci: add asset preparation step in test installer build workflow ci: update concurrency settings for test installer build workflow ci: fix signtool ci: modify setup.iss in workflow to remove signing for test builds - Add step to remove SignTool, SignedUninstaller, and signonce flags from setup.iss - Simplify Make-Installer.bat to only accept release state parameter - Eliminates complex parameter passing for signing configuration - Enables clean unsigned installer builds for testing purposes --- .github/scripts/dummy_sign.bat | 2 + .github/workflows/test-installer-build.yml | 67 ++++++++++++++++++++-- Installer/Make-Installer.bat | 8 ++- Make.bat | 2 +- 4 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 .github/scripts/dummy_sign.bat diff --git a/.github/scripts/dummy_sign.bat b/.github/scripts/dummy_sign.bat new file mode 100644 index 0000000000..cc080443d5 --- /dev/null +++ b/.github/scripts/dummy_sign.bat @@ -0,0 +1,2 @@ +@echo off +exit /b 0 \ No newline at end of file diff --git a/.github/workflows/test-installer-build.yml b/.github/workflows/test-installer-build.yml index c83f45b5f8..b67a1ab451 100644 --- a/.github/workflows/test-installer-build.yml +++ b/.github/workflows/test-installer-build.yml @@ -1,7 +1,13 @@ name: Test Installer Build (No Sign) +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: + branches: + - dev paths: - "Installer/**" pull_request: @@ -10,19 +16,70 @@ on: jobs: build-installer-no-sign: - runs-on: windows-latest + runs-on: windows-2025 steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Inno Setup - run: winget install --id=JRSoftware.InnoSetup --exact --source winget --accept-package-agreements --accept-source-agreements + run: winget install --id=JRSoftware.InnoSetup --exact --source winget --scope machine --accept-package-agreements --accept-source-agreements shell: pwsh - - name: Modify SignTool in setup.iss + - name: Setup .NET 9.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "9.0.x" + cache: true + cache-dependency-path: Directory.Packages.props + + - name: Build SoundSwitch Application run: | - powershell -Command "(Get-Content -Path Installer/setup.iss) -replace 'SignTool=Certum', 'SignTool=none' | Set-Content -Path Installer/setup.iss" - shell: pwsh + echo "Cleaning build directories" + if exist bin rmdir /q /s bin + if exist obj rmdir /q /s obj + if exist Release rmdir /q /s Release + if exist Final rmdir /q /s Final + mkdir Final + + echo "Building SoundSwitch CLI and Main Application" + dotnet publish -c Release SoundSwitch.CLI\SoundSwitch.CLI.csproj -o Final + dotnet publish -c Release SoundSwitch\SoundSwitch.csproj -o Final + shell: cmd + + - name: Prepare Installer Assets + run: | + echo "Generate dummy Changelog" + echo ^^^Dummy Changelog, markdown-html is required^^^ > Final\Changelog.html + + echo "Generate dummy README" + echo ^^^Dummy README, markdown-html is required^^^ > Final\Readme.html + + echo "Copy soundSwitched image" + xcopy /y img\soundSwitched.png Final >nul 2>nul + + echo "Copy CLI README" + xcopy /y SoundSwitch.CLI\README.md Final >nul 2>nul + + echo "Copy LICENSE" + xcopy /y LICENSE.txt Final >nul 2>nul + xcopy /y Terms.txt Final >nul 2>nul + shell: cmd + + - name: Remove Signing from Installer Script + run: | + echo "Removing signing configurations from setup.iss" + + REM Remove SignTool lines + powershell -Command "(Get-Content 'Installer\setup.iss') -replace '^SignTool=.*$', '' | Set-Content 'Installer\setup.iss'" + + REM Remove SignedUninstaller + powershell -Command "(Get-Content 'Installer\setup.iss') -replace '^SignedUninstaller=.*$', '' | Set-Content 'Installer\setup.iss'" + + REM Remove signonce flags from Files section + powershell -Command "(Get-Content 'Installer\setup.iss') -replace ' signonce', '' | Set-Content 'Installer\setup.iss'" + + echo "Signing configurations removed" + shell: cmd - name: Build Installer run: .\\Installer\\Make-Installer.bat Nightly diff --git a/Installer/Make-Installer.bat b/Installer/Make-Installer.bat index 51fd1dde1a..794cfdb1ee 100644 --- a/Installer/Make-Installer.bat +++ b/Installer/Make-Installer.bat @@ -22,14 +22,20 @@ for /f "eol=; tokens=1,2*" %%a in ('REG QUERY "%innoSetupRegistryNode%" /v Insta ) set innoSetupExe="%innoSetupExe%ISCC.exe" if not exist %innoSetupExe% (set errorMessage=Inno Setup not found in %innoSetupExe% & goto ERROR_QUIT) -if not exist "..\Final\Installer" mkdir ..\Final\Installer +if not exist "..\Final\Installer" ( + echo Creating installer output directory: ..\Final\Installer + mkdir ..\Final\Installer +) +echo Cleaning previous installer files: ..\Final\Installer\*Installer.exe del ..\Final\Installer\*Installer.exe echo Building installer... +echo Running Inno Setup: %innoSetupExe% setup.iss /DReleaseState=%1 %innoSetupExe% setup.iss /DReleaseState=%1 if not %ERRORLEVEL%==0 (set errorMessage=Installer script setup.iss failed & goto ERROR_QUIT) +echo Moving installer to final location: ..\Final\*Installer.exe -^> ..\Final\Installer\ move ..\Final\*Installer.exe ..\Final\Installer\ echo Installer created successfully. diff --git a/Make.bat b/Make.bat index ccd06a1942..8b85ff0a3b 100644 --- a/Make.bat +++ b/Make.bat @@ -87,7 +87,7 @@ xcopy /y Terms.txt %finalDir% >nul 2>nul echo Build Installer rem Run installer compiler script -call ./Installer/Make-Installer.bat %buildPlatform% +call ./Installer/Make-Installer.bat %buildPlatform% "%~2" if not %ERRORLEVEL% == 0 (set errorMessage=Make-installer.bat failed or not found & goto ERROR_QUIT) echo. From 97d717b793e6967ef018995a6bfe2d93b4959978 Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 18:36:03 +0000 Subject: [PATCH 6/9] ci: Add silent installation step for installer in machine scope --- .github/workflows/test-installer-build.yml | 26 +++++++++++++++++++++ Installer/scripts/windows_update_helper.iss | 26 +++++++++++++++++---- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-installer-build.yml b/.github/workflows/test-installer-build.yml index b67a1ab451..5487d95348 100644 --- a/.github/workflows/test-installer-build.yml +++ b/.github/workflows/test-installer-build.yml @@ -84,3 +84,29 @@ jobs: - name: Build Installer run: .\\Installer\\Make-Installer.bat Nightly shell: cmd + + - name: Run Installer in Silent Mode (Machine Scope) + run: | + echo "Finding installer executable..." + for /f "delims=" %%i in ('dir /b Final\Installer\*.exe') do set INSTALLER_NAME=%%i + if not defined INSTALLER_NAME ( + echo "ERROR: No installer found in Final\Installer\" + exit /b 1 + ) + echo "Found installer: %INSTALLER_NAME%" + + echo "Running installer in full silent mode for machine scope..." + Final\Installer\%INSTALLER_NAME% /VERYSILENT /ALLUSERS /NORESTART /SUPPRESSMSGBOXES /LOG="Final\Installer\install.log" + + echo "Installation completed. Exit code: %ERRORLEVEL%" + if %ERRORLEVEL% neq 0 ( + echo "Installation failed with exit code %ERRORLEVEL%" + if exist "Final\Installer\install.log" ( + echo "Installation log:" + type "Final\Installer\install.log" + ) + exit /b %ERRORLEVEL% + ) + + echo "Installation successful!" + shell: cmd diff --git a/Installer/scripts/windows_update_helper.iss b/Installer/scripts/windows_update_helper.iss index ddef2b36d0..07754c5145 100644 --- a/Installer/scripts/windows_update_helper.iss +++ b/Installer/scripts/windows_update_helper.iss @@ -131,14 +131,30 @@ end; function InitializeSetup_WindowsUpdatePrerequisites(): Boolean; begin PowerShellExePath_WU := FindPowerShell_WU(); - if PowerShellExePath_WU = 'powershell.exe' then // Relying on PATH + if PowerShellExePath_WU = 'powershell.exe' then // FindPowerShell_WU is relying on PATH begin - LogMsg_WU('PowerShell.exe path set to "powershell.exe", relying on system PATH.'); + LogMsg_WU('PowerShell.exe path determined as "powershell.exe" by FindPowerShell_WU, attempting to use system PATH.'); + // Explicitly test if 'powershell.exe' is resolvable by FileExists (which should check PATH) + if not FileExists(PowerShellExePath_WU) then + begin + LogMsg_WU('Critical: PowerShell.exe (when relying on PATH) was not found by FileExists. Update checks will be skipped.'); + PowerShellExePath_WU := ''; // Mark as unusable + end + else + begin + LogMsg_WU('PowerShell.exe (via PATH) confirmed by FileExists.'); + end; end - else if not FileExists(PowerShellExePath_WU) then // Explicit path not found + else if PowerShellExePath_WU <> '' then // This means FindPowerShell_WU returned an explicit, existing path begin - LogMsg_WU('Critical: PowerShell.exe could not be definitively located via explicit path: ' + PowerShellExePath_WU + '. Update checks might fail.'); - PowerShellExePath_WU := ''; // Prevent attempts if not found explicitly + // This is an explicit path. FindPowerShell_WU already confirmed FileExists. + // Re-check in case the file got deleted since FindPowerShell_WU was called. + if not FileExists(PowerShellExePath_WU) then + begin + LogMsg_WU('Critical: PowerShell.exe was found at explicit path "' + PowerShellExePath_WU + '" but is now missing. Update checks will be skipped.'); + PowerShellExePath_WU := ''; // Mark as unusable + end; + // If it's still present, FindPowerShell_WU already logged its discovery. end; LogMsg_WU('Starting Windows Update prerequisite check.'); From ebb663bd3b4ee3324d3a6c675ffd4e2253dc8b3d Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 18:41:23 +0000 Subject: [PATCH 7/9] chore: cleanup --- .github/scripts/dummy_sign.bat | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .github/scripts/dummy_sign.bat diff --git a/.github/scripts/dummy_sign.bat b/.github/scripts/dummy_sign.bat deleted file mode 100644 index cc080443d5..0000000000 --- a/.github/scripts/dummy_sign.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -exit /b 0 \ No newline at end of file From beaeca3e890738ac0a60ea50ef6c213ee24e79eb Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 18:44:03 +0000 Subject: [PATCH 8/9] fix: refine update search criteria and add sanity check in Invoke-KBInstall function --- Installer/scripts/ManageWindowsUpdate.ps1 | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Installer/scripts/ManageWindowsUpdate.ps1 b/Installer/scripts/ManageWindowsUpdate.ps1 index 23b3f4397f..832dd61066 100644 --- a/Installer/scripts/ManageWindowsUpdate.ps1 +++ b/Installer/scripts/ManageWindowsUpdate.ps1 @@ -64,7 +64,11 @@ function Invoke-KBInstall { # May need refinement if the KBID isn't directly searchable this way, # or if it's part of a cumulative update. # Using a general search for the KBID in the title or description. - $searchCriteria = "IsInstalled=0 and Type='Software' and BrowseOnly=0 and SearchHidden=1" + # Use a more targeted search to reduce the number of updates returned. + # Try to match the KBID in the title, which is usually present for individual updates. + $searchCriteria = "IsInstalled=0 and Type='Software' and Title like '%$KBID%'" + # Note: The Windows Update API does not support direct search by KBArticleID in the query string. + # If $KBID is in the form "KB123456", this will match updates with that KB in the title. # It's often better to search for "KBxxxxxxx" within the title or description if a direct ID search fails. # $searchCriteria = "IsInstalled=0 and Type='Software' and Title like '%$KBID%'" # For more specific targeting, one might need to know if it's a security update, critical update etc. @@ -119,9 +123,10 @@ function Invoke-KBInstall { Exit 3 # Update not found } + # Sanity check - if we reach here, we should have at least one update if ($updatesToDownload.Count -eq 0) { - Write-LogOutput "No updates matching $KBID require downloading or installation (already downloaded or not found)." - Exit 0 # Or appropriate code if it means already installed/not applicable + Write-LogOutput "Unexpected state: updateFound was true but no updates in collection. This should not happen." + Exit 5 # Unexpected error state } $needsDownload = $false From 1e74c3c689976d692e6685b39c9adba3af2f7989 Mon Sep 17 00:00:00 2001 From: Antoine Aflalo <197810+Belphemur@users.noreply.github.com> Date: Fri, 23 May 2025 18:44:57 +0000 Subject: [PATCH 9/9] fix: remove redundant sanity check in Invoke-KBInstall function --- Installer/scripts/ManageWindowsUpdate.ps1 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Installer/scripts/ManageWindowsUpdate.ps1 b/Installer/scripts/ManageWindowsUpdate.ps1 index 832dd61066..a1b8b6a8a8 100644 --- a/Installer/scripts/ManageWindowsUpdate.ps1 +++ b/Installer/scripts/ManageWindowsUpdate.ps1 @@ -123,12 +123,6 @@ function Invoke-KBInstall { Exit 3 # Update not found } - # Sanity check - if we reach here, we should have at least one update - if ($updatesToDownload.Count -eq 0) { - Write-LogOutput "Unexpected state: updateFound was true but no updates in collection. This should not happen." - Exit 5 # Unexpected error state - } - $needsDownload = $false foreach ($updateToInstall in $updatesToDownload) { if (-not $updateToInstall.IsDownloaded) {