diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index 97fb2dfd8..a051f2161 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -37,6 +37,7 @@ set(SOURCES string.cpp SubProcess.cpp svccomm.cpp + timestamp.cpp VTSupport.cpp WindowsUpdateIntegration.cpp WSLCContainerLauncher.cpp @@ -125,6 +126,7 @@ set(HEADERS Stringify.h SubProcess.h svccomm.hpp + timestamp.hpp VTSupport.h WindowsUpdateIntegration.h WSLCContainerLauncher.h diff --git a/src/windows/common/precomp.h b/src/windows/common/precomp.h index c7b53ee54..3921f4bfd 100644 --- a/src/windows/common/precomp.h +++ b/src/windows/common/precomp.h @@ -146,6 +146,7 @@ Module Name: #include "conncheckshared.h" #include "helpers.hpp" #include "string.hpp" +#include "timestamp.hpp" #include "filesystem.hpp" #include "Localization.h" #include "wslutil.h" diff --git a/src/windows/common/string.cpp b/src/windows/common/string.cpp index c3b8f954a..1807e52d6 100644 --- a/src/windows/common/string.cpp +++ b/src/windows/common/string.cpp @@ -425,66 +425,3 @@ std::string wsl::windows::common::string::TruncateId(_In_ std::string_view id, b { return TruncateIdImpl(id, shortenLength); } - -std::uint64_t wsl::windows::common::string::Rfc3339ToEpoch(const std::string& timestamp) -{ - std::chrono::sys_seconds utcSeconds; - std::istringstream stream(timestamp); - stream >> std::chrono::parse("%FT%H:%M:%S%Z", utcSeconds); - THROW_HR_IF_MSG(E_INVALIDARG, stream.fail(), "Failed to parse timestamp '%hs'", timestamp.c_str()); - - return static_cast(utcSeconds.time_since_epoch().count()); -} - -std::string wsl::windows::common::string::EpochToLocalDisplayTime(LONGLONG timestamp) -{ - const auto time = - std::chrono::floor(std::chrono::system_clock::from_time_t(static_cast(timestamp))); - - try - { - const auto* zone = std::chrono::current_zone(); - return std::format("{:%F %T %z} {}", std::chrono::zoned_time{zone, time}, zone->get_info(time).abbrev); - } - catch (...) - { - // The time zone database is unavailable, so report UTC rather than failing the caller. - LOG_CAUGHT_EXCEPTION(); - return std::format("{:%F %T} +0000 UTC", time); - } -} - -std::string wsl::windows::common::string::Rfc3339ToUtcDisplayTime(std::string_view timestamp) -{ - if (timestamp.empty()) - { - return {}; - } - - // Fractional digits vary in length, so they are captured verbatim and re-inserted after formatting. - std::string parsable{timestamp}; - std::string fraction; - const auto separator = parsable.find('.'); - if (separator != std::string::npos) - { - auto end = separator + 1; - while (end < parsable.size() && (std::isdigit(static_cast(parsable[end])) != 0)) - { - end++; - } - - fraction = parsable.substr(separator, end - separator); - parsable.erase(separator, end - separator); - } - - std::chrono::sys_seconds parsed{}; - std::istringstream stream(parsable); - stream >> std::chrono::parse("%FT%H:%M:%S%Z", parsed); - if (stream.fail()) - { - return std::string{timestamp}; - } - - // Network timestamps are reported in UTC rather than the local time zone. - return std::format("{:%F %T}{} +0000 UTC", parsed, fraction); -} diff --git a/src/windows/common/string.hpp b/src/windows/common/string.hpp index 2a5c7403c..4776acf93 100644 --- a/src/windows/common/string.hpp +++ b/src/windows/common/string.hpp @@ -67,18 +67,6 @@ std::string WideToMultiByte(_In_ std::wstring_view Source); std::wstring TruncateId(_In_ std::wstring_view id, bool shortenLength = true); std::string TruncateId(_In_ std::string_view id, bool shortenLength = true); -// Converts an RFC 3339 timestamp to seconds since the unix epoch. Only the 'Z' zone designator is -// accepted; numeric offsets are not. -std::uint64_t Rfc3339ToEpoch(const std::string& timestamp); - -// Renders seconds since the unix epoch in the local time zone, using the layout -// "2006-01-02 15:04:05 -0700 MST". Falls back to UTC when the time zone database is unavailable. -std::string EpochToLocalDisplayTime(LONGLONG timestamp); - -// Renders an RFC 3339 timestamp in the same layout, but as UTC and with its fractional seconds -// preserved. The input is returned unchanged when it cannot be parsed. -std::string Rfc3339ToUtcDisplayTime(std::string_view timestamp); - // Template implementation for TruncateId to avoid code duplication. // Algorithm inspired from Moby for consistency in presentation of shortened IDs. // Always strips the algorithm prefix (e.g., "sha256:") if present, and optionally shortens to 12 characters. diff --git a/src/windows/common/timestamp.cpp b/src/windows/common/timestamp.cpp new file mode 100644 index 000000000..418eab096 --- /dev/null +++ b/src/windows/common/timestamp.cpp @@ -0,0 +1,383 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + timestamp.cpp + +Abstract: + + This file contains timestamp and duration helper function definitions. + +--*/ + +#include "precomp.h" +#include +#include +#include +#include +#include +#include +#include +#include + +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(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"; + } +} + +std::string wsl::windows::common::timestamp::ExpandToRfc3339(const std::string& timestamp) +{ + std::string_view value{timestamp}; + std::string_view zone; + + if (!value.empty() && (value.back() == 'Z' || value.back() == 'z')) + { + zone = value.substr(value.size() - 1); + value.remove_suffix(1); + } + else if (const auto plus = value.find('+'); plus != std::string_view::npos) + { + zone = value.substr(plus); + value = value.substr(0, plus); + } + else if (std::ranges::count(value, '-') == 3) + { + // The first two dashes belong to the date, so a third one opens a negative zone offset. + auto offset = value.find('-'); + offset = value.find('-', offset + 1); + offset = value.find('-', offset + 1); + + zone = value.substr(offset); + value = value.substr(0, offset); + } + + const auto separator = value.find('T'); + const auto time = separator == std::string_view::npos ? std::string_view{} : value.substr(separator + 1); + + std::string expanded{value.substr(0, separator)}; + expanded += 'T'; + + if (time.empty()) + { + expanded += "00:00:00"; + } + else + { + expanded += time; + + // Pad an hour-only or minute-only time out to a full HH:MM:SS. + const auto colons = std::ranges::count(time, ':'); + if (colons == 0) + { + expanded += ":00:00"; + } + else if (colons == 1) + { + expanded += ":00"; + } + } + + expanded += zone.empty() ? LocalUtcOffset() : std::string{zone}; + + return expanded; +} + +std::int64_t wsl::windows::common::timestamp::Rfc3339ToEpoch(const std::string& timestamp) +{ + // Normalize a trailing 'Z' or 'z' to '+00:00' so that %Ez parses every zone uniformly. + std::string normalized{timestamp}; + if (!normalized.empty() && (normalized.back() == 'Z' || normalized.back() == 'z')) + { + normalized.pop_back(); + normalized += "+00:00"; + } + + // Strip any fractional seconds. The value is truncated to whole seconds regardless, and parsing at + // second precision keeps timestamps outside the nanosecond range from silently wrapping. + const auto separator = normalized.find('.'); + if (separator != std::string::npos) + { + auto end = separator + 1; + while (end < normalized.size() && std::isdigit(static_cast(normalized[end])) != 0) + { + ++end; + } + + // A separator with no fractional digits is invalid, but std::chrono::parse otherwise accepts it. + THROW_HR_IF_MSG(E_INVALIDARG, end == separator + 1, "Failed to parse timestamp '%hs'", timestamp.c_str()); + + normalized.erase(separator, end - separator); + } + + // Validate the day up front since std::chrono::parse silently wraps invalid dates (e.g. Feb 31 -> Mar 2). + if (normalized.size() >= 10 && normalized[4] == '-' && normalized[7] == '-') + { + int year{}; + int month{}; + int day{}; + const auto yearResult = std::from_chars(normalized.data(), normalized.data() + 4, year); + const auto monthResult = std::from_chars(normalized.data() + 5, normalized.data() + 7, month); + const auto dayResult = std::from_chars(normalized.data() + 8, normalized.data() + 10, day); + if (yearResult.ec == std::errc() && monthResult.ec == std::errc() && dayResult.ec == std::errc()) + { + const auto date = std::chrono::year{year} / std::chrono::month{static_cast(month)} / + std::chrono::day{static_cast(day)}; + + THROW_HR_IF_MSG(E_INVALIDARG, !date.ok(), "Failed to parse timestamp '%hs'", timestamp.c_str()); + } + } + + std::chrono::sys_seconds parsed{}; + std::istringstream stream(normalized); + stream >> std::chrono::parse("%FT%T%Ez", parsed); + THROW_HR_IF_MSG(E_INVALIDARG, stream.fail(), "Failed to parse timestamp '%hs'", timestamp.c_str()); + THROW_HR_IF_MSG( + E_INVALIDARG, + stream.peek() != std::istringstream::traits_type::eof(), + "Unexpected trailing characters in timestamp '%hs'", + timestamp.c_str()); + + return parsed.time_since_epoch().count(); +} + +std::optional wsl::windows::common::timestamp::TryParseDuration(const std::string& duration) +{ + if (duration.empty()) + { + return std::nullopt; + } + + size_t pos = 0; + bool negative = false; + if (duration[pos] == '+' || duration[pos] == '-') + { + negative = duration[pos] == '-'; + pos++; + } + + // Special case: a bare "0" (with optional sign) is a valid zero duration. + if (duration.substr(pos) == "0") + { + return std::chrono::nanoseconds{0}; + } + + // Accumulate in a long double so fractional units (e.g. "1.5h") are handled, then round. + long double totalNanos = 0.0L; + bool sawValue = false; + + while (pos < duration.size()) + { + // Parse the numeric part (integer and/or fraction). + const size_t numberStart = pos; + while (pos < duration.size() && (std::isdigit(static_cast(duration[pos])) || duration[pos] == '.')) + { + pos++; + } + + const std::string numberStr = duration.substr(numberStart, pos - numberStart); + if (numberStr.empty() || numberStr == "." || std::count(numberStr.begin(), numberStr.end(), '.') > 1) + { + return std::nullopt; + } + + // Parse the unit (everything up to the next digit or '.'). + const size_t unitStart = pos; + while (pos < duration.size() && !std::isdigit(static_cast(duration[pos])) && duration[pos] != '.') + { + pos++; + } + + const std::string unit = duration.substr(unitStart, pos - unitStart); + + long double multiplier{}; + if (unit == "ns") + { + multiplier = 1.0L; + } + else if (unit == "us" || unit == "\xC2\xB5s" /* µs (U+00B5) */ || unit == "\xCE\xBCs" /* μs (U+03BC) */) + { + multiplier = 1000L; + } + else if (unit == "ms") + { + multiplier = 1000000L; + } + else if (unit == "s") + { + multiplier = 1000000000L; + } + else if (unit == "m") + { + multiplier = 60000000000L; + } + else if (unit == "h") + { + multiplier = 3600000000000L; + } + else + { + return std::nullopt; + } + + long double value{}; + auto [ptr, ec] = std::from_chars(numberStr.data(), numberStr.data() + numberStr.size(), value, std::chars_format::fixed); + if (ptr != numberStr.data() + numberStr.size() || ec != std::errc()) + { + return std::nullopt; + } + + totalNanos += value * multiplier; + sawValue = true; + } + + if (!sawValue) + { + return std::nullopt; + } + + if (negative) + { + totalNanos = -totalNanos; + } + + if (totalNanos > static_cast(std::numeric_limits::max()) || + totalNanos < static_cast(std::numeric_limits::min())) + { + return std::nullopt; + } + + return std::chrono::nanoseconds{static_cast(std::llroundl(totalNanos))}; +} + +std::string wsl::windows::common::timestamp::EpochToLocalDisplayTime(LONGLONG timestamp) +{ + const auto time = + std::chrono::floor(std::chrono::system_clock::from_time_t(static_cast(timestamp))); + + try + { + const auto* zone = std::chrono::current_zone(); + return std::format("{:%F %T %z} {}", std::chrono::zoned_time{zone, time}, zone->get_info(time).abbrev); + } + catch (...) + { + // The time zone database is unavailable, so report UTC rather than failing the caller. + LOG_CAUGHT_EXCEPTION(); + return std::format("{:%F %T} +0000 UTC", time); + } +} + +std::string wsl::windows::common::timestamp::Rfc3339ToUtcDisplayTime(std::string_view timestamp) +{ + if (timestamp.empty()) + { + return {}; + } + + // Fractional digits are dropped by the parse and vary in length, so they are captured verbatim + // and re-inserted after formatting. + std::string fraction; + const auto separator = timestamp.find('.'); + if (separator != std::string_view::npos) + { + auto end = separator + 1; + while (end < timestamp.size() && (std::isdigit(static_cast(timestamp[end])) != 0)) + { + end++; + } + + fraction = timestamp.substr(separator, end - separator); + } + + const std::chrono::sys_seconds parsed{std::chrono::seconds{Rfc3339ToEpoch(std::string{timestamp})}}; + + // Network timestamps are reported in UTC rather than the local time zone. + return std::format("{:%F %T}{} +0000 UTC", parsed, fraction); +} + +std::wstring wsl::windows::common::timestamp::FormatElapsedSeconds(LONGLONG elapsedSeconds) +{ + using namespace std::chrono_literals; + using wsl::shared::Localization; + + constexpr LONGLONG SecondsPerMinute = std::chrono::duration_cast(1min).count(); + constexpr LONGLONG SecondsPerHour = std::chrono::duration_cast(1h).count(); + constexpr LONGLONG HoursPerDay = 24; + constexpr LONGLONG MinutesPerHour = 60; + + const auto elapsed = std::max(elapsedSeconds, 0); + + if (elapsed < 1) + { + return Localization::WSLCCLI_RelativeTimeLessThanASecond(); + } + else if (elapsed == 1) + { + return Localization::WSLCCLI_RelativeTimeOneSecond(); + } + else if (elapsed < SecondsPerMinute) + { + return Localization::WSLCCLI_RelativeTimeSeconds(elapsed); + } + + const auto minutes = elapsed / SecondsPerMinute; + if (minutes == 1) + { + return Localization::WSLCCLI_RelativeTimeAboutAMinute(); + } + else if (minutes < MinutesPerHour) + { + return Localization::WSLCCLI_RelativeTimeMinutes(minutes); + } + + // Rounded to the nearest hour rather than truncated. + const auto hours = (elapsed + (SecondsPerHour / 2)) / SecondsPerHour; + if (hours == 1) + { + return Localization::WSLCCLI_RelativeTimeAboutAnHour(); + } + else if (hours < HoursPerDay * 2) + { + return Localization::WSLCCLI_RelativeTimeHours(hours); + } + else if (hours < HoursPerDay * 7 * 2) + { + return Localization::WSLCCLI_RelativeTimeDays(hours / HoursPerDay); + } + else if (hours < HoursPerDay * 30 * 2) + { + return Localization::WSLCCLI_RelativeTimeWeeks(hours / HoursPerDay / 7); + } + else if (hours < HoursPerDay * 365 * 2) + { + return Localization::WSLCCLI_RelativeTimeMonths(hours / HoursPerDay / 30); + } + + return Localization::WSLCCLI_RelativeTimeYears(elapsed / SecondsPerHour / HoursPerDay / 365); +} + +std::wstring wsl::windows::common::timestamp::FormatRelativeTime(LONGLONG timestamp) +{ + if (timestamp == 0) + { + return {}; + } + + return FormatElapsedSeconds(static_cast(std::time(nullptr)) - timestamp); +} diff --git a/src/windows/common/timestamp.hpp b/src/windows/common/timestamp.hpp new file mode 100644 index 000000000..7f6f0945e --- /dev/null +++ b/src/windows/common/timestamp.hpp @@ -0,0 +1,56 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + timestamp.hpp + +Abstract: + + This file contains timestamp and duration helper function declarations. + +--*/ + +#pragma once + +#include +#include +#include + +namespace wsl::windows::common::timestamp { + +// 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. The input is not validated, so an unrecognized value is +// expanded as-is and left for the parser to reject. +std::string ExpandToRfc3339(const std::string& timestamp); + +// Converts an RFC 3339 timestamp to seconds since the unix epoch. Accepts a 'Z' designator or a +// numeric +HH:MM offset, with optional fractional seconds. Timestamps that predate the epoch convert +// to a negative value. Throws E_INVALIDARG if the timestamp is malformed, names an invalid date, or +// has trailing characters. +std::int64_t Rfc3339ToEpoch(const std::string& timestamp); + +// Parses a Go duration such as "1h30m", "-1.5h" or "300ms": an optional sign followed by one or more +// decimal values that each carry a unit of ns, us, ms, s, m or h. Returns nothing if the value does +// not match that grammar or overflows. +std::optional TryParseDuration(const std::string& duration); + +// Renders seconds since the unix epoch in the local time zone, using the layout +// "2006-01-02 15:04:05 -0700 MST". Falls back to UTC when the time zone database is unavailable. +std::string EpochToLocalDisplayTime(LONGLONG timestamp); + +// Renders an RFC 3339 timestamp in the same layout, but as UTC and with its fractional seconds +// preserved. An empty input returns an empty string; anything else that cannot be parsed throws. +std::string Rfc3339ToUtcDisplayTime(std::string_view timestamp); + +// Renders an elapsed number of seconds as a coarse, localized description such as "About a minute" +// or "3 weeks". Negative values are treated as zero. +std::wstring FormatElapsedSeconds(LONGLONG elapsedSeconds); + +// Renders how long ago a timestamp given in seconds since the unix epoch occurred. A timestamp of +// zero means "unset" and returns an empty string. +std::wstring FormatRelativeTime(LONGLONG timestamp); + +} // namespace wsl::windows::common::timestamp diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h index 256fe0646..bcb509271 100644 --- a/src/windows/inc/docker_schema.h +++ b/src/windows/inc/docker_schema.h @@ -22,6 +22,10 @@ namespace wsl::windows::common::docker_schema { using wsl::shared::EmptyObject; +// The daemon formats timestamps that were never set as the zero value of Go's time.Time rather than +// omitting them, so this value means "unset" instead of an actual point in time. +inline constexpr std::string_view c_unsetTimestamp = "0001-01-01T00:00:00Z"; + // Reads a value, treating both a missing key and an explicit null as absent. The daemon reports some // empty maps and objects as null, which the default deserializer rejects. template diff --git a/src/windows/service/exe/ServiceMain.cpp b/src/windows/service/exe/ServiceMain.cpp index 40852b551..932f400f9 100644 --- a/src/windows/service/exe/ServiceMain.cpp +++ b/src/windows/service/exe/ServiceMain.cpp @@ -23,6 +23,7 @@ Module Name: using namespace wsl::windows::common::registry; using namespace wsl::windows::common::string; +using namespace wsl::windows::common::timestamp; using namespace wsl::windows::common::wslutil; using namespace wsl::windows::policies; @@ -306,13 +307,10 @@ try SetThreadpoolTimer(static_cast(Context)->m_updateCheckTimer.get(), nullptr, 0, 0); // Get current release date - std::wstring currentReleaseCreatedAtDate = GetGitHubReleaseByTag(TEXT(WSL_PACKAGE_VERSION)).created_at; + const std::wstring currentReleaseCreatedAtDate = GetGitHubReleaseByTag(TEXT(WSL_PACKAGE_VERSION)).created_at; - std::tm tm = {}; - std::wstring dateTimeFormat = L"%Y-%m-%dT%H:%M:%SZ"; - std::wistringstream ss(currentReleaseCreatedAtDate); - ss >> std::get_time(&tm, dateTimeFormat.c_str()); - auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm)); + const auto tp = std::chrono::system_clock::from_time_t( + static_cast(Rfc3339ToEpoch(WideToMultiByte(currentReleaseCreatedAtDate)))); // If their release of WSL is older than 30 days, then show a notification to update if (std::chrono::system_clock::now() - std::chrono::days(30) > tp) diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 1bd1fce68..20dfb0f8a 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -374,8 +374,8 @@ typedef struct _WSLCContainerEntry char Name[WSLC_MAX_CONTAINER_NAME_LENGTH + 1]; char Image[WSLC_MAX_IMAGE_NAME_LENGTH + 1]; WSLCContainerId Id; - ULONGLONG StateChangedAt; - ULONGLONG CreatedAt; + LONGLONG StateChangedAt; + LONGLONG CreatedAt; WSLCContainerState State; } WSLCContainerEntry; @@ -565,7 +565,7 @@ interface IWSLCContainer : IUnknown HRESULT GetInitProcess([out] IWSLCProcess** Process); HRESULT Exec([in, ref] const WSLCProcessOptions* Options, [in, unique] const WSLCProcessStartOptions* StartOptions, [out] IWSLCProcess** Process); HRESULT Inspect([out] LPSTR* Output); - HRESULT Logs([in] WSLCLogsFlags Flags, [out] WSLCHandle* Stdout, [out] WSLCHandle* Stderr, [in] ULONGLONG Since, [in] ULONGLONG Until, [in] ULONGLONG Tail); + HRESULT Logs([in] WSLCLogsFlags Flags, [out] WSLCHandle* Stdout, [out] WSLCHandle* Stderr, [in] LONGLONG Since, [in] LONGLONG Until, [in] ULONGLONG Tail); HRESULT GetId([out, string] WSLCContainerId Id); HRESULT GetName([out, string] LPSTR* Name); HRESULT GetLabels([out, size_is(, *Count)] WSLCLabelInformation** Labels, [out] ULONG* Count); diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index 3a37150cb..4c153722f 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -63,8 +63,8 @@ _(File, "file", L"f", Kind::Value, _(Filter, "filter", L"f", Kind::Value, KeyValuePair, Localization::WSLCCLI_FilterArgDescription()) \ _(Follow, "follow", L"f", Kind::Flag, NoConversion, Localization::WSLCCLI_FollowArgDescription()) \ _(Timestamps, "timestamps", L"t", Kind::Flag, NoConversion, Localization::WSLCCLI_TimestampsArgDescription()) \ -_(Since, "since", NO_ALIAS, Kind::Value, ULONGLONG, Localization::WSLCCLI_SinceArgDescription()) \ -_(Until, "until", NO_ALIAS, Kind::Value, ULONGLONG, Localization::WSLCCLI_UntilArgDescription()) \ +_(Since, "since", NO_ALIAS, Kind::Value, LONGLONG, Localization::WSLCCLI_SinceArgDescription()) \ +_(Until, "until", NO_ALIAS, Kind::Value, LONGLONG, Localization::WSLCCLI_UntilArgDescription()) \ _(Format, "format", NO_ALIAS, Kind::Value, FormatType, Localization::WSLCCLI_FormatArgDescription()) \ _(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, NoConversion, Localization::WSLCCLI_ForwardArgsDescription()) \ _(Gateway, "gateway", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkGatewayArgDescription()) \ diff --git a/src/windows/wslc/arguments/SpecParsing.cpp b/src/windows/wslc/arguments/SpecParsing.cpp index 7407bdda8..3f88cbaca 100644 --- a/src/windows/wslc/arguments/SpecParsing.cpp +++ b/src/windows/wslc/arguments/SpecParsing.cpp @@ -29,7 +29,6 @@ Module Name: #include #include #include -#include #include #include @@ -594,78 +593,12 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring return static_cast(signalValue); } -// Parses an RFC3339 timestamp (e.g. "2024-01-15T10:30:00Z" or "2024-01-15T10:30:00+05:30") -// into a ULONGLONG Unix epoch seconds value using std::chrono::parse. -// Note: +HHMM (no colon) offsets are not supported; use +HH:MM format. -static std::optional TryParseRfc3339(const std::string& input) -{ - std::string normalized = input; - - // Normalize trailing 'Z'/'z' to '+00:00' so %Ez can parse it uniformly. - if (!normalized.empty() && (normalized.back() == 'Z' || normalized.back() == 'z')) - { - normalized.pop_back(); - normalized += "+00:00"; - } - - // Reject bare dot with no fractional digits (e.g. "10:30:00.+00:00") since - // std::chrono::parse is lenient about this. - auto dotPos = normalized.find('.'); - if (dotPos != std::string::npos && (dotPos + 1 >= normalized.size() || !std::isdigit(normalized[dotPos + 1]))) - { - return std::nullopt; - } - - // Pre-validate day-of-month since std::chrono::parse silently wraps invalid dates (e.g. Feb 31 → Mar 2). - if (normalized.size() >= 10 && normalized[4] == '-' && normalized[7] == '-') - { - int year = 0, month = 0, day = 0; - auto yResult = std::from_chars(normalized.data(), normalized.data() + 4, year); - auto mResult = std::from_chars(normalized.data() + 5, normalized.data() + 7, month); - auto dResult = std::from_chars(normalized.data() + 8, normalized.data() + 10, day); - - if (yResult.ec == std::errc() && mResult.ec == std::errc() && dResult.ec == std::errc()) - { - auto ymd = std::chrono::year{year} / std::chrono::month{static_cast(month)} / - std::chrono::day{static_cast(day)}; - if (!ymd.ok()) - { - return std::nullopt; - } - } - } - - // Parse into nanosecond precision so fractional seconds (e.g. ".123456789") are consumed - // by std::chrono::parse rather than requiring manual stripping. - std::chrono::sys_time utcTime; - std::istringstream stream(normalized); - stream >> std::chrono::parse("%FT%T%Ez", utcTime); - if (stream.fail()) - { - return std::nullopt; - } - - // Reject if there are trailing characters after the parsed timestamp - if (stream.peek() != std::istringstream::traits_type::eof()) - { - return std::nullopt; - } - - auto epochSeconds = std::chrono::duration_cast(utcTime.time_since_epoch()).count(); - if (epochSeconds < 0) - { - return std::nullopt; - } - - return static_cast(epochSeconds); -} - -ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName) +LONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName) { std::string narrowValue = wsl::windows::common::string::WideToMultiByte(value); // Try integer (Unix epoch seconds) first - ULONGLONG intValue{}; + LONGLONG intValue{}; const char* begin = narrowValue.c_str(); const char* end = begin + narrowValue.size(); auto result = std::from_chars(begin, end, intValue); @@ -674,14 +607,23 @@ ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& return intValue; } - // Try RFC3339 timestamp - auto rfc3339Value = TryParseRfc3339(narrowValue); - if (rfc3339Value.has_value()) + if (const auto duration = wsl::windows::common::timestamp::TryParseDuration(narrowValue); duration.has_value()) { - return rfc3339Value.value(); + // Apply the duration at full precision and truncate once, so that a sub-second value keeps its sign. + const auto target = std::chrono::system_clock::now() - duration.value(); + + return std::chrono::floor(target.time_since_epoch()).count(); } - throw ArgumentException(Localization::WSLCCLI_InvalidTimestampArgumentError(argName, value)); + try + { + return wsl::windows::common::timestamp::Rfc3339ToEpoch(wsl::windows::common::timestamp::ExpandToRfc3339(narrowValue)); + } + // Name the offending argument rather than surfacing the raw parse failure. + catch (...) + { + throw ArgumentException(Localization::WSLCCLI_InvalidTimestampArgumentError(argName, value)); + } } models::FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName) @@ -824,134 +766,17 @@ int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& a return static_cast(bytes.value()); } -// Parses duration string into nanoseconds. -static std::optional TryParseDuration(const std::string& input) -{ - if (input.empty()) - { - return std::nullopt; - } - - size_t pos = 0; - bool negative = false; - if (input[pos] == '+' || input[pos] == '-') - { - negative = input[pos] == '-'; - pos++; - } - - // Special case: a bare "0" (with optional sign) is a valid zero duration. - if (input.substr(pos) == "0") - { - return 0; - } - - // Accumulate in a long double so fractional units (e.g. "1.5h") are handled, then round. - long double totalNanos = 0.0L; - bool sawValue = false; - - while (pos < input.size()) - { - // Parse the numeric part (integer and/or fraction). - const size_t numberStart = pos; - while (pos < input.size() && (std::isdigit(static_cast(input[pos])) || input[pos] == '.')) - { - pos++; - } - - const std::string numberStr = input.substr(numberStart, pos - numberStart); - if (numberStr.empty() || numberStr == "." || std::count(numberStr.begin(), numberStr.end(), '.') > 1) - { - return std::nullopt; - } - - // Parse the unit (everything up to the next digit or '.'). - const size_t unitStart = pos; - while (pos < input.size() && !std::isdigit(static_cast(input[pos])) && input[pos] != '.') - { - pos++; - } - - const std::string unit = input.substr(unitStart, pos - unitStart); - - long double multiplier{}; - if (unit == "ns") - { - multiplier = 1.0L; - } - else if (unit == "us" || unit == "\xC2\xB5s" /* µs (U+00B5) */ || unit == "\xCE\xBCs" /* μs (U+03BC) */) - { - multiplier = 1000L; - } - else if (unit == "ms") - { - multiplier = 1000000L; - } - else if (unit == "s") - { - multiplier = 1000000000L; - } - else if (unit == "m") - { - multiplier = 60000000000L; - } - else if (unit == "h") - { - multiplier = 3600000000000L; - } - else - { - return std::nullopt; - } - - long double value{}; - try - { - auto [ptr, ec] = std::from_chars(numberStr.data(), numberStr.data() + numberStr.size(), value, std::chars_format::fixed); - if (ptr != numberStr.data() + numberStr.size() || ec != std::errc()) - { - return std::nullopt; - } - } - catch (...) - { - return std::nullopt; - } - - totalNanos += value * multiplier; - sawValue = true; - } - - if (!sawValue) - { - return std::nullopt; - } - - if (negative) - { - totalNanos = -totalNanos; - } - - if (totalNanos > static_cast(std::numeric_limits::max()) || - totalNanos < static_cast(std::numeric_limits::min())) - { - return std::nullopt; - } - - return static_cast(std::llroundl(totalNanos)); -} - int64_t GetDurationNanosFromString(const std::wstring& input, const std::wstring& argName) { const std::string narrow = WideToMultiByte(input); - const auto parsed = TryParseDuration(narrow); + const auto parsed = wsl::windows::common::timestamp::TryParseDuration(narrow); - if (!parsed.has_value() || parsed.value() < 0) + if (!parsed.has_value() || parsed.value() < std::chrono::nanoseconds::zero()) { throw ArgumentException(Localization::WSLCCLI_InvalidDurationError(argName, input)); } - return parsed.value(); + return parsed.value().count(); } int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName) diff --git a/src/windows/wslc/arguments/SpecParsing.h b/src/windows/wslc/arguments/SpecParsing.h index dab917bdc..957042766 100644 --- a/src/windows/wslc/arguments/SpecParsing.h +++ b/src/windows/wslc/arguments/SpecParsing.h @@ -87,7 +87,7 @@ ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value, const std::w WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName = {}); // Parses a timestamp given as Unix epoch seconds or an RFC3339 string into epoch seconds. -ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName = {}); +LONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName = {}); // Parses an output format ("json"/"table") into a FormatType. models::FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {}); diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index 86c27e6ce..0e9d634fd 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -135,8 +135,8 @@ struct ContainerInformation std::string Name; std::string Image; WSLCContainerState State; - ULONGLONG StateChangedAt{}; - ULONGLONG CreatedAt{}; + LONGLONG StateChangedAt{}; + LONGLONG CreatedAt{}; std::vector Ports; NLOHMANN_DEFINE_TYPE_INTRUSIVE(ContainerInformation, Id, Name, Image, State, StateChangedAt, CreatedAt, Ports); diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 06d30c655..300a4a90b 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -299,74 +299,6 @@ static PortInformation PortInformationFromWSLCPortMapping(const WSLCPortMapping& }; } -std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp) -{ - if (timestamp == 0) - { - return L""; - } - - return FormatElapsedSeconds(static_cast(std::time(nullptr)) - static_cast(timestamp)); -} - -std::wstring ContainerService::FormatElapsedSeconds(LONGLONG elapsedSeconds) -{ - constexpr LONGLONG SecondsPerMinute = std::chrono::duration_cast(1min).count(); - constexpr LONGLONG SecondsPerHour = std::chrono::duration_cast(1h).count(); - constexpr LONGLONG HoursPerDay = 24; - constexpr LONGLONG MinutesPerHour = 60; - - const auto elapsed = std::max(elapsedSeconds, 0); - - if (elapsed < 1) - { - return Localization::WSLCCLI_RelativeTimeLessThanASecond(); - } - else if (elapsed == 1) - { - return Localization::WSLCCLI_RelativeTimeOneSecond(); - } - else if (elapsed < SecondsPerMinute) - { - return Localization::WSLCCLI_RelativeTimeSeconds(elapsed); - } - - const auto minutes = elapsed / SecondsPerMinute; - if (minutes == 1) - { - return Localization::WSLCCLI_RelativeTimeAboutAMinute(); - } - else if (minutes < MinutesPerHour) - { - return Localization::WSLCCLI_RelativeTimeMinutes(minutes); - } - - // Rounded to the nearest hour rather than truncated. - const auto hours = (elapsed + (SecondsPerHour / 2)) / SecondsPerHour; - if (hours == 1) - { - return Localization::WSLCCLI_RelativeTimeAboutAnHour(); - } - else if (hours < HoursPerDay * 2) - { - return Localization::WSLCCLI_RelativeTimeHours(hours); - } - else if (hours < HoursPerDay * 7 * 2) - { - return Localization::WSLCCLI_RelativeTimeDays(hours / HoursPerDay); - } - else if (hours < HoursPerDay * 30 * 2) - { - return Localization::WSLCCLI_RelativeTimeWeeks(hours / HoursPerDay / 7); - } - else if (hours < HoursPerDay * 365 * 2) - { - return Localization::WSLCCLI_RelativeTimeMonths(hours / HoursPerDay / 30); - } - - return Localization::WSLCCLI_RelativeTimeYears(elapsed / SecondsPerHour / HoursPerDay / 365); -} - int ContainerService::Attach(Terminal& terminal, Session& session, const std::string& id) { [[maybe_unused]] auto operation = session.BeginContainerOperation(); @@ -408,7 +340,7 @@ int ContainerService::Attach(Terminal& terminal, Session& session, const std::st return runningProcess.Wait(); } -std::wstring ContainerService::ContainerStateToString(WSLCContainerState state, ULONGLONG stateChangedAt) +std::wstring ContainerService::ContainerStateToString(WSLCContainerState state, LONGLONG stateChangedAt) { std::wstring stateString; switch (state) @@ -436,7 +368,7 @@ std::wstring ContainerService::ContainerStateToString(WSLCContainerState state, return stateString; } - return std::format(L"{} {}", stateString, FormatRelativeTime(stateChangedAt)); + return std::format(L"{} {}", stateString, wsl::windows::common::timestamp::FormatRelativeTime(stateChangedAt)); } std::wstring ContainerService::FormatPorts(WSLCContainerState state, const std::vector& ports) @@ -718,7 +650,7 @@ void ContainerService::CopyFromContainer(Session& session, const std::string& id THROW_IF_FAILED(container->DownloadArchive(srcPath.c_str(), ToCOMInputHandle(outputHandle))); } -void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail) +void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, LONGLONG since, LONGLONG until, ULONGLONG tail) { [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h index 09203056d..4e0e5bbbb 100644 --- a/src/windows/wslc/services/ContainerService.h +++ b/src/windows/wslc/services/ContainerService.h @@ -22,9 +22,7 @@ Module Name: namespace wsl::windows::wslc::services { struct ContainerService { - static std::wstring ContainerStateToString(WSLCContainerState state, ULONGLONG stateChangedAt = 0); - static std::wstring FormatRelativeTime(ULONGLONG timestamp); - static std::wstring FormatElapsedSeconds(LONGLONG elapsedSeconds); + static std::wstring ContainerStateToString(WSLCContainerState state, LONGLONG stateChangedAt = 0); static std::wstring FormatPorts(WSLCContainerState state, const std::vector& ports); static int Attach(Terminal& terminal, models::Session& session, const std::string& id); static int Run(Terminal& terminal, models::Session& session, const std::string& image, models::ContainerOptions options); @@ -42,7 +40,7 @@ struct ContainerService static void CopyToContainer(models::Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize); static void CopyFromContainer(models::Session& session, const std::string& id, const std::string& srcPath, HANDLE outputHandle); static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id); - static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail = 0); + static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, LONGLONG since, LONGLONG until, ULONGLONG tail = 0); static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id); static models::PruneContainersResult Prune(models::Session& session); }; diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 695fb61c5..da3c2582a 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -31,6 +31,7 @@ Module Name: using namespace wsl::shared; using namespace wsl::windows::common; using namespace wsl::windows::common::string; +using namespace wsl::windows::common::timestamp; using namespace wsl::windows::common::wslutil; using namespace wsl::windows::wslc::execution; using namespace wsl::windows::wslc::models; @@ -576,7 +577,7 @@ void ListContainers(CLIExecutionContext& context) MultiByteToWide(trunc ? TruncateId(container.Id) : container.Id), MultiByteToWide(container.Name), MultiByteToWide(container.Image), - ContainerService::FormatRelativeTime(container.CreatedAt), + FormatRelativeTime(container.CreatedAt), ContainerService::ContainerStateToString(container.State, container.StateChangedAt), ContainerService::FormatPorts(container.State, container.Ports), }); @@ -1040,13 +1041,13 @@ void ViewContainerLogs(CLIExecutionContext& context) // N.B. since=0 and until=0 mean "unset" — the Docker API omits the parameter when the value is 0, // which is equivalent to "no lower/upper bound". This matches Docker CLI behavior where // `docker logs --since 0` returns all logs and `docker logs --until 0` applies no upper bound. - ULONGLONG since = 0; + LONGLONG since = 0; if (context.Args.Contains(ArgType::Since)) { since = context.Args.GetValue(); } - ULONGLONG until = 0; + LONGLONG until = 0; if (context.Args.Contains(ArgType::Until)) { until = context.Args.GetValue(); diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index a3e3371c2..f8fbbae6a 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -29,6 +29,7 @@ Module Name: using namespace wsl::shared; using namespace wsl::windows::common; using namespace wsl::windows::common::string; +using namespace wsl::windows::common::timestamp; using namespace wsl::windows::common::wslutil; using namespace wsl::windows::wslc::execution; using namespace wsl::windows::wslc::models; @@ -83,8 +84,7 @@ namespace { entry.Containers = image.Containers < 0 ? std::string{c_notAvailable} : std::to_string(image.Containers); entry.CreatedAt = EpochToLocalDisplayTime(image.Created); - entry.CreatedSince = - WideToMultiByte(ContainerService::FormatRelativeTime(image.Created > 0 ? static_cast(image.Created) : 0)); + entry.CreatedSince = WideToMultiByte(FormatRelativeTime(image.Created)); entry.Digest = c_none; entry.ID = truncate ? TruncateId(image.Id, true) : image.Id; entry.Repository = image.Repository.value_or(std::string{c_none}); diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp index d3b148df1..d15df0e8a 100644 --- a/src/windows/wslc/tasks/NetworkTasks.cpp +++ b/src/windows/wslc/tasks/NetworkTasks.cpp @@ -23,6 +23,7 @@ Module Name: using namespace wsl::shared; using namespace wsl::windows::common; using namespace wsl::windows::common::string; +using namespace wsl::windows::common::timestamp; using namespace wsl::windows::common::wslutil; using namespace wsl::windows::wslc::execution; using namespace wsl::windows::wslc::models; diff --git a/src/windows/wslcsession/DockerEventTracker.cpp b/src/windows/wslcsession/DockerEventTracker.cpp index 95c06b0e7..28909f894 100644 --- a/src/windows/wslcsession/DockerEventTracker.cpp +++ b/src/windows/wslcsession/DockerEventTracker.cpp @@ -114,7 +114,7 @@ void DockerEventTracker::OnEvent(const std::string_view& event) auto timeEntry = parsed.find("time"); THROW_HR_IF_MSG( E_INVALIDARG, timeEntry == parsed.end(), "Failed to parse time from event: %.*hs", static_cast(event.size()), event.data()); - std::uint64_t eventTime = timeEntry->get(); + std::int64_t eventTime = timeEntry->get(); auto actionStr = action->get(); @@ -154,7 +154,7 @@ void DockerEventTracker::OnEvent(const std::string_view& event) } } -void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime) +void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime) { static std::map events{ {"start", ContainerEvent::Start}, {"die", ContainerEvent::Stop}, {"destroy", ContainerEvent::Destroy}, {"exec_die", ContainerEvent::ExecDied}}; @@ -202,7 +202,7 @@ void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const st } } -void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime) +void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime) { static std::map events{{"create", VolumeEvent::Create}, {"destroy", VolumeEvent::Destroy}}; diff --git a/src/windows/wslcsession/DockerEventTracker.h b/src/windows/wslcsession/DockerEventTracker.h index 68b819597..e5638890a 100644 --- a/src/windows/wslcsession/DockerEventTracker.h +++ b/src/windows/wslcsession/DockerEventTracker.h @@ -61,8 +61,8 @@ class DockerEventTracker DockerEventTracker* m_tracker = nullptr; }; - using ContainerStateChangeCallback = std::function, std::uint64_t)>; - using VolumeEventCallback = std::function; + using ContainerStateChangeCallback = std::function, std::int64_t)>; + using VolumeEventCallback = std::function; explicit DockerEventTracker(WSLCSession& session); ~DockerEventTracker(); @@ -81,8 +81,8 @@ class DockerEventTracker private: void OnEvent(const std::string_view& event); - void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime); - void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime); + void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime); + void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime); struct ContainerCallback { diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 7a6bd398c..ae04b4511 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -522,7 +522,7 @@ docker_schema::PruneNetworkResult DockerHTTPClient::PruneNetworks(const std::map return Transaction(verb::post, url); } -wil::unique_socket DockerHTTPClient::ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) +wil::unique_socket DockerHTTPClient::ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, LONGLONG Since, LONGLONG Until, ULONGLONG Tail) { auto url = URL::Create("/containers/{}/logs", Id); url.SetParameter("follow", WI_IsFlagSet(Flags, WSLCLogsFlagsFollow)); diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index dc66a8df5..2f8f23253 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -141,7 +141,7 @@ class DockerHTTPClient common::docker_schema::InspectExec InspectExec(const std::string& Id); wil::unique_socket AttachContainer(const std::string& Id, const std::optional& DetachKeys); void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns); - wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail); + wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, LONGLONG Since, LONGLONG Until, ULONGLONG Tail); std::pair ExportContainer(const std::string& ContainerID); std::unique_ptr PutArchive(const std::string& ContainerID, const std::string& Path, std::optional ContentLength); std::tuple GetArchive(const std::string& ContainerID, const std::string& Path); diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 979a26bcc..3809602b2 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -843,7 +843,7 @@ WSLCContainerImpl::WSLCContainerImpl( std::map&& labels, std::function&& onDeleted, WSLCContainerState InitialState, - std::uint64_t CreatedAt, + std::int64_t CreatedAt, WSLCProcessFlags InitProcessFlags, WSLCContainerFlags ContainerFlags) : m_wslcSession(wslcSession), @@ -976,13 +976,13 @@ std::vector WSLCContainerImpl::GetPorts() const return result; } -void WSLCContainerImpl::GetStateChangedAt(ULONGLONG* Result) +void WSLCContainerImpl::GetStateChangedAt(LONGLONG* Result) { auto lock = m_lock.lock_shared(); *Result = m_stateChangedAt; } -void WSLCContainerImpl::GetCreatedAt(ULONGLONG* Result) +void WSLCContainerImpl::GetCreatedAt(LONGLONG* Result) { auto lock = m_lock.lock_shared(); *Result = m_createdAt; @@ -1188,7 +1188,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt cleanup.release(); } -void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTime) +void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCode, std::int64_t eventTime) { // We must release m_lock and m_stopLock before the wrapper's destructor calls // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers. @@ -1304,7 +1304,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) } // Wait for the stop event to get the Docker timestamp. - std::optional stopTimestamp; + std::optional stopTimestamp; if (m_wslcSession.WaitForEventOrSessionTerminating(m_stopNotification.Event.get(), 60s)) { stopTimestamp = m_stopNotification.EventTime.load(std::memory_order_acquire); @@ -1322,7 +1322,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) } } -__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::OnStopped(std::optional stopTimestamp) +__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::OnStopped(std::optional stopTimestamp) { unique_com_disconnect comWrapper; @@ -2421,7 +2421,7 @@ std::shared_ptr WSLCContainerImpl::Create( std::move(mergedLabels), std::move(OnDeleted), WslcContainerStateCreated, - wsl::windows::common::string::Rfc3339ToEpoch(inspectData.Created), + wsl::windows::common::timestamp::Rfc3339ToEpoch(inspectData.Created), containerOptions.InitProcessOptions.Flags, containerOptions.Flags); @@ -2509,7 +2509,7 @@ std::shared_ptr WSLCContainerImpl::Open( std::move(labels), std::move(OnDeleted), DockerStateToWSLCState(dockerContainer.State), - static_cast(dockerContainer.Created), + dockerContainer.Created, metadata.InitProcessFlags, metadata.Flags); @@ -2525,15 +2525,15 @@ std::shared_ptr WSLCContainerImpl::Open( { // A created-but-never-started container has no StartedAt/FinishedAt; its state last // changed when it was created. - container->m_stateChangedAt = static_cast(dockerContainer.Created); + container->m_stateChangedAt = dockerContainer.Created; } else { const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt; - if (!timestamp.empty()) + if (!timestamp.empty() && timestamp != c_unsetTimestamp) { - container->m_stateChangedAt = wsl::windows::common::string::Rfc3339ToEpoch(timestamp); + container->m_stateChangedAt = wsl::windows::common::timestamp::Rfc3339ToEpoch(timestamp); } } } @@ -2575,7 +2575,7 @@ std::string WSLCContainerImpl::InspectLockHeld() const return wsl::shared::ToJson(wslcInspect); } -void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const +void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, LONGLONG Since, LONGLONG Until, ULONGLONG Tail) const { auto lock = m_lock.lock_shared(); @@ -2816,7 +2816,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl:: return unique_com_disconnect{std::exchange(m_comWrapper, nullptr)}; } -__requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerState State, std::optional stateChangedAt) noexcept +__requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerState State, std::optional stateChangedAt) noexcept { // N.B. A deleted container cannot transition back to any other state. WI_ASSERT(m_state != WslcContainerStateDeleted); @@ -2828,7 +2828,7 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta TraceLoggingValue(m_id.c_str(), "ID")); m_state = State; - m_stateChangedAt = stateChangedAt.value_or(static_cast(std::time(nullptr))); + m_stateChangedAt = stateChangedAt.value_or(static_cast(std::time(nullptr))); // Keep the VM alive while this container is Running and release the hold once it leaves that // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping @@ -3081,7 +3081,7 @@ try } CATCH_RETURN(); -HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) +HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, LONGLONG Since, LONGLONG Until, ULONGLONG Tail) try { WSLCExecutionContext context(&m_session); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index c9f42f01c..8f0bea2a1 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -86,7 +86,7 @@ class WSLCContainerImpl : public std::enable_shared_from_this std::map&& labels, std::function&& OnDeleted, WSLCContainerState InitialState, - std::uint64_t CreatedAt, + std::int64_t CreatedAt, WSLCProcessFlags InitProcessFlags, WSLCContainerFlags ContainerFlags); @@ -101,13 +101,13 @@ class WSLCContainerImpl : public std::enable_shared_from_this void Export(WSLCHandle TarHandle) const; void UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const; void DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) const; - void GetStateChangedAt(_Out_ ULONGLONG* StateChangedAt); - void GetCreatedAt(_Out_ ULONGLONG* CreatedAt); + void GetStateChangedAt(_Out_ LONGLONG* StateChangedAt); + void GetCreatedAt(_Out_ LONGLONG* CreatedAt); void GetState(_Out_ WSLCContainerState* State); void GetInitProcess(_Out_ IWSLCProcess** process) const; void Exec(_In_ const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process); void Inspect(LPSTR* Output) const; - void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const; + void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, LONGLONG Since, LONGLONG Until, ULONGLONG Tail) const; void Stats(LPSTR* Output) const; void GetLabels(WSLCLabelInformation** Labels, ULONG* Count) const; void ConnectToNetwork(const WSLCNetworkConnectionOptions* Options); @@ -123,7 +123,7 @@ class WSLCContainerImpl : public std::enable_shared_from_this // Re-registers a stopped container's VM-scoped port allocations against the restarted VM. void RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer); - __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional stateChangedAt = std::nullopt) noexcept; + __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional stateChangedAt = std::nullopt) noexcept; const std::string& ID() const noexcept; @@ -154,14 +154,14 @@ class WSLCContainerImpl : public std::enable_shared_from_this __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags); void AllocateBridgedModePorts(); - void OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTime); + void OnEvent(ContainerEvent event, std::optional exitCode, std::int64_t eventTime); __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect ReleaseResources(); __requires_exclusive_lock_held(m_lock) void ReleaseRuntimeResources(); __requires_exclusive_lock_held(m_lock) void ReleaseProcesses(); __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect PrepareDisconnectComWrapper(); - __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect OnStopped(std::optional stopTimestamp); + __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect OnStopped(std::optional stopTimestamp); void SetExitCode(int ExitCode) noexcept; void SignalInitProcessExit() noexcept; @@ -192,7 +192,7 @@ class WSLCContainerImpl : public std::enable_shared_from_this struct StopNotification { - std::atomic EventTime{0}; + std::atomic EventTime{0}; wil::unique_event Event{wil::EventOptions::None}; } m_stopNotification; @@ -207,8 +207,8 @@ class WSLCContainerImpl : public std::enable_shared_from_this // fetched from the (stable) runtime at each use rather than cached, since a cached reference // would dangle across a restart. They are only valid while a VM lease is held. WSLCSessionRuntime& m_runtime; - std::uint64_t m_stateChangedAt{static_cast(std::time(nullptr))}; - std::uint64_t m_createdAt{}; + std::int64_t m_stateChangedAt{static_cast(std::time(nullptr))}; + std::int64_t m_createdAt{}; WSLCContainerState m_state = WslcContainerStateInvalid; WSLCSession& m_wslcSession; IWSLCPluginNotifier* m_pluginNotifier; @@ -248,7 +248,7 @@ class DECLSPEC_UUID("B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4") WSLCContainer IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process) override; IFACEMETHOD(Start)(WSLCContainerStartFlags Flags, _In_opt_ const WSLCProcessStartOptions* StartOptions, _In_opt_ IWarningCallback* WarningCallback) override; IFACEMETHOD(Inspect)(_Out_ LPSTR* Output) override; - IFACEMETHOD(Logs)(_In_ WSLCLogsFlags Flags, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr, _In_ ULONGLONG Since, _In_ ULONGLONG Until, _In_ ULONGLONG Tail) override; + IFACEMETHOD(Logs)(_In_ WSLCLogsFlags Flags, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr, _In_ LONGLONG Since, _In_ LONGLONG Until, _In_ ULONGLONG Tail) override; IFACEMETHOD(GetId)(_Out_ WSLCContainerId Id) override; IFACEMETHOD(GetName)(_Out_ LPSTR* Name) override; IFACEMETHOD(GetLabels)(_Out_ WSLCLabelInformation** Labels, _Out_ ULONG* Count) override; diff --git a/src/windows/wslcsession/WSLCProcessControl.cpp b/src/windows/wslcsession/WSLCProcessControl.cpp index 8c8da1837..0d78286c8 100644 --- a/src/windows/wslcsession/WSLCProcessControl.cpp +++ b/src/windows/wslcsession/WSLCProcessControl.cpp @@ -172,7 +172,7 @@ void DockerExecProcessControl::SetExitCode(int ExitCode) } } -void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t /*eventTime*/) +void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional ExitCode, std::int64_t /*eventTime*/) { if (Event == ContainerEvent::ExecDied && !m_exitEvent.is_signaled()) { diff --git a/src/windows/wslcsession/WSLCProcessControl.h b/src/windows/wslcsession/WSLCProcessControl.h index 54cb2aca5..b4dc65a14 100644 --- a/src/windows/wslcsession/WSLCProcessControl.h +++ b/src/windows/wslcsession/WSLCProcessControl.h @@ -72,7 +72,7 @@ class DockerExecProcessControl : public WSLCProcessControl void SetExitCode(int ExitCode); private: - void OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t eventTime); + void OnEvent(ContainerEvent Event, std::optional ExitCode, std::int64_t eventTime); mutable std::mutex m_lock; std::string m_id; diff --git a/src/windows/wslcsession/WSLCVolumes.cpp b/src/windows/wslcsession/WSLCVolumes.cpp index 143bca123..6dcd35191 100644 --- a/src/windows/wslcsession/WSLCVolumes.cpp +++ b/src/windows/wslcsession/WSLCVolumes.cpp @@ -67,7 +67,7 @@ __requires_lock_held(m_lock) void WSLCVolumes::OpenVolumeExclusiveLockHeld(const m_volumes.insert({vol.Name, WSLCGuestVolumeImpl::Open(vol, m_dockerClient)}); } -void WSLCVolumes::OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::uint64_t) +void WSLCVolumes::OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::int64_t) { auto lock = m_lock.lock_exclusive(); diff --git a/src/windows/wslcsession/WSLCVolumes.h b/src/windows/wslcsession/WSLCVolumes.h index 893a8c3b3..e673bdad0 100644 --- a/src/windows/wslcsession/WSLCVolumes.h +++ b/src/windows/wslcsession/WSLCVolumes.h @@ -59,7 +59,7 @@ class WSLCVolumes __requires_lock_held(m_lock) void OpenVolumeExclusiveLockHeld(const std::string& volumeName); __requires_lock_held(m_lock) void OnVolumeDeletedExclusiveLockHeld(const std::string& volumeName); - void OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::uint64_t eventTime); + void OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::int64_t eventTime); mutable wil::srwlock m_lock; _Guarded_by_(m_lock) std::unordered_map> m_volumes; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 4e0ea24d1..d75696ca4 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6781,8 +6781,8 @@ class WSLCTests expectContainerList({{"test-container-1", "debian:latest", WslcContainerStateRunning}}); // Capture StateChangedAt and CreatedAt while the container is running. - ULONGLONG runningStateChangedAt{}; - ULONGLONG runningCreatedAt{}; + LONGLONG runningStateChangedAt{}; + LONGLONG runningCreatedAt{}; { auto [containers, ports] = ListContainers(m_defaultSession.get()); VERIFY_ARE_EQUAL(containers.size(), 1); @@ -6813,7 +6813,7 @@ class WSLCTests auto [containers, ports] = ListContainers(m_defaultSession.get()); VERIFY_ARE_EQUAL(containers.size(), 1); - auto now = static_cast(time(nullptr)); + auto now = static_cast(time(nullptr)); VERIFY_IS_TRUE(containers[0].StateChangedAt <= now); VERIFY_IS_TRUE(containers[0].StateChangedAt >= runningStateChangedAt); @@ -10467,8 +10467,8 @@ class WSLCTests auto restore = ResetTestSession(); // Required to access the storage folder. std::string containerName = "test-container"; - ULONGLONG originalStateChangedAt{}; - ULONGLONG originalCreatedAt{}; + LONGLONG originalStateChangedAt{}; + LONGLONG originalCreatedAt{}; // Phase 1: Create session and container, then stop the container { diff --git a/test/windows/wslc/CommandLineTestCases.h b/test/windows/wslc/CommandLineTestCases.h index 6ff12ac55..df3fc5fce 100644 --- a/test/windows/wslc/CommandLineTestCases.h +++ b/test/windows/wslc/CommandLineTestCases.h @@ -278,9 +278,14 @@ COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00+05:30 cont1" COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00.123456789Z cont1", L"logs", true) COMMAND_LINE_TEST_CASE(L"container logs --since 2024-13-15T10:30:00Z cont1", L"logs", false) COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T25:30:00Z cont1", L"logs", false) -COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15 cont1", L"logs", false) +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15 cont1", L"logs", true) +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10 cont1", L"logs", true) +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30 cont1", L"logs", true) +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00 cont1", L"logs", true) +COMMAND_LINE_TEST_CASE(L"container logs --since 10m cont1", L"logs", true) +COMMAND_LINE_TEST_CASE(L"container logs --since 1h30m cont1", L"logs", true) COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00Zextra cont1", L"logs", false) -COMMAND_LINE_TEST_CASE(L"container logs --since 1960-01-15T10:30:00Z cont1", L"logs", false) +COMMAND_LINE_TEST_CASE(L"container logs --since 1960-01-15T10:30:00Z cont1", L"logs", true) COMMAND_LINE_TEST_CASE(L"container logs --since 2024-02-31T10:30:00Z cont1", L"logs", false) COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00.Z cont1", L"logs", false) COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00+0530 cont1", L"logs", false) diff --git a/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp b/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp index 805ead31e..97b5e4599 100644 --- a/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIArgumentUnitTests.cpp @@ -22,6 +22,7 @@ Module Name: #include "ImageService.h" #include "JsonUtils.h" #include "Exceptions.h" +#include #include using namespace wsl::windows::wslc; @@ -732,36 +733,114 @@ class WSLCCLIArgumentUnitTests TEST_METHOD(ValidateTimestamp_ValidUnixEpochSeconds) { // Integer timestamps should parse directly - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"0"), 0ULL); - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1700000000"), 1700000000ULL); - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1"), 1ULL); - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"9999999999"), 9999999999ULL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"0"), 0LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1700000000"), 1700000000LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1"), 1LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"9999999999"), 9999999999LL); } TEST_METHOD(ValidateTimestamp_ValidRfc3339_UTC) { // Basic UTC timestamps with Z suffix - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00Z"), 1705314600ULL); - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1970-01-01T00:00:00Z"), 0ULL); - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00z"), 1705314600ULL); // lowercase z + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00Z"), 1705314600LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1970-01-01T00:00:00Z"), 0LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00z"), 1705314600LL); // lowercase z } TEST_METHOD(ValidateTimestamp_ValidRfc3339_WithOffset) { // Timestamps with timezone offsets (+HH:MM / -HH:MM) // 2024-01-15T10:30:00+05:30 = 2024-01-15T05:00:00Z = 1705294800 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+05:30"), 1705294800ULL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+05:30"), 1705294800LL); // 2024-01-15T10:30:00-05:00 = 2024-01-15T15:30:00Z = 1705332600 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00-05:00"), 1705332600ULL); - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+00:00"), 1705314600ULL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00-05:00"), 1705332600LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+00:00"), 1705314600LL); } TEST_METHOD(ValidateTimestamp_ValidRfc3339_FractionalSeconds) { // Fractional seconds should be consumed (truncated to seconds) - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123Z"), 1705314600ULL); - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123456789Z"), 1705314600ULL); - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.1+05:30"), 1705294800ULL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123Z"), 1705314600LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123456789Z"), 1705314600LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.1+05:30"), 1705294800LL); + } + + 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"), -314371800LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"0001-01-01T00:00:00Z"), -62135596800LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"-314371800"), -314371800LL); + } + + TEST_METHOD(ValidateTimestamp_OutsideNanosecondRange) + { + // Values beyond the range of a nanosecond representation still convert exactly. + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1600-01-01T00:00:00Z"), -11676096000LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2300-01-01T00:00:00Z"), 10413792000LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"9999-12-31T23:59:59Z"), 253402300799LL); + } + + TEST_METHOD(ValidateTimestamp_ValidZoneLessLocalTime) + { + // A value with no zone designator is resolved against the local UTC offset. + const auto offset = std::chrono::duration_cast( + std::chrono::current_zone()->get_info(std::chrono::system_clock::now()).offset) + .count(); + const auto expected = 1705314600LL - offset; + + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00"), expected); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123"), expected); + } + + TEST_METHOD(ValidateTimestamp_ValidPartialAndDateOnly) + { + // Hour-only, minute-only and date-only values are padded out to a full time. + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15"), validation::GetTimestampFromString(L"2024-01-15T00:00:00")); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10"), validation::GetTimestampFromString(L"2024-01-15T10:00:00")); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30"), validation::GetTimestampFromString(L"2024-01-15T10:30:00")); + + // The same padding applies when an explicit zone is present. + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10Z"), 1705312800LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30Z"), 1705314600LL); + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15Z"), 1705276800LL); + } + + TEST_METHOD(ValidateTimestamp_ValidGoDuration) + { + const auto now = std::chrono::floor(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); + }; + + verifyDuration(L"10m", 600LL); + verifyDuration(L"1h30m", 5400LL); + verifyDuration(L"90s", 90LL); + verifyDuration(L"1.5h", 5400LL); + verifyDuration(L"2h45m30s", 9930LL); + + // A negative duration selects a time in the future. + verifyDuration(L"-1h", -3600LL); + } + + TEST_METHOD(ValidateTimestamp_SubSecondGoDuration) + { + // A sub-second duration is applied before the value is truncated, so a negative one still + // resolves at or after the current second rather than being rounded away. + const auto now = std::chrono::floor(std::chrono::system_clock::now().time_since_epoch()).count(); + + const auto future = validation::GetTimestampFromString(L"-500ms"); + VERIFY_IS_GREATER_THAN_OR_EQUAL(future, now); + VERIFY_IS_LESS_THAN_OR_EQUAL(future, now + 30); + + const auto past = validation::GetTimestampFromString(L"500ms"); + VERIFY_IS_GREATER_THAN_OR_EQUAL(past, now - 1); + VERIFY_IS_LESS_THAN_OR_EQUAL(past, now + 30); } TEST_METHOD(ValidateTimestamp_InvalidRfc3339_Rejected) @@ -772,10 +851,6 @@ class WSLCCLIArgumentUnitTests VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T25:30:00Z"), ArgumentException); // Invalid day (Feb 31) VERIFY_THROWS(validation::GetTimestampFromString(L"2024-02-31T10:30:00Z"), ArgumentException); - // Missing timezone - VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T10:30:00"), ArgumentException); - // Date only (no time) - VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15"), ArgumentException); // Trailing characters VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T10:30:00Zextra"), ArgumentException); // +HHMM without colon (not supported by %Ez) @@ -785,8 +860,9 @@ class WSLCCLIArgumentUnitTests // Random text VERIFY_THROWS(validation::GetTimestampFromString(L"abc"), ArgumentException); VERIFY_THROWS(validation::GetTimestampFromString(L"not-a-timestamp"), ArgumentException); - // Negative epoch (pre-1970) - VERIFY_THROWS(validation::GetTimestampFromString(L"1960-01-15T10:30:00Z"), ArgumentException); + // Duration with no unit + VERIFY_THROWS(validation::GetTimestampFromString(L"10x"), ArgumentException); + VERIFY_THROWS(validation::GetTimestampFromString(L"1h30"), ArgumentException); } }; } // namespace WSLCCLIArgumentUnitTests diff --git a/test/windows/wslc/WSLCCLIRelativeTimeUnitTests.cpp b/test/windows/wslc/WSLCCLIRelativeTimeUnitTests.cpp index 4a1e7fbeb..fbf014e67 100644 --- a/test/windows/wslc/WSLCCLIRelativeTimeUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIRelativeTimeUnitTests.cpp @@ -4,11 +4,10 @@ #include "windows/Common.h" #include "WSLCCLITestHelpers.h" -#include "ContainerService.h" +#include "timestamp.hpp" using namespace wsl::shared; -using namespace wsl::windows::wslc; -using namespace wsl::windows::wslc::services; +using namespace wsl::windows::common::timestamp; using namespace WSLCTestHelpers; using namespace WEX::Logging; using namespace WEX::Common; @@ -32,17 +31,17 @@ class WSLCCLIRelativeTimeUnitTests static std::wstring FormatElapsed(LONGLONG secondsAgo) { - return ContainerService::FormatElapsedSeconds(secondsAgo); + return FormatElapsedSeconds(secondsAgo); } TEST_METHOD(RelativeTime_ZeroTimestamp_ReturnsEmpty) { - VERIFY_ARE_EQUAL(std::wstring{}, ContainerService::FormatRelativeTime(0)); + VERIFY_ARE_EQUAL(std::wstring{}, FormatRelativeTime(0)); } TEST_METHOD(RelativeTime_NegativeElapsed_ClampsToLessThanASecond) { - VERIFY_ARE_EQUAL(Localization::WSLCCLI_RelativeTimeLessThanASecond(), ContainerService::FormatElapsedSeconds(-600)); + VERIFY_ARE_EQUAL(Localization::WSLCCLI_RelativeTimeLessThanASecond(), FormatElapsedSeconds(-600)); } TEST_METHOD(RelativeTime_Seconds)