Microsoft Scripting Guy Ed Wilson here. It may seem like a “well duh” thing for a Scripting Guy to say, but I love writing scripts. In particular, I love writing Windows PowerShell scripts. One problem with sharing everything I write is that people always have a better idea about how to do things. But better can be relative. In my mind, shorter is not always better, especially for scripts. If I am working interactively at the Windows PowerShell console—oh wait, that is next week’s article.

Anyway, after I wrote yesterday’s Weekend Scripter article, I decided to write a script that would automate collecting four process snapshots on a computer after it had rebooted and logged on. I was a little carried away and spent all day on the script. It is actually a fun script to play around with, and the information that it returns is simply fascinating.

The first function I create is the Get-ProcessStartUp function. After defining the parameters for the function, I create the path that will be used for saving the process information. To create the path, I use the format specifier to substitute values in a string. The $path variable goes in the first position and is followed by a backslash and the word Process. The $pass variable value goes into the second position followed by an underscore. Lastly, the computer name (contained in the $computer variable goes into the last position, followed by a period and the letters xml. In this manner, the path to the file that will contain the process information (in xml format obviously) is created. (I could have used the Join-Path cmdlet to create the path to the file, but because I wanted to use a combination of three different values from three different variables, I decided this approach was best). This line of code is shown here:

$ppath = ("{0}\Process{1}_{2}.xml" -f $path,$pass,$computer)

After the path has been created, I check to see if there is an old file in the location. If an older file exists, I delete it and write an entry into a custom event log by calling the Add-EventLogEntry function. This section of the Get-ProcessStartUp function is shown here:

if(Test-Path -Path $ppath) 
{
Remove-Item -Path $ppath
Add-EventLogEntry -source gpst -eventType information -eventID 4 `
-message "$ppath exists and is being removed" -logName ForScripting
}

I now use the Get-Process cmdlet to retrieve process information from the computer. The System.Diagnostics.Process .NET Framework object is passed to the Export-CliXML cmdlet with the path we created earlier. The XML file will be used later to “reconstitute” the Process object. This behavior is just like saving the result of running the Get-Process cmdlet into a variable so that it can be worked with later. The difference is we are saving the process object into an XML file for later use. After the XML file is reconstituted, I can work with it in exactly the same way as if I had stored it in a variable. In this way, they behave like “freeze-dried objects.” I pause the script for 60 seconds between passes. If the script has completed the fourth pass, there is no need to pause the script. This portion of the Get-ProcessStartUp function is seen here:

Get-Process -ComputerName $computer| 
Export-Clixml -Path $ppath
if($pass -ne 4)
{Start-Sleep -Seconds 60}
} #end function Get-ProcessStartUp

The Add-EventLogEntry function is used to create a new EventLog and define a new EventLog source. In reality, I guess I should check to see if the EventLog exists. I should also check to see if the EventLog source exists. If these things exist, I should go ahead and use the EventLog and the EventLog source instead of trying to create new ones. If you are interested in this technique, I have an entire chapter in the Microsoft Press Windows PowerShell Scripting Guide.

Because it is the weekend, I decided to use a little structured error handling, specify the error action preference on the New-EventLog cmdlet to silentlycontinue, and catch the errors that arise. After the new EventLog and EventLog source has been created, the finally block writes the entry to the new EventLog. The Add-EventLogEntry function is seen here:

Function Add-EventLogEntry
{
param($source, $eventType, $eventID, $message, $logName)
try
{
New-EventLog -source $source -logname $logName -EA silentlyContinue
}
Catch{ [System.Exception] }
Finally
{ 
Write-EventLog -LogName $logName -Source $source `
-EntryType $eventType `
-EventId $eventID `
-Message $message
} #end catch
} #end function add-eventlogEntry

The ForScripting event log that is created is shown in the following image.

Image of ForScripting event log

To create a new EventLog or to retrieve process information from certain protected system processes requires that the script runs with administrator rights. This is one of the things I kept forgetting to do when I was writing the Get-ProcessStartUpTimes.ps1 script. I decided to copy the Test-IsAdministrator function from my MonitorDiskFormatDrive.ps1 script that I wrote about in Hey, Scripting Guy! Can I Format a Portable Drive When It Is Inserted Into a Computer? (a way cool article by the way). Anyway, the whole idea of the Test-IsAdministrator function is to tell you if you are running with admin rights. If you are, it returns True; otherwise, it returns false. The complete function is shown here. If you want to know more about it, refer to the previously mentioned Hey, Scripting Guy! Blog post.

