Friday Fun: A PowerShell Alarm Clock

Today’s Friday Fun is a continuation of my exploration of ways to use Start-Job. A few weeks ago I wrote about using Start-Job to create “scheduled” tasks. I realized I could take this further and turn this into a sort of alarm clock. The goal is to execute at command at a given time, but I wanted to make it easy to specify the time. What I have so far is a function called New-Alarm. I have some other ideas and hope to expand this into a module, but for now I thought I’d toss this out to you and get some feedback.

Function New-Alarm {

[cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="Time")]

Param (
[Parameter(Position=0,ValueFromPipelineByPropertyName=$True)]
[ValidateNotNullorEmpty()]
[string]$Command="Notepad",
[Parameter(Position=1,ValueFromPipelineByPropertyName=$True,ParameterSetName="Time")]
[ValidateNotNullorEmpty()]
[Alias("time")]
[datetime]$Alarm=(Get-Date).AddMinutes(5),
[Parameter(ValueFromPipelineByPropertyName=$True,ParameterSetName="Seconds")]
[int]$Seconds,
[Parameter(ValueFromPipelineByPropertyName=$True,ParameterSetName="Minutes")]
[int]$Minutes,
[Parameter(ValueFromPipelineByPropertyName=$True,ParameterSetName="Hours")]
[int]$Hours,
[Parameter(ValueFromPipelineByPropertyName=$True)]
[Alias("init","is")]
[string]$InitializationScript
)

