PowerShell Reminder Jobs

timerThis is something that might be better suited to one of my Friday Fun columns, but I’m enjoying this so much I couldn’t wait to share it. I don’t know about you but I spend much of my day in PowerShell or at least with a PowerShell session running. I have an ongoing quest to do as much as I can from PowerShell. This includes just about any sort of task that might be automated.

In the past I’ve blogged about my tickle system that run when I start PowerShell. But often I have short term tickler or reminder needs. For example, since I work at home I may need to remember to switch over the laundry or that I have a phone call at 2:00PM. Sure, I could set up calendar alerts in an email client but I’d rather have something as quick and easy as a PowerShell command. So I wrote this script, New-Reminderjob.ps1.

In short, the script creates a background job that will use the MSG.EXE command line tool to display a message to myself. The job action sleeps the specified number of seconds and then runs my MSG.EXE command. I could have used a variety of ways to display the message, but MSG.EXE is built in and I didn’t see any reason to re-invent the wheel.

The script, which you could revise into a function if you want, requires the reminder text and then when to “deliver” the reminder. You can specify a number of minutes, the default is 1, or a date and/or time. If you specify a time like “9:00AM” the script will assume you mean 9AM today. The script converts your start time into a number of seconds it has to wait and builds that into the job scriptblock.

When the time comes I get a popup message like this.
msg01
The message will automatically dismiss after about 1 minute. Or I’ve configure my script to allow you to require that you acknowledge the message through the -Wait parameter. This is useful for important reminders you want to make sure you don’t miss.

Because I might have several daily reminders, I wanted an easy way to identify them. One thing I did with my script is to give all of my reminder jobs a custom name that starts with ‘Reminder’. I use a regular expression to find the number from the most recent reminder job and increment it by one. The other useful step is that I added some custom properties to the job object itself. These properties embed values from the script into the job object. Now I can do interesting commands like this:

msg02

The custom properties have no effect on any other job objects. If I find myself using these properties a lot, I might create some additional functions to save some typing.

This system is meant for ad-hoc, daily reminders to myself which is why I didn’t use scheduled jobs. I didn’t want to have to deal with cleaning up a bunch of one time jobs. These reminder jobs only last for as long as my PowerShell session is open. But be aware, that each running reminder will start a new PowerShell process so I wouldn’t recommend setting this up with dozens of reminders. Actually, if you need that many reminders you either need to get a new job or an assistant!

I hope you’ll try it out and let me know what you think or where you think it can be improved. Enjoy!

The PowerShell Morning Report

ZazuI love how easy it is to manage computers with Windows PowerShell. It is a great reporting tool, but often I find people getting locked into one approach. I’m a big believer in flexibility and re-use and using objects in the pipeline wherever I can. So I put together a PowerShell script that I can run every morning on my computer and get a quick summary about what is happening, or perhaps not happening. My script, MorningReport.ps1, relies on WMI to gather a variety of system information. By default it connects to the local host, but I’ve provided a computername parameter. I’m assuming current credentials are good enough for any remote system, but you can always add support for alternate credentials, assuming I don’t in some future version.

By default the script writes a custom object to the pipeline that contains all of the other WMI information like disk utilization, service status, and event logs. But I wanted flexibility and ease of use, so the script also supports parameters of -Text and -HTML. The former creates nicely formatted text suitable for printing or saving to a file. The latter creates HTML code from the original objects. I rely heavily on creating HTML fragments with ConvertTo-HTML and then assembling everything at the end. The script writes the HTML code to the pipeline so if you want to save results to a file, simply pipe to Out-File. I did this because there may be times when you want the “raw” HTML code. You might want to save the HTML and create an HTML mail message with Send-MailMessage. Or maybe further tweak the HTML before saving it to a file. Again, I didn’t want to lock myself in. Here’s the main part of the script.


[cmdletbinding(DefaultParameterSetName="object")]

Param(
[Parameter(Position=0,ValueFromPipeline=$True)]
[ValidateNotNullOrEmpty()]
[string]$Computername=$env:computername,
[ValidateNotNullOrEmpty()]
[alias("title")]
[string]$ReportTitle="System Report",
[ValidateScript({$_ -ge 1})]
[int]$Hours=24,
[Parameter(ParameterSetName="HTML")]
[switch]$HTML,
[Parameter(ParameterSetName="TEXT")]
[switch]$Text
)

