Skip to content

Commit 6a7ef14

Browse files
authored
BREAKING CHANGE: ScheduledTask: Allow better handling of multiple time zones (#440)
1 parent 5410bde commit 6a7ef14

File tree

9 files changed

+279
-89
lines changed

9 files changed

+279
-89
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
### Fixed
2626

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

4448
- BREAKING CHANGE: ScheduledTask
49+
- StartTime has chnage the type from DateTime to String.
50+
- StartTime is now processed on the device, rather than at compile time. This makes it possible
51+
to configure start times based on each device's timezone, rather than being fixed to the time zone
52+
configured on the device where the Desired State Configuration compilation was run.
4553
- Allow StartTime to be used to set the 'Activate' setting when adding ScheduleType triggers
4654
other than 'Once', 'Daily' and 'Weekly'.
55+
- Changed the default StartTime date from today to 1st January 1980 to prevent configuration flip flopping,
56+
and added note to configuration README to advise always supplying a date, and not just a time.
57+
Fixes [Issue #148](https://github.com/dsccommunity/ComputerManagementDsc/issues/148).
58+
Fixes [Issue #411](https://github.com/dsccommunity/ComputerManagementDsc/issues/411).
59+
- Added examples & note to configuration README to supply a timezone when using SynchronizeAcrossTimeZone.
60+
- Allow SynchronizeAcrossTimeZone to be used when adding ScheduleType triggers other than 'Once',
61+
'Daily' and 'Weekly'.
4762
- Updated Delay parameter to support ScheduleType AtLogon, AtStartup, AtCreation, OnSessionState.
4863
Fixes [Issue #345](https://github.com/dsccommunity/ComputerManagementDsc/issues/345).
4964
- Updated User parameter for use with ScheduleType OnSessionState in addition to AtLogon.

source/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.psm1

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ function Get-TargetResource
9797
How many units (minutes, hours, days) between each run of this task?
9898
9999
.PARAMETER StartTime
100-
The time of day this task should start at, or activate on - defaults to 12:00 AM.
100+
The date and time of day this task should start at, or activate on, represented
101+
as a string for local conversion to DateTime format - defaults to 1st January 1980 at 12:00 AM.
101102
102103
.PARAMETER SynchronizeAcrossTimeZone
103104
Enable the scheduled task option to synchronize across time zones. This is enabled
@@ -301,8 +302,8 @@ function Set-TargetResource
301302
$RepeatInterval = '00:00:00',
302303

303304
[Parameter()]
304-
[System.DateTime]
305-
$StartTime = [System.DateTime]::Today,
305+
[System.String]
306+
$StartTime = '1980-01-01T00:00:00',
306307

307308
[Parameter()]
308309
[System.Boolean]
@@ -480,6 +481,9 @@ function Set-TargetResource
480481

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

484+
# Convert the strings containing dates & times to DateTime objects
485+
[System.DateTime] $StartTime = [System.DateTime]::Parse($StartTime)
486+
483487
# Convert the strings containing time spans to TimeSpan Objects
484488
[System.TimeSpan] $RepeatInterval = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RepeatInterval
485489
[System.TimeSpan] $RandomDelay = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RandomDelay
@@ -574,13 +578,6 @@ function Set-TargetResource
574578
-ArgumentName ExecuteAsGMSA
575579
}
576580

577-
if ($SynchronizeAcrossTimeZone -and ($ScheduleType -notin @('Once', 'Daily', 'Weekly')))
578-
{
579-
New-ArgumentException `
580-
-Message ($script:localizedData.SynchronizeAcrossTimeZoneInvalidScheduleType) `
581-
-ArgumentName SynchronizeAcrossTimeZone
582-
}
583-
584581
# Configure the action
585582
$actionParameters = @{
586583
Execute = $ActionExecutable
@@ -1038,8 +1035,11 @@ function Set-TargetResource
10381035
10391036
2018-09-27T18:45:08
10401037
1041-
The problem in New-ScheduledTaskTrigger is that it always writes the time the format that
1042-
includes the full timezone offset (W2016 behaviour, W2012R2 does it the other way around).
1038+
The problem in New-ScheduledTaskTrigger is that it always writes the time in the UTC format, which
1039+
includes the full timezone offset: (W2016+ behaviour, W2012R2 does it the other way around)
1040+
1041+
2018-09-27 16:45:08Z
1042+
10431043
Which means "Synchronize across time zones" is enabled by default on W2016 and disabled by
10441044
default on W2012R2. To prevent that, we are overwriting the StartBoundary here to insert
10451045
the time in the format we want it, so we can enable or disable "Synchronize across time zones".
@@ -1106,7 +1106,8 @@ function Set-TargetResource
11061106
How many units (minutes, hours, days) between each run of this task?
11071107
11081108
.PARAMETER StartTime
1109-
The time of day this task should start at, or activate on - defaults to 12:00 AM.
1109+
The date and time of day this task should start at, or activate on, represented
1110+
as a string for local conversion to DateTime format - defaults to 1st January 1980 at 12:00 AM.
11101111
11111112
.PARAMETER SynchronizeAcrossTimeZone
11121113
Enable the scheduled task option to synchronize across time zones. This is enabled
@@ -1311,8 +1312,8 @@ function Test-TargetResource
13111312
$RepeatInterval = '00:00:00',
13121313

13131314
[Parameter()]
1314-
[System.DateTime]
1315-
$StartTime = [System.DateTime]::Today,
1315+
[System.String]
1316+
$StartTime = '1980-01-01T00:00:00',
13161317

13171318
[Parameter()]
13181319
[System.Boolean]
@@ -1492,6 +1493,12 @@ function Test-TargetResource
14921493

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

1496+
# Convert the strings containing dates & times to DateTime objects
1497+
if ($PSBoundParameters.ContainsKey('StartTime'))
1498+
{
1499+
$PSBoundParameters['StartTime'] = [System.DateTime]::Parse($StartTime)
1500+
}
1501+
14951502
# Convert the strings containing time spans to TimeSpan Objects
14961503
if ($PSBoundParameters.ContainsKey('RepeatInterval'))
14971504
{
@@ -1569,7 +1576,7 @@ function Test-TargetResource
15691576
$PSBoundParameters['StartTime'] = Get-DateTimeString -Date $StartTime -SynchronizeAcrossTimeZone $SynchronizeAcrossTimeZone
15701577
<#
15711578
If the current StartTime is null then we need to set it to
1572-
the desired StartTime (which defaults to Today if not passed)
1579+
the desired StartTime (which defaults to 1st January 1980 at 12:00 AM if not passed)
15731580
so that the test does not fail.
15741581
#>
15751582
if ($currentValues['StartTime'])
@@ -1877,7 +1884,7 @@ function Disable-ScheduledTaskEx
18771884
The date to format.
18781885
18791886
.PARAMETER SynchronizeAcrossTimeZone
1880-
Boolean to specifiy if the returned string is formatted in synchronize
1887+
Boolean to specify if the returned string is formatted in synchronize
18811888
across time zone format.
18821889
#>
18831890
function Get-DateTimeString
@@ -1894,14 +1901,14 @@ function Get-DateTimeString
18941901
$SynchronizeAcrossTimeZone
18951902
)
18961903

1897-
$format = (Get-Culture).DateTimeFormat.SortableDateTimePattern
1898-
18991904
if ($SynchronizeAcrossTimeZone)
19001905
{
1901-
$returnDate = (Get-Date -Date $Date -Format $format) + (Get-Date -Format 'zzz')
1906+
$format = (Get-Culture).DateTimeFormat.SortableDateTimePattern + 'zzz'
1907+
$returnDate = Get-Date -Date $Date.ToLocalTime() -Format $format
19021908
}
19031909
else
19041910
{
1911+
$format = (Get-Culture).DateTimeFormat.SortableDateTimePattern
19051912
$returnDate = Get-Date -Date $Date -Format $format
19061913
}
19071914

@@ -2040,7 +2047,7 @@ function Get-CurrentResource
20402047
if ($startAt)
20412048
{
20422049
$synchronizeAcrossTimeZone = Test-DateStringContainsTimeZone -DateString $startAt
2043-
$startTime = [System.DateTime] $startAt
2050+
$startTime = $startAt
20442051
}
20452052
else
20462053
{
@@ -2228,7 +2235,9 @@ function Test-DateStringContainsTimeZone
22282235
$DateString
22292236
)
22302237

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

22342243
<#

source/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.schema.mof

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class DSC_ScheduledTask : OMI_BaseResource
99
[Write, Description("The working path to specify for the executable.")] string ActionWorkingPath;
1010
[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;
1111
[Write, Description("How many units (minutes, hours, days) between each run of this task?")] String RepeatInterval;
12-
[Write, Description("The time of day this task should start at, or activate on - defaults to 12:00 AM.")] DateTime StartTime;
12+
[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;
1313
[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;
1414
[Write, Description("Present if the task should exist, Absent if it should be removed."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure;
1515
[Write, Description("True if the task should be enabled, false if it should be disabled.")] boolean Enable;

source/DSCResources/DSC_ScheduledTask/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ scheduled tasks.
66

77
## Known Issues
88

9+
When creating a scheduled task with a StartTime, you should always specify both
10+
a date and a time, with the SortableDateTimePattern format (e.g. 1980-01-01T00:00:00).
11+
Not providing a date may result in 'flip flopping' if the remote server enters daylight
12+
savings time. The date and time specified will be set based on the time zone that has been
13+
configured on the device. If you want to synchronize a scheduled task across timezones,
14+
use the SynchronizeAcrossTimeZone parameter, and specify the timezone offset that is needed
15+
(e.g. 1980-01-01T00:00:00-08:00).
16+
917
One of the values needed for the `MultipleInstances` parameter is missing from the
1018
`Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.MultipleInstancesEnum`
1119
enumerator. There are four valid values defined for the `MultipleInstances` property of the

source/DSCResources/DSC_ScheduledTask/en-US/DSC_ScheduledTask.strings.psd1

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ ConvertFrom-StringData @'
1313
OnEventSubscriptionError = No (valid) XML Event Subscription was provided. This is required when the scheduletype is OnEvent.
1414
OnSessionStateChangeError = No kind of session state change was provided. This is required when the scheduletype is OnSessionState.
1515
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.
16-
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.
1716
TriggerCreationError = Error creating new scheduled task trigger.
1817
ConfigureTriggerRepetitionMessage = Configuring trigger repetition.
1918
RepetitionIntervalError = Repetition interval is set to '{0}' but repetition duration is '{1}'.

source/Examples/Resources/ScheduledTask/15-ScheduledTask_CreateScheduledTaskOnceSynchronizeAcrossTimeZoneEnabled_Config.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
<#
2121
.DESCRIPTION
2222
This example creates a scheduled task called 'Test task sync across time zone enabled'
23-
in the folder 'MyTasks' that starts a new powershell process once at 2018-10-01 01:00.
24-
The task will have the option Synchronize across time zone enabled.
23+
in the folder 'MyTasks' that starts a new powershell process once at 2018-10-01 01:00
24+
in the -08:00 timezone. The task will have the option Synchronize across time zone enabled.
2525
#>
2626
Configuration ScheduledTask_CreateScheduledTaskOnceSynchronizeAcrossTimeZoneEnabled_Config
2727
{
@@ -35,7 +35,7 @@ Configuration ScheduledTask_CreateScheduledTaskOnceSynchronizeAcrossTimeZoneEnab
3535
TaskPath = '\MyTasks\'
3636
ActionExecutable = 'C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe'
3737
ScheduleType = 'Once'
38-
StartTime = '2018-10-01T01:00:00'
38+
StartTime = '2018-10-01T01:00:00-08:00'
3939
SynchronizeAcrossTimeZone = $true
4040
ActionWorkingPath = (Get-Location).Path
4141
Enable = $true

0 commit comments

Comments
 (0)