Process {

if ($seconds) {$Alarm=(Get-Date).AddSeconds($seconds)}
if ($minutes) {$Alarm=(Get-Date).AddMinutes($minutes)}
if ($Hours) {$Alarm=(Get-Date).AddHours($hours)}

Write-Verbose ("{0} Creating an alarm for {1} to execute {2}" -f (Get-Date),$Alarm,$Command)

#define a scriptblock that takes parameters. Parameters are validated in the
#function so we don't need to do it here.
$sbText=@"
    Param ([string]`$Command,[datetime]`$Alarm,[string]`$Init)
   
    #define a boolean flag
    `$Done=`$False
   
    #loop until the time is greater or equal to the alarm time
    #sleeping every 10 seconds
    do  {
        if ((get-date) -ge `$Alarm) {
          #run the command
          `$ActualTime=Get-Date
          Invoke-Expression `$Command
          #set the flag to True
          `$Done=`$True
          }
        else {
         sleep -Seconds 10
    }
    } while (-Not `$Done)
   
    #write an alarm summary object which can be retrieved with Receive-Job
    New-Object -TypeName PSObject -Property @{
      ScheduledTime=`$Alarm
      ActualTime=`$ActualTime
      Command=`$Command
      Initialization=`$Init
    }
"
@

#append metadata to the scriptblock text so they can be parsed out with Get-Alarm
#to discover information for currently running alarm jobs

$meta=@"

#Alarm Command::$Command
#Alarm Time::$Alarm
#Alarm Init::$InitializationScript
#Alarm Created::$(Get-Date)

"
@

#add meta data to scriptblock text
$sbText+=$meta

Write-Debug "Scriptblock text:"
Write-Debug $sbText
Write-Debug "Creating the scriptblock"

#create a scriptblock to use with Start-Job
$sb=$ExecutionContext.InvokeCommand.NewScriptBlock($sbText)

Try {
    If ($InitializationScript) {
        #turn $initializationscript into a script block
        $initsb=$ExecutionContext.InvokeCommand.NewScriptBlock($initializationscript)
        Write-Verbose ("{0} Using an initialization script: {1}" -f (Get-Date),$InitializationScript)
    }
    else {
        #no initialization command so create an empty scriptblock
        $initsb={}
    }
   
    #WhatIf
    if ($pscmdlet.ShouldProcess("$command at $Alarm")) {
        #create a background job
        Start-job -ScriptBlock $sb -ArgumentList @($Command,$Alarm,$InitializationScript) -ErrorAction "Stop" -InitializationScript $Initsb
        Write-Verbose ("{0} Alarm Created" -f (Get-Date))
    }
}

Catch {
    $msg="{0} Exception creating the alarm job. {1}" -f (Get-Date),$_.Exception.Message
    Write-Warning $msg
}
} #Process

} #end function

The function includes full help.

To use the function you specify a command string to execute at a given time. The default’s are to run Notepad in 5 minutes. You can either specify an exact time.

PS C:\> new-alarm "get-process | out-file c:\work\noonprocs.txt" -alarm "12:00PM"

Or X number of seconds, minutes or hours.

PS C:\> $s='$f=[system.io.path]::GetTempFilename(); "Hey! Are you paying attention??" > $f;start-process notepad $f -wait;del $f'
PS C:\> new-alarm $s -minutes 15 -verbose

The first command defines a command string, $s. This creates a temporary file, writes some text to it, displays it with Notepad and then deletes it. The second command creates a new alarm that will invoke the expression in 15 minutes.

For now, the command is passed as text. This is so that I can create an internal scriptblock. I use a Do loop to compare the current time to the alarm time. When the time is right, the command string is executed using Invoke-Expression.

$sbText=@"
    Param ([string]`$Command,[datetime]`$Alarm,[string]`$Init)
   
    #define a boolean flag
    `$Done=`$False
   
    #loop until the time is greater or equal to the alarm time
    #sleeping every 10 seconds
    do  {
        if ((get-date) -ge `$Alarm) {
          #run the command
          `$ActualTime=Get-Date
          Invoke-Expression `$Command
          #set the flag to True
          `$Done=`$True
          }
        else {
         sleep -Seconds 10
    }
    } while (-Not `$Done)
   
    #write an alarm summary object which can be retrieved with Receive-Job
    New-Object -TypeName PSObject -Property @{
      ScheduledTime=`$Alarm
      ActualTime=`$ActualTime
      Command=`$Command
      Initialization=`$Init
    }
"
@

I also add some metadata to the script block which gets written as the job’s result.

#append metadata to the scriptblock text so they can be parsed out with Get-Alarm
#to discover information for currently running alarm jobs

$meta=@"

#Alarm Command::$Command
#Alarm Time::$Alarm
#Alarm Init::$InitializationScript
#Alarm Created::$(Get-Date)

"@

#add meta data to scriptblock text
$sbText+=$meta

Write-Debug "Scriptblock text:"
Write-Debug $sbText
Write-Debug "Creating the scriptblock"

#create a scriptblock to use with Start-Job
$sb=$ExecutionContext.InvokeCommand.NewScriptBlock($sbText)

Finally, the alarm function allows for an initialization command, like you might use with Start-Job. This permits you to run commands such as importing modules or dot sourcing scripts. I have a function that displays a VB style message box. Here’s how I might use it as an alarm job.

PS C:\> new-alarm "get-messagebox 'It is time for that thing' -title 'Alert!'" -init ". c:\scripts\get-messagebox.ps1" -min 5

In 5 minutes the alarm will go off and I’ll get this.

Remember, the function is creating new jobs with the Start-Job cmdlet. Which means I can get job results.

PS C:\> receive-job 7 -keep

Initialization : . c:\scripts\get-messagebox.ps1
ActualTime     : 1/20/2012 8:47:07 AM
ScheduledTime  : 1/20/2012 8:47:06 AM
Command        : get-messagebox 'It is time for that thing' -title 'Alert!'
RunspaceId     : d3461b78-11ce-4c84-a8ab-9e3fcd482637

What do you think? As I said, I have a few more ideas and there are certainly a few tweaks I can make even to this code. I’ve added my Get-MessageBox function in case you want to toy with that. Download AlarmScripts.zip and let me know what you think.

Post to Twitter Post to Plurk Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to FriendFeed Post to Google Buzz Post to Ping.fm Post to Reddit Post to Slashdot Post to StumbleUpon Post to Technorati

This entry was posted in Friday Fun, PowerShell, PowerShell v2.0 and tagged , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>