#script internal version number used in output
[string]$reportVersion="1.0.8"

<# define some HTML style here's a source for HTML color codes http://www.immigration-usa.com/html_colors.html the code must be left justified #>
$head = @"

$ReportTitle
"@

If ($computername -eq $env:computername) {
#local computer so no ping test is necessary
$OK=$True
}
elseIf (($computername -ne $env:computername) -AND (Test-Connection -ComputerName $computername -quiet -Count 2)) {
#not local computer and it can be pinged so proceed
$OK=$True
}

If ($OK) {

Try {
$os=Get-WmiObject Win32_operatingSystem -ComputerName $computername -ErrorAction Stop
#set a variable to indicate WMI can be reached
$wmi=$True
}
Catch {
Write-Warning "WMI failed to connect to $($computername.ToUpper())"
}

if ($wmi) {
Write-Host "Preparing morning report for $($os.CSname)" -ForegroundColor Cyan

#OS Summary
Write-Host "...Operating System" -ForegroundColor Cyan
$osdata=$os | Select @{Name="Computername";Expression={$_.CSName}},
@{Name="OS";Expression={$_.Caption}},
@{Name="ServicePack";Expression={$_.CSDVersion}},
free*memory,totalv*,NumberOfProcesses,
@{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootupTime)}},
@{Name="Uptime";Expression={(Get-Date) - ($_.ConvertToDateTime($_.LastBootupTime))}}

#Computer system
Write-Host "...Computer System" -ForegroundColor Cyan
$cs=Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
$csdata=$cs | Select Status,Manufacturer,Model,SystemType,Number*

#services
Write-Host "...Services" -ForegroundColor Cyan
#get services via WMI and group into a hash table
$wmiservices=Get-WmiObject -class Win32_Service -ComputerName $computername
$services=$wmiservices | Group State -AsHashTable

#get services set to auto start that are not running
$failedAutoStart=$wmiservices | Where { ($_.startmode -eq "Auto") -AND ($_.state -ne "Running")}

#Disk Utilization
Write-Host "...Logical Disks" -ForegroundColor Cyan
$disks=Get-WmiObject -Class Win32_logicaldisk -Filter "Drivetype=3" -ComputerName $computername
$diskData=$disks | Select DeviceID,
@{Name="SizeGB";Expression={$_.size/1GB -as [int]}},
@{Name="FreeGB";Expression={"{0:N2}" -f ($_.Freespace/1GB)}},
@{Name="PercentFree";Expression={"{0:P2}" -f ($_.Freespace/$_.Size)}}

#NetworkAdapters
Write-Host "...Network Adapters" -ForegroundColor Cyan
#get NICS that have a MAC address only
$nics=Get-WmiObject -Class Win32_NetworkAdapter -filter "MACAddress Like '%'" -ComputerName $Computername
$nicdata=$nics | Foreach {
$tempHash=@{Name=$_.Name;DeviceID=$_.DeviceID;AdapterType=$_.AdapterType;MACAddress=$_.MACAddress}
#get related configuation information
$config=$_.GetRelated() | where {$_.__CLASS -eq "Win32_NetworkadapterConfiguration"}
#add to temporary hash
$tempHash.Add("IPAddress",$config.IPAddress)
$tempHash.Add("IPSubnet",$config.IPSubnet)
$tempHash.Add("DefaultGateway",$config.DefaultIPGateway)
$tempHash.Add("DHCP",$config.DHCPEnabled)
#convert lease information if found
if ($config.DHCPEnabled -AND $config.DHCPLeaseObtained) {
$tempHash.Add("DHCPLeaseExpires",($config.ConvertToDateTime($config.DHCPLeaseExpires)))
$tempHash.Add("DHCPLeaseObtained",($config.ConvertToDateTime($config.DHCPLeaseObtained)))
$tempHash.Add("DHCPServer",$config.DHCPServer)
}

New-Object -TypeName PSObject -Property $tempHash

}

