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)