uscheduler/examples/SchedulerTemplate.psm1
2026-01-24 16:58:01 +01:00

250 lines
7.8 KiB
PowerShell

<#
.SYNOPSIS
SchedulerTemplate.psm1 - Scheduling + Lock + Interval + Callback Runner
.DESCRIPTION
Reusable PowerShell module for scheduled script execution with lock files,
interval control, and credential management.
.VERSION
1.0.0
.DATE
2026-01-24
.NOTES
- Provides Write-Log function with timestamp and level support
- Provides Get-CredentialFromEnvVar for secure credential retrieval
- Provides Invoke-ScheduledExecution for scheduled task management
#>
# Module Version (exported for external scripts to check version)
$script:ModuleVersion = "1.0.0"
$script:ModuleDate = "2026-01-24"
# Module load confirmation
Write-Verbose "SchedulerTemplate.psm1 v$ModuleVersion loaded ($ModuleDate)"
function Write-Log {
param(
[string]$Message,
[ValidateSet('Info', 'Success', 'Warning', 'Error')]
[string]$Level = 'Info',
[switch]$Automated
)
$colors = @{
'Info' = 'White'
'Success' = 'Green'
'Warning' = 'Yellow'
'Error' = 'Red'
}
if ($Automated) {
# Service logger adds timestamps, so only include level and message
Write-Output "[$Level] $Message"
}
else {
# Manual execution: include timestamp for better tracking
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $colors[$Level]
}
}
function Get-CredentialFromEnvVar {
param(
[Parameter(Mandatory = $true)]
[string]$EnvVarName,
[switch]$Automated
)
try {
# Retrieve environment variable from Machine level
$envVar = [System.Environment]::GetEnvironmentVariable($EnvVarName, "Machine")
if (-not $envVar) {
Write-Log "Environment variable '$EnvVarName' not found at Machine level!" -Level Error -Automated:$Automated
return $null
}
# Decode Base64
try {
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($envVar))
}
catch {
Write-Log "Failed to decode '$EnvVarName' as Base64. Expected format: Base64('username:password')" -Level Error -Automated:$Automated
return $null
}
# Split credentials
$creds = $decoded -split ':', 2
if ($creds.Count -ne 2) {
Write-Log "Invalid credential format in '$EnvVarName'. Expected 'username:password'" -Level Error -Automated:$Automated
return $null
}
return @{
Username = $creds[0]
Password = $creds[1]
}
}
catch {
Write-Log "Error retrieving credentials from '$EnvVarName': $_" -Level Error -Automated:$Automated
return $null
}
}
function Get-CurrentUtcDateTime {
param([string]$ExternalDateTime, [switch]$Automated)
if ($ExternalDateTime) {
try {
return [datetime]::Parse($ExternalDateTime).ToUniversalTime()
}
catch {
try {
return [datetime]::ParseExact($ExternalDateTime, 'dd/MM/yyyy HH:mm:ss', $null).ToUniversalTime()
}
catch {
Write-Log "Failed to parse CurrentDateTimeUtc ('$ExternalDateTime'). Using system time (UTC)." -Automated:$Automated -Level Error
return (Get-Date).ToUniversalTime()
}
}
}
return (Get-Date).ToUniversalTime()
}
function Test-ScheduleMonth { param([datetime]$DateTime, [array]$Months)
$name = $DateTime.ToString('MMMM')
return ($Months.Count -eq 0) -or ($Months -contains $name)
}
function Test-ScheduleWeekday { param([datetime]$DateTime, [array]$Weekdays)
$name = $DateTime.DayOfWeek.ToString()
return ($Weekdays.Count -eq 0) -or ($Weekdays -contains $name)
}
function Test-ScheduleTime { param([datetime]$DateTime, [array]$Times)
$t = $DateTime.ToString('HH:mm')
return ($Times.Count -eq 0) -or ($Times -contains $t)
}
function Test-Schedule {
param(
[datetime]$DateTime,
[array]$RunMonth,
[array]$RunWeekday,
[array]$RunTime
)
return (Test-ScheduleMonth -DateTime $DateTime -Months $RunMonth) -and
(Test-ScheduleWeekday -DateTime $DateTime -Weekdays $RunWeekday) -and
(Test-ScheduleTime -DateTime $DateTime -Times $RunTime)
}
function Test-Interval {
param([datetime]$LastRun,[datetime]$Now,[int]$MinIntervalMinutes)
return $Now -ge $LastRun.AddMinutes($MinIntervalMinutes)
}
function Test-ScheduledExecution {
param(
[switch]$Automated,
[string]$CurrentDateTimeUtc,
[hashtable]$Config,
[string]$LastRunFilePath
)
$now = Get-CurrentUtcDateTime -ExternalDateTime $CurrentDateTimeUtc -Automated:$Automated
$shouldRun = $true
if ($Automated) {
Write-Log "Automated: $Automated" -Automated:$Automated -Level Success
Write-Log "Current UTC Time: $now" -Automated:$Automated -Level Success
if (-not (Test-Schedule -DateTime $now -RunMonth $Config.RunMonth -RunWeekday $Config.RunWeekday -RunTime $Config.RunTime)) {
Write-Log "Execution skipped due to schedule." -Automated:$Automated -Level Warning
$shouldRun = $false
}
}
if ($shouldRun -and $LastRunFilePath -and (Test-Path $LastRunFilePath)) {
$lastRun = Get-Content $LastRunFilePath | Select-Object -First 1
if ($lastRun) {
[datetime]$lr = $lastRun
if (-not (Test-Interval -LastRun $lr -Now $now -MinIntervalMinutes $Config.MinIntervalMinutes)) {
Write-Log "Last run at $lr. Interval not reached." -Automated:$Automated -Level Warning
$shouldRun = $false
}
}
}
return @{
ShouldExecute = $shouldRun
Now = $now
}
}
function New-LockGuard {
param([string]$LockFile,[switch]$Automated)
if (Test-Path $LockFile) {
Write-Log "Guard: Lock file exists ($LockFile). Skipping." -Automated:$Automated -Level Error
return $false
}
try {
New-Item -Path $LockFile -ItemType File -Force | Out-Null
return $true
}
catch {
Write-Log "Guard: Cannot create lock file ($LockFile)." -Automated:$Automated -Level Error
return $false
}
}
function Remove-LockGuard {
param([string]$LockFile,[switch]$Automated)
if (Test-Path $LockFile) {
Remove-Item $LockFile -Force
Write-Log "Lock removed: $LockFile" -Automated:$Automated -Level Info
}
}
# ======================================================================
# Main unified executor (callback-based)
# ======================================================================
function Invoke-ScheduledExecution {
param(
[scriptblock]$ScriptBlock,
[hashtable]$Config,
[switch]$Automated,
[string]$CurrentDateTimeUtc
)
# Get the calling script's path (not the module path)
$scriptPath = (Get-PSCallStack)[1].ScriptName
if (-not $scriptPath) {
Write-Log "Unable to determine calling script path" -Level Error -Automated:$Automated
return
}
$lastRunFile = [IO.Path]::ChangeExtension($scriptPath, ".lastRun")
$lockFile = [IO.Path]::ChangeExtension($scriptPath, ".lock")
# Check schedule
$schedule = Test-ScheduledExecution -Automated:$Automated -CurrentDateTimeUtc $CurrentDateTimeUtc -Config $Config -LastRunFilePath $lastRunFile
if (-not $schedule.ShouldExecute) {
Write-Log "Execution skipped." -Automated:$Automated -Level Warning
return
}
# Lock
if (-not (New-LockGuard -LockFile $lockFile -Automated:$Automated)) {
return
}
try {
$schedule.Now.ToString("o") | Set-Content $lastRunFile
& $ScriptBlock
}
finally {
Remove-LockGuard -LockFile $lockFile -Automated:$Automated
}
}
Export-ModuleMember -Function * -Alias * -Variable ModuleVersion, ModuleDate