#Event log errors and warnings in the last $Hours hours
$last=(Get-Date).AddHours(-$Hours)
#System Log
Write-Host "...System Event Log Error/Warning since $last" -ForegroundColor Cyan
$syslog=Get-EventLog -LogName System -ComputerName $computername -EntryType Error,Warning -After $last
$syslogdata=$syslog | Select TimeGenerated,EventID,Source,Message

#Application Log
Write-Host "...Application Event Log Error/Warning since $last" -ForegroundColor Cyan
$applog=Get-EventLog -LogName Application -ComputerName $computername -EntryType Error,Warning -After $last
$applogdata=$applog | Select TimeGenerated,EventID,Source,Message

} #if wmi is ok

#write results depending on parameter set
$footer="Report v{3} run {0} by {1}\{2}" -f (Get-Date),$env:USERDOMAIN,$env:USERNAME,$reportVersion

if ($HTML) {
#prepare HTML code
$fragments=@()
#insert a graphic header with one of the two following lines
#$fragments+="

"
$fragments+="Zazu

The Morning Report

"

#insert navigation bookmarks
$nav=@"
Services
Failed Auto Start
Disks
Network
System Log
Application Log
"@
$fragments+=$nav
$fragments+="
"

#add a link to the document top
$nav+="`nTop"

$fragments+="

System Summary

"
$fragments+=$osdata | ConvertTo-Html -as List -Fragment
$fragments+=$csdata | ConvertTo-Html -as List -Fragment
$fragments+=ConvertTo-Html -Fragment -PreContent "

Services

"
$services.keys | foreach {
$fragments+= ConvertTo-Html -Fragment -PreContent "

$_

"
$fragments+=$services.$_ | Select Name,Displayname,StartMode| ConvertTo-HTML -Fragment
#insert navigation link after each section
$fragments+=$nav
}

$fragments+=$failedAutoStart | Select Name,Displayname,StartMode,State |
ConvertTo-Html -Fragment -PreContent "

Failed Auto Start

"
$fragments+=$nav

$fragments+=$diskdata | ConvertTo-HTML -Fragment -PreContent "

Disk Utilization

"
$fragments+=$nav

#convert nested object array properties to strings
$fragments+=$nicdata | Select Name,DeviceID,DHCP*,AdapterType,MACAddress,
@{Name="IPAddress";Expression={$_.IPAddress | Out-String}},
@{Name="IPSubnet";Expression={$_.IPSubnet | Out-String}},
@{Name="IPGateway";Expression={$_.DefaultGateway | Out-String}} |
ConvertTo-HTML -Fragment -PreContent "

Network Adapters

"
$fragments+=$nav

$fragments+=$syslogData | ConvertTo-HTML -Fragment -PreContent "

System Event Log Summary

"
$fragments+=$nav

$fragments+=$applogData | ConvertTo-HTML -Fragment -PreContent "

Application Event Log Summary

"
$fragments+=$nav

Write $fragments | clip
ConvertTo-Html -Head $head -Title $ReportTitle -PreContent ($fragments | out-String) -PostContent "
$footer"
}
elseif ($TEXT) {
#prepare formatted text
$ReportTitle
"-"*($ReportTitle.Length)
"System Summary"
$osdata | Out-String
$csdata | format-List | Out-String
Write "Services"
$services.keys | foreach {
$services.$_ | Select Name,Displayname,StartMode,State
} | Format-List | Out-String
Write "Failed Autostart Services"
$failedAutoStart | Select Name,Displayname,StartMode,State
Write "Disk Utilization"
$diskdata | Format-table -AutoSize | Out-String
Write "Network Adapters"
$nicdata | Format-List | Out-String
Write "System Event Log Summary"
$syslogdata | Format-List | Out-String
Write "Application Event Log Summary"
$applogdata | Format-List | Out-String
Write $Footer
}
else {
#Write data to the pipeline as part of a custom object

New-Object -TypeName PSObject -Property @{
OperatingSystem=$osdata
ComputerSystem=$csdata
Services=$services.keys | foreach {$services.$_ | Select Name,Displayname,StartMode,State}
FailedAutoStart=$failedAutoStart | Select Name,Displayname,StartMode,State
Disks=$diskData
Network=$nicData
SystemLog=$syslogdata
ApplicationLog=$applogdata
ReportVersion=$reportVersion
RunDate=Get-Date
RunBy="$env:USERDOMAIN\$env:USERNAME"
}
}

} #if OK

