Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING CHANGE: ScheduledTask: Allow better handling of multiple time zones #440

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- BREAKING CHANGE: ScheduledTask
- Fixed SynchronizeAcrossTimeZone issue where Test always throws False when a date & time is used
where Daylight Savings Time is in operation. Fixes [Issue #374](https://github.com/dsccommunity/ComputerManagementDsc/issues/374).
- Fixed Test-DateStringContainsTimeZone to correctly process date strings behind UTC (-), as well
as UTC Zulu 'Z' strings.
- Fixed User parameter to correctly return the user that triggers an AtLogon or OnSessionState
Schedule Type, instead of the current value of ExecuteAsCredential. This parameter
is only valid when using the AtLogon and OnSessionState Schedule Types.
Expand All @@ -41,8 +45,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- BREAKING CHANGE: ScheduledTask
- StartTime is now processed on the device, rather than at compile time. This makes it possible
to configure start times based on each device's timezone, rather than being fixed to the time zone
configured on the device where the Desired State Configuration compilation was run.
- Allow StartTime to be used to set the 'Activate' setting when adding ScheduleType triggers
other than 'Once', 'Daily' and 'Weekly'.
- Changed the default StartTime date from today to 1st January 1980 to prevent configuration flip flopping,
and added note to configuration README to advise always supplying a date, and not just a time.
Fixes [Issue #148](https://github.com/dsccommunity/ComputerManagementDsc/issues/148).
Fixes [Issue #411](https://github.com/dsccommunity/ComputerManagementDsc/issues/411).
- Added examples & note to configuration README to supply a timezone when using SynchronizeAcrossTimeZone.
- Allow SynchronizeAcrossTimeZone to be used when adding ScheduleType triggers other than 'Once',
'Daily' and 'Weekly'.
- Updated Delay parameter to support ScheduleType AtLogon, AtStartup, AtCreation, OnSessionState.
Fixes [Issue #345](https://github.com/dsccommunity/ComputerManagementDsc/issues/345).
- Updated User parameter for use with ScheduleType OnSessionState in addition to AtLogon.
Expand Down
53 changes: 31 additions & 22 deletions source/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ function Get-TargetResource
How many units (minutes, hours, days) between each run of this task?

.PARAMETER StartTime
The time of day this task should start at, or activate on - defaults to 12:00 AM.
The date and time of day this task should start at, or activate on, represented
as a string for local conversion to DateTime format - defaults to 1st January 1980 at 12:00 AM.

.PARAMETER SynchronizeAcrossTimeZone
Enable the scheduled task option to synchronize across time zones. This is enabled
Expand Down Expand Up @@ -301,8 +302,8 @@ function Set-TargetResource
$RepeatInterval = '00:00:00',

[Parameter()]
[System.DateTime]
$StartTime = [System.DateTime]::Today,
[System.String]
$StartTime = '1980-01-01T00:00:00',

[Parameter()]
[System.Boolean]
Expand Down Expand Up @@ -480,6 +481,9 @@ function Set-TargetResource

Write-Verbose -Message ($script:localizedData.SetScheduledTaskMessage -f $TaskName, $TaskPath)

# Convert the strings containing dates & times to DateTime objects
[System.DateTime] $StartTime = [System.DateTime]::Parse($StartTime)

# Convert the strings containing time spans to TimeSpan Objects
[System.TimeSpan] $RepeatInterval = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RepeatInterval
[System.TimeSpan] $RandomDelay = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RandomDelay
Expand Down Expand Up @@ -574,13 +578,6 @@ function Set-TargetResource
-ArgumentName ExecuteAsGMSA
}

if ($SynchronizeAcrossTimeZone -and ($ScheduleType -notin @('Once', 'Daily', 'Weekly')))
{
New-InvalidArgumentException `
-Message ($script:localizedData.SynchronizeAcrossTimeZoneInvalidScheduleType) `
-ArgumentName SynchronizeAcrossTimeZone
}

# Configure the action
$actionParameters = @{
Execute = $ActionExecutable
Expand Down Expand Up @@ -1038,8 +1035,11 @@ function Set-TargetResource

2018-09-27T18:45:08

The problem in New-ScheduledTaskTrigger is that it always writes the time the format that
includes the full timezone offset (W2016 behaviour, W2012R2 does it the other way around).
The problem in New-ScheduledTaskTrigger is that it always writes the time in the UTC format, which
includes the full timezone offset: (W2016+ behaviour, W2012R2 does it the other way around)

2018-09-27 16:45:08Z

Which means "Synchronize across time zones" is enabled by default on W2016 and disabled by
default on W2012R2. To prevent that, we are overwriting the StartBoundary here to insert
the time in the format we want it, so we can enable or disable "Synchronize across time zones".
Expand Down Expand Up @@ -1106,7 +1106,8 @@ function Set-TargetResource
How many units (minutes, hours, days) between each run of this task?

.PARAMETER StartTime
The time of day this task should start at, or activate on - defaults to 12:00 AM.
The date and time of day this task should start at, or activate on, represented
as a string for local conversion to DateTime format - defaults to 1st January 1980 at 12:00 AM.

.PARAMETER SynchronizeAcrossTimeZone
Enable the scheduled task option to synchronize across time zones. This is enabled
Expand Down Expand Up @@ -1311,8 +1312,8 @@ function Test-TargetResource
$RepeatInterval = '00:00:00',

[Parameter()]
[System.DateTime]
$StartTime = [System.DateTime]::Today,
[System.String]
$StartTime = '1980-01-01T00:00:00',

[Parameter()]
[System.Boolean]
Expand Down Expand Up @@ -1492,6 +1493,12 @@ function Test-TargetResource

$currentValues = Get-CurrentResource -TaskName $TaskName -TaskPath $TaskPath

# Convert the strings containing dates & times to DateTime objects
if ($PSBoundParameters.ContainsKey('StartTime'))
{
$PSBoundParameters['StartTime'] = [System.DateTime]::Parse($StartTime)
}

# Convert the strings containing time spans to TimeSpan Objects
if ($PSBoundParameters.ContainsKey('RepeatInterval'))
{
Expand Down Expand Up @@ -1569,7 +1576,7 @@ function Test-TargetResource
$PSBoundParameters['StartTime'] = Get-DateTimeString -Date $StartTime -SynchronizeAcrossTimeZone $SynchronizeAcrossTimeZone
<#
If the current StartTime is null then we need to set it to
the desired StartTime (which defaults to Today if not passed)
the desired StartTime (which defaults to 1st January 1980 at 12:00 AM if not passed)
so that the test does not fail.
#>
if ($currentValues['StartTime'])
Expand Down Expand Up @@ -1877,7 +1884,7 @@ function Disable-ScheduledTaskEx
The date to format.

.PARAMETER SynchronizeAcrossTimeZone
Boolean to specifiy if the returned string is formatted in synchronize
Boolean to specify if the returned string is formatted in synchronize
across time zone format.
#>
function Get-DateTimeString
Expand All @@ -1894,14 +1901,14 @@ function Get-DateTimeString
$SynchronizeAcrossTimeZone
)

$format = (Get-Culture).DateTimeFormat.SortableDateTimePattern

if ($SynchronizeAcrossTimeZone)
{
$returnDate = (Get-Date -Date $Date -Format $format) + (Get-Date -Format 'zzz')
$format = (Get-Culture).DateTimeFormat.SortableDateTimePattern + 'zzz'
$returnDate = Get-Date -Date $Date.ToLocalTime() -Format $format
}
else
{
$format = (Get-Culture).DateTimeFormat.SortableDateTimePattern
$returnDate = Get-Date -Date $Date -Format $format
}

Expand Down Expand Up @@ -2040,7 +2047,7 @@ function Get-CurrentResource
if ($startAt)
{
$synchronizeAcrossTimeZone = Test-DateStringContainsTimeZone -DateString $startAt
$startTime = [System.DateTime] $startAt
$startTime = $startAt
}
else
{
Expand Down Expand Up @@ -2228,7 +2235,9 @@ function Test-DateStringContainsTimeZone
$DateString
)

return $DateString.Contains('+')
# When parsing a DateTime, Kind will be 'Unspecified' unless parsed string includes time zone information
# See https://learn.microsoft.com/en-us/dotnet/api/system.datetime.parse?view=netframework-4.5#the-return-value-and-datetimekind
return [DateTime]::Parse($DateString).Kind -ne 'Unspecified'
}

<#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class DSC_ScheduledTask : OMI_BaseResource
[Write, Description("The working path to specify for the executable.")] string ActionWorkingPath;
[Write, Description("When should the task be executed."), ValueMap{"Once", "Daily", "Weekly", "AtStartup", "AtLogon", "OnIdle", "OnEvent", "AtCreation", "OnSessionState"}, Values{"Once", "Daily", "Weekly", "AtStartup", "AtLogon", "OnIdle", "OnEvent", "AtCreation", "OnSessionState"}] string ScheduleType;
[Write, Description("How many units (minutes, hours, days) between each run of this task?")] String RepeatInterval;
[Write, Description("The time of day this task should start at, or activate on - defaults to 12:00 AM.")] DateTime StartTime;
[Write, Description("The date and time of day this task should start at, or activate on, represented as a string for local conversion to DateTime format - defaults to 1st January 1980 at 12:00 AM.")] String StartTime;
[Write, Description("Enable the scheduled task option to synchronize across time zones. This is enabled by including the timezone offset in the scheduled task trigger. Defaults to false which does not include the timezone offset.")] boolean SynchronizeAcrossTimeZone;
[Write, Description("Present if the task should exist, Absent if it should be removed."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure;
[Write, Description("True if the task should be enabled, false if it should be disabled.")] boolean Enable;
Expand Down
8 changes: 8 additions & 0 deletions source/DSCResources/DSC_ScheduledTask/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ scheduled tasks.

## Known Issues

When creating a scheduled task with a StartTime, you should always specify both
a date and a time, with the SortableDateTimePattern format (e.g. 1980-01-01T00:00:00).
Not providing a date may result in 'flip flopping' if the remote server enters daylight
savings time. The date and time specified will be set based on the time zone that has been
configured on the device. If you want to synchronize a scheduled task across timezones,
use the SynchronizeAcrossTimeZone parameter, and specify the timezone offset that is needed
(e.g. 1980-01-01T00:00:00-08:00).

One of the values needed for the `MultipleInstances` parameter is missing from the
`Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.MultipleInstancesEnum`
enumerator. There are four valid values defined for the `MultipleInstances` property of the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ ConvertFrom-StringData @'
OnEventSubscriptionError = No (valid) XML Event Subscription was provided. This is required when the scheduletype is OnEvent.
OnSessionStateChangeError = No kind of session state change was provided. This is required when the scheduletype is OnSessionState.
gMSAandCredentialError = Both ExecuteAsGMSA and (ExecuteAsCredential or BuiltInAccount) parameters have been specified. A task can run as a gMSA (Group Managed Service Account), a builtin service account or as a custom credential. Please modify your configuration to include just one of the three options.
SynchronizeAcrossTimeZoneInvalidScheduleType = Setting SynchronizeAcrossTimeZone to true when the ScheduleType is not Once, Daily or Weekly is not a valid configuration. Please keep the default value of false when using other schedule types.
TriggerCreationError = Error creating new scheduled task trigger.
ConfigureTriggerRepetitionMessage = Configuring trigger repetition.
RepetitionIntervalError = Repetition interval is set to '{0}' but repetition duration is '{1}'.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
<#
.DESCRIPTION
This example creates a scheduled task called 'Test task sync across time zone enabled'
in the folder 'MyTasks' that starts a new powershell process once at 2018-10-01 01:00.
The task will have the option Synchronize across time zone enabled.
in the folder 'MyTasks' that starts a new powershell process once at 2018-10-01 01:00
in the -08:00 timezone. The task will have the option Synchronize across time zone enabled.
#>
Configuration ScheduledTask_CreateScheduledTaskOnceSynchronizeAcrossTimeZoneEnabled_Config
{
Expand All @@ -35,7 +35,7 @@ Configuration ScheduledTask_CreateScheduledTaskOnceSynchronizeAcrossTimeZoneEnab
TaskPath = '\MyTasks\'
ActionExecutable = 'C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe'
ScheduleType = 'Once'
StartTime = '2018-10-01T01:00:00'
StartTime = '2018-10-01T01:00:00-08:00'
SynchronizeAcrossTimeZone = $true
ActionWorkingPath = (Get-Location).Path
Enable = $true
Expand Down
Loading