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"
Related articles
- PowerShell Clean up after yourself! (clan8blog.wordpress.com)
- Active Roles Scheduled Task Parameters (clan8blog.wordpress.com)