Skip to content

Commit de2f36c

Browse files
Update-ServiceStatus - Fix worker CIM session cleanup and credential use (review of #10274)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 43d7d05 commit de2f36c

3 files changed

Lines changed: 289 additions & 141 deletions

File tree

docs/trackers/features/commit-bug-review-TRACKER.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Find real bugs (logic errors, null refs, incorrect behavior) and fix them. Skip
9292
| d6552592e | Update-DbaInstance - Add early validation for empty -Path parameter (#10283) | DONE | Fixed whitespace-only Path validation gap and added regression coverage. |
9393
| c82ae190c | Get-DbaAgRingBuffer - Add command for HADR ring buffer diagnostics (#10282) | DONE | Fixed timestamp DataTable expansion and routed query failures through Stop-Function; added unit regression tests. |
9494
| 552b77af4 | Invoke-DbaDbDataMasking - Fix StaticValue empty string fallback and FilterQuery in Actions (#10281) | DONE | Fixed action FilterQuery updates to honor the selected row set and added a unit regression test. |
95-
| 99a4a9067 | Update-ServiceStatus - Fix WinRM error on machines without WinRM configured (#10274) | PENDING | |
95+
| 99a4a9067 | Update-ServiceStatus - Fix WinRM error on machines without WinRM configured (#10274) | DONE | Fixed worker CIM session credential propagation and cleanup; added unit regression test. |
9696
| 1f70b62bd | Add Remove-DbaAgentJobSchedule cmdlet (#10273) | PENDING | |
9797
| 50c0bfdaf | Connect-DbaInstance - Add -AuthenticationType parameter for Entra ID support (#10271) | PENDING | |
9898
| 27e4da9d1 | Get-DbaDbOrphanUser - Skip SQL login orphan check for contained databases (#10270) | PENDING | |

private/functions/Update-ServiceStatus.ps1

Lines changed: 147 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -71,168 +71,175 @@ function Update-ServiceStatus {
7171
$svcControlBlock = {
7272
$group = $_.Group
7373
$computerName = $_.Name
74-
# Create a CIM session preferring DCOM to avoid requiring WinRM on the target machine.
75-
# CIM instances become deserialized when crossing runspace boundaries and lose their
76-
# session context, causing Invoke-CimMethod to attempt a new WinRM connection by default.
77-
# Using DCOM avoids this WinRM dependency for machines where WinRM is not configured.
78-
$splatCimDcomOption = @{
79-
Protocol = "Dcom"
74+
$cimSession = $null
75+
$splatCimSession = @{
76+
ComputerName = $computerName
77+
ErrorAction = "Stop"
8078
}
81-
$cimDcomOption = New-CimSessionOption @splatCimDcomOption
82-
$splatCimSessionDcom = @{
83-
ComputerName = $computerName
84-
SessionOption = $cimDcomOption
85-
ErrorAction = "Stop"
79+
if ($Credential) {
80+
$splatCimSession.Credential = $Credential
8681
}
82+
8783
try {
88-
$cimSession = New-CimSession @splatCimSessionDcom
89-
} catch {
90-
# Fall back to default protocol (WinRM) if DCOM is unavailable
84+
# Create a CIM session preferring DCOM to avoid requiring WinRM on the target machine.
85+
# CIM instances become deserialized when crossing runspace boundaries and lose their
86+
# session context, causing Invoke-CimMethod to attempt a new WinRM connection by default.
87+
# Using DCOM avoids this WinRM dependency for machines where WinRM is not configured.
88+
$splatCimDcomOption = @{
89+
Protocol = "Dcom"
90+
}
91+
$cimDcomOption = New-CimSessionOption @splatCimDcomOption
92+
$splatCimSession.SessionOption = $cimDcomOption
9193
try {
92-
$cimSession = New-CimSession -ComputerName $computerName -ErrorAction "Stop"
94+
$cimSession = New-CimSession @splatCimSession
9395
} catch {
94-
$cimSession = $null
95-
}
96-
}
97-
$servicePriorityCollection = $group.ServicePriority | Select-Object -unique | Sort-Object -Property @{ Expression = { [int]$_ }; Descending = $action -ne 'stop' }
98-
foreach ($priority in $servicePriorityCollection) {
99-
$services = $group | Where-Object { $_.ServicePriority -eq $priority }
100-
$servicesToRestart = @()
101-
foreach ($service in $services) {
102-
if ('dbatools.DbaSqlService' -in $service.PSObject.TypeNames) {
103-
$cimObject = $service._CimObject
104-
if (($cimObject.State -eq 'Running' -and $action -eq 'start') -or ($cimObject.State -eq 'Stopped' -and $action -eq 'stop')) {
105-
$service | Add-Member -Force -NotePropertyName Status -NotePropertyValue 'Successful' -PassThru |
106-
Add-Member -Force -NotePropertyName Message -NotePropertyValue "The service is already $actionText, no action required" -PassThru
107-
} elseif ($cimObject.StartMode -eq 'Disabled' -and $action -in 'start', 'restart') {
108-
$service | Add-Member -Force -NotePropertyName Status -NotePropertyValue 'Failed' -PassThru |
109-
Add-Member -Force -NotePropertyName Message -NotePropertyValue "The service is disabled and cannot be $actionText" -PassThru
110-
} else {
111-
$servicesToRestart += $service
112-
}
113-
} else {
114-
throw "Unknown object in pipeline - make sure to use Get-DbaService cmdlet"
115-
}
116-
}
117-
#Set desired $action
118-
if ($action -in 'start', 'restart') {
119-
$methodName = 'StartService'
120-
$desiredState = 'Running'
121-
$undesiredState = 'Stopped'
122-
} elseif ($action -eq 'stop') {
123-
$methodName = 'StopService'
124-
$desiredState = 'Stopped'
125-
$undesiredState = 'Running'
126-
}
127-
$invokeResults = @()
128-
foreach ($service in $servicesToRestart) {
129-
if ($Pscmdlet.ShouldProcess("Sending $action request to service $($service.ServiceName) on $($service.ComputerName)")) {
130-
# Get a fresh CIM instance via the DCOM session to avoid issues with deserialized
131-
# CIM objects crossing runspace boundaries without their session context.
132-
if ($cimSession) {
96+
# Fall back to default protocol (WinRM) if DCOM is unavailable
97+
$null = $splatCimSession.Remove("SessionOption")
13398
try {
134-
$splatGetFreshCim = @{
135-
CimSession = $cimSession
136-
Namespace = "root\cimv2"
137-
Query = "SELECT * FROM Win32_Service WHERE Name = '$($service.ServiceName)'"
138-
}
139-
$freshCimObj = Get-CimInstance @splatGetFreshCim
140-
if ($freshCimObj) {
141-
$service._CimObject = $freshCimObj
142-
}
99+
$cimSession = New-CimSession @splatCimSession
143100
} catch {
144-
# Fall back to using the existing deserialized CIM object if session refresh fails
101+
$cimSession = $null
145102
}
146103
}
147-
#Invoke corresponding CIM method
148-
$invokeResult = Invoke-CimMethod -InputObject $service._CimObject -MethodName $methodName
149-
$invokeResults += [psobject]@{
150-
InvokeResult = $invokeResult
151-
ServiceState = $invokeResult.State
152-
ServiceExitCode = $invokeResult.ReturnValue
153-
CheckPending = $true
154-
Service = $service
104+
$servicePriorityCollection = $group.ServicePriority | Select-Object -unique | Sort-Object -Property @{ Expression = { [int]$_ }; Descending = $action -ne "stop" }
105+
foreach ($priority in $servicePriorityCollection) {
106+
$services = $group | Where-Object { $_.ServicePriority -eq $priority }
107+
$servicesToRestart = @()
108+
foreach ($service in $services) {
109+
if ("dbatools.DbaSqlService" -in $service.PSObject.TypeNames) {
110+
$cimObject = $service._CimObject
111+
if (($cimObject.State -eq "Running" -and $action -eq "start") -or ($cimObject.State -eq "Stopped" -and $action -eq "stop")) {
112+
$service | Add-Member -Force -NotePropertyName Status -NotePropertyValue "Successful" -PassThru |
113+
Add-Member -Force -NotePropertyName Message -NotePropertyValue "The service is already $actionText, no action required" -PassThru
114+
} elseif ($cimObject.StartMode -eq "Disabled" -and $action -in "start", "restart") {
115+
$service | Add-Member -Force -NotePropertyName Status -NotePropertyValue "Failed" -PassThru |
116+
Add-Member -Force -NotePropertyName Message -NotePropertyValue "The service is disabled and cannot be $actionText" -PassThru
117+
} else {
118+
$servicesToRestart += $service
119+
}
120+
} else {
121+
throw "Unknown object in pipeline - make sure to use Get-DbaService cmdlet"
155122
}
156123
}
157-
}
158-
159-
$startTime = Get-Date
160-
if ($Pscmdlet.ShouldProcess("Waiting the services to $action on $computerName")) {
161-
#Wait for the service to complete the action until timeout
162-
while ($invokeResults.CheckPending -contains $true) {
163-
foreach ($result in ($invokeResults | Where-Object CheckPending -eq $true)) {
164-
try {
165-
#Refresh Cim instance - not using Get-DbaCmObject because module is not loaded here, but it only refreshes existing object
166-
if ($cimSession) {
167-
$splatRefreshCim = @{
124+
#Set desired $action
125+
if ($action -in "start", "restart") {
126+
$methodName = "StartService"
127+
$desiredState = "Running"
128+
$undesiredState = "Stopped"
129+
} elseif ($action -eq "stop") {
130+
$methodName = "StopService"
131+
$desiredState = "Stopped"
132+
$undesiredState = "Running"
133+
}
134+
$invokeResults = @()
135+
foreach ($service in $servicesToRestart) {
136+
if ($Pscmdlet.ShouldProcess("Sending $action request to service $($service.ServiceName) on $($service.ComputerName)")) {
137+
# Get a fresh CIM instance via the DCOM session to avoid issues with deserialized
138+
# CIM objects crossing runspace boundaries without their session context.
139+
if ($cimSession) {
140+
try {
141+
$splatGetFreshCim = @{
168142
CimSession = $cimSession
169143
Namespace = "root\cimv2"
170-
Query = "SELECT State FROM Win32_Service WHERE Name = '$($result.Service.ServiceName)'"
144+
Query = "SELECT * FROM Win32_Service WHERE Name = '$($service.ServiceName)'"
171145
}
172-
$refreshedCimObj = Get-CimInstance @splatRefreshCim
173-
if ($refreshedCimObj) {
174-
$result.Service._CimObject = $refreshedCimObj
146+
$freshCimObj = Get-CimInstance @splatGetFreshCim
147+
if ($freshCimObj) {
148+
$service._CimObject = $freshCimObj
175149
}
176-
} else {
177-
$result.Service._CimObject = $result.Service._CimObject | Get-CimInstance
150+
} catch {
151+
# Fall back to using the existing deserialized CIM object if session refresh fails
178152
}
179-
} catch {
180-
$result.ServiceExitCode = -3
181-
$result.ServiceState = 'Unknown'
182-
$result.CheckPending = $false
183-
continue
184-
}
185-
$result.ServiceState = $result.Service._CimObject.State
186-
#Failed or succeeded
187-
if ($result.ServiceExitCode -ne 0 -or $result.ServiceState -eq $desiredState) {
188-
$result.CheckPending = $false
189-
continue
190153
}
191-
#Failed after being in the Pending state
192-
if ($result.CheckPending -and $result.ServiceState -eq $undesiredState) {
193-
$result.ServiceExitCode = -2
194-
$result.CheckPending = $false
195-
continue
154+
#Invoke corresponding CIM method
155+
$invokeResult = Invoke-CimMethod -InputObject $service._CimObject -MethodName $methodName
156+
$invokeResults += [psobject]@{
157+
InvokeResult = $invokeResult
158+
ServiceState = $invokeResult.State
159+
ServiceExitCode = $invokeResult.ReturnValue
160+
CheckPending = $true
161+
Service = $service
196162
}
197-
#Timed out
198-
if ($timeout -gt 0 -and ((Get-Date) - $startTime).TotalSeconds -gt $timeout) {
199-
$result.ServiceExitCode = -1
200-
$result.CheckPending = $false
201-
continue
202-
}
203-
#Still pending - leave CheckPending as is and run again
204163
}
205-
Start-Sleep -Milliseconds 200
206164
}
207-
}
208-
foreach ($result in $invokeResults) {
209-
#Add status
210-
$status = switch ($result.ServiceExitCode) {
211-
0 { 'Successful' }
212-
10 { 'Successful ' } #Already running - FullText service is started automatically
213-
default { 'Failed' }
165+
166+
$startTime = Get-Date
167+
if ($Pscmdlet.ShouldProcess("Waiting the services to $action on $computerName")) {
168+
#Wait for the service to complete the action until timeout
169+
while ($invokeResults.CheckPending -contains $true) {
170+
foreach ($result in ($invokeResults | Where-Object CheckPending -eq $true)) {
171+
try {
172+
#Refresh Cim instance - not using Get-DbaCmObject because module is not loaded here, but it only refreshes existing object
173+
if ($cimSession) {
174+
$splatRefreshCim = @{
175+
CimSession = $cimSession
176+
Namespace = "root\cimv2"
177+
Query = "SELECT State FROM Win32_Service WHERE Name = '$($result.Service.ServiceName)'"
178+
}
179+
$refreshedCimObj = Get-CimInstance @splatRefreshCim
180+
if ($refreshedCimObj) {
181+
$result.Service._CimObject = $refreshedCimObj
182+
}
183+
} else {
184+
$result.Service._CimObject = $result.Service._CimObject | Get-CimInstance
185+
}
186+
} catch {
187+
$result.ServiceExitCode = -3
188+
$result.ServiceState = "Unknown"
189+
$result.CheckPending = $false
190+
continue
191+
}
192+
$result.ServiceState = $result.Service._CimObject.State
193+
#Failed or succeeded
194+
if ($result.ServiceExitCode -ne 0 -or $result.ServiceState -eq $desiredState) {
195+
$result.CheckPending = $false
196+
continue
197+
}
198+
#Failed after being in the Pending state
199+
if ($result.CheckPending -and $result.ServiceState -eq $undesiredState) {
200+
$result.ServiceExitCode = -2
201+
$result.CheckPending = $false
202+
continue
203+
}
204+
#Timed out
205+
if ($timeout -gt 0 -and ((Get-Date) - $startTime).TotalSeconds -gt $timeout) {
206+
$result.ServiceExitCode = -1
207+
$result.CheckPending = $false
208+
continue
209+
}
210+
#Still pending - leave CheckPending as is and run again
211+
}
212+
Start-Sleep -Milliseconds 200
213+
}
214214
}
215-
Add-Member -Force -InputObject $result.Service -NotePropertyName Status -NotePropertyValue $status
216-
#Add error message
217-
$errorMessageFromReturnValue = if ($result.ServiceExitCode -in 0..($errorCodes.Length - 1)) {
218-
$errorCodes[$result.ServiceExitCode]
219-
} else { "Unknown error." }
220-
$message = switch ($result.ServiceExitCode) {
221-
-2 { "The service failed to $action." }
222-
-1 { "The attempt to $action the service has timed out." }
223-
0 { "Service was successfully $actionText." }
224-
default { "The attempt to $action the service returned the following error: $errorMessageFromReturnValue" }
215+
foreach ($result in $invokeResults) {
216+
#Add status
217+
$status = switch ($result.ServiceExitCode) {
218+
0 { "Successful" }
219+
10 { "Successful " } #Already running - FullText service is started automatically
220+
default { "Failed" }
221+
}
222+
Add-Member -Force -InputObject $result.Service -NotePropertyName Status -NotePropertyValue $status
223+
#Add error message
224+
$errorMessageFromReturnValue = if ($result.ServiceExitCode -in 0..($errorCodes.Length - 1)) {
225+
$errorCodes[$result.ServiceExitCode]
226+
} else { "Unknown error." }
227+
$message = switch ($result.ServiceExitCode) {
228+
-2 { "The service failed to $action." }
229+
-1 { "The attempt to $action the service has timed out." }
230+
0 { "Service was successfully $actionText." }
231+
default { "The attempt to $action the service returned the following error: $errorMessageFromReturnValue" }
232+
}
233+
Add-Member -Force -InputObject $result.Service -NotePropertyName Message -NotePropertyValue $message
234+
# Refresh service state for the object
235+
if ($result.ServiceState) { $result.Service.State = $result.ServiceState }
236+
$result
225237
}
226-
Add-Member -Force -InputObject $result.Service -NotePropertyName Message -NotePropertyValue $message
227-
# Refresh service state for the object
228-
if ($result.ServiceState) { $result.Service.State = $result.ServiceState }
229-
$result
230238
}
231-
}
232-
# Clean up the CIM session created for DCOM connections
233-
if ($cimSession) {
234-
Remove-CimSession -CimSession $cimSession -ErrorAction "SilentlyContinue"
235-
$cimSession = $null
239+
} finally {
240+
if ($cimSession) {
241+
Remove-CimSession -CimSession $cimSession -ErrorAction "SilentlyContinue"
242+
}
236243
}
237244
}
238245

@@ -286,4 +293,4 @@ process {
286293
}
287294
end {
288295
}
289-
}
296+
}

0 commit comments

Comments
 (0)