function Test-IsAdministrator
{
<#
.Synopsis
Tests if the user is an administrator
.Description
Returns true if a user is an administrator, false if the user 
is not an administrator
.Example
Test-IsAdministrator
.Notes
NAME: Test-IsAdministrator
AUTHOR: Ed Wilson
LASTEDIT: 5/20/2009
KEYWORDS:
.Link
Http://www.ScriptingGuys.com
#Requires -Version 2.0
#>
param() 
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
(New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole(`
[Security.Principal.WindowsBuiltinRole]::Administrator)
} #end function Test-IsAdministrator

The entry point to the script first calls the Test-IsAdministrator function. If the function returns true, the script calls the Add-EventLogEntry function and writes an event that the script is starting. This is shown here:

If(-not (Test-IsAdministrator)) 
{ "Admin rights are required for this script" ; exit }
Add-EventLogEntry -source gpst -eventType information -eventID 1 `
-message "beginning $($MyInvocation.InvocationName) at $(get-date)" `
-logName ForScripting

The script then makes four passes. On each pass, it writes a new EventLog entry with the pass number and current time. Next, it calls the Get-ProcessStartup function and passes the path, pass number, and computer name. The pass number comes from the pipeline. The path is a hard-coded UNC path on my network. You should modify it to fit your needs. Because the script is running locally, I pick up the computer name from the environment variables. When the pass is completed, another EventLog entry is added that states the script completed and adds the date to the entry. By the way, eventID 1 is starting the script. EventID 2 is starting the pass, and eventID 3 is completing the script. You can make up your own event ID numbers if you do not like mine. Here is this portion of the script:

1..4 | ForEach-Object {
Add-EventLogEntry -source gpst -eventType information -eventID 2 `
-message "Starting pass $_ at $(get-date)" -logName ForScripting
Get-ProcessStartup -path "\\hyperv-box\shared" -pass $_ -computer ($env:COMPUTERNAME) 
} #end foreach-object
Add-EventLogEntry -source gpst -eventType information -eventID 3 `
-message "Completed $($MyInvocation.InvocationName) at $(get-date)" `
-logName ForScripting

The complete Get-ProcessStartUpTimes.ps1 script is shown here.

Get-ProcessStartUpTimes.ps1

Function Get-ProcessStartUp 
{
Param( 
$path,
$pass,
$computer
)
$ppath = ("{0}\Process{1}_{2}.xml" -f $path,$pass,$computer)
if(Test-Path -Path $ppath) 
{
Remove-Item -Path $ppath
Add-EventLogEntry -source gpst -eventType information -eventID 4 `
-message "$ppath exists and is being removed" -logName ForScripting
}
Get-Process -ComputerName $computer| 
Export-Clixml -Path $ppath
if($pass -ne 4)
{Start-Sleep -Seconds 60}
} #end function Get-ProcessStartUp
Function Add-EventLogEntry
{
param($source, $eventType, $eventID, $message, $logName)
try
{
New-EventLog -source $source -logname $logName -EA silentlyContinue
}
Catch{ [System.Exception] }
Finally
{ 
Write-EventLog -LogName $logName -Source $source `
-EntryType $eventType `
-EventId $eventID `
-Message $message
} #end catch
} #end function add-eventlogEntry
function Test-IsAdministrator
{
<#
.Synopsis
Tests if the user is an administrator
.Description
Returns true if a user is an administrator, false if the user 
is not an administrator
.Example
Test-IsAdministrator
.Notes
NAME: Test-IsAdministrator
AUTHOR: Ed Wilson
LASTEDIT: 5/20/2009
KEYWORDS:
.Link
Http://www.ScriptingGuys.com
#Requires -Version 2.0
#>
param() 
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
(New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole(`
[Security.Principal.WindowsBuiltinRole]::Administrator)
} #end function Test-IsAdministrator
# *** Entry Point to Script ***
If(-not (Test-IsAdministrator)) 
{ "Admin rights are required for this script" ; exit }
Add-EventLogEntry -source gpst -eventType information -eventID 1 `
-message "beginning $($MyInvocation.InvocationName) at $(get-date)" `
-logName ForScripting
1..4 | ForEach-Object {
Add-EventLogEntry -source gpst -eventType information -eventID 2 `
-message "Starting pass $_ at $(get-date)" -logName ForScripting
Get-ProcessStartup -path "\\hyperv-box\shared" -pass $_ -computer ($env:COMPUTERNAME) 
} #end foreach-object
Add-EventLogEntry -source gpst -eventType information -eventID 3 `
-message "Completed $($MyInvocation.InvocationName) at $(get-date)" `
-logName ForScripting

Instead of providing command-line variables for the Get-ProcessStartUpTimes.ps1 script, I decided to use hard-coded values. This is because I anticipate running the script via Group Policy. Next Saturday, I will talk about using Microsoft Group Policy to deploy the script. And next Sunday, I will develop a script to parse the XML files that are saved by the Get-ProcessStartUpTimes.ps1 file. The XML files are shown in Windows Explorer in the shared folder. Note how each of the files begins with “Process,” the pass number, and the computer name. These files, shown in the following image, were created in the Get-ProcessStartup function discussed earlier.

Image of XML files starting with "Process"

The XML file is shown in XML Notepad in the following image. We do not have to manually parse the XML file; the script developed next Sunday will do that for us.

Image of XML file in XML Notepad

Tomorrow, I will begin a series of four articles that describe working with scripts and functions in Windows PowerShell. I will talk about when and how you use a function and the role that scripts play. As the week progresses, I will talk about pausing and restarting the script when a user presses a key on the keyboard, and will even talk about masking passwords a script may require. It is a pretty cool series.

We would love for you to follow us on Twitter or Facebook. If you have any questions, send email to us at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys