Skip to content

[Robustness] ServyFailureEmail.ps1 and ServyFailureNotification.ps1 — concurrent watermark re-read uses bare [DateTime]::Parse instead of ParseExact, can silently misinterpret Kind under non-en-US culture #945

@Christophe-Rogiers

Description

@Christophe-Rogiers

Severity: Warning

Files:

  • setup/taskschd/ServyFailureEmail.ps1 (line 289)
  • setup/taskschd/ServyFailureNotification.ps1 (line 234)

Code:

# ServyFailureEmail.ps1, line 207-212 — INITIAL read uses ParseExact correctly
$lastProcessed = [DateTime]::ParseExact(
    (Get-Content $timestampFile -ErrorAction Stop),
    'o',
    [System.Globalization.CultureInfo]::InvariantCulture,
    [System.Globalization.DateTimeStyles]::RoundtripKind
)

# ServyFailureEmail.ps1, line 289 — IN-LOOP concurrent re-read uses bare Parse
$fileTimestamp = [DateTime]::Parse($currentFileContent)

# Same pattern in ServyFailureNotification.ps1 line 170 vs line 234

Explanation:
Both scheduled-task scripts write the watermark with ToString("o") (round-trip ISO 8601, e.g. 2026-04-26T17:32:00.1234567+00:00). On the first read of the file ($lastProcessed), they correctly use ParseExact with 'o' and CultureInfo.InvariantCulture.

But the concurrent re-read inside the foreach loop (the safety check that prevents one script instance from rolling back another's watermark) uses bare [DateTime]::Parse($currentFileContent):

  • [DateTime]::Parse defaults to the current thread's culture, not InvariantCulture.
  • On most non-en-US Windows machines (fr-FR, de-DE, sv-SE, ja-JP, et al.), [DateTime]::Parse does interpret ISO 8601 strings produced by ToString("o") correctly most of the time, but it silently re-interprets the kind: a serialized timestamp marked +00:00 (UTC) is sometimes returned as Unspecified or local-converted, depending on the locale's DateTimeFormatInfo.
  • Result: the comparison $newestTimestamp -le $fileTimestamp (line 290 / 235) compares two DateTime instances of potentially different Kind. .NET's DateTime comparison ignores Kind, so a UTC timestamp can be compared against a local-converted timestamp and produce a wrong-direction result, causing the watermark to either:
    • never advance (if the parsed file timestamp ends up "in the future" relative to the new event), or
    • advance backwards (allowing duplicate notifications/emails).

The catch block on line 294 / 239 swallows parse failures and just overwrites the file, so the only observable failure mode is silent miscompare — not an exception.

Suggested fix:
Use the same ParseExact invocation everywhere the file is read:

$fileTimestamp = [DateTime]::ParseExact(
    $currentFileContent,
    'o',
    [System.Globalization.CultureInfo]::InvariantCulture,
    [System.Globalization.DateTimeStyles]::RoundtripKind
)

Same change in both scripts. This guarantees Kind consistency across reads/writes regardless of which thread culture the Scheduled Task ends up running under.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions