Calling an External Script from an Active Roles Server Scheduled Task

Some tasks on an ARS server can be batched up which suits a scheduled task nicely, especially if the task takes a fair while to run.  My little trick when doing this is to use a single scheduled task script and just change the parameters to call different external scripts.  I also figured out how to pass completion messages between the scheduled task and the external script.

In my environment I also like to be able to quickly see who was the last person to run the task, if it was run manually as some of my scripts are.  I do this by reading the event logs and finding the last scheduled run.   This may not be 100% reliable so should not be used for audit purposes as it’s possible some other action happens in-between the script run and the event log being read.

I also came up with a way of providing a one shot capability, i.e. I might want the scheduled task to do a report on a single user when run manually but the daily schedule would report on all users.   Rather than edit the code I add an additional parameter which the called script will clear when the external script is run.  This prevents someone forgetting to clear the attribute so that the scheduled run will report on all users.

I prefix any parameters that are to be oneshot with “a_”.  This makes sure the parameters all show at the top of the scheduled task parameter list making them easier to find.

How the script works is it calls a PowerShell job that will run in the background on the ARS server.

There are only there mandatory parameters.  The PowerShell script name, the PowerShell script path and the PowerShell job name – used to managed the PowerShell jobs.

The scheduled task can have many other parameters but these are not passed as arguments.  The external script will be passed the DN of the task and it will directly read the additional parameters.

Once the external script is called the script will execute a loop that checks if the PowerShell job has completed and uses the return codes to update the scheduled task last run message by throwing an error.

#===========================================================================
# run-ExternalScript
$scriptversion = "2.1"
#===========================================================================
#
# This ActiveRoles Server Scheduled Task Script runs a Powershell Job
# when it is invoked by the ARS Scheduled Task that specifies it.
#
# MANDATORY PARAMETERS:
#
#  nameForPowershellJobs - used to clean up running tasks
#
#  powershellScriptName - the name of the external PowerShell script to run
#                         must include the extention .ps1
#
#  powershellScriptPath - the path to the external script
#
# OPTIONAL PARAMETERS:
#
#  TimeOutMinutes - used to wait for the scrip to finish and return
#                   an exit code
#
#===========================================================================
#
# Version 1.0 : calls an external PosH script with all paramters set in pairs
#               adds the TaskDN and instigator so the called script
#               can use these if required.
#
# Version 2.0 : only sends the TaskDN and instigator parameters the rest are
#               processed in the external script using the TaskDN and
#               get-qadobject
#
#===========================================================================
#
#Region Helper Functions
function Clean-Memory {
 if ($Global:startupvariables) {
	 # if the startup variable exists we assume the call is to clean up after a script run
	 Get-Variable |
	 Where-Object { $Global:startupVariables -notcontains $_.Name } |
		ForEach-Object {
		 try { Remove-Variable -Name "$($_.Name)" -Force -Scope "global" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue}
			catch { }
		}
		# now clean the startupVariables
	 try {Remove-Variable -Name startupvariables  -Scope "Global" -Force -ErrorAction SilentlyContinue }
		catch { }
		# just in case this is an inital run after the script had failed in the last run lets set up the variable again
		New-Variable -name startupVariables -Force -Scope "Global" -value ( Get-Variable | ForEach-Object { $_.Name } )
	}
	else {
	 # Store all the start up variables so you can clean up when the script finishes.
  New-Variable -name startupVariables -Force -Scope "Global" -value ( Get-Variable | ForEach-Object { $_.Name } )
	}
}
Clean-Memory # reset any variables
# prevents command line polution from previous runs
# especially when working in an ISE

