From 2d197ce78f93526c319fc9740bd7e3dcd567df5b Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 3 Feb 2026 22:19:49 -0700 Subject: [PATCH 1/4] Require explicit +semver: prefix for version bumps Previous loose patterns matched common words like "major", "fix", and "feature" anywhere in commit messages, causing unintended version bumps. Now only explicit "+semver: major/minor/patch" triggers a bump. Co-Authored-By: Claude Opus 4.5 --- GitVersion.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/GitVersion.yml b/GitVersion.yml index 5f05d68..8d57e14 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -28,9 +28,11 @@ branches: tag: PR increment: Inherit -major-version-bump-message: '(breaking\schange|breaking|major)\b' -minor-version-bump-message: '(adds?|features?|minor)\b' -patch-version-bump-message: '\s?(fix|patch)' +# Version bumps require explicit +semver: prefix in commit messages +# e.g. "+semver: major", "+semver: minor", "+semver: patch" +major-version-bump-message: '\+semver:\s?major' +minor-version-bump-message: '\+semver:\s?minor' +patch-version-bump-message: '\+semver:\s?patch' no-bump-message: '\+semver:\s?(none|skip)' ignore: From b68a633273659a239d30045594e4c89cbd0fc89a Mon Sep 17 00:00:00 2001 From: Chris Taylor <36066223+christaylorcodes@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:48:36 -0600 Subject: [PATCH 2/4] Add WMI dependency service enforcement to agent lifecycle Introduce Confirm-CWAADependencyService to ensure winmgmt (WMI) is set to Automatic startup and Running before install, repair, and service start. WMI underpins agent inventory, scripting, and check-in; when disabled, Get-CimInstance calls silently no-op. Wired into Install/Repair/Start, configurable via $Script:CWAADependencyServiceNames, respects -WhatIf. Reserves event IDs 2030-2039. Includes coverage across PrivateHelpers/Installation/ServiceOps/CrossCutting. Also fix Test-CWAAPort port-availability tests that mocked Invoke-Expression and a bare netstat function while the code called netstat.exe by full path, making the mocks inert. The tests passed only on machines with nothing on the tray port; on a box running the real agent (port 42000 in use) they returned false. Extract the external call into Get-CWAANetstat as a mockable seam and update the three affected tests to mock it. Reconcile CHANGELOG: the released 2.0.0 was mislabeled [1.0.0]; rename it and add the WMI feature under [Unreleased]. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 +- ...utomateAgent.Mocked.CrossCutting.Tests.ps1 | 5 +- ...utomateAgent.Mocked.Installation.Tests.ps1 | 42 +++++-- ...omateAgent.Mocked.PrivateHelpers.Tests.ps1 | 103 ++++++++++++++++++ ...eAutomateAgent.Mocked.ServiceOps.Tests.ps1 | 14 +++ .../Private/Confirm-CWAADependencyService.ps1 | 102 +++++++++++++++++ source/Private/Get-CWAANetstat.ps1 | 24 ++++ source/Private/Initialize/Initialize-CWAA.ps1 | 6 + source/Private/Write-CWAAEventLog.ps1 | 1 + .../Public/InstallUninstall/Install-CWAA.ps1 | 3 + source/Public/Service/Repair-CWAA.ps1 | 4 + source/Public/Service/Start-CWAA.ps1 | 3 + source/Public/Test-CWAAPort.ps1 | 2 +- 13 files changed, 302 insertions(+), 11 deletions(-) create mode 100644 source/Private/Confirm-CWAADependencyService.ps1 create mode 100644 source/Private/Get-CWAANetstat.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ca52a..3e6969e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **WMI dependency service enforcement** — `Confirm-CWAADependencyService` ensures `winmgmt` (WMI) is set to Automatic startup and Running before install, repair, and service start. Configurable via `$Script:CWAADependencyServiceNames`; respects `-WhatIf`. Event IDs 2030–2039. + ### Changed ### Fixed -## [1.0.0] - 2026-02-03 +## [2.0.0] - 2026-02-03 ### Added diff --git a/Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 b/Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 index 4183e88..d83ea84 100644 --- a/Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 +++ b/Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 @@ -95,6 +95,7 @@ Describe 'Pipeline Support' { } Mock Write-CWAAEventLog {} Mock Get-CimInstance { return @() } + Mock Confirm-CWAADependencyService {} # Pipe an object with Server and LocationID — bind via ValueFromPipelineByPropertyName # InstallerToken is provided explicitly (it wouldn't come from Get-CWAAInfo output) @@ -179,8 +180,8 @@ Describe 'Pipeline Support' { It 'accepts Server and TrayPort from piped PSCustomObject' { $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CWAAInfo { [PSCustomObject]@{ TrayPort = '42000' } } - Mock Invoke-Expression { return $null } - function netstat { return @() } + # No process is using the TrayPort: netstat returns nothing matching it. + Mock Get-CWAANetstat { @() } [PSCustomObject]@{ Server = 'automate.example.com'; TrayPort = 42000 } | Test-CWAAPort -Quiet } diff --git a/Tests/ConnectWiseAutomateAgent.Mocked.Installation.Tests.ps1 b/Tests/ConnectWiseAutomateAgent.Mocked.Installation.Tests.ps1 index 29de007..1069ae9 100644 --- a/Tests/ConnectWiseAutomateAgent.Mocked.Installation.Tests.ps1 +++ b/Tests/ConnectWiseAutomateAgent.Mocked.Installation.Tests.ps1 @@ -205,6 +205,7 @@ Describe 'Repair-CWAA' { It 'returns ActionTaken=None with success' { $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CimInstance { @() } + Mock Confirm-CWAADependencyService {} Mock Stop-Process {} Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } } Mock Get-CWAAInfo { @@ -230,6 +231,7 @@ Describe 'Repair-CWAA' { $script:callCount = 0 $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CimInstance { @() } + Mock Confirm-CWAADependencyService {} Mock Stop-Process {} Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } } # First call returns old LastSuccessStatus, subsequent calls return recent @@ -268,6 +270,7 @@ Describe 'Repair-CWAA' { It 'triggers reinstall after failed restart' { $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CimInstance { @() } + Mock Confirm-CWAADependencyService {} Mock Stop-Process {} Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } } # Return old date consistently. The wait loop calls Get-CWAAInfo @@ -308,6 +311,7 @@ Describe 'Repair-CWAA' { It 'attempts a fresh install with provided parameters' { $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CimInstance { @() } + Mock Confirm-CWAADependencyService {} Mock Stop-Process {} Mock Get-Service { return $null } Mock Redo-CWAA {} @@ -323,6 +327,7 @@ Describe 'Repair-CWAA' { It 'reports error when no install settings are available' { $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CimInstance { @() } + Mock Confirm-CWAADependencyService {} Mock Stop-Process {} Mock Get-Service { return $null } Mock Get-CWAAInfo { throw 'Not installed' } @@ -340,6 +345,7 @@ Describe 'Repair-CWAA' { It 'returns error about unreachable server' { $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CimInstance { @() } + Mock Confirm-CWAADependencyService {} Mock Stop-Process {} Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } } Mock Get-CWAAInfo { @@ -364,6 +370,7 @@ Describe 'Repair-CWAA' { It 'reinstalls with the correct server' { $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CimInstance { @() } + Mock Confirm-CWAADependencyService {} Mock Stop-Process {} Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } } Mock Get-CWAAInfo { @@ -383,6 +390,30 @@ Describe 'Repair-CWAA' { $result.Message | Should -Match 'correct server' } } + + Context 'WMI dependency service' { + It 'ensures the dependency service before remediation' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-CimInstance { @() } + Mock Confirm-CWAADependencyService {} + Mock Stop-Process {} + Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } } + Mock Get-CWAAInfo { + [PSCustomObject]@{ + Server = @('automate.example.com') + LastSuccessStatus = (Get-Date).AddMinutes(-30).ToString() + HeartbeatLastSent = (Get-Date).AddMinutes(-15).ToString() + HeartbeatLastReceived = (Get-Date).AddMinutes(-15).ToString() + } + } + Mock Write-CWAAEventLog {} + + Repair-CWAA -InstallerToken 'abc123' -Confirm:$false + + Should -Invoke Confirm-CWAADependencyService -Scope It -Times 1 + } + } + } } # ----------------------------------------------------------------------------- @@ -795,11 +826,8 @@ Describe 'Test-CWAAPort' { It 'returns $true' { $result = InModuleScope 'ConnectWiseAutomateAgent' { Mock Get-CWAAInfo { [PSCustomObject]@{ TrayPort = '42000' } } - # netstat returns no matching output for the port - $env_windir = $env:windir - Mock Invoke-Expression { return $null } - # Mock netstat by ensuring no process is found on the port - function netstat { return @() } + # No process is using the TrayPort: netstat returns nothing matching it. + Mock Get-CWAANetstat { @() } Test-CWAAPort -TrayPort 42000 -Quiet } $result | Should -BeTrue @@ -813,8 +841,8 @@ Describe 'Test-CWAAPort' { Mock Get-CWAAInfoBackup { return $null } Mock Get-Process { [PSCustomObject]@{ ProcessName = 'LTSvc'; Id = 1234 } } Mock Test-Connection { return $true } - # Mock netstat to return a line matching the port with a PID - $Script:MockNetstatOutput = " TCP 0.0.0.0:42000 0.0.0.0:0 LISTENING 1234" + # netstat reports a process (PID 1234) listening on the TrayPort. + Mock Get-CWAANetstat { ' TCP 0.0.0.0:42000 0.0.0.0:0 LISTENING 1234' } # We need to test the output message Test-CWAAPort -TrayPort 42000 -Server 'automate.example.com' 2>&1 diff --git a/Tests/ConnectWiseAutomateAgent.Mocked.PrivateHelpers.Tests.ps1 b/Tests/ConnectWiseAutomateAgent.Mocked.PrivateHelpers.Tests.ps1 index c2e350d..d539df0 100644 --- a/Tests/ConnectWiseAutomateAgent.Mocked.PrivateHelpers.Tests.ps1 +++ b/Tests/ConnectWiseAutomateAgent.Mocked.PrivateHelpers.Tests.ps1 @@ -614,3 +614,106 @@ Describe 'Invoke-CWAAMsiInstaller' { } } } + +# ----------------------------------------------------------------------------- +# Confirm-CWAADependencyService Tests +# ----------------------------------------------------------------------------- + +Describe 'Confirm-CWAADependencyService' { + + It 'sets startup type to Automatic for the dependency service' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Running' } } + Mock Set-Service {} + Mock Start-Service {} + Mock Wait-CWAACondition { $true } + + Confirm-CWAADependencyService -Confirm:$false + + Should -Invoke Set-Service -Scope It -ParameterFilter { $StartupType -eq 'Automatic' } + } + } + + It 'starts the service when it is not running' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Stopped' } } + Mock Set-Service {} + Mock Start-Service {} + Mock Wait-CWAACondition { $true } + Mock Write-CWAAEventLog {} + + Confirm-CWAADependencyService -Confirm:$false + + Should -Invoke Start-Service -Scope It -Times 1 + Should -Invoke Write-CWAAEventLog -Scope It -ParameterFilter { $EventId -eq 2030 } + } + } + + It 'does not start the service when already running' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Running' } } + Mock Set-Service {} + Mock Start-Service {} + Mock Wait-CWAACondition { $true } + + Confirm-CWAADependencyService -Confirm:$false + + Should -Invoke Start-Service -Scope It -Times 0 + } + } + + It 'falls back to sc.exe when Start-Service throws' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Stopped' } } + Mock Set-Service {} + Mock Start-Service { throw 'cannot start' } + Mock Wait-CWAACondition { $true } + Mock Write-CWAAEventLog {} + + { Confirm-CWAADependencyService -Confirm:$false } | Should -Not -Throw + # No assertion on sc.exe (native exe is not easily mockable); the test confirms + # the throw is caught and the function completes without error. + } + } + + It 'logs a warning event when the service never reaches Running' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Stopped' } } + Mock Set-Service {} + Mock Start-Service {} + Mock Wait-CWAACondition { $false } + Mock Write-CWAAEventLog {} + + Confirm-CWAADependencyService -Confirm:$false -WarningAction SilentlyContinue + + Should -Invoke Write-CWAAEventLog -Scope It -ParameterFilter { $EventId -eq 2031 } + } + } + + It 'skips a service that does not exist without error' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-Service { $null } + Mock Set-Service {} + Mock Start-Service {} + + { Confirm-CWAADependencyService -Confirm:$false } | Should -Not -Throw + + Should -Invoke Set-Service -Scope It -Times 0 + Should -Invoke Start-Service -Scope It -Times 0 + } + } + + It 'makes no changes under -WhatIf' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Stopped' } } + Mock Set-Service {} + Mock Start-Service {} + Mock Wait-CWAACondition { $true } + + Confirm-CWAADependencyService -WhatIf + + Should -Invoke Set-Service -Scope It -Times 0 + Should -Invoke Start-Service -Scope It -Times 0 + } + } +} diff --git a/Tests/ConnectWiseAutomateAgent.Mocked.ServiceOps.Tests.ps1 b/Tests/ConnectWiseAutomateAgent.Mocked.ServiceOps.Tests.ps1 index 3227e04..6e37853 100644 --- a/Tests/ConnectWiseAutomateAgent.Mocked.ServiceOps.Tests.ps1 +++ b/Tests/ConnectWiseAutomateAgent.Mocked.ServiceOps.Tests.ps1 @@ -268,6 +268,20 @@ Describe 'Start-CWAA' { Should -Invoke Invoke-CWAACommand -Times 1 -Scope It -ParameterFilter { $Command -eq 'Send Status' } } } + + It 'ensures the WMI dependency service before starting the agent' { + InModuleScope 'ConnectWiseAutomateAgent' { + Mock Get-CWAAInfo { [PSCustomObject]@{ TrayPort = '42000' } } + Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } } + Mock Set-Service {} + Mock Invoke-CWAACommand {} + Mock Confirm-CWAADependencyService {} + + Start-CWAA -Confirm:$false + + Should -Invoke Confirm-CWAADependencyService -Times 1 -Scope It + } + } } # ============================================================================= diff --git a/source/Private/Confirm-CWAADependencyService.ps1 b/source/Private/Confirm-CWAADependencyService.ps1 new file mode 100644 index 0000000..e675a5c --- /dev/null +++ b/source/Private/Confirm-CWAADependencyService.ps1 @@ -0,0 +1,102 @@ +function Confirm-CWAADependencyService { + <# + .SYNOPSIS + Ensures the OS services the Automate agent depends on are Automatic and Running. + .DESCRIPTION + Iterates the services named in $Script:CWAADependencyServiceNames (winmgmt/WMI by + default) and makes sure each is set to Automatic startup and is Running. The agent + relies on WMI for inventory, scripting, and check-in, and this module's own + Get-CimInstance calls fail silently when winmgmt is disabled, so this runs before + install, repair, and service start. + + For each dependency service: + - If the service is not present, it is skipped (logged via Write-Debug). + - Startup type is set to Automatic unconditionally. This is done without reading + the current StartMode (which would require Win32_Service via WMI, the very + service that may be down) and is idempotent. + - If the service is not Running, a start is attempted via Start-Service (which + resolves dependencies such as RpcSs), falling back to sc.exe start. The function + then polls up to $Script:CWAAServiceWaitTimeoutSec for the Running state. + + Remediation outcomes are written to the Windows Event Log (EventIds 2030-2032). + Honors -WhatIf via ShouldProcess; under -WhatIf no changes are made. + .PARAMETER ServiceName + One or more service names to ensure. Defaults to $Script:CWAADependencyServiceNames. + Exposed primarily for testing. + .NOTES + Version: 1.0.0 + Author: Chris Taylor + Private function - not exported. + .LINK + https://github.com/christaylorcodes/ConnectWiseAutomateAgent + #> + [CmdletBinding(SupportsShouldProcess = $True)] + Param( + [Parameter()] + [string[]]$ServiceName = $Script:CWAADependencyServiceNames + ) + + Begin { + Write-Debug "Starting $($MyInvocation.InvocationName)" + } + + Process { + foreach ($name in $ServiceName) { + $service = Get-Service $name -ErrorAction SilentlyContinue + if (-not $service) { + Write-Debug "Dependency service '$name' not found. Skipping." + continue + } + + Try { + # Force Automatic startup. Set unconditionally; reading the current start mode + # reliably needs WMI, which may be the disabled service we are repairing. + if ($PSCmdlet.ShouldProcess($name, 'Set service startup type to Automatic')) { + Set-Service $name -StartupType Automatic -EA 0 -Confirm:$False -WhatIf:$False + } + + # Start the service if it is not already running. + $service = Get-Service $name -ErrorAction SilentlyContinue + if ($service -and $service.Status -ne 'Running') { + if ($PSCmdlet.ShouldProcess($name, 'Start service')) { + Write-Verbose "Dependency service '$name' is $($service.Status). Starting." + Try { + Start-Service $name -ErrorAction Stop -WhatIf:$False -Confirm:$False + } + Catch { + # Fall back to sc.exe, consistent with the rest of the module. + Write-Debug "Start-Service failed for '$name' ($($_.Exception.Message)). Falling back to sc.exe." + $Null = & "$env:windir\system32\sc.exe" start "$name" 2>'' + } + + # GetNewClosure bakes the loop-local $name into the scriptblock so it + # resolves correctly when Wait-CWAACondition invokes it from its own scope. + $running = Wait-CWAACondition -Condition { + (Get-Service $name -EA 0 | Select-Object -Expand Status -EA 0) -eq 'Running' + }.GetNewClosure() -TimeoutSeconds $Script:CWAAServiceWaitTimeoutSec -IntervalSeconds 2 -Activity "Dependency service '$name' starting" + + if ($running) { + Write-Verbose "Dependency service '$name' is Running." + Write-CWAAEventLog -EventId 2030 -EntryType Information -Message "Dependency service '$name' set to Automatic and started." + } + else { + Write-Warning "Dependency service '$name' did not reach the Running state." + Write-CWAAEventLog -EventId 2031 -EntryType Warning -Message "Dependency service '$name' was set to Automatic but did not reach the Running state." + } + } + } + else { + Write-Debug "Dependency service '$name' is already Running." + } + } + Catch { + Write-Warning "Failed to ensure dependency service '$name'. $($_.Exception.Message)" + Write-CWAAEventLog -EventId 2032 -EntryType Error -Message "Failed to ensure dependency service '$name'. Error: $($_.Exception.Message)" + } + } + } + + End { + Write-Debug "Exiting $($MyInvocation.InvocationName)" + } +} diff --git a/source/Private/Get-CWAANetstat.ps1 b/source/Private/Get-CWAANetstat.ps1 new file mode 100644 index 0000000..e7ebd6d --- /dev/null +++ b/source/Private/Get-CWAANetstat.ps1 @@ -0,0 +1,24 @@ +function Get-CWAANetstat { + <# + .SYNOPSIS + Returns raw output from the local computer's netstat.exe. + .DESCRIPTION + Thin wrapper around the system netstat.exe (invoked with -a -o -n) that exists to + provide a single, mockable seam for the external process call. Callers such as + Test-CWAAPort pipe the output to Select-String to locate processes using a port. + + The full system path ($env:windir\system32\netstat.exe) is used deliberately to + avoid PATH hijacking, which is why this cannot be intercepted by mocking a bare + 'netstat' command - tests mock this function instead. + .NOTES + Version: 1.0.0 + Author: Chris Taylor + Private function - not exported. + .LINK + https://github.com/christaylorcodes/ConnectWiseAutomateAgent + #> + [CmdletBinding()] + Param() + + & "$env:windir\system32\netstat.exe" -a -o -n +} diff --git a/source/Private/Initialize/Initialize-CWAA.ps1 b/source/Private/Initialize/Initialize-CWAA.ps1 index a9a17f9..a9e9031 100644 --- a/source/Private/Initialize/Initialize-CWAA.ps1 +++ b/source/Private/Initialize/Initialize-CWAA.ps1 @@ -155,6 +155,12 @@ function Initialize-CWAA { # All service names including LabVNC — for full service cleanup in Uninstall-CWAA. $Script:CWAAAllServiceNames = @('LTService', 'LTSvcMon', 'LabVNC') + # OS services the agent depends on — ensured Automatic + Running before install/repair/start + # via Confirm-CWAADependencyService. The agent relies on WMI (winmgmt) for inventory, + # scripting, and check-in; a disabled winmgmt breaks the agent (and this module's own + # Get-CimInstance calls). Kept as a list so additional dependencies are trivial to add. + $Script:CWAADependencyServiceNames = @('winmgmt') + # Service credential storage -- populated on-demand by Get-CWAAProxy $Script:LTServiceKeys = [PSCustomObject]@{ ServerPasswordString = '' diff --git a/source/Private/Write-CWAAEventLog.ps1 b/source/Private/Write-CWAAEventLog.ps1 index b6b6827..1af1f69 100644 --- a/source/Private/Write-CWAAEventLog.ps1 +++ b/source/Private/Write-CWAAEventLog.ps1 @@ -14,6 +14,7 @@ function Write-CWAAEventLog { Event ID ranges by category: 1000-1039 Installation (Install, Uninstall, Redo, Update) 2000-2029 Service Control (Start, Stop, Restart) + 2030-2039 Dependency Services (winmgmt/WMI auto-start + start) 3000-3069 Configuration (Reset, Backup, Proxy, LogLevel, AddRemove) 4000-4039 Health/Monitoring (Repair, Register/Unregister task) .NOTES diff --git a/source/Public/InstallUninstall/Install-CWAA.ps1 b/source/Public/InstallUninstall/Install-CWAA.ps1 index d3cee53..d61cc71 100644 --- a/source/Public/InstallUninstall/Install-CWAA.ps1 +++ b/source/Public/InstallUninstall/Install-CWAA.ps1 @@ -144,6 +144,9 @@ function Install-CWAA { Throw 'Needs to be ran as Administrator' } + # Ensure WMI (winmgmt) is Automatic + Running before the MSI runs and the agent registers. + Confirm-CWAADependencyService + $Null = Test-CWAADotNetPrerequisite -SkipDotNet:$SkipDotNet -Force:$Force $InstallBase = $Script:CWAAInstallerTempPath diff --git a/source/Public/Service/Repair-CWAA.ps1 b/source/Public/Service/Repair-CWAA.ps1 index 1ac2158..7e05014 100644 --- a/source/Public/Service/Repair-CWAA.ps1 +++ b/source/Public/Service/Repair-CWAA.ps1 @@ -79,6 +79,10 @@ Begin { Write-Debug "Starting $($MyInvocation.InvocationName)" + # Ensure WMI (winmgmt) is Automatic + Running first. The agent depends on it, and the + # Get-CimInstance call below silently no-ops on a WMI-disabled box, so this must run before it. + Confirm-CWAADependencyService + # Kill duplicate Repair-CWAA processes to prevent overlapping remediation # Uses CIM for reliable command-line matching (Get-Process cannot filter by arguments) if ($PSCmdlet.ShouldProcess('Duplicate Repair-CWAA processes', 'Terminate')) { diff --git a/source/Public/Service/Start-CWAA.ps1 b/source/Public/Service/Start-CWAA.ps1 index a183f0d..e1b96bf 100644 --- a/source/Public/Service/Start-CWAA.ps1 +++ b/source/Public/Service/Start-CWAA.ps1 @@ -38,6 +38,9 @@ function Start-CWAA { Process { if (-not (Test-CWAAServiceExists -WriteErrorOnMissing)) { return } + # Ensure WMI (winmgmt) is Automatic + Running before starting the agent. The agent + # depends on it for check-in, inventory, and scripting. + Confirm-CWAADependencyService Try { if ((('LTService') | Get-Service -EA 0 | Where-Object { $_.Status -eq 'Stopped' } | Measure-Object | Select-Object -Expand Count) -gt 0) { Try { $netstat = & "$env:windir\system32\netstat.exe" -a -o -n 2>'' | Select-String -Pattern " .*[0-9\.]+:$($Port).*[0-9\.]+:[0-9]+ .*?([0-9]+)" -EA 0 } diff --git a/source/Public/Test-CWAAPort.ps1 b/source/Public/Test-CWAAPort.ps1 index d211102..0f16c12 100644 --- a/source/Public/Test-CWAAPort.ps1 +++ b/source/Public/Test-CWAAPort.ps1 @@ -101,7 +101,7 @@ function Test-CWAAPort { [array]$processes = @() # Get all processes using the TrayPort (default 42000) Try { - $netstatOutput = & "$env:windir\system32\netstat.exe" -a -o -n | Select-String -Pattern " .*[0-9\.]+:$($TrayPort).*[0-9\.]+:[0-9]+ .*?([0-9]+)" -EA 0 + $netstatOutput = Get-CWAANetstat | Select-String -Pattern " .*[0-9\.]+:$($TrayPort).*[0-9\.]+:[0-9]+ .*?([0-9]+)" -EA 0 } Catch { Write-Output 'Error calling netstat.exe.' From 9e58222879cf3266bd4a8c1ab9780d81c4da7da5 Mon Sep 17 00:00:00 2001 From: Chris Taylor <36066223+christaylorcodes@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:58:10 -0600 Subject: [PATCH 3/4] Fix flaky Register-CWAAHealthCheckTask test (LASTEXITCODE leak) The CrossCutting pipeline test mocked schtasks with a bare 'return $null', which does not set $LASTEXITCODE. Register-CWAAHealthCheckTask checks $LASTEXITCODE after the native /CREATE call, so the test inherited whatever exit code a prior native command left behind. It passed locally and on lucky CI runs but failed when the ambient code was non-zero (CI saw 1060). Use the same conditional mock pattern already established in the Installation tests, setting $global:LASTEXITCODE = 0 on the /CREATE branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 b/Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 index d83ea84..59cb404 100644 --- a/Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 +++ b/Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1 @@ -224,7 +224,13 @@ Describe 'Pipeline Support' { It 'Register-CWAAHealthCheckTask accepts Server as string[] and builds valid command' { $result = InModuleScope 'ConnectWiseAutomateAgent' { - Mock schtasks { return $null } + # The mock must set $LASTEXITCODE for the /CREATE branch; the code checks it + # after the native call, and a bare 'return $null' leaks a prior exit code. + Mock schtasks { + if ($args -contains '/QUERY') { throw 'Task not found' } + elseif ($args -contains '/DELETE') { return $null } + elseif ($args -contains '/CREATE') { $global:LASTEXITCODE = 0; return 'SUCCESS' } + } Mock New-CWAABackup {} [PSCustomObject]@{ From 6e2b03f03ae9f695919471975f5b75292dff032b Mon Sep 17 00:00:00 2001 From: Chris Taylor <36066223+christaylorcodes@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:49:12 -0600 Subject: [PATCH 4/4] Finalize CHANGELOG for 2.1.0 release Move the WMI dependency service enforcement entry from [Unreleased] into a dated [2.1.0] section so the stable release job can extract proper GitHub Release notes (it matches on "## []"). Leaves an empty [Unreleased] for the next cycle. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6969e..aa50a4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **WMI dependency service enforcement** — `Confirm-CWAADependencyService` ensures `winmgmt` (WMI) is set to Automatic startup and Running before install, repair, and service start. Configurable via `$Script:CWAADependencyServiceNames`; respects `-WhatIf`. Event IDs 2030–2039. - ### Changed ### Fixed +## [2.1.0] - 2026-06-17 + +### Added + +- **WMI dependency service enforcement** — `Confirm-CWAADependencyService` ensures `winmgmt` (WMI) is set to Automatic startup and Running before install, repair, and service start. Configurable via `$Script:CWAADependencyServiceNames`; respects `-WhatIf`. Event IDs 2030–2039. + ## [2.0.0] - 2026-02-03 ### Added