BW.Utils.PSCron.psm1
using namespace System.Management.Automation using namespace System.Collections using namespace Microsoft.PowerShell using module '.\classes\PSCronDateTime.psm1' using module '.\classes\PSCronJobObject.psm1' $__ScriptPath = Split-Path (Get-Variable MyInvocation -Scope Script).Value.Mycommand.Definition -Parent Add-Type -Path "$__ScriptPath\lib\Cronos-0.7.0\netstandard2.0\Cronos.dll" # .ExternalHelp BW.Utils.PSCron-help.xml function Get-PSCronDate { [OutputType( [PSCronDateTime] )] param( [Parameter(Position=1)] [datetime] $Date = ( Get-Date ), [Parameter(Position=2)] [PSCronTicks] $Resolution = [PSCronTicks]::Minute ) return [PSCronDateTime]::new( $Date, $Resolution ) } # .ExternalHelp BW.Utils.PSCron-help.xml function Test-PSCronShouldRun { [OutputType( [bool] )] [CmdletBinding()] param( [Parameter( Mandatory, Position=1 )] [string] $Schedule, [Parameter( Position=2 )] [PSCronDateTime] $ReferenceDate = ( Get-PSCronDate ), [Parameter( ValueFromRemainingArguments, DontShow )] $IgnoredArguments ) $ThisRun = Get-PSCronNextRun -Schedule $Schedule -ReferenceDate $ReferenceDate -Inclusive return $ThisRun -eq $ReferenceDate } # .ExternalHelp BW.Utils.PSCron-help.xml function Get-PSCronNextRun { [OutputType( [PSCronDateTime] )] [CmdletBinding()] param( [Parameter(Mandatory, Position=1)] [string] $Schedule, [Parameter( Position=2 )] [PSCronDateTime] $ReferenceDate = ( Get-PSCronDate ), [switch] $Inclusive, [Parameter( ValueFromRemainingArguments, DontShow )] $IgnoredArguments ) $Offset = $ReferenceDate.Local - $ReferenceDate.Utc $ReferenceDate = $ReferenceDate + $Offset try { $CronSchedule = [Cronos.CronExpression]::Parse( $Schedule ) } catch { throw $_ } [PSCronDateTime]$NextRun = $CronSchedule.GetNextOccurrence( $ReferenceDate.Utc, [System.TimeZoneInfo]::Utc, $Inclusive ) return ( $NextRun - $Offset ) } # .ExternalHelp BW.Utils.PSCron-help.xml function Get-PSCronSchedule { [OutputType( [PSCronDateTime[]] )] param( [Parameter(Mandatory, Position=1)] [string] $Schedule, [Parameter(Position=2)] [PSCronDateTime] $Start = ( Get-PSCronDate -Resolution Day ), [Parameter(Position=3)] [PSCronDateTime] $End = ( Get-PSCronDate -Date (Get-Date).AddDays( 1 ) -Resolution Day ), [switch] $IncludeStart, [switch] $IncludeEnd, [Parameter( ValueFromRemainingArguments, DontShow )] $IgnoredArguments ) $CronSchedule = [Cronos.CronExpression]::Parse( $Schedule ) return [PSCronDateTime[]]$CronSchedule.GetOccurrences( $Start.Utc, $End.Utc, $IncludeStart, $IncludeEnd ) } # .ExternalHelp BW.Utils.PSCron-help.xml function Invoke-PSCronJob { [CmdletBinding( DefaultParameterSetName='ScriptBlock' )] param( [Parameter( Mandatory, Position=1 )] [string] $Schedule, [Parameter( Mandatory, Position=2 )] [string] $Name, [Parameter( Mandatory, Position=3, ParameterSetName='ScriptBlock' )] [scriptblock] $Definition, [Parameter( Mandatory, ParameterSetName='File' )] [string] $FilePath, [string[]] $IncludeScripts, [string] $Description, [string] $WorkingDirectory, [hashtable] $Parameters, [object[]] $Arguments, [string] $LogPath, [switch] $Append, [int] $TimeOut, [ActionPreference] $JobInformationPreference, [ActionPreference] $JobDebugPreference, [ActionPreference] $JobWarningPreference, [ActionPreference] $JobErrorActionPreference, [PSCronDateTime] $ReferenceDate, [switch] $PassThru ) if ( -not( Test-PSCronShouldRun @PSBoundParameters ) ) { Write-Verbose ( 'SKIPPING: ' + $Name ) return } # resolve -FilePath to a full path if ( $PSBoundParameters.ContainsKey( 'FilePath' ) ) { $PSBoundParameters['FilePath'] = $FilePath = Resolve-Path $FilePath -ErrorAction Stop | Select-Object -ExpandProperty Path } # resolve -WorkingDirectory to a full path if ( $PSBoundParameters.ContainsKey( 'WorkingDirectory' ) ) { $PSBoundParameters['WorkingDirectory'] = $WorkingDirectory = Resolve-Path $WorkingDirectory -ErrorAction Stop | Select-Object -ExpandProperty Path } # resolve -LogPath to a full path if ( $PSBoundParameters.ContainsKey( 'LogPath' ) ) { $LogFile = Split-Path $LogPath -Leaf $LogDirectory = Split-Path $LogPath -Parent $PSBoundParameters['LogPath'] = $LogPath = Resolve-Path $LogDirectory -ErrorAction Stop | Select-Object -ExpandProperty Path | ForEach-Object { $LogDirectory = $_ Join-Path $LogDirectory $LogFile } } # initialize a cron result object $CronJob = [PSCronJobObject]::new( $PSBoundParameters ) $CronJob.Source = $PSCmdlet.ParameterSetName # create a powershell runspace $PowerShell = [PowerShell]::Create( [RunspaceMode]::NewRunspace ) # create events for logging streams Register-ObjectEvent -InputObject $PowerShell.Streams.Information -EventName DataAdded -Action { New-Event -SourceIdentifier 'PSCronLog:Info' -MessageData $Event.Sender[-1].MessageData } > $null Register-ObjectEvent -InputObject $PowerShell.Streams.Verbose -EventName DataAdded -Action { New-Event -SourceIdentifier 'PSCronLog:Verbose' -MessageData $Event.Sender[-1].Message } > $null Register-ObjectEvent -InputObject $PowerShell.Streams.Debug -EventName DataAdded -Action { New-Event -SourceIdentifier 'PSCronLog:Debug' -MessageData $Event.Sender[-1].Message } > $null Register-ObjectEvent -InputObject $PowerShell.Streams.Warning -EventName DataAdded -Action { New-Event -SourceIdentifier 'PSCronLog:Warning' -MessageData $Event.Sender[-1].Message } > $null Register-ObjectEvent -InputObject $PowerShell.Streams.Error -EventName DataAdded -Action { New-Event -SourceIdentifier 'PSCronLog:Error' -MessageData ( '{0}: {1}' -f $Event.Sender[-1].FullyQualifiedErrorId, $Event.Sender[-1].Exception.Message ) } > $null # create an init script for default output settings [ArrayList]$StreamPreferences = @( "`$Global:ProgressPreference = 'SilentlyContinue'" "`$Global:InformationPreference = '$($CronJob.JobInformationPreference)'" "`$Global:DebugPreference = '$($CronJob.JobDebugPreference)'" "`$Global:WarningPreference = '$($CronJob.JobWarningPreference)'" "`$Global:ErrorActionPreference = '$($CronJob.JobErrorActionPreference)'" ) # if a working directory is provided we switch to that location in the init script if ( $CronJob.WorkingDirectory ) { $StreamPreferences.Add( "Set-Location -Path '$($CronJob.WorkingDirectory)' -ErrorAction Stop" ) > $null } # we add a variable to the $StreamPreferences with the $File name if ( $CronJob.FilePath ) { $StreamPreferences.Add( "`$Global:PSCronFile = '$($CronJob.FilePath)'" ) > $null } # add the init script $InitScript = [scriptblock]::Create( ( $StreamPreferences | Out-String ) ) $PowerShell.AddScript( $InitScript, $true ) > $null # if we are prepending scripts we do that now if ( $IncludeScripts ) { Resolve-Path -Path $IncludeScripts -ErrorAction SilentlyContinue | Where-Object { Test-Path -Path $_ -PathType Leaf } | ForEach-Object { if ( $IncludeContent = Get-Content $_ -ErrorAction SilentlyContinue | Out-String ) { $IncludeScript = [scriptblock]::Create( $IncludeContent ) $PowerShell.AddScript( $IncludeScript, $true ) > $null } } } # add the script $PowerShell.AddScript( $CronJob.Definition, $true ) > $null # add any parameters if ( $Parameters.Count -gt 0 ) { $Parameters.Keys | ForEach-Object { $PowerShell.AddParameter( $_, $Parameters[$_] ) > $null } } # add any arguments if ( $Arguments.Count -gt 0 ) { $Arguments | ForEach-Object { $PowerShell.AddArgument( $_ ) > $null } } # collection for output # note: input cannot be assigned directly to the PSCronJobObject.Output property # because of the variable reference scope $Output = New-Object 'System.Management.Automation.PSDataCollection[psobject]' # run the script $Handle = $PowerShell.BeginInvoke( $Output, $Output ) # wait for completion while ( -not $Handle.IsCompleted ) { Start-Sleep -Milliseconds 500 # kill the job? if ( ( (Get-Date) - ([datetime]$CronJob.StartDate) ).TotalSeconds -gt $CronJob.TimeOut ) { Write-Warning ( '{0} has timed out, the job was stopped after {1} seconds' -f $CronJob.Name, $CronJob.TimeOut ) $PowerShell.RunSpace.Dispose() > $null $PowerShell.Stop() > $null } } # record the results $CronJob.Output = $Output $CronJob.State = $PowerShell.InvocationStateInfo.State $CronJob.HadErrors = $PowerShell.HadErrors # end timestamp $CronJob.EndDate = Get-PSCronDate -Resolution Millisecond # calculate the job runtime $CronJob.RunTime = $CronJob.EndDate - $CronJob.StartDate # write status to the screen in case job is run interactively ''.PadRight( 80, '-' ), ( 'Name: ' + $CronJob.Name ), ( 'Description: ' + $CronJob.Description ), ( 'Schedule: ' + $CronJob.Schedule ), ( 'Reference Date: ' + $CronJob.ReferenceDate ), ( 'Started: ' + $CronJob.StartDate ), ( 'Finished: ' + $CronJob.EndDate ), ( 'Elapsed: {0} seconds' -f $CronJob.RunTime.TotalSeconds ), ( 'Result: ' + $PowerShell.InvocationStateInfo.State ), ( 'Errors: ' + $PowerShell.HadErrors ), ''.PadRight( 80, '-' ) | ForEach-Object { $CronJob.LogRaw( $_ ) } # dump the job information streams collected by the events above $InfoStreamIndex = 0 Get-Event -SourceIdentifier 'PSCronLog:*' | Select-Object TimeGenerated, @{N='OutputStream';E={$_.SourceIdentifier.Split(':')[1].ToUpper()}}, MessageData | ForEach-Object { # do some hacky shit since the info events contain all the output # for some reason if ( $_.OutputStream -eq 'INFO' ) { # get the corresponding info object from the RunSpace $RunSpaceInfo = $PowerShell.Streams.Information[ $InfoStreamIndex ] # replace the MessageData $_.MessageData = $RunSpaceInfo.MessageData # if the $RunSpaceInfo has the 'PSHOST' tag it's from Write-Host, # change the OutputStream to 'HOST' if ( $RunSpaceInfo.Tags -contains 'PSHOST' ) { $_.OutputStream = 'HOST' } # increment the counter $InfoStreamIndex ++ } # send to JobLog $CronJob.LogMessage( $_.TimeGenerated, $_.OutputStream, $_.MessageData ) } # if there are non-terminating errors attach them if ( $PowerShell.Streams.Error.Count -gt 0 ) { $CronJob.Errors = $PowerShell.Streams.Error | ForEach-Object { $_ } } # if there is a TerminatingError attach to the log file if ( $PowerShell.InvocationStateInfo.Reason -is [Exception] ) { $CronJob.LogMessage( $CronJob.EndDate, 'ERROR', $PowerShell.InvocationStateInfo.Reason.ToString() ) $CronJob.TerminatingError = $PowerShell.InvocationStateInfo.Reason } # clean up events Get-Event -SourceIdentifier 'PSCronLog:*' | Remove-Event # clean up the runspace $PowerShell.RunSpace.Dispose() > $null $PowerShell.Dispose() > $null # pass through the job? if ( $PassThru ) { $CronJob } } # .ExternalHelp BW.Utils.PSCron-help.xml function Send-PSCronNotification { param( [Parameter( Mandatory, ValueFromPipeline )] [PSCronJobObject[]] $CronJob, [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string[]] $To, [ValidateNotNullOrEmpty()] [string[]] $Cc, [ValidateNotNullOrEmpty()] [string[]] $Bcc, [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $From, [Alias( 'ComputerName' )] [ValidateNotNullOrEmpty()] [string] $SmtpServer, [ValidateNotNullOrEmpty()] [System.Net.Mail.MailPriority] $Priority, [Alias( 'DNO' )] [ValidateNotNullOrEmpty()] [System.Net.Mail.DeliveryNotificationOptions] $DeliveryNotificationOption, [Alias('sub')] [ValidateNotNullOrEmpty()] [string] $Subject = '[CRON] {0}', [ValidateNotNullOrEmpty()] [pscredential] [System.Management.Automation.CredentialAttribute()] $Credential, [switch] $UseSsl, [ValidateRange(0, 2147483647)] [int] $Port, [switch] $PassThru ) begin { $SendOptions = @{ BodyAsHtml = $true } 'To', 'Cc', 'Bcc', 'From', 'SmtpServer', 'Priority', 'DeliveryNotificationOption', 'Credential', 'Port' | Where-Object { $_ -in $PSBoundParameters.Keys } | ForEach-Object { $SendOptions.$_ = $PSBoundParameters.$_ } } process { $CronJob | ForEach-Object { $_.LogMessage( (Get-Date), 'INFO', 'Send-PSCronNotification - Sending notifications...' ) Send-MailMessage ` -Subject ( $Subject -f $_.Name ) ` -Body ( '<pre>{0}</pre>' -f ( $_.Log ) ) ` @SendOptions if ( $PassThru ) { $_ } } } } |