Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions src/MaintenancePlan/Public/Schedule-MaintenancePlan.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,92 @@ function Schedule-MaintenancePlan {
Assert-ParameterNotNull $PlanPath 'PlanPath'
Assert-ParameterNotNull $Cron 'Cron'

function Parse-CronField {
param(
[string]$Value,
[int]$Min,
[int]$Max,
[string]$Name,
[switch]$AllowStar,
[switch]$AllowStep
)
if ($AllowStar -and $Value -eq '*') { return @{} }
if ($AllowStep -and $Value -match '^\*/(\d+)$') {
$step = [int]$Matches[1]
if ($step -lt 1 -or $step -gt $Max) { throw "Cron $Name step must be between 1 and $Max" }
return @{ Interval = $step }
}
if ($Value -notmatch '^\d+$') { throw "Cron $Name field must be an integer" }
$num = [int]$Value
if ($num -lt $Min -or $num -gt $Max) { throw "Cron $Name field must be between $Min and $Max" }
return @{ Value = $num }
}

function Convert-Cron {
param([string]$CronExpression)
$parts = $CronExpression -split '\s+'
if ($parts.Length -ne 5) {
throw 'Cron expression must have 5 fields: minute hour day-of-month month day-of-week'
}
$minute = Parse-CronField $parts[0] 0 59 'minute'
$hour = Parse-CronField $parts[1] 0 23 'hour'
$dom = Parse-CronField $parts[2] 1 31 'day-of-month' -AllowStar -AllowStep
$month = Parse-CronField $parts[3] 1 12 'month' -AllowStar
$dow = Parse-CronField $parts[4] 0 7 'day-of-week' -AllowStar -AllowStep

return [pscustomobject]@{
Minute = $minute.Value
Hour = $hour.Value
DayOfMonth = $dom.Value
DayInterval = $dom.Interval
Month = $month.Value
DayOfWeek = if ($dow.Value) { if ($dow.Value -eq 7) { 0 } else { $dow.Value } } else { $null }
WeekInterval = $dow.Interval
}
}

$modulePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'MaintenancePlan.psd1'
$command = "Import-Module '$modulePath'; `$plan = Import-MaintenancePlan -Path '$PlanPath'; Invoke-MaintenancePlan -Plan `$plan"

$cronParts = Convert-Cron $Cron

if ($IsWindows) {
Write-STStatus "Registering scheduled task $Name" -Level INFO -Log
$parts = $Cron -split '\s+'
if ($parts.Length -lt 2) { throw 'Cron expression must include minute and hour' }
$time = '{0:D2}:{1:D2}' -f [int]$parts[1], [int]$parts[0]
$action = New-ScheduledTaskAction -Execute 'pwsh' -Argument "-NoProfile -Command \"$command\""
$trigger = New-ScheduledTaskTrigger -Daily -At $time
$time = '{0:D2}:{1:D2}' -f $cronParts.Hour, $cronParts.Minute
$action = New-ScheduledTaskAction -Execute 'pwsh' -Argument "-NoProfile -Command \"$command\""

$triggerParams = @{ At = $time }
if ($cronParts.DayOfWeek -ne $null -or $cronParts.WeekInterval) {
$triggerParams.Weekly = $true
if ($cronParts.DayOfWeek -ne $null) {
$triggerParams.DaysOfWeek = [System.DayOfWeek]$cronParts.DayOfWeek
}
if ($cronParts.WeekInterval) {
$triggerParams.WeeksInterval = $cronParts.WeekInterval
}
} elseif ($cronParts.DayOfMonth -ne $null -or $cronParts.Month -ne $null) {
$triggerParams.Monthly = $true
if ($cronParts.DayOfMonth -ne $null) {
$triggerParams.DaysOfMonth = $cronParts.DayOfMonth
}
if ($cronParts.Month -ne $null) {
$triggerParams.MonthsOfYear = $cronParts.Month
}
if ($cronParts.DayInterval) {
$triggerParams.DaysInterval = $cronParts.DayInterval
}
} else {
$triggerParams.Daily = $true
if ($cronParts.DayInterval) {
$triggerParams.DaysInterval = $cronParts.DayInterval
}
}

$trigger = New-ScheduledTaskTrigger @triggerParams
Register-ScheduledTask -TaskName $Name -Action $action -Trigger $trigger -Force | Out-Null
} else {
# validate cron on non-windows too
$null = $cronParts
$entry = "$Cron pwsh -NoProfile -Command \"$command\" # $Name"
Write-STStatus "Cron entry generated" -Level INFO -Log
return $entry
Expand Down
24 changes: 23 additions & 1 deletion tests/MaintenancePlan.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,33 @@ Describe 'MaintenancePlan Module' {
Set-Variable -Name IsWindows -Value $true -Scope Script -Force
function Register-ScheduledTask {}
function New-ScheduledTaskAction { param($Execute,$Argument) $script:arg = $Argument; [pscustomobject]@{Execute=$Execute;Argument=$Argument} }
function New-ScheduledTaskTrigger { param([string]$Daily,[string]$At) [pscustomobject]@{At=$At} }
function New-ScheduledTaskTrigger {
param($At,$Daily,$Weekly,$DaysOfWeek)
$script:trigger = @{ At=$At;Daily=$Daily;Weekly=$Weekly;DaysOfWeek=$DaysOfWeek }
[pscustomobject]@{ At=$At }
}
Schedule-MaintenancePlan -PlanPath '/tmp/p.json' -Cron '5 1 * * *' -Name 'MP'
$expectedModule = Join-Path (Get-Module MaintenancePlan).ModuleBase 'MaintenancePlan.psd1'
$script:arg | Should -Match [Regex]::Escape($expectedModule)
$script:arg | Should -Match '/tmp/p.json'
$script:trigger.At | Should -Be '01:05'
$script:trigger.Daily | Should -Be $true
}
}

Safe-It 'uses day-of-week from cron' {
InModuleScope MaintenancePlan {
Set-Variable -Name IsWindows -Value $true -Scope Script -Force
function Register-ScheduledTask {}
function New-ScheduledTaskAction { param($Execute,$Argument) }
function New-ScheduledTaskTrigger {
param($At,$Weekly,$DaysOfWeek)
$script:trigger = @{ At=$At;Weekly=$Weekly;DaysOfWeek=$DaysOfWeek }
[pscustomobject]@{ At=$At }
}
Schedule-MaintenancePlan -PlanPath '/tmp/p.json' -Cron '5 1 * * 0' -Name 'MP'
$script:trigger.Weekly | Should -Be $true
$script:trigger.DaysOfWeek | Should -Be [System.DayOfWeek]::Sunday
}
}

Expand Down
Loading