Consolidate timestamp parsing and support docker-style options - #41403
Consolidate timestamp parsing and support docker-style options#41403ggarzia-MSFT wants to merge 5 commits into
Conversation
… accept docker-style --since/--until values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR consolidates timestamp/duration parsing used by wslc into wsl::windows::common::string, expands the --since/--until grammar to accept Docker/OCI-style inputs (partial timestamps, zone-less local times, Go durations, and pre-1970 epochs), and fixes a service-side UTC parsing bug by switching to strict RFC 3339 parsing.
Changes:
- Centralized strict RFC 3339 parsing (
Rfc3339ToEpoch) plus canonicalization (ExpandToRfc3339) and shared Go-duration parsing (TryParseDuration) insrc/windows/common/string.*. - Switched
Since/Untilplumbing end-to-end fromULONGLONGtoLONGLONG(IDL → session → HTTP client → tasks/services/args) to support pre-epoch timestamps. - Updated/expanded unit test coverage for the newly accepted/rejected timestamp forms.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/wslc/WSLCCLIArgumentUnitTests.cpp | Expands timestamp argument tests (pre-epoch, zone-less local, partial/date-only, Go durations). |
| src/windows/wslcsession/WSLCContainer.h | Updates container logs API to use signed epochs for Since/Until. |
| src/windows/wslcsession/WSLCContainer.cpp | Updates timestamp handling and introduces unset-timestamp guarding for daemon output. |
| src/windows/wslcsession/DockerHTTPClient.h | Changes ContainerLogs signature to accept signed Since/Until. |
| src/windows/wslcsession/DockerHTTPClient.cpp | Sends signed since/until query parameters when non-zero. |
| src/windows/wslc/tasks/ContainerTasks.cpp | Uses signed since/until types for CLI logs task plumbing. |
| src/windows/wslc/services/ContainerService.h | Updates service Logs API to take signed epochs. |
| src/windows/wslc/services/ContainerService.cpp | Implements signed-epoch Logs forwarding to session container. |
| src/windows/wslc/arguments/SpecParsing.h | Changes GetTimestampFromString return type to LONGLONG. |
| src/windows/wslc/arguments/SpecParsing.cpp | Implements broader timestamp grammar via integer → duration → ExpandToRfc3339+Rfc3339ToEpoch. |
| src/windows/wslc/arguments/ArgumentDefinitions.h | Changes Since/Until argument types to LONGLONG. |
| src/windows/service/inc/wslc.idl | Updates IWSLCContainer::Logs to accept signed Since/Until. |
| src/windows/service/exe/ServiceMain.cpp | Fixes update-check release date parsing by using Rfc3339ToEpoch (UTC-correct). |
| src/windows/inc/docker_schema.h | Adds c_unsetTimestamp sentinel for daemon “unset” timestamps. |
| src/windows/common/string.hpp | Adds new shared helpers for RFC 3339 expansion/parsing and Go-duration parsing. |
| src/windows/common/string.cpp | Implements ExpandToRfc3339, signed Rfc3339ToEpoch, and shared TryParseDuration; tightens UTC display-time behavior. |
Suppressed comments (1)
src/windows/wslcsession/WSLCContainer.cpp:2537
- m_stateChangedAt is stored as an unsigned epoch, but Rfc3339ToEpoch() can return negative seconds. The current static_caststd::uint64_t(...) will wrap negative values and produce an incorrect (very large) timestamp instead of treating it as an invalid/unset daemon value.
if (!timestamp.empty() && timestamp != c_unsetTimestamp)
{
container->m_stateChangedAt = static_cast<std::uint64_t>(wsl::windows::common::string::Rfc3339ToEpoch(timestamp));
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (3)
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp:782
- This test calls std::chrono::current_zone() without handling the case where the time zone database is unavailable (current_zone() can throw). The production code falls back to UTC in that case; the test should mirror that fallback to avoid spurious failures.
const auto offset = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::current_zone()->get_info(std::chrono::system_clock::now()).offset)
.count();
const auto expected = 1705314600LL - offset;
src/windows/common/string.hpp:75
- The ExpandToRfc3339 comment says unrecognized input is returned unchanged, but the implementation always constructs an expanded string (e.g. "abc" becomes "abcT00:00:00+00:00"). Update the comment (or the function) so the contract matches reality.
// Expands a partial timestamp into a full RFC 3339 one. Hour-only, minute-only and date-only values
// are padded out to a complete time, and a value with no zone designator is resolved against the
// offset currently in effect locally. Input that is not recognized is returned for the parser to
// reject.
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp:811
- ValidateTimestamp_ValidGoDuration captures "now" once and then allows only a fixed +30s window for all parses. If the test is paused (debugger) or runs slowly under load, it can fail even when parsing is correct. Capture time before/after each parse and assert the parsed value falls within that interval.
const auto now = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
// Durations are measured back from the current time, so allow a small window for the clock
// to advance while the test runs.
auto verifyDuration = [&](LPCWSTR value, LONGLONG expectedOffset) {
const auto parsed = validation::GetTimestampFromString(value);
VERIFY_IS_GREATER_THAN_OR_EQUAL(parsed, now - expectedOffset);
VERIFY_IS_LESS_THAN_OR_EQUAL(parsed, now - expectedOffset + 30);
};
…iner timestamps signed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (2)
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp:781
- This test assumes
std::chrono::current_zone()is always available. Production code (LocalUtcOffset/EpochToLocalDisplayTime) explicitly catches time-zone database failures and falls back to UTC, so this test can become flaky in environments where the time-zone DB is unavailable. Consider mirroring the production fallback by treating the offset as 0 on exception.
const auto offset = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::current_zone()->get_info(std::chrono::system_clock::now()).offset)
.count();
src/windows/common/string.cpp:449
- LocalUtcOffset() uses the current offset (
current_zone()->get_info(system_clock::now())). When ExpandToRfc3339() canonicalizes a zone-less timestamp, this can produce incorrect epoch values for timestamps that fall under a different DST offset than “now” (e.g., parsing a winter date during summer time). Consider resolving zone-less inputs using the offset rules at the parsed local timestamp (and handling ambiguous/nonexistent local times) rather than the current offset.
static std::string LocalUtcOffset()
{
try
{
const auto* zone = std::chrono::current_zone();
const auto offset = zone->get_info(std::chrono::system_clock::now()).offset;
const auto minutes = std::chrono::duration_cast<std::chrono::minutes>(offset).count();
const auto magnitude = std::abs(minutes);
return std::format("{}{:02}:{:02}", minutes < 0 ? '-' : '+', magnitude / 60, magnitude % 60);
}
catch (...)
{
// The time zone database is unavailable, so fall back to UTC rather than failing the caller.
LOG_CAUGHT_EXCEPTION();
return "+00:00";
}
… module Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated no new comments.
Suppressed comments (4)
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp:781
- This test calls
std::chrono::current_zone()directly, which can throw when the time zone database is unavailable. The production code explicitly falls back to UTC in that case, so the test should handle the same fallback to avoid spurious failures in constrained environments.
const auto offset = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::current_zone()->get_info(std::chrono::system_clock::now()).offset)
.count();
src/windows/common/timestamp.cpp:149
Rfc3339ToEpochconverts the parsed time_point to seconds withduration_cast, which truncates toward zero for negative durations. For pre-1970 timestamps with fractional seconds (e.g. 1969-12-31T23:59:59.9Z), this can return 0 instead of -1. Usestd::chrono::floor<std::chrono::seconds>to ensure correct flooring for negative values.
return std::chrono::duration_cast<std::chrono::seconds>(parsed.time_since_epoch()).count();
src/windows/wslc/arguments/SpecParsing.cpp:625
- Catching
...here will also convert unrelated failures (e.g. allocation failures) into anArgumentException, which can hide real errors. Limit the catch to the parse failure type / error code (e.g.wil::ResultExceptionwithE_INVALIDARG) and rethrow anything else.
// Name the offending argument rather than surfacing the raw parse failure.
catch (...)
{
throw ArgumentException(Localization::WSLCCLI_InvalidTimestampArgumentError(argName, value));
}
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp:773
- Pre-epoch support is newly introduced, but this test set doesn't cover the pre-1970 + fractional-seconds case. Adding one assertion around the epoch boundary helps prevent regressions in the seconds-rounding behavior for negative timestamps.
TEST_METHOD(ValidateTimestamp_ValidPreEpoch)
{
// No lower bound is applied, so pre-1970 values convert to a negative epoch.
VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1960-01-15T10:30:00Z"), -315610200LL);
VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"0001-01-01T00:00:00Z"), -62135596800LL);
VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"-315610200"), -315610200LL);
}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/windows/common/timestamp.hpp:19
timestamp.hppusesstd::string_viewandstd::int64_tbut does not include<string_view>/<cstdint>. This makes the header non-self-contained and can break compilation when it’s included without relying on precompiled-header include order.
#include <chrono>
#include <optional>
#include <string>
src/windows/common/timestamp.cpp:34
- Zone-less timestamps are expanded using the current local UTC offset (
current_zone()->get_info(system_clock::now())). This will produce incorrect epoch values for inputs whose local offset differs from “now” (e.g., timestamps across DST transitions). Consider computing the offset for the parsed local date/time (or parsing aslocal_timeand converting viazoned_time) instead of appending a fixed offset.
const auto* zone = std::chrono::current_zone();
const auto offset = zone->get_info(std::chrono::system_clock::now()).offset;
const auto minutes = std::chrono::duration_cast<std::chrono::minutes>(offset).count();
const auto magnitude = std::abs(minutes);
return std::format("{}{:02}:{:02}", minutes < 0 ? '-' : '+', magnitude / 60, magnitude % 60);
Summary of the Pull Request
Follow-up to #41377. Consolidates all timestamp and duration parsing used by
wslcintowsl::windows::common::string, fixes a parser that silently swallowed errors, and broadens the--since/--untilgrammar to match what users expect from an OCI container CLI.PR Checklist
Detailed Description of the Pull Request / Additional comments
Silent parse failures (review feedback from #41377)
Rfc3339ToUtcDisplayTimeused aTryParse-style helper and fell back to echoing the raw input when parsing failed, so a malformed daemon timestamp surfaced as a garbled display string instead of an error. It now delegates toRfc3339ToEpoch, which throws. TheTryParsevariant is deleted.Parser consolidation
SpecParsing.cpphad grown its own copies of RFC 3339 and Go-duration parsers, one of which duplicated logic already present a few hundred lines away in the same file. Everything now lives insrc/windows/common/string.{hpp,cpp}:Rfc3339ToEpochExpandToRfc3339TryParseDuration10m,1h30m,1.5h) →std::chrono::nanoseconds.EpochToLocalDisplayTimeRfc3339ToUtcDisplayTimeThe loose CLI grammar is implemented by canonicalizing input and then handing it to the strict parser, so validation and error messages exist in exactly one place.
ServiceMain.cppalso picks upRfc3339ToEpoch, which fixes a latent bug where it usedmktime(local time) on a UTC timestamp.Broader
--since/--untilinputGetTimestampFromString(used only by--since/--until) now tries, in order: raw integer epoch → Go duration →Rfc3339ToEpoch(ExpandToRfc3339(...)). Newly accepted:2024-01-02T15:04:05(interpreted in the local zone)2024-01-02,2024-01-02T15,2024-01-02T15:0410m,1h30m,1.5h(relative to now)Still rejected:
10x,1h30,2024-13-01,2024-02-31, and other malformed values. Note thatstd::chrono::parsesilently wraps invalid calendar dates (Feb 31 → Mar 2), soRfc3339ToEpochpre-validates viastd::chrono::year_month_day::ok().Signed epoch plumbing
Supporting pre-1970 values required
Since/Untilto change fromULONGLONGtoLONGLONGthroughwslc.idl→WSLCContainer→DockerHTTPClient→ContainerService→ContainerTasks→ArgumentDefinitions.h.TailstaysULONGLONG. This toucheswslc.idl, which is internal and explicitly non-ABI-stable (see the comment at the top of that file); the SDK-facingWSLCCompat.idland the publicWslPluginApi.hare untouched.Sentinel handling
0001-01-01T00:00:00Zmeans "unset" in daemon output but "from the beginning of time" as CLI input. Ac_unsetTimestampconstant indocker_schema.hguards the daemon-side call site; the parser itself simply returns-62135596800.Validation Steps Performed
cmake --build .from a clean tree — 0 errors across all targets.FormatSource.ps1/ clang-format 19.1.5 run over all changed files.test/windows/wslc/WSLCCLIArgumentUnitTests.cppextended from 3 to 7ValidateTimestamp_*cases, covering the newly accepted forms (_ValidPreEpoch,_ValidZoneLessLocalTime,_ValidPartialAndDateOnly,_ValidGoDuration) and additional rejections (10x,1h30) in_InvalidRfc3339_Rejected. Existing epoch literals updated fromULLtoLL.