Partager via


Stopping Services in a Specific Order

A lot of the work that I do in product support involves setting up labs to try and reproduce a problem in order to troubleshoot it. In that arena, one of the most frequent sub tasks is bulk starting and stopping of services. A lot of times the services have to be started in a particular order sequentially—which means you can’t just let them fly. It’s a simple thing really, but what I want to do here is to use the stopping and starting of services to illustrate a really important concept—refreshing the properties of a WMI object.

It actually took me quite a while to figure this out—although after the fact I kicked myself… hard. This used to be simple with VBScript. You could just get any old WMI instance object and call the Refresh_ method, which came from the SWbemObjectEx interface. Unfortunately this much needed method isn’t present in System.Management.ManagementObject. Before I get to that, I’ll show you the complicated script I wrote to get this done the long way—this one is an earlier version which just stops services in a particular order, but you’ll see what I mean.

The Ugly Way

#### Stop Services sequentially

#### List your servers individually

$Servers = ("Localhost","127.0.0.1")

#### ...or read them in from a file

# $Servers = (type c:\serverlist.txt)

#### You must specify a timeout (in seconds) or the script could potentially never end

$TimeOut = 10

#### Use this line to get all services matching a pattern.

# $ServiceFilters = "name like 'msex%'"

#### This will stop a single service on all servers in sequence

# $ServiceFilters = "name = 'spooler'"

#### This is just a more complex filter but the sequence is not guarateed

# $ServiceFilters = "(name = 'spooler' OR name = 'msdtc')"

#### Specifying individual filters in sequence will guarantee the order or services to stop

$ServiceFilters = "(name = 'spooler')","(name = 'msdtc')"

$Servers | %{

      $Server = $_

      $Locator = new-object -com "WbemScripting.SWbemLocator"

      $WMI = $Locator.ConnectServer($Server, "root\cimv2")

      $ServiceFilters | %{

            $ThisFilter = $_

            (Get-WmiObject -Class Win32_Service -ComputerName $Server -filter "$ThisFilter AND state='running'") | %{

                  $Service = $_

                  $Refresher = new-object -comobject "WbemScripting.SWbemRefresher"

                  $FreshObject = $Refresher.Add($WMI,$Service.__RELPATH)

                  $Refresher.Refresh()

                  $Then = Get-Date

                  :Checking Do {

                        $Service.StopService() | out-null

                        $Refresher.Refresh()

                        if (($FreshObject.Object.properties_ | ?{$_.name -eq "state"}).value -eq "Stopped") {

                              "Stopped $Service on $Server";

                              break :Checking;

                        } Else {

                              If (((Get-Date) - $Then).seconds -ge $TimeOut){

                                    "!!! TIMEOUT - $Service on $Server";

                                    break :Checking;

                              }

                        }

                  } While ($True)

            }

      }

}

"Services are all stopped"

I was never really a fan of the SWbemRefresher object. It always seemed a bit counter-intuitive and require me to jump through too many hoops, like using the SWbemLocator object. Ugh.

As it turns out there is a method I can use from System.Management.ManagementObject which refreshes the instance data. I was looking for something named Refresh or similar, but the actual method is called Get(). Sure it makes sense now—I can hear the condescension across space/time. And this allows me to make the script a lot friendlier.

The Much Nicer Version

Aside from removed the dreadful SWbemLocator, I also cleaned the script up a bit and made it more flexible.

Arguments
-Operation <start|stop>

This makes the script work for either starting or stopping services. If you don’t provide an operation, the script will just return the current state of all services that match the filter.

-ServiceFilters <Filter String(s)>

This is where you specify which services and in what order to process. We’re taking one or more filter strings here to make the script more flexible. Some potential ServiceFilters values are:

  • "Name = 'spooler'"
  • "Name like 'msex%'"
  • "StartMode = 'Auto'"
  • "Name = 'spooler'","Name = 'msdtc'"

Note that in order to guarantee the order of processing, you should specify each filter string separately, as it the last example.

-Servers <ServerList>

Specify one or more servers by any name that will resolve—Netbios, DNS, IP. If you don’t specify a server, then the script will assume the local machine.

-TimeOutSeconds <NumberOfSeconds>

Specify how many seconds to wait before timing out processing the service.  Default is 10 seconds.  Once the timeout expires, it will just move on to the next service.

#### Stop Services sequentially

Param ( $Operation ,

        $ServiceFilters = "(name='')" ,

        $Servers = "Localhost" ,

        $TimeOutSeconds = 10 )