function Send-AlertEmail {
<#
.SYNOPSIS
   Sends an alert email when a script failure occurs.
.DESCRIPTION
   Sends an alert email when a script failure occurs
   Uses Splatting to simplyfy the parameter list.
.PARAMETER to
  The email address (array of email addresses) to send the alert to
.PARAMETER from
  Sets the Email From used to replying to the Alert
.PARAMETER BodyText
  The bodytext of the alert email
.PARAMETER subject
  The subject line of the alert email
 .PARAMETER exitMessage
  This is used to display a message on the lastrun message on an ARS task
 .EXAMPLE
     Send-AlertEmail @mailParameters -smtpServer "smtp.mydomain.com"
    .INPUTS
    .OUTPUTS
    .NOTES
        NAME     : Send-AlertEmail
        VERSION  : Version 1.0
        AUTHOR   : Lee Andrews
        CREATED  : 13th November 2012
        LASTEDIT : 13th November 2012 - Version 1.0
    .LINK
        https://clan8blog.wordpress.com

#>
#
[CmdletBinding(SupportsShouldProcess=$False,ConfirmImpact="None")]
param(
[Parameter(HelpMessage="An Array of email addresses to send the alert to",Position=0)]
[string[]]$to="SupportEmail@clan8.com" ,
[Parameter(HelpMessage="The replyto email address",Position=1)]
[string]$from="DoNotReply@clan8.com" ,
[Parameter(HelpMessage="The email body text",Position=2)]
[string]$BodyText="Error in script run" ,
[Parameter(HelpMessage="The email subject text",Position=3)]
[string]$subject="Error in script Run" ,
[Parameter(HelpMessage="Defaults to smtp.clan8.com",Position=4)]
[string]$smtpServer="smtp.clan8.com",
[Parameter(HelpMessage="Text to be displayed when the script exits"Position=5)]
[string]$exitMessage="Error in Script Run"
)
 if ( $debug -eq $true ) {
  $To = "lee.andrews@MyDomain.com"
 }
 $mailAlertParameterDefaults = @{
  "To"         = $To
  "From"       = $from
  "SmtpServer" = $smtpServer
  "Body"       = $BodyText
  "Subject"    = $subject
 }
 Send-MailMessage @mailAlertParameterDefaults
 Clean-Memory
 throw $exitMessage
}
function Clear-Jobs {
 if ( $jobList ) { Remove-Variable -Name jobList -ErrorAction SilentlyContinue }
 if ( $psJob ) { Remove-Variable -Name psJob -ErrorAction SilentlyContinue }
 # get the list of running PosH jobs.
 $jobList = Get-Job -name $nameForPowershellJobs -ErrorAction SilentlyContinue
 # clean up the jobs
 if ( ($jobList) -and ($($jobList | Measure-Object).count -gt 0  ) ) {
  foreach ( $psJob in $jobList ) {
   if ( $psJob.state -eq 'blocked' ) { Stop-Job -Id $psJob.id }
   if (
    ( $psJob.state -eq 'Stopped' ) -or
    ( $psJob.State -eq 'Completed' ) -or
    ( $psJob.State -eq 'Failed' )
   ) {
    try { Remove-Job -Id $psJob.Id } catch {}
   }
  }
 }
 # release the variables from memory
 if ( $jobList ) { Remove-Variable -Name jobList -ErrorAction SilentlyContinue }
 if ( $psJob )   { Remove-Variable -Name psJob   -ErrorAction SilentlyContinue }
}
#endregion
# get the details of the person calling the scheduled task - this assumes that
# no other transaction took place after this one, which is reasonable but
# not perfect so treat this as info only not as an audit trail
$instigator = $(Get-EventLog -logname "EDM Server"  -Newest 1 -InstanceID 2692).username
$instigator = $instigator.Substring($instigator.IndexOf("\")+1)
$DN = $null
#region Set Debug variables
#===========================================================================
# uncomment the path below when running a debug
#$debug = $true
#$DebugPreference = "Continue"
#$taskDN = "CN=Run-ExternanlScript,CN=Service Desk Tasks,CN=Clan8,CN=Scheduled Tasks,CN=Server Configuration,CN=Configuration"
#===========================================================================
#endregion
# Retrieve Scheduled Task Parameters
try {
 $Task.DirObj.GetInfo()
 $Task.DirObj.GetInfoEx(@("edsaParameters"),0)
 $parameters = $Task.DirObj.Get("edsaParameters")
 $DN = $Task.DirObj.DN
 $DebugPreference = "SilentlyContinue"
}
catch { }
if ( $DN -eq $null ) {
 $Task = Get-QADObject -Identity $TaskDN  -Proxy -IncludedProperties edsaParameters
 if ( $Task -ne $null ) {
  $parameters = $Task.edsaParameters
  $DN = $TaskDN
 }
 else { throw "Unable to get TASK DN script cannot continue" }
}
# now get the mandatory parameters
$nameForPowershellJobs = $null
$powershellScriptName = $null
$powershellScriptPath = $null
$timeOutMinutes = $null

foreach ( $parameter in $parameters ) {
 switch ( $($parameter.Substring( 1,$parameter.IndexOf(">")-1)) ) {
  "nameForPowershellJobs" {
   $nameForPowershellJobs = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
   break
  }
  "powershellScriptName" {
   $powershellScriptName  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
   break
  }
  "powershellScriptPath" {
   $powershellScriptPath  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
   break
  }
  "TimeOutMinutes" {
   $timeOutMinutes  = $($parameter.Substring( $parameter.IndexOf(">")+1,$parameter.IndexOf("</")-($parameter.IndexOf(">")+1)))
   break
  }
 }
}
# now check we got all 3 parameters
if (
    ( $nameForPowershellJobs -eq $null) -or
    ( $powershellScriptName -eq $null ) -or
    ( $powershellScriptPath -eq $null ) -or
    ( $nameForPowershellJobs -eq "" )   -or
    ( $powershellScriptName -eq "" )    -or
    ( $powershellScriptPath -eq "" )
   ) {
 throw "Missing one or more of the mandatory parameters, nameForPowershellJobs, powershellScriptName or powershellScriptPath"
}
# set a timer to periodiclaly check if the external script has finished
if ( ( $timeOutMinutes -eq $null  ) -or ( $timeOutMinutes -eq "" ) ) {
 $timeOutMinutes = 10 # default is 10 minutes
}
# check that the PS path does not include the PS script name
if ( $powershellScriptPath.EndsWith(".ps1" ) )  { $powershellScriptPath = $powershellScriptPath.Substring(0,$powershellScriptPath.LastIndexOf("\")) }
# check if the path ends with a backslash if not add it
if ( ! ( $powershellScriptPath.EndsWith("\") 	) ) { $powershellScriptPath = "$powershellScriptPath\" }
$powershellScript =  "$powershellScriptPath$powershellScriptName"
# call the powershell job
# first do some cleanup - get the list of running jobs
Clear-Jobs
# build the powershell command parameters
$argumentlist = ""
$argumentlist += """Instigator"",""$instigator"",""TaskDN"",""$DN"""
if ( $debug -eq $true ) {
 $DebugLogParameters = @{
  "filePath"      = "c:\temp\RunExternalTask.txt"
  "InputObject"   = "Start-Job -filepath $powershellScript  -name $nameForPowershellJobs -Argumentlist $argumentlist"
 }
 Out-File @DebugLogParameters
}
$powershellScript = """$powershellScript"""
$command = '$job=' + "Start-Job -filepath $powershellScript  -name $nameForPowershellJobs -Argumentlist $argumentlist"
Invoke-Expression $command
$iteration = 0
$Interations = ($timeOutMinutes * 60)/60
$waitinterval = 60 # seconds
while ( ($job.State.ToString() -eq "Running") -and ( $iteration -le $Interations ) ) {
 if ( $debug -eq $true ) {
  $DebugLogParameters = @{
   "filePath"      = "c:\temp\RunExternalTask.txt"
   "InputObject"   = "Job Status: $($job.State.ToString()) - iteration $iteration"
  }
  Out-File @DebugLogParameters
 }
 $iteration++
	Write-Debug "Job State = $($job.State.ToString())"
 Start-Sleep -Seconds $waitinterval
}
$exitMessage = $($job.ChildJobs[0].JobStateInfo.Reason.Message)
$jobStatus = $job.State.ToString()
Clear-jobs
# output the completion message by Throwing an error so it shows on the last run message status
switch ( $jobStatus ) {
 "Failed" {
  throw $exitMessage
 }
 "Running" {
  $BodyText = "ERROR Script running for more than 1 hour: $instigator called $powershellScriptName"
  $subject  = "ERROR Script running for more than 1 hour: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script running for more than 1 hour: $instigator called $powershellScriptName"
 }
 "Blocked" {
  # end the job - this means something in the script is awaiting user input and therefore the script will never complete
  $BodyText = "ERROR Script BLOCKED: $instigator called $powershellScriptName"
  $subject  = "ERROR Script BLOCKED: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script BLOCKED: $instigator called $powershellScriptName"
  break
 }
 "Stopped" {
  $BodyText = "ERROR Script STOPPED: $instigator called $powershellScriptName"
  $subject  = "ERROR Script STOPPED: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script STOPPED: $instigator called $powershellScriptName"
  break
 }
 "Stopping" {
  $BodyText = "ERROR Script STOPPING: $instigator called $powershellScriptName"
  $subject  = "ERROR Script STOPPING: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script STOPPING: $instigator called $powershellScriptName"
  break
 }
 "Suspended" {
  $BodyText = "ERROR Script SUSPENDED: $instigator called $powershellScriptName"
  $subject  = "ERROR Script SUSPENDED: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script SUSPENDED: $instigator called $powershellScriptName"
  break
 }
 "Suspending" {
  $BodyText = "ERROR Script SUSPENDING: $instigator called $powershellScriptName"
  $subject  = "ERROR Script SUSPENDING: $instigator called $powershellScriptName"
  Send-AlertEmail -Body $BodyText -Subject $subject
  throw "ERROR Script SUSPENDED: $instigator called $powershellScriptName"
  break
 }
}
throw "NO RETURN CODE : $instigator called $powershellScriptName"