else {
#can't ping computer so fail
Write-Warning "Failed to ping $computername"
}

As you can see, it is a lengthy script, but I’ve tried to include a fair amount of internal comments and documentation, so I won’t repeat it here, but I will touch on a few key points.

First, I’m embedding a style sheet directly in any HTML output so any files I create can stand alone. You could certainly modify the relevant sections and use the -CSSUri parameter with ConvertTo-HTML. Next, I use a combination of Test-Connection and Try/Catch to handle computers that are offline or I can’t access. This speeds up the script and makes it nicer for the script user. Assuming all is good, I create a number of variables that hold the WMI information I am interested in.


...
#OS Summary
Write-Host "...Operating System" -ForegroundColor Cyan
$osdata=$os | Select @{Name="Computername";Expression={$_.CSName}},
@{Name="OS";Expression={$_.Caption}},
@{Name="ServicePack";Expression={$_.CSDVersion}},
free*memory,totalv*,NumberOfProcesses,
@{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootupTime)}},
@{Name="Uptime";Expression={(Get-Date) - ($_.ConvertToDateTime($_.LastBootupTime))}}

#Computer system
Write-Host "...Computer System" -ForegroundColor Cyan
$cs=Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
$csdata=$cs | Select Status,Manufacturer,Model,SystemType,Number*
...

After everything is collected, then I can write output using an If/ElseIf statement depending on what parameters were passed. If no parameters were specified then a custom object is assembled and written to the pipeline.


else {
#Write data to the pipeline as part of a custom object

New-Object -TypeName PSObject -Property @{
OperatingSystem=$osdata
ComputerSystem=$csdata
Services=$services.keys | foreach {$services.$_ | Select Name,Displayname,StartMode,State}
FailedAutoStart=$failedAutoStart | Select Name,Displayname,StartMode,State
Disks=$diskData
Network=$nicData
SystemLog=$syslogdata
ApplicationLog=$applogdata
ReportVersion=$reportVersion
RunDate=Get-Date
RunBy="$env:USERDOMAIN\$env:USERNAME"
}

But perhaps you’d like to see this in action. Here’s a sample HTML SampleReport. In this report, there are no recent errors or warnings in the Application event log.

Or if you have a few minutes, here’s a short clip of the script in action.

I hope you find this a useful jumping off point for your own script, although I think I’ve already worked out information you are most likely interested in. Download MorningReport.ps1 and try it out for yourself.

Update: Read about a revised version that accepts pipelined input.

New Petri Author

New Petri Author

I trust many of you are familiar with the Petri web site. This has long been a very valuable site for IT Pros. You can always find a wealth of reliable and practical information. Starting in December, I will be adding my voice to the mix. I will be contributing content that I hope you find valuable and worth your time. I expect my articles will be wide ranging, and not limited to Windows PowerShell. With the impending arrival of Windows 8 there will be much to write about. I hope you'll join me for the ride.

Petri IT Knowledgebase

One of the world's leading MCSE and IT related knowledge bases with thousands of Windows, Exchange, and Virtualization related tips, tricks and how-to articles.

My How To Guide to using the Easy Transfer…

My How To Guide to using the Easy Transfer Wizard in Windows 7. Very handy to move files between computers or provide a quick backup.

The Guide to Migrating Windows User Files & Settings

The Guide to Migrating Windows User Files & Settings. OCT 20. By Jeffery Hicks 3 hours ago under Help Desk Management. Tweet. Despite our best efforts, sometimes the best resolution to a help desk…

Using the Problem Steps Recorder

Using the Problem Steps Recorder in Windows 7 http://bit.ly/obasPi #SMBIT

Problem Steps Recorder for Windows 7 Troubleshooting. SEP 27. By Jeffery Hicks 17 days ago under Help Desk Management. Tweet. Save. Often, the first challenge in solving a problem is understanding it….