Summary: Guest blogger, Karl Mitschke, discusses a Windows PowerShell script to start non-running services that are set to start automatically.
Microsoft Scripting Guy, Ed Wilson, is here. Our guest blogger today is Karl Mitschke, one of the authors of the Windows PowerShell 2.0 Bible. Here is a little bit about Karl.
Karl Mitschke is an IT veteran with over 20 years of experience. He has specialized in messaging since the early 1990s. Karl has been automating tasks with script since moving to Microsoft Exchange 5.0, starting with WinBatch. He started using Windows PowerShell in 2007 when he moved to Exchange Server 2007. When he’s not writing Windows PowerShell scripts, or writing about Windows PowerShell scripts, Karl enjoys spending time with his bride, Sherry, and their two dogs.
Blog: Unlock-PowerShell
Windows PowerShell 2.0 Bible
Starting services on remote servers
In the “Performing Advanced Server Management” chapter in the Windows PowerShell 2.0 Bible, I presented a script that would start services on remote servers that are set to start automatically and were not started. Due to the constraints of the book, the script was severely limited. That script is shown here.
$Computers = “FileServer01”,”FileServer02”
$WmiObject = @{
Class = “Win32_Service”
Filter = “StartMode=’Auto’ and State!=’Running’”
}
foreach ($Computer in $Computers)
{
foreach ($Svc in Get-WmiObject @WmiObject -ComputerName $Computer)
{
Write-Host “Starting the” $Svc.DisplayName “service on $Computer”
$Svc.StartService() | Out-Null
}
}
This was fine, but the script could really use some improvement. For one thing, you needed to hard code the server names. In addition, it did not accept credentials, you could not simulate the action, and there was no Help.
The concept was sound, however, so I decided to extend the script to put it into production at work, where I use it to verify that services are running after performing updates on our Exchange Server infrastructure. Unfortunately, extending the script makes it too long for the book and too long for this blog. The complete script is available in the Scripting Guys Script Repository, but I will discuss highlights of the script in this blog post.
Extending the script with Help
The first several objections are easy to overcome with advanced parameters. The final objection is overcome with comment-based Help. Windows PowerShell even includes a method to add Help. I am referring to the built in Get-Help cmdlet. In fact, you can potentially get more Help than you will ever need by typing the following script into your Windows PowerShell console:
Get-Help -Name functions_advanced_parameters
Get-Help -Name about_comment_based_help
The Help about advanced parameters is too verbose to summarize, so I would suggest that you read it and familiarize yourself with the content.
The Help for comment-based Help shows that for script-based Help, comment-based Help can appear in two locations:
- At the beginning of the script file. Script Help can be preceded in the script only by comments and blank lines. If the first item in the script body (after the Help) is a function declaration, there must be at least two blank lines between the end of the script Help and the function declaration. Otherwise, the Help is interpreted as being Help for the function, not Help for the script.
- At the end of the script file. If the script is signed, place comment-based Help at the beginning of the script file. The end of the script file is occupied by the signature block.
As I sign the scripts that I use at work, I include comment-based Help at the beginning of the file.
I also choose to use the comment-block syntax as opposed to using the comment character (#) in front of each line, because I find it to be more readable. An example of the comment-block syntax is shown here.
<#
.SYNOPSIS
Invoke-StartService - Start all essential services that are not running.
.DESCRIPTION
This script finds services that are set to start automatically, and starts them.
#>
Therefore, Help is out of the way with the comment-based Help. Next, I tackle the parameters.
Adding arguments
I wanted to allow the script to accept server names from the command line or from a pipeline, so I used the ValueFromPipeline argument when I declare the ComputerName parameter for the script. I also want to allow the script to target multiple computers, so I declared the ComputerName parameter as an array of strings as shown here.
[Parameter(
Position = 0,
ValueFromPipeline=$true,
Mandatory = $false,
HelpMessage = "The computer to start services on"
)]
[string[]]$ComputerName = $env:ComputerName
As you can see, I set the ComputerName parameter to not be required by using the local computer name if it is not specified. I also set the parameter to be positional—that means that you do not need to use the name of the parameter as long as the computer name(s) are specified as the first-passed parameter to the script.
I wanted the script to accept credentials so that I could start my Windows PowerShell session with a non-administrator account. I added the optional parameter Credential to handle these credentials.
Finally, I wanted to emulate the WhatIf functionality in so many of my favorite cmdlets, so I added the optional switch parameter WhatIf. The entire parameter declaration is shown here.
param (
[Parameter(
Position = 0,
ValueFromPipeline=$true,
Mandatory = $false
)]
[string[]]$ComputerName = $env:ComputerName,
[Parameter(
Position = 1,
Mandatory = $false
)]
$Credential,
[Parameter(
Mandatory = $false
)]
[switch]$WhatIf
)
Using $PSBoundParameters
I wanted to avoid If statements as much as possible, so I take advantage of the automatic variable $PSBoundParameters, which is a hash table of the parameter names and values that are passed to the script.
You can pass the $PSBoundParameters to the Get-WmiObject cmdlet as a splatted hash table as shown here:
Get-WmiObject -Class Win32_Service @PSBoundParameters
The parameters ComputerName and Credential can be passed directly to the Get-WmiObject cmdlet. However, that cmdlet does not accept the WhatIf parameter, if present, which would cause the error message shown here.
This can be resolved by using the Remove() method of the hash table. You can check for the existence of the parameters with an If statement, or you can pass the command to the Out-Null cmdlet to avoid the If statement as shown here.
$PSBoundParameters.Remove("WhatIf") | Out-Null
I also have to handle the two methods of passing credentials to the script. The parameter Credential is not typed, so it accepts a string or a credential object as shown here.
$cred = Get-Credential -Credential "mitschke\karlm"
.\Invoke-StartService.ps1 -Credential $cred
When it is supplied like that, the $PSBoundParameters hash table passes a credential object to the Get-WmiObject cmdlet. However, if you supply the Credential parameter with a string, allowing the script to prompt for a password, the $PSBoundParameters hash table passes the string to the Get-WmiObject cmdlet and prompts for a password on every service that needs starting.
This is resolved by once again using the Remove() method of the hash table, and then using the Add() method to add the valid credential object to the $PSBoundParameters hash table as shown here.
if ($Credential -ne $null -and $Credential.GetType().Name -eq "String")
{
$PSBoundParameters.Remove("Credential") | Out-Null
$PSBoundParameters.Add("Credential", (Get-Credential -Credential $Credential))
}
I modified the Filter parameter for the Get-WmiObject cmdlet to find only the services that I was interested in. Specifically, I am not interested in starting the Microsoft .NET Framework Runtime Optimization Services, which may be set to start automatically. The service would start and then stop because it finds that no action is needed. It is not really a problem starting these services, I just wanted to avoid the time it takes to start them, and avoid event log entries that show they had started and stopped.
These services have a service name starting with clr_optimization_. The filter I use is shown here.
Filter "startmode='auto' and state='stopped' and (name > 'clra' or name < 'clr')"
The filter isn’t perfect—I suppose there could be a service with a name that starts with clra, but I have not seen one, so the filter serves my purposes. A better filter could use the grave accent character (`). The grave accent character is also the escape character for Windows PowerShell, so you would need to use two grave accents as “name > 'clr``’”. You could also use a client-side filter by using the Where-Object cmdlet as shown here:
Get-WmiObject -Class Win32_Service | Where-Object -FilterScript {$_.Name -notmatch "clr_*" -and $_.StartMode -eq "Auto" -And $_.State -ne "Running"}
That is perfectly valid; however, using the Filter parameter of the Get-WmiObject cmdlet performs server-side filtering, which should be quicker because only the objects we are interested in are passed back to the client. The complete server-side filtering example is shown here:
Get-WmiObject -Class Win32_Service –Filter "startmode='auto' and state='stopped' and (name > 'clra' or name < 'clr')"
So now we have a working filter, working parameters, and working Help. It is now time to test the parameters with the WhatIf switch. The command line and its associated output are shown here.
When I see what the script will do, I run the script without the WhatIf parameter. The output from the command appears in the following image.
As mentioned, the entire script is available in the Scripting Guys Script Repository. The output of the script follows the script. I believe the script is pretty well written; but as always, I am open to suggestions. You can reach me at:
-join ("6B61726C6D69747363686B65406D742E6E6574"-split"(?<=\G.{2})",19|%{[char][int]"0x$_"})
Thank you Karl, for a very useful and interesting blog post. Join me tomorrow for more Windows PowerShell goodness.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy