Consolidate timestamp parsing and support docker-style options (#41403)

Consolidate timestamp/duration parsing into common string helpers and accept docker-style --since/--until values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

ggarzia-MSFT committed Aug 24, 2026 at 09:24 UTC b5fc646b3e681b29d1de5c6562d1da1ab9e04e1f
32 files changed +640 -434
src/windows/common/CMakeLists.txt
+2
@@ -37,6 +37,7 @@ set(SOURCES
37 string.cpp
38 SubProcess.cpp
39 svccomm.cpp
40 + timestamp.cpp
41 VTSupport.cpp
42 WindowsUpdateIntegration.cpp
43 WSLCContainerLauncher.cpp
@@ -125,6 +126,7 @@ set(HEADERS
126 Stringify.h
127 SubProcess.h
128 svccomm.hpp
129 + timestamp.hpp
130 VTSupport.h
131 WindowsUpdateIntegration.h
132 WSLCContainerLauncher.h
src/windows/common/precomp.h
+1
@@ -147,6 +147,7 @@ Abstract:
147 #include "conncheckshared.h"
148 #include "helpers.hpp"
149 #include "string.hpp"
150 +#include "timestamp.hpp"
151 #include "filesystem.hpp"
152 #include "Localization.h"
153 #include "wslutil.h"
src/windows/common/string.cpp
-63
@@ -425,66 +425,3 @@ std::string wsl::windows::common::string::TruncateId(_In_ std::string_view id, b
425 {
426 return TruncateIdImpl(id, shortenLength);
427 }
428 -
429 -std::uint64_t wsl::windows::common::string::Rfc3339ToEpoch(const std::string& timestamp)
430 -{
431 - std::chrono::sys_seconds utcSeconds;
432 - std::istringstream stream(timestamp);
433 - stream >> std::chrono::parse("%FT%H:%M:%S%Z", utcSeconds);
434 - THROW_HR_IF_MSG(E_INVALIDARG, stream.fail(), "Failed to parse timestamp '%hs'", timestamp.c_str());
435 -
436 - return static_cast<std::uint64_t>(utcSeconds.time_since_epoch().count());
437 -}
438 -
439 -std::string wsl::windows::common::string::EpochToLocalDisplayTime(LONGLONG timestamp)
440 -{
441 - const auto time =
442 - std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::from_time_t(static_cast<std::time_t>(timestamp)));
443 -
444 - try
445 - {
446 - const auto* zone = std::chrono::current_zone();
447 - return std::format("{:%F %T %z} {}", std::chrono::zoned_time{zone, time}, zone->get_info(time).abbrev);
448 - }
449 - catch (...)
450 - {
451 - // The time zone database is unavailable, so report UTC rather than failing the caller.
452 - LOG_CAUGHT_EXCEPTION();
453 - return std::format("{:%F %T} +0000 UTC", time);
454 - }
455 -}
456 -
457 -std::string wsl::windows::common::string::Rfc3339ToUtcDisplayTime(std::string_view timestamp)
458 -{
459 - if (timestamp.empty())
460 - {
461 - return {};
462 - }
463 -
464 - // Fractional digits vary in length, so they are captured verbatim and re-inserted after formatting.
465 - std::string parsable{timestamp};
466 - std::string fraction;
467 - const auto separator = parsable.find('.');
468 - if (separator != std::string::npos)
469 - {
470 - auto end = separator + 1;
471 - while (end < parsable.size() && (std::isdigit(static_cast<unsigned char>(parsable[end])) != 0))
472 - {
473 - end++;
474 - }
475 -
476 - fraction = parsable.substr(separator, end - separator);
477 - parsable.erase(separator, end - separator);
478 - }
479 -
480 - std::chrono::sys_seconds parsed{};
481 - std::istringstream stream(parsable);
482 - stream >> std::chrono::parse("%FT%H:%M:%S%Z", parsed);
483 - if (stream.fail())
484 - {
485 - return std::string{timestamp};
486 - }
487 -
488 - // Network timestamps are reported in UTC rather than the local time zone.
489 - return std::format("{:%F %T}{} +0000 UTC", parsed, fraction);
490 -}
src/windows/common/string.hpp
-12
@@ -67,18 +67,6 @@ std::string WideToMultiByte(_In_ std::wstring_view Source);
67 std::wstring TruncateId(_In_ std::wstring_view id, bool shortenLength = true);
68 std::string TruncateId(_In_ std::string_view id, bool shortenLength = true);
69
70 -// Converts an RFC 3339 timestamp to seconds since the unix epoch. Only the 'Z' zone designator is
71 -// accepted; numeric offsets are not.
72 -std::uint64_t Rfc3339ToEpoch(const std::string& timestamp);
73 -
74 -// Renders seconds since the unix epoch in the local time zone, using the layout
75 -// "2006-01-02 15:04:05 -0700 MST". Falls back to UTC when the time zone database is unavailable.
76 -std::string EpochToLocalDisplayTime(LONGLONG timestamp);
77 -
78 -// Renders an RFC 3339 timestamp in the same layout, but as UTC and with its fractional seconds
79 -// preserved. The input is returned unchanged when it cannot be parsed.
80 -std::string Rfc3339ToUtcDisplayTime(std::string_view timestamp);
81 -
70 // Template implementation for TruncateId to avoid code duplication.
71 // Algorithm inspired from Moby for consistency in presentation of shortened IDs.
72 // Always strips the algorithm prefix (e.g., "sha256:") if present, and optionally shortens to 12 characters.
src/windows/common/timestamp.cpp new
+383
@@ -0,0 +1,383 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + timestamp.cpp
8 +
9 +Abstract:
10 +
11 + This file contains timestamp and duration helper function definitions.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include <algorithm>
17 +#include <cctype>
18 +#include <charconv>
19 +#include <chrono>
20 +#include <cmath>
21 +#include <limits>
22 +#include <optional>
23 +#include <sstream>
24 +
25 +static std::string LocalUtcOffset()
26 +{
27 + try
28 + {
29 + const auto* zone = std::chrono::current_zone();
30 + const auto offset = zone->get_info(std::chrono::system_clock::now()).offset;
31 + const auto minutes = std::chrono::duration_cast<std::chrono::minutes>(offset).count();
32 + const auto magnitude = std::abs(minutes);
33 +
34 + return std::format("{}{:02}:{:02}", minutes < 0 ? '-' : '+', magnitude / 60, magnitude % 60);
35 + }
36 + catch (...)
37 + {
38 + // The time zone database is unavailable, so fall back to UTC rather than failing the caller.
39 + LOG_CAUGHT_EXCEPTION();
40 + return "+00:00";
41 + }
42 +}
43 +
44 +std::string wsl::windows::common::timestamp::ExpandToRfc3339(const std::string& timestamp)
45 +{
46 + std::string_view value{timestamp};
47 + std::string_view zone;
48 +
49 + if (!value.empty() && (value.back() == 'Z' || value.back() == 'z'))
50 + {
51 + zone = value.substr(value.size() - 1);
52 + value.remove_suffix(1);
53 + }
54 + else if (const auto plus = value.find('+'); plus != std::string_view::npos)
55 + {
56 + zone = value.substr(plus);
57 + value = value.substr(0, plus);
58 + }
59 + else if (std::ranges::count(value, '-') == 3)
60 + {
61 + // The first two dashes belong to the date, so a third one opens a negative zone offset.
62 + auto offset = value.find('-');
63 + offset = value.find('-', offset + 1);
64 + offset = value.find('-', offset + 1);
65 +
66 + zone = value.substr(offset);
67 + value = value.substr(0, offset);
68 + }
69 +
70 + const auto separator = value.find('T');
71 + const auto time = separator == std::string_view::npos ? std::string_view{} : value.substr(separator + 1);
72 +
73 + std::string expanded{value.substr(0, separator)};
74 + expanded += 'T';
75 +
76 + if (time.empty())
77 + {
78 + expanded += "00:00:00";
79 + }
80 + else
81 + {
82 + expanded += time;
83 +
84 + // Pad an hour-only or minute-only time out to a full HH:MM:SS.
85 + const auto colons = std::ranges::count(time, ':');
86 + if (colons == 0)
87 + {
88 + expanded += ":00:00";
89 + }
90 + else if (colons == 1)
91 + {
92 + expanded += ":00";
93 + }
94 + }
95 +
96 + expanded += zone.empty() ? LocalUtcOffset() : std::string{zone};
97 +
98 + return expanded;
99 +}
100 +
101 +std::int64_t wsl::windows::common::timestamp::Rfc3339ToEpoch(const std::string& timestamp)
102 +{
103 + // Normalize a trailing 'Z' or 'z' to '+00:00' so that %Ez parses every zone uniformly.
104 + std::string normalized{timestamp};
105 + if (!normalized.empty() && (normalized.back() == 'Z' || normalized.back() == 'z'))
106 + {
107 + normalized.pop_back();
108 + normalized += "+00:00";
109 + }
110 +
111 + // Strip any fractional seconds. The value is truncated to whole seconds regardless, and parsing at
112 + // second precision keeps timestamps outside the nanosecond range from silently wrapping.
113 + const auto separator = normalized.find('.');
114 + if (separator != std::string::npos)
115 + {
116 + auto end = separator + 1;
117 + while (end < normalized.size() && std::isdigit(static_cast<unsigned char>(normalized[end])) != 0)
118 + {
119 + ++end;
120 + }
121 +
122 + // A separator with no fractional digits is invalid, but std::chrono::parse otherwise accepts it.
123 + THROW_HR_IF_MSG(E_INVALIDARG, end == separator + 1, "Failed to parse timestamp '%hs'", timestamp.c_str());
124 +
125 + normalized.erase(separator, end - separator);
126 + }
127 +
128 + // Validate the day up front since std::chrono::parse silently wraps invalid dates (e.g. Feb 31 -> Mar 2).
129 + if (normalized.size() >= 10 && normalized[4] == '-' && normalized[7] == '-')
130 + {
131 + int year{};
132 + int month{};
133 + int day{};
134 + const auto yearResult = std::from_chars(normalized.data(), normalized.data() + 4, year);
135 + const auto monthResult = std::from_chars(normalized.data() + 5, normalized.data() + 7, month);
136 + const auto dayResult = std::from_chars(normalized.data() + 8, normalized.data() + 10, day);
137 + if (yearResult.ec == std::errc() && monthResult.ec == std::errc() && dayResult.ec == std::errc())
138 + {
139 + const auto date = std::chrono::year{year} / std::chrono::month{static_cast<unsigned>(month)} /
140 + std::chrono::day{static_cast<unsigned>(day)};
141 +
142 + THROW_HR_IF_MSG(E_INVALIDARG, !date.ok(), "Failed to parse timestamp '%hs'", timestamp.c_str());
143 + }
144 + }
145 +
146 + std::chrono::sys_seconds parsed{};
147 + std::istringstream stream(normalized);
148 + stream >> std::chrono::parse("%FT%T%Ez", parsed);
149 + THROW_HR_IF_MSG(E_INVALIDARG, stream.fail(), "Failed to parse timestamp '%hs'", timestamp.c_str());
150 + THROW_HR_IF_MSG(
151 + E_INVALIDARG,
152 + stream.peek() != std::istringstream::traits_type::eof(),
153 + "Unexpected trailing characters in timestamp '%hs'",
154 + timestamp.c_str());
155 +
156 + return parsed.time_since_epoch().count();
157 +}
158 +
159 +std::optional<std::chrono::nanoseconds> wsl::windows::common::timestamp::TryParseDuration(const std::string& duration)
160 +{
161 + if (duration.empty())
162 + {
163 + return std::nullopt;
164 + }
165 +
166 + size_t pos = 0;
167 + bool negative = false;
168 + if (duration[pos] == '+' || duration[pos] == '-')
169 + {
170 + negative = duration[pos] == '-';
171 + pos++;
172 + }
173 +
174 + // Special case: a bare "0" (with optional sign) is a valid zero duration.
175 + if (duration.substr(pos) == "0")
176 + {
177 + return std::chrono::nanoseconds{0};
178 + }
179 +
180 + // Accumulate in a long double so fractional units (e.g. "1.5h") are handled, then round.
181 + long double totalNanos = 0.0L;
182 + bool sawValue = false;
183 +
184 + while (pos < duration.size())
185 + {
186 + // Parse the numeric part (integer and/or fraction).
187 + const size_t numberStart = pos;
188 + while (pos < duration.size() && (std::isdigit(static_cast<unsigned char>(duration[pos])) || duration[pos] == '.'))
189 + {
190 + pos++;
191 + }
192 +
193 + const std::string numberStr = duration.substr(numberStart, pos - numberStart);
194 + if (numberStr.empty() || numberStr == "." || std::count(numberStr.begin(), numberStr.end(), '.') > 1)
195 + {
196 + return std::nullopt;
197 + }
198 +
199 + // Parse the unit (everything up to the next digit or '.').
200 + const size_t unitStart = pos;
201 + while (pos < duration.size() && !std::isdigit(static_cast<unsigned char>(duration[pos])) && duration[pos] != '.')
202 + {
203 + pos++;
204 + }
205 +
206 + const std::string unit = duration.substr(unitStart, pos - unitStart);
207 +
208 + long double multiplier{};
209 + if (unit == "ns")
210 + {
211 + multiplier = 1.0L;
212 + }
213 + else if (unit == "us" || unit == "\xC2\xB5s" /* µs (U+00B5) */ || unit == "\xCE\xBCs" /* μs (U+03BC) */)
214 + {
215 + multiplier = 1000L;
216 + }
217 + else if (unit == "ms")
218 + {
219 + multiplier = 1000000L;
220 + }
221 + else if (unit == "s")
222 + {
223 + multiplier = 1000000000L;
224 + }
225 + else if (unit == "m")
226 + {
227 + multiplier = 60000000000L;
228 + }
229 + else if (unit == "h")
230 + {
231 + multiplier = 3600000000000L;
232 + }
233 + else
234 + {
235 + return std::nullopt;
236 + }
237 +
238 + long double value{};
239 + auto [ptr, ec] = std::from_chars(numberStr.data(), numberStr.data() + numberStr.size(), value, std::chars_format::fixed);
240 + if (ptr != numberStr.data() + numberStr.size() || ec != std::errc())
241 + {
242 + return std::nullopt;
243 + }
244 +
245 + totalNanos += value * multiplier;
246 + sawValue = true;
247 + }
248 +
249 + if (!sawValue)
250 + {
251 + return std::nullopt;
252 + }
253 +
254 + if (negative)
255 + {
256 + totalNanos = -totalNanos;
257 + }
258 +
259 + if (totalNanos > static_cast<long double>(std::numeric_limits<int64_t>::max()) ||
260 + totalNanos < static_cast<long double>(std::numeric_limits<int64_t>::min()))
261 + {
262 + return std::nullopt;
263 + }
264 +
265 + return std::chrono::nanoseconds{static_cast<int64_t>(std::llroundl(totalNanos))};
266 +}
267 +
268 +std::string wsl::windows::common::timestamp::EpochToLocalDisplayTime(LONGLONG timestamp)
269 +{
270 + const auto time =
271 + std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::from_time_t(static_cast<std::time_t>(timestamp)));
272 +
273 + try
274 + {
275 + const auto* zone = std::chrono::current_zone();
276 + return std::format("{:%F %T %z} {}", std::chrono::zoned_time{zone, time}, zone->get_info(time).abbrev);
277 + }
278 + catch (...)
279 + {
280 + // The time zone database is unavailable, so report UTC rather than failing the caller.
281 + LOG_CAUGHT_EXCEPTION();
282 + return std::format("{:%F %T} +0000 UTC", time);
283 + }
284 +}
285 +
286 +std::string wsl::windows::common::timestamp::Rfc3339ToUtcDisplayTime(std::string_view timestamp)
287 +{
288 + if (timestamp.empty())
289 + {
290 + return {};
291 + }
292 +
293 + // Fractional digits are dropped by the parse and vary in length, so they are captured verbatim
294 + // and re-inserted after formatting.
295 + std::string fraction;
296 + const auto separator = timestamp.find('.');
297 + if (separator != std::string_view::npos)
298 + {
299 + auto end = separator + 1;
300 + while (end < timestamp.size() && (std::isdigit(static_cast<unsigned char>(timestamp[end])) != 0))
301 + {
302 + end++;
303 + }
304 +
305 + fraction = timestamp.substr(separator, end - separator);
306 + }
307 +
308 + const std::chrono::sys_seconds parsed{std::chrono::seconds{Rfc3339ToEpoch(std::string{timestamp})}};
309 +
310 + // Network timestamps are reported in UTC rather than the local time zone.
311 + return std::format("{:%F %T}{} +0000 UTC", parsed, fraction);
312 +}
313 +
314 +std::wstring wsl::windows::common::timestamp::FormatElapsedSeconds(LONGLONG elapsedSeconds)
315 +{
316 + using namespace std::chrono_literals;
317 + using wsl::shared::Localization;
318 +
319 + constexpr LONGLONG SecondsPerMinute = std::chrono::duration_cast<std::chrono::seconds>(1min).count();
320 + constexpr LONGLONG SecondsPerHour = std::chrono::duration_cast<std::chrono::seconds>(1h).count();
321 + constexpr LONGLONG HoursPerDay = 24;
322 + constexpr LONGLONG MinutesPerHour = 60;
323 +
324 + const auto elapsed = std::max<LONGLONG>(elapsedSeconds, 0);
325 +
326 + if (elapsed < 1)
327 + {
328 + return Localization::WSLCCLI_RelativeTimeLessThanASecond();
329 + }
330 + else if (elapsed == 1)
331 + {
332 + return Localization::WSLCCLI_RelativeTimeOneSecond();
333 + }
334 + else if (elapsed < SecondsPerMinute)
335 + {
336 + return Localization::WSLCCLI_RelativeTimeSeconds(elapsed);
337 + }
338 +
339 + const auto minutes = elapsed / SecondsPerMinute;
340 + if (minutes == 1)
341 + {
342 + return Localization::WSLCCLI_RelativeTimeAboutAMinute();
343 + }
344 + else if (minutes < MinutesPerHour)
345 + {
346 + return Localization::WSLCCLI_RelativeTimeMinutes(minutes);
347 + }
348 +
349 + // Rounded to the nearest hour rather than truncated.
350 + const auto hours = (elapsed + (SecondsPerHour / 2)) / SecondsPerHour;
351 + if (hours == 1)
352 + {
353 + return Localization::WSLCCLI_RelativeTimeAboutAnHour();
354 + }
355 + else if (hours < HoursPerDay * 2)
356 + {
357 + return Localization::WSLCCLI_RelativeTimeHours(hours);
358 + }
359 + else if (hours < HoursPerDay * 7 * 2)
360 + {
361 + return Localization::WSLCCLI_RelativeTimeDays(hours / HoursPerDay);
362 + }
363 + else if (hours < HoursPerDay * 30 * 2)
364 + {
365 + return Localization::WSLCCLI_RelativeTimeWeeks(hours / HoursPerDay / 7);
366 + }
367 + else if (hours < HoursPerDay * 365 * 2)
368 + {
369 + return Localization::WSLCCLI_RelativeTimeMonths(hours / HoursPerDay / 30);
370 + }
371 +
372 + return Localization::WSLCCLI_RelativeTimeYears(elapsed / SecondsPerHour / HoursPerDay / 365);
373 +}
374 +
375 +std::wstring wsl::windows::common::timestamp::FormatRelativeTime(LONGLONG timestamp)
376 +{
377 + if (timestamp == 0)
378 + {
379 + return {};
380 + }
381 +
382 + return FormatElapsedSeconds(static_cast<LONGLONG>(std::time(nullptr)) - timestamp);
383 +}
src/windows/common/timestamp.hpp new
+56
@@ -0,0 +1,56 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + timestamp.hpp
8 +
9 +Abstract:
10 +
11 + This file contains timestamp and duration helper function declarations.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include <chrono>
18 +#include <optional>
19 +#include <string>
20 +
21 +namespace wsl::windows::common::timestamp {
22 +
23 +// Expands a partial timestamp into a full RFC 3339 one. Hour-only, minute-only and date-only values
24 +// are padded out to a complete time, and a value with no zone designator is resolved against the
25 +// offset currently in effect locally. The input is not validated, so an unrecognized value is
26 +// expanded as-is and left for the parser to reject.
27 +std::string ExpandToRfc3339(const std::string& timestamp);
28 +
29 +// Converts an RFC 3339 timestamp to seconds since the unix epoch. Accepts a 'Z' designator or a
30 +// numeric +HH:MM offset, with optional fractional seconds. Timestamps that predate the epoch convert
31 +// to a negative value. Throws E_INVALIDARG if the timestamp is malformed, names an invalid date, or
32 +// has trailing characters.
33 +std::int64_t Rfc3339ToEpoch(const std::string& timestamp);
34 +
35 +// Parses a Go duration such as "1h30m", "-1.5h" or "300ms": an optional sign followed by one or more
36 +// decimal values that each carry a unit of ns, us, ms, s, m or h. Returns nothing if the value does
37 +// not match that grammar or overflows.
38 +std::optional<std::chrono::nanoseconds> TryParseDuration(const std::string& duration);
39 +
40 +// Renders seconds since the unix epoch in the local time zone, using the layout
41 +// "2006-01-02 15:04:05 -0700 MST". Falls back to UTC when the time zone database is unavailable.
42 +std::string EpochToLocalDisplayTime(LONGLONG timestamp);
43 +
44 +// Renders an RFC 3339 timestamp in the same layout, but as UTC and with its fractional seconds
45 +// preserved. An empty input returns an empty string; anything else that cannot be parsed throws.
46 +std::string Rfc3339ToUtcDisplayTime(std::string_view timestamp);
47 +
48 +// Renders an elapsed number of seconds as a coarse, localized description such as "About a minute"
49 +// or "3 weeks". Negative values are treated as zero.
50 +std::wstring FormatElapsedSeconds(LONGLONG elapsedSeconds);
51 +
52 +// Renders how long ago a timestamp given in seconds since the unix epoch occurred. A timestamp of
53 +// zero means "unset" and returns an empty string.
54 +std::wstring FormatRelativeTime(LONGLONG timestamp);
55 +
56 +} // namespace wsl::windows::common::timestamp
src/windows/inc/docker_schema.h
+4
@@ -22,6 +22,10 @@ namespace wsl::windows::common::docker_schema {
22
23 using wsl::shared::EmptyObject;
24
25 +// The daemon formats timestamps that were never set as the zero value of Go's time.Time rather than
26 +// omitting them, so this value means "unset" instead of an actual point in time.
27 +inline constexpr std::string_view c_unsetTimestamp = "0001-01-01T00:00:00Z";
28 +
29 // Reads a value, treating both a missing key and an explicit null as absent. The daemon reports some
30 // empty maps and objects as null, which the default deserializer rejects.
31 template <typename T>
src/windows/service/exe/ServiceMain.cpp
+4 -6
@@ -23,6 +23,7 @@ Abstract:
23
24 using namespace wsl::windows::common::registry;
25 using namespace wsl::windows::common::string;
26 +using namespace wsl::windows::common::timestamp;
27 using namespace wsl::windows::common::wslutil;
28 using namespace wsl::windows::policies;
29
@@ -306,13 +307,10 @@ try
307 SetThreadpoolTimer(static_cast<WslService*>(Context)->m_updateCheckTimer.get(), nullptr, 0, 0);
308
309 // Get current release date
309 - std::wstring currentReleaseCreatedAtDate = GetGitHubReleaseByTag(TEXT(WSL_PACKAGE_VERSION)).created_at;
310 + const std::wstring currentReleaseCreatedAtDate = GetGitHubReleaseByTag(TEXT(WSL_PACKAGE_VERSION)).created_at;
311
311 - std::tm tm = {};
312 - std::wstring dateTimeFormat = L"%Y-%m-%dT%H:%M:%SZ";
313 - std::wistringstream ss(currentReleaseCreatedAtDate);
314 - ss >> std::get_time(&tm, dateTimeFormat.c_str());
315 - auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm));
312 + const auto tp = std::chrono::system_clock::from_time_t(
313 + static_cast<std::time_t>(Rfc3339ToEpoch(WideToMultiByte(currentReleaseCreatedAtDate))));
314
315 // If their release of WSL is older than 30 days, then show a notification to update
316 if (std::chrono::system_clock::now() - std::chrono::days(30) > tp)
src/windows/service/inc/wslc.idl
+3 -3
@@ -374,8 +374,8 @@ typedef struct _WSLCContainerEntry
374 char Name[WSLC_MAX_CONTAINER_NAME_LENGTH + 1];
375 char Image[WSLC_MAX_IMAGE_NAME_LENGTH + 1];
376 WSLCContainerId Id;
377 - ULONGLONG StateChangedAt;
378 - ULONGLONG CreatedAt;
377 + LONGLONG StateChangedAt;
378 + LONGLONG CreatedAt;
379 WSLCContainerState State;
380 } WSLCContainerEntry;
381
@@ -565,7 +565,7 @@ interface IWSLCContainer : IUnknown
565 HRESULT GetInitProcess([out] IWSLCProcess** Process);
566 HRESULT Exec([in, ref] const WSLCProcessOptions* Options, [in, unique] const WSLCProcessStartOptions* StartOptions, [out] IWSLCProcess** Process);
567 HRESULT Inspect([out] LPSTR* Output);
568 - HRESULT Logs([in] WSLCLogsFlags Flags, [out] WSLCHandle* Stdout, [out] WSLCHandle* Stderr, [in] ULONGLONG Since, [in] ULONGLONG Until, [in] ULONGLONG Tail);
568 + HRESULT Logs([in] WSLCLogsFlags Flags, [out] WSLCHandle* Stdout, [out] WSLCHandle* Stderr, [in] LONGLONG Since, [in] LONGLONG Until, [in] ULONGLONG Tail);
569 HRESULT GetId([out, string] WSLCContainerId Id);
570 HRESULT GetName([out, string] LPSTR* Name);
571 HRESULT GetLabels([out, size_is(, *Count)] WSLCLabelInformation** Labels, [out] ULONG* Count);
src/windows/wslc/arguments/ArgumentDefinitions.h
+2 -2
@@ -63,8 +63,8 @@ _(File, "file", L"f", Kind::Value,
63 _(Filter, "filter", L"f", Kind::Value, KeyValuePair, Localization::WSLCCLI_FilterArgDescription()) \
64 _(Follow, "follow", L"f", Kind::Flag, NoConversion, Localization::WSLCCLI_FollowArgDescription()) \
65 _(Timestamps, "timestamps", L"t", Kind::Flag, NoConversion, Localization::WSLCCLI_TimestampsArgDescription()) \
66 -_(Since, "since", NO_ALIAS, Kind::Value, ULONGLONG, Localization::WSLCCLI_SinceArgDescription()) \
67 -_(Until, "until", NO_ALIAS, Kind::Value, ULONGLONG, Localization::WSLCCLI_UntilArgDescription()) \
66 +_(Since, "since", NO_ALIAS, Kind::Value, LONGLONG, Localization::WSLCCLI_SinceArgDescription()) \
67 +_(Until, "until", NO_ALIAS, Kind::Value, LONGLONG, Localization::WSLCCLI_UntilArgDescription()) \
68 _(Format, "format", NO_ALIAS, Kind::Value, FormatType, Localization::WSLCCLI_FormatArgDescription()) \
69 _(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, NoConversion, Localization::WSLCCLI_ForwardArgsDescription()) \
70 _(Gateway, "gateway", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_NetworkGatewayArgDescription()) \
src/windows/wslc/arguments/SpecParsing.cpp
+19 -194
@@ -29,7 +29,6 @@ Abstract:
29 #include <format>
30 #include <limits>
31 #include <optional>
32 -#include <sstream>
32 #include <unordered_map>
33 #include <wslc.h>
34
@@ -594,78 +593,12 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring
593 return static_cast<WSLCSignal>(signalValue);
594 }
595
597 -// Parses an RFC3339 timestamp (e.g. "2024-01-15T10:30:00Z" or "2024-01-15T10:30:00+05:30")
598 -// into a ULONGLONG Unix epoch seconds value using std::chrono::parse.
599 -// Note: +HHMM (no colon) offsets are not supported; use +HH:MM format.
600 -static std::optional<ULONGLONG> TryParseRfc3339(const std::string& input)
601 -{
602 - std::string normalized = input;
603 -
604 - // Normalize trailing 'Z'/'z' to '+00:00' so %Ez can parse it uniformly.
605 - if (!normalized.empty() && (normalized.back() == 'Z' || normalized.back() == 'z'))
606 - {
607 - normalized.pop_back();
608 - normalized += "+00:00";
609 - }
610 -
611 - // Reject bare dot with no fractional digits (e.g. "10:30:00.+00:00") since
612 - // std::chrono::parse is lenient about this.
613 - auto dotPos = normalized.find('.');
614 - if (dotPos != std::string::npos && (dotPos + 1 >= normalized.size() || !std::isdigit(normalized[dotPos + 1])))
615 - {
616 - return std::nullopt;
617 - }
618 -
619 - // Pre-validate day-of-month since std::chrono::parse silently wraps invalid dates (e.g. Feb 31 → Mar 2).
620 - if (normalized.size() >= 10 && normalized[4] == '-' && normalized[7] == '-')
621 - {
622 - int year = 0, month = 0, day = 0;
623 - auto yResult = std::from_chars(normalized.data(), normalized.data() + 4, year);
624 - auto mResult = std::from_chars(normalized.data() + 5, normalized.data() + 7, month);
625 - auto dResult = std::from_chars(normalized.data() + 8, normalized.data() + 10, day);
626 -
627 - if (yResult.ec == std::errc() && mResult.ec == std::errc() && dResult.ec == std::errc())
628 - {
629 - auto ymd = std::chrono::year{year} / std::chrono::month{static_cast<unsigned>(month)} /
630 - std::chrono::day{static_cast<unsigned>(day)};
631 - if (!ymd.ok())
632 - {
633 - return std::nullopt;
634 - }
635 - }
636 - }
637 -
638 - // Parse into nanosecond precision so fractional seconds (e.g. ".123456789") are consumed
639 - // by std::chrono::parse rather than requiring manual stripping.
640 - std::chrono::sys_time<std::chrono::nanoseconds> utcTime;
641 - std::istringstream stream(normalized);
642 - stream >> std::chrono::parse("%FT%T%Ez", utcTime);
643 - if (stream.fail())
644 - {
645 - return std::nullopt;
646 - }
647 -
648 - // Reject if there are trailing characters after the parsed timestamp
649 - if (stream.peek() != std::istringstream::traits_type::eof())
650 - {
651 - return std::nullopt;
652 - }
653 -
654 - auto epochSeconds = std::chrono::duration_cast<std::chrono::seconds>(utcTime.time_since_epoch()).count();
655 - if (epochSeconds < 0)
656 - {
657 - return std::nullopt;
658 - }
659 -
660 - return static_cast<ULONGLONG>(epochSeconds);
661 -}
662 -
663 -ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName)
596 +LONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName)
597 {
598 std::string narrowValue = wsl::windows::common::string::WideToMultiByte(value);
599
600 // Try integer (Unix epoch seconds) first
668 - ULONGLONG intValue{};
601 + LONGLONG intValue{};
602 const char* begin = narrowValue.c_str();
603 const char* end = begin + narrowValue.size();
604 auto result = std::from_chars(begin, end, intValue);
@@ -674,14 +607,23 @@ ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring&
607 return intValue;
608 }
609
677 - // Try RFC3339 timestamp
678 - auto rfc3339Value = TryParseRfc3339(narrowValue);
679 - if (rfc3339Value.has_value())
610 + if (const auto duration = wsl::windows::common::timestamp::TryParseDuration(narrowValue); duration.has_value())
611 {
681 - return rfc3339Value.value();
612 + // Apply the duration at full precision and truncate once, so that a sub-second value keeps its sign.
613 + const auto target = std::chrono::system_clock::now() - duration.value();
614 +
615 + return std::chrono::floor<std::chrono::seconds>(target.time_since_epoch()).count();
616 }
617
684 - throw ArgumentException(Localization::WSLCCLI_InvalidTimestampArgumentError(argName, value));
618 + try
619 + {
620 + return wsl::windows::common::timestamp::Rfc3339ToEpoch(wsl::windows::common::timestamp::ExpandToRfc3339(narrowValue));
621 + }
622 + // Name the offending argument rather than surfacing the raw parse failure.
623 + catch (...)
624 + {
625 + throw ArgumentException(Localization::WSLCCLI_InvalidTimestampArgumentError(argName, value));
626 + }
627 }
628
629 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
766 return static_cast<int64_t>(bytes.value());
767 }
768
827 -// Parses duration string into nanoseconds.
828 -static std::optional<int64_t> TryParseDuration(const std::string& input)
829 -{
830 - if (input.empty())
831 - {
832 - return std::nullopt;
833 - }
834 -
835 - size_t pos = 0;
836 - bool negative = false;
837 - if (input[pos] == '+' || input[pos] == '-')
838 - {
839 - negative = input[pos] == '-';
840 - pos++;
841 - }
842 -
843 - // Special case: a bare "0" (with optional sign) is a valid zero duration.
844 - if (input.substr(pos) == "0")
845 - {
846 - return 0;
847 - }
848 -
849 - // Accumulate in a long double so fractional units (e.g. "1.5h") are handled, then round.
850 - long double totalNanos = 0.0L;
851 - bool sawValue = false;
852 -
853 - while (pos < input.size())
854 - {
855 - // Parse the numeric part (integer and/or fraction).
856 - const size_t numberStart = pos;
857 - while (pos < input.size() && (std::isdigit(static_cast<unsigned char>(input[pos])) || input[pos] == '.'))
858 - {
859 - pos++;
860 - }
861 -
862 - const std::string numberStr = input.substr(numberStart, pos - numberStart);
863 - if (numberStr.empty() || numberStr == "." || std::count(numberStr.begin(), numberStr.end(), '.') > 1)
864 - {
865 - return std::nullopt;
866 - }
867 -
868 - // Parse the unit (everything up to the next digit or '.').
869 - const size_t unitStart = pos;
870 - while (pos < input.size() && !std::isdigit(static_cast<unsigned char>(input[pos])) && input[pos] != '.')
871 - {
872 - pos++;
873 - }
874 -
875 - const std::string unit = input.substr(unitStart, pos - unitStart);
876 -
877 - long double multiplier{};
878 - if (unit == "ns")
879 - {
880 - multiplier = 1.0L;
881 - }
882 - else if (unit == "us" || unit == "\xC2\xB5s" /* µs (U+00B5) */ || unit == "\xCE\xBCs" /* μs (U+03BC) */)
883 - {
884 - multiplier = 1000L;
885 - }
886 - else if (unit == "ms")
887 - {
888 - multiplier = 1000000L;
889 - }
890 - else if (unit == "s")
891 - {
892 - multiplier = 1000000000L;
893 - }
894 - else if (unit == "m")
895 - {
896 - multiplier = 60000000000L;
897 - }
898 - else if (unit == "h")
899 - {
900 - multiplier = 3600000000000L;
901 - }
902 - else
903 - {
904 - return std::nullopt;
905 - }
906 -
907 - long double value{};
908 - try
909 - {
910 - auto [ptr, ec] = std::from_chars(numberStr.data(), numberStr.data() + numberStr.size(), value, std::chars_format::fixed);
911 - if (ptr != numberStr.data() + numberStr.size() || ec != std::errc())
912 - {
913 - return std::nullopt;
914 - }
915 - }
916 - catch (...)
917 - {
918 - return std::nullopt;
919 - }
920 -
921 - totalNanos += value * multiplier;
922 - sawValue = true;
923 - }
924 -
925 - if (!sawValue)
926 - {
927 - return std::nullopt;
928 - }
929 -
930 - if (negative)
931 - {
932 - totalNanos = -totalNanos;
933 - }
934 -
935 - if (totalNanos > static_cast<long double>(std::numeric_limits<int64_t>::max()) ||
936 - totalNanos < static_cast<long double>(std::numeric_limits<int64_t>::min()))
937 - {
938 - return std::nullopt;
939 - }
940 -
941 - return static_cast<int64_t>(std::llroundl(totalNanos));
942 -}
943 -
769 int64_t GetDurationNanosFromString(const std::wstring& input, const std::wstring& argName)
770 {
771 const std::string narrow = WideToMultiByte(input);
947 - const auto parsed = TryParseDuration(narrow);
772 + const auto parsed = wsl::windows::common::timestamp::TryParseDuration(narrow);
773
949 - if (!parsed.has_value() || parsed.value() < 0)
774 + if (!parsed.has_value() || parsed.value() < std::chrono::nanoseconds::zero())
775 {
776 throw ArgumentException(Localization::WSLCCLI_InvalidDurationError(argName, input));
777 }
778
954 - return parsed.value();
779 + return parsed.value().count();
780 }
781
782 int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName)
src/windows/wslc/arguments/SpecParsing.h
+1 -1
@@ -87,7 +87,7 @@ ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value, const std::w
87 WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName = {});
88
89 // Parses a timestamp given as Unix epoch seconds or an RFC3339 string into epoch seconds.
90 -ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName = {});
90 +LONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName = {});
91
92 // Parses an output format ("json"/"table") into a FormatType.
93 models::FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {});
src/windows/wslc/services/ContainerModel.h
+2 -2
@@ -135,8 +135,8 @@ struct ContainerInformation
135 std::string Name;
136 std::string Image;
137 WSLCContainerState State;
138 - ULONGLONG StateChangedAt{};
139 - ULONGLONG CreatedAt{};
138 + LONGLONG StateChangedAt{};
139 + LONGLONG CreatedAt{};
140 std::vector<PortInformation> Ports;
141
142 NLOHMANN_DEFINE_TYPE_INTRUSIVE(ContainerInformation, Id, Name, Image, State, StateChangedAt, CreatedAt, Ports);
src/windows/wslc/services/ContainerService.cpp
+3 -71
@@ -299,74 +299,6 @@ static PortInformation PortInformationFromWSLCPortMapping(const WSLCPortMapping&
299 };
300 }
301
302 -std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp)
303 -{
304 - if (timestamp == 0)
305 - {
306 - return L"";
307 - }
308 -
309 - return FormatElapsedSeconds(static_cast<LONGLONG>(std::time(nullptr)) - static_cast<LONGLONG>(timestamp));
310 -}
311 -
312 -std::wstring ContainerService::FormatElapsedSeconds(LONGLONG elapsedSeconds)
313 -{
314 - constexpr LONGLONG SecondsPerMinute = std::chrono::duration_cast<std::chrono::seconds>(1min).count();
315 - constexpr LONGLONG SecondsPerHour = std::chrono::duration_cast<std::chrono::seconds>(1h).count();
316 - constexpr LONGLONG HoursPerDay = 24;
317 - constexpr LONGLONG MinutesPerHour = 60;
318 -
319 - const auto elapsed = std::max<LONGLONG>(elapsedSeconds, 0);
320 -
321 - if (elapsed < 1)
322 - {
323 - return Localization::WSLCCLI_RelativeTimeLessThanASecond();
324 - }
325 - else if (elapsed == 1)
326 - {
327 - return Localization::WSLCCLI_RelativeTimeOneSecond();
328 - }
329 - else if (elapsed < SecondsPerMinute)
330 - {
331 - return Localization::WSLCCLI_RelativeTimeSeconds(elapsed);
332 - }
333 -
334 - const auto minutes = elapsed / SecondsPerMinute;
335 - if (minutes == 1)
336 - {
337 - return Localization::WSLCCLI_RelativeTimeAboutAMinute();
338 - }
339 - else if (minutes < MinutesPerHour)
340 - {
341 - return Localization::WSLCCLI_RelativeTimeMinutes(minutes);
342 - }
343 -
344 - // Rounded to the nearest hour rather than truncated.
345 - const auto hours = (elapsed + (SecondsPerHour / 2)) / SecondsPerHour;
346 - if (hours == 1)
347 - {
348 - return Localization::WSLCCLI_RelativeTimeAboutAnHour();
349 - }
350 - else if (hours < HoursPerDay * 2)
351 - {
352 - return Localization::WSLCCLI_RelativeTimeHours(hours);
353 - }
354 - else if (hours < HoursPerDay * 7 * 2)
355 - {
356 - return Localization::WSLCCLI_RelativeTimeDays(hours / HoursPerDay);
357 - }
358 - else if (hours < HoursPerDay * 30 * 2)
359 - {
360 - return Localization::WSLCCLI_RelativeTimeWeeks(hours / HoursPerDay / 7);
361 - }
362 - else if (hours < HoursPerDay * 365 * 2)
363 - {
364 - return Localization::WSLCCLI_RelativeTimeMonths(hours / HoursPerDay / 30);
365 - }
366 -
367 - return Localization::WSLCCLI_RelativeTimeYears(elapsed / SecondsPerHour / HoursPerDay / 365);
368 -}
369 -
302 int ContainerService::Attach(Terminal& terminal, Session& session, const std::string& id)
303 {
304 [[maybe_unused]] auto operation = session.BeginContainerOperation();
@@ -408,7 +340,7 @@ int ContainerService::Attach(Terminal& terminal, Session& session, const std::st
340 return runningProcess.Wait();
341 }
342
411 -std::wstring ContainerService::ContainerStateToString(WSLCContainerState state, ULONGLONG stateChangedAt)
343 +std::wstring ContainerService::ContainerStateToString(WSLCContainerState state, LONGLONG stateChangedAt)
344 {
345 std::wstring stateString;
346 switch (state)
@@ -436,7 +368,7 @@ std::wstring ContainerService::ContainerStateToString(WSLCContainerState state,
368 return stateString;
369 }
370
439 - return std::format(L"{} {}", stateString, FormatRelativeTime(stateChangedAt));
371 + return std::format(L"{} {}", stateString, wsl::windows::common::timestamp::FormatRelativeTime(stateChangedAt));
372 }
373
374 std::wstring ContainerService::FormatPorts(WSLCContainerState state, const std::vector<PortInformation>& ports)
@@ -718,7 +650,7 @@ void ContainerService::CopyFromContainer(Session& session, const std::string& id
650 THROW_IF_FAILED(container->DownloadArchive(srcPath.c_str(), ToCOMInputHandle(outputHandle)));
651 }
652
721 -void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail)
653 +void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, LONGLONG since, LONGLONG until, ULONGLONG tail)
654 {
655 [[maybe_unused]] auto operation = session.BeginContainerOperation();
656 wil::com_ptr<IWSLCContainer> container;
src/windows/wslc/services/ContainerService.h
+2 -4
@@ -22,9 +22,7 @@ Abstract:
22 namespace wsl::windows::wslc::services {
23 struct ContainerService
24 {
25 - static std::wstring ContainerStateToString(WSLCContainerState state, ULONGLONG stateChangedAt = 0);
26 - static std::wstring FormatRelativeTime(ULONGLONG timestamp);
27 - static std::wstring FormatElapsedSeconds(LONGLONG elapsedSeconds);
25 + static std::wstring ContainerStateToString(WSLCContainerState state, LONGLONG stateChangedAt = 0);
26 static std::wstring FormatPorts(WSLCContainerState state, const std::vector<models::PortInformation>& ports);
27 static int Attach(Terminal& terminal, models::Session& session, const std::string& id);
28 static int Run(Terminal& terminal, models::Session& session, const std::string& image, models::ContainerOptions options);
@@ -42,7 +40,7 @@ struct ContainerService
40 static void CopyToContainer(models::Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize);
41 static void CopyFromContainer(models::Session& session, const std::string& id, const std::string& srcPath, HANDLE outputHandle);
42 static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
45 - static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail = 0);
43 + static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, LONGLONG since, LONGLONG until, ULONGLONG tail = 0);
44 static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id);
45 static models::PruneContainersResult Prune(models::Session& session);
46 };
src/windows/wslc/tasks/ContainerTasks.cpp
+4 -3
@@ -31,6 +31,7 @@ Abstract:
31 using namespace wsl::shared;
32 using namespace wsl::windows::common;
33 using namespace wsl::windows::common::string;
34 +using namespace wsl::windows::common::timestamp;
35 using namespace wsl::windows::common::wslutil;
36 using namespace wsl::windows::wslc::execution;
37 using namespace wsl::windows::wslc::models;
@@ -576,7 +577,7 @@ void ListContainers(CLIExecutionContext& context)
577 MultiByteToWide(trunc ? TruncateId(container.Id) : container.Id),
578 MultiByteToWide(container.Name),
579 MultiByteToWide(container.Image),
579 - ContainerService::FormatRelativeTime(container.CreatedAt),
580 + FormatRelativeTime(container.CreatedAt),
581 ContainerService::ContainerStateToString(container.State, container.StateChangedAt),
582 ContainerService::FormatPorts(container.State, container.Ports),
583 });
@@ -1040,13 +1041,13 @@ void ViewContainerLogs(CLIExecutionContext& context)
1041 // N.B. since=0 and until=0 mean "unset" — the Docker API omits the parameter when the value is 0,
1042 // which is equivalent to "no lower/upper bound". This matches Docker CLI behavior where
1043 // `docker logs --since 0` returns all logs and `docker logs --until 0` applies no upper bound.
1043 - ULONGLONG since = 0;
1044 + LONGLONG since = 0;
1045 if (context.Args.Contains(ArgType::Since))
1046 {
1047 since = context.Args.GetValue<ArgType::Since>();
1048 }
1049
1049 - ULONGLONG until = 0;
1050 + LONGLONG until = 0;
1051 if (context.Args.Contains(ArgType::Until))
1052 {
1053 until = context.Args.GetValue<ArgType::Until>();
src/windows/wslc/tasks/ImageTasks.cpp
+2 -2
@@ -29,6 +29,7 @@ Abstract:
29 using namespace wsl::shared;
30 using namespace wsl::windows::common;
31 using namespace wsl::windows::common::string;
32 +using namespace wsl::windows::common::timestamp;
33 using namespace wsl::windows::common::wslutil;
34 using namespace wsl::windows::wslc::execution;
35 using namespace wsl::windows::wslc::models;
@@ -83,8 +84,7 @@ namespace {
84 entry.Containers = image.Containers < 0 ? std::string{c_notAvailable} : std::to_string(image.Containers);
85
86 entry.CreatedAt = EpochToLocalDisplayTime(image.Created);
86 - entry.CreatedSince =
87 - WideToMultiByte(ContainerService::FormatRelativeTime(image.Created > 0 ? static_cast<ULONGLONG>(image.Created) : 0));
87 + entry.CreatedSince = WideToMultiByte(FormatRelativeTime(image.Created));
88 entry.Digest = c_none;
89 entry.ID = truncate ? TruncateId(image.Id, true) : image.Id;
90 entry.Repository = image.Repository.value_or(std::string{c_none});
src/windows/wslc/tasks/NetworkTasks.cpp
+1
@@ -23,6 +23,7 @@ Abstract:
23 using namespace wsl::shared;
24 using namespace wsl::windows::common;
25 using namespace wsl::windows::common::string;
26 +using namespace wsl::windows::common::timestamp;
27 using namespace wsl::windows::common::wslutil;
28 using namespace wsl::windows::wslc::execution;
29 using namespace wsl::windows::wslc::models;
src/windows/wslcsession/DockerEventTracker.cpp
+3 -3
@@ -114,7 +114,7 @@ void DockerEventTracker::OnEvent(const std::string_view& event)
114 auto timeEntry = parsed.find("time");
115 THROW_HR_IF_MSG(
116 E_INVALIDARG, timeEntry == parsed.end(), "Failed to parse time from event: %.*hs", static_cast<int>(event.size()), event.data());
117 - std::uint64_t eventTime = timeEntry->get<std::uint64_t>();
117 + std::int64_t eventTime = timeEntry->get<std::int64_t>();
118
119 auto actionStr = action->get<std::string>();
120
@@ -154,7 +154,7 @@ void DockerEventTracker::OnEvent(const std::string_view& event)
154 }
155 }
156
157 -void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime)
157 +void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime)
158 {
159 static std::map<std::string, ContainerEvent> events{
160 {"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
202 }
203 }
204
205 -void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime)
205 +void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime)
206 {
207 static std::map<std::string, VolumeEvent> events{{"create", VolumeEvent::Create}, {"destroy", VolumeEvent::Destroy}};
208
src/windows/wslcsession/DockerEventTracker.h
+4 -4
@@ -61,8 +61,8 @@ public:
61 DockerEventTracker* m_tracker = nullptr;
62 };
63
64 - using ContainerStateChangeCallback = std::function<void(ContainerEvent, std::optional<int>, std::uint64_t)>;
65 - using VolumeEventCallback = std::function<void(const std::string&, VolumeEvent, std::uint64_t)>;
64 + using ContainerStateChangeCallback = std::function<void(ContainerEvent, std::optional<int>, std::int64_t)>;
65 + using VolumeEventCallback = std::function<void(const std::string&, VolumeEvent, std::int64_t)>;
66
67 explicit DockerEventTracker(WSLCSession& session);
68 ~DockerEventTracker();
@@ -81,8 +81,8 @@ public:
81
82 private:
83 void OnEvent(const std::string_view& event);
84 - void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime);
85 - void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime);
84 + void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime);
85 + void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime);
86
87 struct ContainerCallback
88 {
src/windows/wslcsession/DockerHTTPClient.cpp
+1 -1
@@ -522,7 +522,7 @@ docker_schema::PruneNetworkResult DockerHTTPClient::PruneNetworks(const std::map
522 return Transaction<docker_schema::EmptyRequest, docker_schema::PruneNetworkResult>(verb::post, url);
523 }
524
525 -wil::unique_socket DockerHTTPClient::ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail)
525 +wil::unique_socket DockerHTTPClient::ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, LONGLONG Since, LONGLONG Until, ULONGLONG Tail)
526 {
527 auto url = URL::Create("/containers/{}/logs", Id);
528 url.SetParameter("follow", WI_IsFlagSet(Flags, WSLCLogsFlagsFollow));
src/windows/wslcsession/DockerHTTPClient.h
+1 -1
@@ -141,7 +141,7 @@ public:
141 common::docker_schema::InspectExec InspectExec(const std::string& Id);
142 wil::unique_socket AttachContainer(const std::string& Id, const std::optional<std::string>& DetachKeys);
143 void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns);
144 - wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail);
144 + wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, LONGLONG Since, LONGLONG Until, ULONGLONG Tail);
145 std::pair<uint32_t, wil::unique_socket> ExportContainer(const std::string& ContainerID);
146 std::unique_ptr<HTTPRequestContext> PutArchive(const std::string& ContainerID, const std::string& Path, std::optional<uint64_t> ContentLength);
147 std::tuple<uint32_t, wil::unique_socket, bool> GetArchive(const std::string& ContainerID, const std::string& Path);
src/windows/wslcsession/WSLCContainer.cpp
+15 -15
@@ -843,7 +843,7 @@ WSLCContainerImpl::WSLCContainerImpl(
843 std::map<std::string, std::string>&& labels,
844 std::function<void(const WSLCContainerImpl*)>&& onDeleted,
845 WSLCContainerState InitialState,
846 - std::uint64_t CreatedAt,
846 + std::int64_t CreatedAt,
847 WSLCProcessFlags InitProcessFlags,
848 WSLCContainerFlags ContainerFlags) :
849 m_wslcSession(wslcSession),
@@ -976,13 +976,13 @@ std::vector<WSLCPortMapping> WSLCContainerImpl::GetPorts() const
976 return result;
977 }
978
979 -void WSLCContainerImpl::GetStateChangedAt(ULONGLONG* Result)
979 +void WSLCContainerImpl::GetStateChangedAt(LONGLONG* Result)
980 {
981 auto lock = m_lock.lock_shared();
982 *Result = m_stateChangedAt;
983 }
984
985 -void WSLCContainerImpl::GetCreatedAt(ULONGLONG* Result)
985 +void WSLCContainerImpl::GetCreatedAt(LONGLONG* Result)
986 {
987 auto lock = m_lock.lock_shared();
988 *Result = m_createdAt;
@@ -1188,7 +1188,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
1188 cleanup.release();
1189 }
1190
1191 -void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::uint64_t eventTime)
1191 +void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime)
1192 {
1193 // We must release m_lock and m_stopLock before the wrapper's destructor calls
1194 // 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)
1304 }
1305
1306 // Wait for the stop event to get the Docker timestamp.
1307 - std::optional<std::uint64_t> stopTimestamp;
1307 + std::optional<std::int64_t> stopTimestamp;
1308 if (m_wslcSession.WaitForEventOrSessionTerminating(m_stopNotification.Event.get(), 60s))
1309 {
1310 stopTimestamp = m_stopNotification.EventTime.load(std::memory_order_acquire);
@@ -1322,7 +1322,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1322 }
1323 }
1324
1325 -__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::OnStopped(std::optional<std::uint64_t> stopTimestamp)
1325 +__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::OnStopped(std::optional<std::int64_t> stopTimestamp)
1326 {
1327 unique_com_disconnect comWrapper;
1328
@@ -2421,7 +2421,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2421 std::move(mergedLabels),
2422 std::move(OnDeleted),
2423 WslcContainerStateCreated,
2424 - wsl::windows::common::string::Rfc3339ToEpoch(inspectData.Created),
2424 + wsl::windows::common::timestamp::Rfc3339ToEpoch(inspectData.Created),
2425 containerOptions.InitProcessOptions.Flags,
2426 containerOptions.Flags);
2427
@@ -2509,7 +2509,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2509 std::move(labels),
2510 std::move(OnDeleted),
2511 DockerStateToWSLCState(dockerContainer.State),
2512 - static_cast<std::uint64_t>(dockerContainer.Created),
2512 + dockerContainer.Created,
2513 metadata.InitProcessFlags,
2514 metadata.Flags);
2515
@@ -2525,15 +2525,15 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2525 {
2526 // A created-but-never-started container has no StartedAt/FinishedAt; its state last
2527 // changed when it was created.
2528 - container->m_stateChangedAt = static_cast<std::uint64_t>(dockerContainer.Created);
2528 + container->m_stateChangedAt = dockerContainer.Created;
2529 }
2530 else
2531 {
2532 const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt;
2533
2534 - if (!timestamp.empty())
2534 + if (!timestamp.empty() && timestamp != c_unsetTimestamp)
2535 {
2536 - container->m_stateChangedAt = wsl::windows::common::string::Rfc3339ToEpoch(timestamp);
2536 + container->m_stateChangedAt = wsl::windows::common::timestamp::Rfc3339ToEpoch(timestamp);
2537 }
2538 }
2539 }
@@ -2575,7 +2575,7 @@ std::string WSLCContainerImpl::InspectLockHeld() const
2575 return wsl::shared::ToJson(wslcInspect);
2576 }
2577
2578 -void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const
2578 +void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, LONGLONG Since, LONGLONG Until, ULONGLONG Tail) const
2579 {
2580 auto lock = m_lock.lock_shared();
2581
@@ -2816,7 +2816,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
2816 return unique_com_disconnect{std::exchange(m_comWrapper, nullptr)};
2817 }
2818
2819 -__requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerState State, std::optional<std::uint64_t> stateChangedAt) noexcept
2819 +__requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt) noexcept
2820 {
2821 // N.B. A deleted container cannot transition back to any other state.
2822 WI_ASSERT(m_state != WslcContainerStateDeleted);
@@ -2828,7 +2828,7 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta
2828 TraceLoggingValue(m_id.c_str(), "ID"));
2829
2830 m_state = State;
2831 - m_stateChangedAt = stateChangedAt.value_or(static_cast<std::uint64_t>(std::time(nullptr)));
2831 + m_stateChangedAt = stateChangedAt.value_or(static_cast<std::int64_t>(std::time(nullptr)));
2832
2833 // Keep the VM alive while this container is Running and release the hold once it leaves that
2834 // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping
@@ -3081,7 +3081,7 @@ try
3081 }
3082 CATCH_RETURN();
3083
3084 -HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail)
3084 +HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, LONGLONG Since, LONGLONG Until, ULONGLONG Tail)
3085 try
3086 {
3087 WSLCExecutionContext context(&m_session);
src/windows/wslcsession/WSLCContainer.h
+11 -11
@@ -86,7 +86,7 @@ public:
86 std::map<std::string, std::string>&& labels,
87 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
88 WSLCContainerState InitialState,
89 - std::uint64_t CreatedAt,
89 + std::int64_t CreatedAt,
90 WSLCProcessFlags InitProcessFlags,
91 WSLCContainerFlags ContainerFlags);
92
@@ -101,13 +101,13 @@ public:
101 void Export(WSLCHandle TarHandle) const;
102 void UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const;
103 void DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) const;
104 - void GetStateChangedAt(_Out_ ULONGLONG* StateChangedAt);
105 - void GetCreatedAt(_Out_ ULONGLONG* CreatedAt);
104 + void GetStateChangedAt(_Out_ LONGLONG* StateChangedAt);
105 + void GetCreatedAt(_Out_ LONGLONG* CreatedAt);
106 void GetState(_Out_ WSLCContainerState* State);
107 void GetInitProcess(_Out_ IWSLCProcess** process) const;
108 void Exec(_In_ const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process);
109 void Inspect(LPSTR* Output) const;
110 - void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const;
110 + void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, LONGLONG Since, LONGLONG Until, ULONGLONG Tail) const;
111 void Stats(LPSTR* Output) const;
112 void GetLabels(WSLCLabelInformation** Labels, ULONG* Count) const;
113 void ConnectToNetwork(const WSLCNetworkConnectionOptions* Options);
@@ -123,7 +123,7 @@ public:
123 // Re-registers a stopped container's VM-scoped port allocations against the restarted VM.
124 void RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer);
125
126 - __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional<std::uint64_t> stateChangedAt = std::nullopt) noexcept;
126 + __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt = std::nullopt) noexcept;
127
128 const std::string& ID() const noexcept;
129
@@ -154,14 +154,14 @@ private:
154 __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags);
155
156 void AllocateBridgedModePorts();
157 - void OnEvent(ContainerEvent event, std::optional<int> exitCode, std::uint64_t eventTime);
157 + void OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime);
158
159 __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect ReleaseResources();
160 __requires_exclusive_lock_held(m_lock) void ReleaseRuntimeResources();
161 __requires_exclusive_lock_held(m_lock) void ReleaseProcesses();
162 __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect PrepareDisconnectComWrapper();
163
164 - __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect OnStopped(std::optional<std::uint64_t> stopTimestamp);
164 + __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect OnStopped(std::optional<std::int64_t> stopTimestamp);
165
166 void SetExitCode(int ExitCode) noexcept;
167 void SignalInitProcessExit() noexcept;
@@ -192,7 +192,7 @@ private:
192
193 struct StopNotification
194 {
195 - std::atomic<std::uint64_t> EventTime{0};
195 + std::atomic<std::int64_t> EventTime{0};
196 wil::unique_event Event{wil::EventOptions::None};
197 } m_stopNotification;
198
@@ -207,8 +207,8 @@ private:
207 // fetched from the (stable) runtime at each use rather than cached, since a cached reference
208 // would dangle across a restart. They are only valid while a VM lease is held.
209 WSLCSessionRuntime& m_runtime;
210 - std::uint64_t m_stateChangedAt{static_cast<std::uint64_t>(std::time(nullptr))};
211 - std::uint64_t m_createdAt{};
210 + std::int64_t m_stateChangedAt{static_cast<std::int64_t>(std::time(nullptr))};
211 + std::int64_t m_createdAt{};
212 WSLCContainerState m_state = WslcContainerStateInvalid;
213 WSLCSession& m_wslcSession;
214 IWSLCPluginNotifier* m_pluginNotifier;
@@ -248,7 +248,7 @@ public:
248 IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process) override;
249 IFACEMETHOD(Start)(WSLCContainerStartFlags Flags, _In_opt_ const WSLCProcessStartOptions* StartOptions, _In_opt_ IWarningCallback* WarningCallback) override;
250 IFACEMETHOD(Inspect)(_Out_ LPSTR* Output) override;
251 - IFACEMETHOD(Logs)(_In_ WSLCLogsFlags Flags, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr, _In_ ULONGLONG Since, _In_ ULONGLONG Until, _In_ ULONGLONG Tail) override;
251 + IFACEMETHOD(Logs)(_In_ WSLCLogsFlags Flags, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr, _In_ LONGLONG Since, _In_ LONGLONG Until, _In_ ULONGLONG Tail) override;
252 IFACEMETHOD(GetId)(_Out_ WSLCContainerId Id) override;
253 IFACEMETHOD(GetName)(_Out_ LPSTR* Name) override;
254 IFACEMETHOD(GetLabels)(_Out_ WSLCLabelInformation** Labels, _Out_ ULONG* Count) override;
src/windows/wslcsession/WSLCProcessControl.cpp
+1 -1
@@ -172,7 +172,7 @@ void DockerExecProcessControl::SetExitCode(int ExitCode)
172 }
173 }
174
175 -void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::uint64_t /*eventTime*/)
175 +void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::int64_t /*eventTime*/)
176 {
177 if (Event == ContainerEvent::ExecDied && !m_exitEvent.is_signaled())
178 {
src/windows/wslcsession/WSLCProcessControl.h
+1 -1
@@ -72,7 +72,7 @@ public:
72 void SetExitCode(int ExitCode);
73
74 private:
75 - void OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::uint64_t eventTime);
75 + void OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::int64_t eventTime);
76
77 mutable std::mutex m_lock;
78 std::string m_id;
src/windows/wslcsession/WSLCVolumes.cpp
+1 -1
@@ -67,7 +67,7 @@ __requires_lock_held(m_lock) void WSLCVolumes::OpenVolumeExclusiveLockHeld(const
67 m_volumes.insert({vol.Name, WSLCGuestVolumeImpl::Open(vol, m_dockerClient)});
68 }
69
70 -void WSLCVolumes::OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::uint64_t)
70 +void WSLCVolumes::OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::int64_t)
71 {
72 auto lock = m_lock.lock_exclusive();
73
src/windows/wslcsession/WSLCVolumes.h
+1 -1
@@ -59,7 +59,7 @@ private:
59 __requires_lock_held(m_lock) void OpenVolumeExclusiveLockHeld(const std::string& volumeName);
60 __requires_lock_held(m_lock) void OnVolumeDeletedExclusiveLockHeld(const std::string& volumeName);
61
62 - void OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::uint64_t eventTime);
62 + void OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::int64_t eventTime);
63
64 mutable wil::srwlock m_lock;
65 _Guarded_by_(m_lock) std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>> m_volumes;
test/windows/WSLCTests.cpp
+5 -5
@@ -6985,8 +6985,8 @@ class WSLCTests
6985 expectContainerList({{"test-container-1", "debian:latest", WslcContainerStateRunning}});
6986
6987 // Capture StateChangedAt and CreatedAt while the container is running.
6988 - ULONGLONG runningStateChangedAt{};
6989 - ULONGLONG runningCreatedAt{};
6988 + LONGLONG runningStateChangedAt{};
6989 + LONGLONG runningCreatedAt{};
6990 {
6991 auto [containers, ports] = ListContainers(m_defaultSession.get());
6992 VERIFY_ARE_EQUAL(containers.size(), 1);
@@ -7017,7 +7017,7 @@ class WSLCTests
7017 auto [containers, ports] = ListContainers(m_defaultSession.get());
7018 VERIFY_ARE_EQUAL(containers.size(), 1);
7019
7020 - auto now = static_cast<ULONGLONG>(time(nullptr));
7020 + auto now = static_cast<LONGLONG>(time(nullptr));
7021 VERIFY_IS_TRUE(containers[0].StateChangedAt <= now);
7022 VERIFY_IS_TRUE(containers[0].StateChangedAt >= runningStateChangedAt);
7023
@@ -10671,8 +10671,8 @@ class WSLCTests
10671 auto restore = ResetTestSession(); // Required to access the storage folder.
10672
10673 std::string containerName = "test-container";
10674 - ULONGLONG originalStateChangedAt{};
10675 - ULONGLONG originalCreatedAt{};
10674 + LONGLONG originalStateChangedAt{};
10675 + LONGLONG originalCreatedAt{};
10676
10677 // Phase 1: Create session and container, then stop the container
10678 {
test/windows/wslc/CommandLineTestCases.h
+7 -2
@@ -278,9 +278,14 @@ COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00+05:30 cont1"
278 COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00.123456789Z cont1", L"logs", true)
279 COMMAND_LINE_TEST_CASE(L"container logs --since 2024-13-15T10:30:00Z cont1", L"logs", false)
280 COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T25:30:00Z cont1", L"logs", false)
281 -COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15 cont1", L"logs", false)
281 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15 cont1", L"logs", true)
282 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10 cont1", L"logs", true)
283 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30 cont1", L"logs", true)
284 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00 cont1", L"logs", true)
285 +COMMAND_LINE_TEST_CASE(L"container logs --since 10m cont1", L"logs", true)
286 +COMMAND_LINE_TEST_CASE(L"container logs --since 1h30m cont1", L"logs", true)
287 COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00Zextra cont1", L"logs", false)
283 -COMMAND_LINE_TEST_CASE(L"container logs --since 1960-01-15T10:30:00Z cont1", L"logs", false)
288 +COMMAND_LINE_TEST_CASE(L"container logs --since 1960-01-15T10:30:00Z cont1", L"logs", true)
289 COMMAND_LINE_TEST_CASE(L"container logs --since 2024-02-31T10:30:00Z cont1", L"logs", false)
290 COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00.Z cont1", L"logs", false)
291 COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00+0530 cont1", L"logs", false)
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+95 -19
@@ -22,6 +22,7 @@ Abstract:
22 #include "ImageService.h"
23 #include "JsonUtils.h"
24 #include "Exceptions.h"
25 +#include <chrono>
26 #include <wslc.h>
27
28 using namespace wsl::windows::wslc;
@@ -732,36 +733,114 @@ class WSLCCLIArgumentUnitTests
733 TEST_METHOD(ValidateTimestamp_ValidUnixEpochSeconds)
734 {
735 // Integer timestamps should parse directly
735 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"0"), 0ULL);
736 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1700000000"), 1700000000ULL);
737 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1"), 1ULL);
738 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"9999999999"), 9999999999ULL);
736 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"0"), 0LL);
737 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1700000000"), 1700000000LL);
738 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1"), 1LL);
739 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"9999999999"), 9999999999LL);
740 }
741
742 TEST_METHOD(ValidateTimestamp_ValidRfc3339_UTC)
743 {
744 // Basic UTC timestamps with Z suffix
744 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00Z"), 1705314600ULL);
745 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1970-01-01T00:00:00Z"), 0ULL);
746 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00z"), 1705314600ULL); // lowercase z
745 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00Z"), 1705314600LL);
746 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1970-01-01T00:00:00Z"), 0LL);
747 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00z"), 1705314600LL); // lowercase z
748 }
749
750 TEST_METHOD(ValidateTimestamp_ValidRfc3339_WithOffset)
751 {
752 // Timestamps with timezone offsets (+HH:MM / -HH:MM)
753 // 2024-01-15T10:30:00+05:30 = 2024-01-15T05:00:00Z = 1705294800
753 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+05:30"), 1705294800ULL);
754 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+05:30"), 1705294800LL);
755 // 2024-01-15T10:30:00-05:00 = 2024-01-15T15:30:00Z = 1705332600
755 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00-05:00"), 1705332600ULL);
756 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+00:00"), 1705314600ULL);
756 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00-05:00"), 1705332600LL);
757 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+00:00"), 1705314600LL);
758 }
759
760 TEST_METHOD(ValidateTimestamp_ValidRfc3339_FractionalSeconds)
761 {
762 // Fractional seconds should be consumed (truncated to seconds)
762 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123Z"), 1705314600ULL);
763 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123456789Z"), 1705314600ULL);
764 - VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.1+05:30"), 1705294800ULL);
763 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123Z"), 1705314600LL);
764 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123456789Z"), 1705314600LL);
765 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.1+05:30"), 1705294800LL);
766 + }
767 +
768 + TEST_METHOD(ValidateTimestamp_ValidPreEpoch)
769 + {
770 + // No lower bound is applied, so pre-1970 values convert to a negative epoch.
771 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1960-01-15T10:30:00Z"), -314371800LL);
772 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"0001-01-01T00:00:00Z"), -62135596800LL);
773 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"-314371800"), -314371800LL);
774 + }
775 +
776 + TEST_METHOD(ValidateTimestamp_OutsideNanosecondRange)
777 + {
778 + // Values beyond the range of a nanosecond representation still convert exactly.
779 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1600-01-01T00:00:00Z"), -11676096000LL);
780 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2300-01-01T00:00:00Z"), 10413792000LL);
781 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"9999-12-31T23:59:59Z"), 253402300799LL);
782 + }
783 +
784 + TEST_METHOD(ValidateTimestamp_ValidZoneLessLocalTime)
785 + {
786 + // A value with no zone designator is resolved against the local UTC offset.
787 + const auto offset = std::chrono::duration_cast<std::chrono::seconds>(
788 + std::chrono::current_zone()->get_info(std::chrono::system_clock::now()).offset)
789 + .count();
790 + const auto expected = 1705314600LL - offset;
791 +
792 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00"), expected);
793 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123"), expected);
794 + }
795 +
796 + TEST_METHOD(ValidateTimestamp_ValidPartialAndDateOnly)
797 + {
798 + // Hour-only, minute-only and date-only values are padded out to a full time.
799 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15"), validation::GetTimestampFromString(L"2024-01-15T00:00:00"));
800 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10"), validation::GetTimestampFromString(L"2024-01-15T10:00:00"));
801 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30"), validation::GetTimestampFromString(L"2024-01-15T10:30:00"));
802 +
803 + // The same padding applies when an explicit zone is present.
804 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10Z"), 1705312800LL);
805 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30Z"), 1705314600LL);
806 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15Z"), 1705276800LL);
807 + }
808 +
809 + TEST_METHOD(ValidateTimestamp_ValidGoDuration)
810 + {
811 + const auto now = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
812 +
813 + // Durations are measured back from the current time, so allow a small window for the clock
814 + // to advance while the test runs.
815 + auto verifyDuration = [&](LPCWSTR value, LONGLONG expectedOffset) {
816 + const auto parsed = validation::GetTimestampFromString(value);
817 + VERIFY_IS_GREATER_THAN_OR_EQUAL(parsed, now - expectedOffset);
818 + VERIFY_IS_LESS_THAN_OR_EQUAL(parsed, now - expectedOffset + 30);
819 + };
820 +
821 + verifyDuration(L"10m", 600LL);
822 + verifyDuration(L"1h30m", 5400LL);
823 + verifyDuration(L"90s", 90LL);
824 + verifyDuration(L"1.5h", 5400LL);
825 + verifyDuration(L"2h45m30s", 9930LL);
826 +
827 + // A negative duration selects a time in the future.
828 + verifyDuration(L"-1h", -3600LL);
829 + }
830 +
831 + TEST_METHOD(ValidateTimestamp_SubSecondGoDuration)
832 + {
833 + // A sub-second duration is applied before the value is truncated, so a negative one still
834 + // resolves at or after the current second rather than being rounded away.
835 + const auto now = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
836 +
837 + const auto future = validation::GetTimestampFromString(L"-500ms");
838 + VERIFY_IS_GREATER_THAN_OR_EQUAL(future, now);
839 + VERIFY_IS_LESS_THAN_OR_EQUAL(future, now + 30);
840 +
841 + const auto past = validation::GetTimestampFromString(L"500ms");
842 + VERIFY_IS_GREATER_THAN_OR_EQUAL(past, now - 1);
843 + VERIFY_IS_LESS_THAN_OR_EQUAL(past, now + 30);
844 }
845
846 TEST_METHOD(ValidateTimestamp_InvalidRfc3339_Rejected)
@@ -772,10 +851,6 @@ class WSLCCLIArgumentUnitTests
851 VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T25:30:00Z"), ArgumentException);
852 // Invalid day (Feb 31)
853 VERIFY_THROWS(validation::GetTimestampFromString(L"2024-02-31T10:30:00Z"), ArgumentException);
775 - // Missing timezone
776 - VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T10:30:00"), ArgumentException);
777 - // Date only (no time)
778 - VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15"), ArgumentException);
854 // Trailing characters
855 VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T10:30:00Zextra"), ArgumentException);
856 // +HHMM without colon (not supported by %Ez)
@@ -785,8 +860,9 @@ class WSLCCLIArgumentUnitTests
860 // Random text
861 VERIFY_THROWS(validation::GetTimestampFromString(L"abc"), ArgumentException);
862 VERIFY_THROWS(validation::GetTimestampFromString(L"not-a-timestamp"), ArgumentException);
788 - // Negative epoch (pre-1970)
789 - VERIFY_THROWS(validation::GetTimestampFromString(L"1960-01-15T10:30:00Z"), ArgumentException);
863 + // Duration with no unit
864 + VERIFY_THROWS(validation::GetTimestampFromString(L"10x"), ArgumentException);
865 + VERIFY_THROWS(validation::GetTimestampFromString(L"1h30"), ArgumentException);
866 }
867 };
868 } // namespace WSLCCLIArgumentUnitTests
test/windows/wslc/WSLCCLIRelativeTimeUnitTests.cpp
+5 -6
@@ -4,11 +4,10 @@
4 #include "windows/Common.h"
5 #include "WSLCCLITestHelpers.h"
6
7 -#include "ContainerService.h"
7 +#include "timestamp.hpp"
8
9 using namespace wsl::shared;
10 -using namespace wsl::windows::wslc;
11 -using namespace wsl::windows::wslc::services;
10 +using namespace wsl::windows::common::timestamp;
11 using namespace WSLCTestHelpers;
12 using namespace WEX::Logging;
13 using namespace WEX::Common;
@@ -32,17 +31,17 @@ class WSLCCLIRelativeTimeUnitTests
31
32 static std::wstring FormatElapsed(LONGLONG secondsAgo)
33 {
35 - return ContainerService::FormatElapsedSeconds(secondsAgo);
34 + return FormatElapsedSeconds(secondsAgo);
35 }
36
37 TEST_METHOD(RelativeTime_ZeroTimestamp_ReturnsEmpty)
38 {
40 - VERIFY_ARE_EQUAL(std::wstring{}, ContainerService::FormatRelativeTime(0));
39 + VERIFY_ARE_EQUAL(std::wstring{}, FormatRelativeTime(0));
40 }
41
42 TEST_METHOD(RelativeTime_NegativeElapsed_ClampsToLessThanASecond)
43 {
45 - VERIFY_ARE_EQUAL(Localization::WSLCCLI_RelativeTimeLessThanASecond(), ContainerService::FormatElapsedSeconds(-600));
44 + VERIFY_ARE_EQUAL(Localization::WSLCCLI_RelativeTimeLessThanASecond(), FormatElapsedSeconds(-600));
45 }
46
47 TEST_METHOD(RelativeTime_Seconds)