Switch ($Operation) {

    {$_ -like "start*"} {

        $Method = "StartService";

        $DesiredState = "Running";

    }

    {$_ -like "stop*"} {

        $Method       = "StopService";

        $DesiredState = "Stopped";

    }

    Default {

        $Method       = $Null;

        $DesiredState = "*";

    }

}

$Servers | %{

      $Server = $_

      $ServiceFilters | %{

            $ThisFilter = $_

            (Get-WmiObject -Class Win32_Service `

                           -ComputerName $Server `

                           -filter "$ThisFilter AND state!='$DesiredState'") | %{

                  $Service = $_

                  $Then = Get-Date

                  :Checking Do {

                        If ($Method) {

                        $ErrorCode = $Service.PSBase.InvokeMethod($Method,$Null)

                        }

                        $Service.Get()

                        if ($Service.State -like "$DesiredState") {

                              "$($Service.State) on $Server - $($Service.caption) ($($Service.name))" | out-host;

                              break :Checking;

                        } Else {

                              If (((Get-Date) - $Then).seconds -ge $TimeOutSeconds){

                                    "!!! TIMEOUT - $Service on $Server" | out-host;

                                    break :Checking;

                              }

                        }

                  } While ($True)

            }

      }

}

Here’s the code in action. Notice what happens if I set the timeout too short.

[PS] .\Set-BulkService.ps1 -operation start -servicefilters "(name = 'spooler')","(name = 'msdtc')" -servers 127.0.0.1 -timeout 2

Running on 127.0.0.1 - Print Spooler (Spooler)

Running on 127.0.0.1 - Distributed Transaction Coordinator (MSDTC)

[PS] .\Set-BulkService.ps1 -operation start -servicefilters "(name = 'spooler')","(name = 'msdtc')" -servers 127.0.0.1 -timeout 0

!!! TIMEOUT - \\STEPHAP1\root\cimv2:Win32_Service.Name="Spooler" on 127.0.0.1

!!! TIMEOUT - \\STEPHAP1\root\cimv2:Win32_Service.Name="MSDTC" on 127.0.0.1

[PS]

So that’s that. My greatest hope for this post is that someone is searching for the Refresh_ method and PowerShell, and they happen upon this code and save themselves some time.

Unfortunately…

I wrote the previous scripts on Windows 7 using PowerShell v2 where it worked like a champ, but when I tried the latter script on my Windows 2003 Exchange 2007 server, it wasn’t so happy.  I’m not sure whether it’s Windows or PowerShell, but the Get() method was not available there.  I had to go back and use reflection to get at it as well as make a couple of other changes that for whatever reason were necessary to make it work.  Here is the safer version of the script that worked in both locations:

#### Stop Services sequentially

 

Param ( $Operation ,

        $ServiceFilters = "(name='')" ,

        $Servers = "Localhost" ,

        $TimeOutSeconds = 10 )

 

Switch ($Operation) {

    {$_ -like "start*"} {

        $Method = "StartService";

        $DesiredState = "Running";

    }

    {$_ -like "stop*"} {

        $Method       = "StopService";

        $DesiredState = "Stopped";

    }

    Default {

        $Method       = $Null;

        $DesiredState = "*";

    }

}

$Servers | %{

      $Server = $_

      $ServiceFilters | %{

            $ThisFilter = $_

             $Services = `
(Get-WmiObject -Class Win32_Service `
-ComputerName $Server `
-filter "$ThisFilter AND state!='$DesiredState'")
$Services | %{

                  $Service = $_

                  $Then = Get-Date

                  :Checking Do {

                        If (-not $Service) { Break :Checking; }

                        If ($Method) {

                    $ErrorCode = $Service.PSBase.InvokeMethod($Method,$Null)

                        }

                        $ObjectType = $Service.GetType()
$GetProperty = [reflection.bindingflags]::InvokeMethod
$ObjectType.InvokeMember("Get", $GetProperty,$Null,$Service,$Null)
if ($Service.State -like "$DesiredState") {

                              "$($Service.State) on $Server - $($Service.caption) ($($Service.name))" | out-host;

                              break :Checking;

                        } Else {

                              If (((Get-Date) - $Then).seconds -ge $TimeOutSeconds){

                                    "!!! TIMEOUT - $Service on $Server" | out-host;

                                    break :Checking;

                              }

                        }

                  } While ($True)

            }

      }

}

 

 

That was quite a journey you took with me. Thanks for sticking around all the way to the end. You must be really into scripting. I hope you weren’t too disappointed. Next time I’ll do something fun—I promise.