Add support for container healthchecks (#41012)
* Save state' * Update flags * Fix tests * Implement --no-healthcheck * Save state * Cleanup diff * Fix tests * Apply PR suggestions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Apply PR feedback * Apply PR feedback * Apply PR feedback --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Blue committed
Jul 9, 2026 at 15:01 UTC
b3decbdbe6291637ffec99cb0dd9e858b4ef6dd2
23 files changed
+1014
-78
localization/strings/en-US/Resources.resw
+29
@@ -2849,6 +2849,24 @@ On first run, creates the file with all settings commented out at their defaults
2849
<value>Invalid {} value: '{}'. Only 'all' is supported.</value>
2850
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2851
</data>
2852
+ <data name="WSLCCLI_HealthCmdArgDescription" xml:space="preserve">
2853
+ <value>Command to run to check container health</value>
2854
+ </data>
2855
+ <data name="WSLCCLI_HealthIntervalArgDescription" xml:space="preserve">
2856
+ <value>Time between running the health check (e.g. 30s, 1m30s)</value>
2857
+ <comment>{Locked="30s"}{Locked="1m30s"}Command line argument example values should not be translated</comment>
2858
+ </data>
2859
+ <data name="WSLCCLI_HealthRetriesArgDescription" xml:space="preserve">
2860
+ <value>Consecutive failures needed to report the container as unhealthy</value>
2861
+ </data>
2862
+ <data name="WSLCCLI_HealthStartPeriodArgDescription" xml:space="preserve">
2863
+ <value>Start period for the container to initialize before health-check countdown (e.g. 30s, 1m30s)</value>
2864
+ <comment>{Locked="30s"}{Locked="1m30s"}Command line argument example values should not be translated</comment>
2865
+ </data>
2866
+ <data name="WSLCCLI_HealthTimeoutArgDescription" xml:space="preserve">
2867
+ <value>Maximum time to allow one health check to run (e.g. 30s, 1m30s)</value>
2868
+ <comment>{Locked="30s"}{Locked="1m30s"}Command line argument example values should not be translated</comment>
2869
+ </data>
2870
<data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2871
<value>Delete images even if they are being used</value>
2872
</data>
@@ -2906,6 +2924,9 @@ On first run, creates the file with all settings commented out at their defaults
2924
<data name="WSLCCLI_NoCacheArgDescription" xml:space="preserve">
2925
<value>Do not use cache when building the image</value>
2926
</data>
2927
+ <data name="WSLCCLI_NoHealthcheckArgDescription" xml:space="preserve">
2928
+ <value>Disable any container-specified health check</value>
2929
+ </data>
2930
<data name="WSLCCLI_NoPruneArgDescription" xml:space="preserve">
2931
<value>Do not delete untagged parents</value>
2932
</data>
@@ -3086,6 +3107,14 @@ On first run, creates the file with all settings commented out at their defaults
3107
<value>Invalid {} argument value: '{}'. Expected a memory size (e.g. 256M, 1G)</value>
3108
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="256M"}{Locked="1G"}</comment>
3109
</data>
3110
+ <data name="WSLCCLI_InvalidDurationError" xml:space="preserve">
3111
+ <value>Invalid {} argument value: '{}'. Expected a duration (e.g. 30s, 1m30s)</value>
3112
+ <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="30s"}{Locked="1m30s"}</comment>
3113
+ </data>
3114
+ <data name="WSLCCLI_NoHealthcheckConflictError" xml:space="preserve">
3115
+ <value>The --no-healthcheck option cannot be combined with other health check options.</value>
3116
+ <comment>{Locked="--no-healthcheck "}Command line arguments, file names and string inserts should not be translated</comment>
3117
+ </data>
3118
<data name="WSLCCLI_CIDFileAlreadyExistsError" xml:space="preserve">
3119
<value>CID file '{}' already exists</value>
3120
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/common/WSLCContainerLauncher.cpp
+45
@@ -139,6 +139,36 @@ void WSLCContainerLauncher::SetShmSize(int64_t ShmSize)
139
m_shmSize = ShmSize;
140
}
141
142
+void WSLCContainerLauncher::SetHealthCmd(std::string&& HealthCmd)
143
+{
144
+ m_healthCmd = std::move(HealthCmd);
145
+}
146
+
147
+void WSLCContainerLauncher::SetHealthInterval(int64_t Nanoseconds)
148
+{
149
+ m_healthInterval = Nanoseconds;
150
+}
151
+
152
+void WSLCContainerLauncher::SetHealthTimeout(int64_t Nanoseconds)
153
+{
154
+ m_healthTimeout = Nanoseconds;
155
+}
156
+
157
+void WSLCContainerLauncher::SetHealthStartPeriod(int64_t Nanoseconds)
158
+{
159
+ m_healthStartPeriod = Nanoseconds;
160
+}
161
+
162
+void WSLCContainerLauncher::SetHealthRetries(LONG Retries)
163
+{
164
+ m_healthRetries = Retries;
165
+}
166
+
167
+void WSLCContainerLauncher::SetNoHealthcheck()
168
+{
169
+ WI_SetFlag(m_containerFlags, WSLCContainerFlagsNoHealthCheck);
170
+}
171
+
172
void WSLCContainerLauncher::SetEntrypoint(std::vector<std::string>&& entrypoint)
173
{
174
m_entrypoint = std::move(entrypoint);
@@ -309,6 +339,21 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
339
340
options.ShmSize = m_shmSize;
341
342
+ if (m_healthCmd.has_value() || m_healthInterval.has_value() || m_healthTimeout.has_value() ||
343
+ m_healthStartPeriod.has_value() || m_healthRetries.has_value())
344
+ {
345
+ if (m_healthCmd.has_value())
346
+ {
347
+ options.HealthCmd = m_healthCmd->c_str();
348
+ }
349
+
350
+ options.HealthIntervalNs = m_healthInterval.value_or(0);
351
+ options.HealthTimeoutNs = m_healthTimeout.value_or(0);
352
+ options.HealthStartPeriodNs = m_healthStartPeriod.value_or(0);
353
+ options.HealthRetries = m_healthRetries.value_or(0);
354
+ WI_SetFlag(options.Flags, WSLCContainerFlagsHealthCheck);
355
+ }
356
+
357
if (!entrypointStorage.empty())
358
{
359
options.Entrypoint = {entrypointStorage.data(), static_cast<ULONG>(entrypointStorage.size())};
src/windows/common/WSLCContainerLauncher.h
+11
@@ -77,6 +77,12 @@ public:
77
void SetDefaultStopSignal(WSLCSignal Signal);
78
void SetStopTimeout(LONG Timeout);
79
void SetShmSize(int64_t ShmSize);
80
+ void SetHealthCmd(std::string&& HealthCmd);
81
+ void SetHealthInterval(int64_t Nanoseconds);
82
+ void SetHealthTimeout(int64_t Nanoseconds);
83
+ void SetHealthStartPeriod(int64_t Nanoseconds);
84
+ void SetHealthRetries(LONG Retries);
85
+ void SetNoHealthcheck();
86
void SetContainerFlags(WSLCContainerFlags Flags);
87
void SetHostname(std::string&& Hostname);
88
void SetDomainname(std::string&& Domainame);
@@ -106,6 +112,11 @@ private:
112
WSLCSignal m_stopSignal = WSLCSignalNone;
113
std::optional<LONG> m_stopTimeout;
114
int64_t m_shmSize = 0;
115
+ std::optional<std::string> m_healthCmd;
116
+ std::optional<int64_t> m_healthInterval;
117
+ std::optional<int64_t> m_healthTimeout;
118
+ std::optional<int64_t> m_healthStartPeriod;
119
+ std::optional<LONG> m_healthRetries;
120
WSLCContainerFlags m_containerFlags = WSLCContainerFlagsNone;
121
std::string m_hostname;
122
std::string m_domainname;
src/windows/inc/docker_schema.h
+36
-3
@@ -284,6 +284,17 @@ struct NetworkSettings
284
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkSettings, Networks);
285
};
286
287
+struct HealthConfig
288
+{
289
+ std::optional<std::vector<std::string>> Test;
290
+ std::optional<std::int64_t> Interval;
291
+ std::optional<std::int64_t> Timeout;
292
+ std::optional<std::int64_t> StartPeriod;
293
+ std::optional<int> Retries;
294
+
295
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HealthConfig, Test, Interval, Timeout, StartPeriod, Retries);
296
+};
297
+
298
struct CreateContainer
299
{
300
using TResponse = CreatedContainer;
@@ -306,11 +317,31 @@ struct CreateContainer
317
std::vector<std::string> Env;
318
std::map<std::string, EmptyObject> ExposedPorts;
319
std::map<std::string, std::string> Labels;
320
+ std::optional<HealthConfig> Healthcheck;
321
HostConfig HostConfig;
322
NetworkingConfig NetworkingConfig;
323
324
NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(
313
- CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, StopTimeout, WorkingDir, User, Hostname, Domainname, Labels, NetworkingConfig);
325
+ CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, StopTimeout, WorkingDir, User, Hostname, Domainname, Labels, Healthcheck, NetworkingConfig);
326
+};
327
+
328
+struct HealthcheckResult
329
+{
330
+ std::string Start;
331
+ std::string End;
332
+ int ExitCode{};
333
+ std::string Output;
334
+
335
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HealthcheckResult, Start, End, ExitCode, Output);
336
+};
337
+
338
+struct Health
339
+{
340
+ std::string Status;
341
+ int FailingStreak{};
342
+ std::vector<HealthcheckResult> Log;
343
+
344
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Health, Status, FailingStreak, Log);
345
};
346
347
struct ContainerInspectState
@@ -320,8 +351,9 @@ struct ContainerInspectState
351
int ExitCode{};
352
std::string StartedAt;
353
std::string FinishedAt;
354
+ std::optional<Health> Health;
355
324
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt);
356
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt, Health);
357
};
358
359
struct ContainerConfig
@@ -334,8 +366,9 @@ struct ContainerConfig
366
std::optional<std::vector<std::string>> Entrypoint;
367
std::optional<std::string> StopSignal;
368
std::optional<int> StopTimeout;
369
+ std::optional<HealthConfig> Healthcheck;
370
338
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image, User, WorkingDir, Env, Cmd, Entrypoint, StopSignal, StopTimeout);
371
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image, User, WorkingDir, Env, Cmd, Entrypoint, StopSignal, StopTimeout, Healthcheck);
372
};
373
374
struct InspectMount
src/windows/inc/wslc_schema.h
+34
-2
@@ -39,6 +39,25 @@ struct InspectMount
39
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectMount, Type, Source, Destination, ReadWrite);
40
};
41
42
+struct HealthcheckResult
43
+{
44
+ std::string Start;
45
+ std::string End;
46
+ int ExitCode{};
47
+ std::string Output;
48
+
49
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HealthcheckResult, Start, End, ExitCode, Output);
50
+};
51
+
52
+struct Health
53
+{
54
+ std::string Status;
55
+ int FailingStreak{};
56
+ std::vector<HealthcheckResult> Log;
57
+
58
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Health, Status, FailingStreak, Log);
59
+};
60
+
61
struct ContainerInspectState
62
{
63
std::string Status;
@@ -46,8 +65,9 @@ struct ContainerInspectState
65
int ExitCode{};
66
std::string StartedAt;
67
std::string FinishedAt;
68
+ std::optional<Health> Health;
69
50
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt);
70
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt, Health);
71
};
72
73
struct Ulimit
@@ -69,6 +89,17 @@ struct InspectHostConfig
89
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectHostConfig, NetworkMode, Memory, NanoCpus, Ulimits);
90
};
91
92
+struct HealthConfig
93
+{
94
+ std::optional<std::vector<std::string>> Test;
95
+ std::optional<std::int64_t> Interval;
96
+ std::optional<std::int64_t> Timeout;
97
+ std::optional<std::int64_t> StartPeriod;
98
+ std::optional<int> Retries;
99
+
100
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HealthConfig, Test, Interval, Timeout, StartPeriod, Retries);
101
+};
102
+
103
struct ContainerConfig
104
{
105
std::optional<std::vector<std::string>> Env;
@@ -77,8 +108,9 @@ struct ContainerConfig
108
std::string User;
109
std::string WorkingDir;
110
std::optional<int> StopTimeout;
111
+ std::optional<HealthConfig> Healthcheck;
112
81
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout);
113
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout, Healthcheck);
114
};
115
116
struct InspectEndpointSettings
src/windows/service/inc/WSLCShared.idl
+3
-1
@@ -99,9 +99,11 @@ typedef enum _WSLCContainerFlags
99
WSLCContainerFlagsInit = 4, // Run the container under an init process.
100
WSLCContainerFlagsPublishAll = 8, // Publish all exposed ports.
101
WSLCContainerFlagsStopTimeout = 16, // The StopTimeout field is set and should be honored (otherwise StopTimeout is ignored).
102
+ WSLCContainerFlagsHealthCheck = 32, // The Health* fields are set and should be honored (otherwise they are ignored).
103
+ WSLCContainerFlagsNoHealthCheck = 64, // Disable any container/image-specified health check.
104
} WSLCContainerFlags;
105
104
-cpp_quote("#define WSLCContainerFlagsValid (WSLCContainerFlagsRm | WSLCContainerFlagsGpu | WSLCContainerFlagsInit | WSLCContainerFlagsPublishAll | WSLCContainerFlagsStopTimeout)")
106
+cpp_quote("#define WSLCContainerFlagsValid (WSLCContainerFlagsRm | WSLCContainerFlagsGpu | WSLCContainerFlagsInit | WSLCContainerFlagsPublishAll | WSLCContainerFlagsStopTimeout | WSLCContainerFlagsHealthCheck | WSLCContainerFlagsNoHealthCheck)")
107
108
cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCContainerFlags);")
109
src/windows/service/inc/wslc.idl
+6
@@ -285,6 +285,12 @@ typedef struct _WSLCContainerOptions
285
286
// Ignored unless WSLCContainerFlagsStopTimeout is set in Flags.
287
LONG StopTimeout;
288
+
289
+ [unique] LPCSTR HealthCmd;
290
+ LONGLONG HealthIntervalNs;
291
+ LONGLONG HealthTimeoutNs;
292
+ LONGLONG HealthStartPeriodNs;
293
+ LONG HealthRetries;
294
} WSLCContainerOptions;
295
296
typedef char WSLCContainerId[WSLC_CONTAINER_ID_LENGTH + 1] ;
src/windows/wslc/arguments/ArgumentDefinitions.h
+6
@@ -64,6 +64,11 @@ _(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, L
64
_(Gateway, "gateway", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkGatewayArgDescription()) \
65
_(Gpus, "gpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GpusArgDescription()) \
66
/*_(GroupId, "groupid", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GroupIdArgDescription())*/ \
67
+_(HealthCmd, "health-cmd", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthCmdArgDescription()) \
68
+_(HealthInterval, "health-interval", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthIntervalArgDescription()) \
69
+_(HealthRetries, "health-retries", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthRetriesArgDescription()) \
70
+_(HealthStartPeriod, "health-start-period", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthStartPeriodArgDescription()) \
71
+_(HealthTimeout, "health-timeout", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthTimeoutArgDescription()) \
72
_(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, Localization::WSLCCLI_HelpArgDescription()) \
73
_(Hostname, "hostname", L"h", Kind::Value, Localization::WSLCCLI_HostnameArgDescription()) \
74
_(ImageForce, "force", L"f", Kind::Flag, Localization::WSLCCLI_ImageForceArgDescription()) \
@@ -83,6 +88,7 @@ _(NetworkName, "network-name", NO_ALIAS, Kind::Positional, L
88
/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
89
_(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \
90
_(NoColor, "no-color", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoColorArgDescription()) \
91
+_(NoHealthcheck, "no-healthcheck", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoHealthcheckArgDescription()) \
92
_(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoPruneArgDescription()) \
93
_(NoTrunc, "no-trunc", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoTruncArgDescription()) \
94
_(ObjectId, "object-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ObjectIdArgDescription()) \
src/windows/wslc/arguments/ArgumentValidation.cpp
+165
@@ -19,8 +19,10 @@ Abstract:
19
#include "ContainerModel.h"
20
#include "Exceptions.h"
21
#include "Localization.h"
22
+#include <algorithm>
23
#include <charconv>
24
#include <chrono>
25
+#include <cmath>
26
#include <format>
27
#include <sstream>
28
#include <unordered_map>
@@ -56,6 +58,31 @@ void Argument::Validate(const ArgMap& execArgs) const
58
validation::ValidateMemorySize(execArgs.GetAll<ArgType::ShmSize>(), m_name);
59
break;
60
61
+ case ArgType::HealthInterval:
62
+ validation::ValidateDuration(execArgs.GetAll<ArgType::HealthInterval>(), m_name);
63
+ break;
64
+
65
+ case ArgType::HealthTimeout:
66
+ validation::ValidateDuration(execArgs.GetAll<ArgType::HealthTimeout>(), m_name);
67
+ break;
68
+
69
+ case ArgType::HealthStartPeriod:
70
+ validation::ValidateDuration(execArgs.GetAll<ArgType::HealthStartPeriod>(), m_name);
71
+ break;
72
+
73
+ case ArgType::HealthRetries:
74
+ validation::ValidateIntegerFromString<int>(
75
+ execArgs.GetAll<ArgType::HealthRetries>(), m_name, [](int value) { return value >= 0; });
76
+ break;
77
+
78
+ case ArgType::NoHealthcheck:
79
+ if (execArgs.Contains(ArgType::HealthCmd) || execArgs.Contains(ArgType::HealthInterval) || execArgs.Contains(ArgType::HealthTimeout) ||
80
+ execArgs.Contains(ArgType::HealthStartPeriod) || execArgs.Contains(ArgType::HealthRetries))
81
+ {
82
+ throw ArgumentException(Localization::WSLCCLI_NoHealthcheckConflictError());
83
+ }
84
+ break;
85
+
86
case ArgType::Memory:
87
validation::ValidateMemorySize(execArgs.GetAll<ArgType::Memory>(), m_name);
88
break;
@@ -417,6 +444,144 @@ int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& a
444
return static_cast<int64_t>(parsed.value());
445
}
446
447
+// Parses duration string into nanoseconds.
448
+static std::optional<int64_t> TryParseDuration(const std::string& input)
449
+{
450
+ if (input.empty())
451
+ {
452
+ return std::nullopt;
453
+ }
454
+
455
+ size_t pos = 0;
456
+ bool negative = false;
457
+ if (input[pos] == '+' || input[pos] == '-')
458
+ {
459
+ negative = input[pos] == '-';
460
+ pos++;
461
+ }
462
+
463
+ // Special case: a bare "0" (with optional sign) is a valid zero duration.
464
+ if (input.substr(pos) == "0")
465
+ {
466
+ return 0;
467
+ }
468
+
469
+ // Accumulate in a long double so fractional units (e.g. "1.5h") are handled, then round.
470
+ long double totalNanos = 0.0L;
471
+ bool sawValue = false;
472
+
473
+ while (pos < input.size())
474
+ {
475
+ // Parse the numeric part (integer and/or fraction).
476
+ const size_t numberStart = pos;
477
+ while (pos < input.size() && (std::isdigit(static_cast<unsigned char>(input[pos])) || input[pos] == '.'))
478
+ {
479
+ pos++;
480
+ }
481
+
482
+ const std::string numberStr = input.substr(numberStart, pos - numberStart);
483
+ if (numberStr.empty() || numberStr == "." || std::count(numberStr.begin(), numberStr.end(), '.') > 1)
484
+ {
485
+ return std::nullopt;
486
+ }
487
+
488
+ // Parse the unit (everything up to the next digit or '.').
489
+ const size_t unitStart = pos;
490
+ while (pos < input.size() && !std::isdigit(static_cast<unsigned char>(input[pos])) && input[pos] != '.')
491
+ {
492
+ pos++;
493
+ }
494
+
495
+ const std::string unit = input.substr(unitStart, pos - unitStart);
496
+
497
+ long double multiplier{};
498
+ if (unit == "ns")
499
+ {
500
+ multiplier = 1.0L;
501
+ }
502
+ else if (unit == "us" || unit == "\xC2\xB5s" /* µs (U+00B5) */ || unit == "\xCE\xBCs" /* μs (U+03BC) */)
503
+ {
504
+ multiplier = 1000L;
505
+ }
506
+ else if (unit == "ms")
507
+ {
508
+ multiplier = 1000000L;
509
+ }
510
+ else if (unit == "s")
511
+ {
512
+ multiplier = 1000000000L;
513
+ }
514
+ else if (unit == "m")
515
+ {
516
+ multiplier = 60000000000L;
517
+ }
518
+ else if (unit == "h")
519
+ {
520
+ multiplier = 3600000000000L;
521
+ }
522
+ else
523
+ {
524
+ return std::nullopt;
525
+ }
526
+
527
+ long double value{};
528
+ try
529
+ {
530
+ auto [ptr, ec] = std::from_chars(numberStr.data(), numberStr.data() + numberStr.size(), value, std::chars_format::fixed);
531
+ if (ptr != numberStr.data() + numberStr.size() || ec != std::errc())
532
+ {
533
+ return std::nullopt;
534
+ }
535
+ }
536
+ catch (...)
537
+ {
538
+ return std::nullopt;
539
+ }
540
+
541
+ totalNanos += value * multiplier;
542
+ sawValue = true;
543
+ }
544
+
545
+ if (!sawValue)
546
+ {
547
+ return std::nullopt;
548
+ }
549
+
550
+ if (negative)
551
+ {
552
+ totalNanos = -totalNanos;
553
+ }
554
+
555
+ if (totalNanos > static_cast<long double>(std::numeric_limits<int64_t>::max()) ||
556
+ totalNanos < static_cast<long double>(std::numeric_limits<int64_t>::min()))
557
+ {
558
+ return std::nullopt;
559
+ }
560
+
561
+ return static_cast<int64_t>(std::llroundl(totalNanos));
562
+}
563
+
564
+void ValidateDuration(const std::vector<std::wstring>& values, const std::wstring& argName)
565
+{
566
+ for (const auto& value : values)
567
+ {
568
+ std::ignore = GetDurationNanosFromString(value, argName);
569
+ }
570
+}
571
+
572
+int64_t GetDurationNanosFromString(const std::wstring& input, const std::wstring& argName)
573
+{
574
+ const std::string narrow = WideToMultiByte(input);
575
+ const auto parsed = TryParseDuration(narrow);
576
+
577
+ if (!parsed.has_value() || parsed.value() < 0)
578
+ {
579
+ throw ArgumentException(Localization::WSLCCLI_InvalidDurationError(argName, input));
580
+ }
581
+
582
+ return parsed.value();
583
+}
584
+
585
void ValidateNanoCpus(const std::vector<std::wstring>& values, const std::wstring& argName)
586
{
587
for (const auto& value : values)
src/windows/wslc/arguments/ArgumentValidation.h
+3
@@ -66,6 +66,9 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring
66
void ValidateMemorySize(const std::vector<std::wstring>& values, const std::wstring& argName);
67
int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName = {});
68
69
+void ValidateDuration(const std::vector<std::wstring>& values, const std::wstring& argName);
70
+int64_t GetDurationNanosFromString(const std::wstring& input, const std::wstring& argName = {});
71
+
72
void ValidateTimestamp(const std::vector<std::wstring>& values, const std::wstring& argName);
73
ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName = {});
74
void ValidateNanoCpus(const std::vector<std::wstring>& values, const std::wstring& argName);
src/windows/wslc/commands/ContainerCreateCommand.cpp
+6
@@ -43,6 +43,11 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
43
Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
44
// Argument::Create(ArgType::GroupId),
45
Argument::Create(ArgType::Gpus),
46
+ Argument::Create(ArgType::HealthCmd),
47
+ Argument::Create(ArgType::HealthInterval),
48
+ Argument::Create(ArgType::HealthRetries),
49
+ Argument::Create(ArgType::HealthStartPeriod),
50
+ Argument::Create(ArgType::HealthTimeout),
51
Argument::Create(ArgType::Hostname),
52
Argument::Create(ArgType::Interactive),
53
Argument::Create(ArgType::Label, false, NO_LIMIT),
@@ -52,6 +57,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
57
Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
58
// Argument::Create(ArgType::NoDNS),
59
// Argument::Create(ArgType::Progress),
60
+ Argument::Create(ArgType::NoHealthcheck),
61
Argument::Create(ArgType::Publish, false, NO_LIMIT),
62
Argument::Create(ArgType::PublishAll),
63
Argument::Create(ArgType::Remove),
src/windows/wslc/commands/ContainerRunCommand.cpp
+6
@@ -43,6 +43,11 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
43
Argument::Create(ArgType::Env, false, NO_LIMIT),
44
Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
45
Argument::Create(ArgType::Gpus),
46
+ Argument::Create(ArgType::HealthCmd),
47
+ Argument::Create(ArgType::HealthInterval),
48
+ Argument::Create(ArgType::HealthRetries),
49
+ Argument::Create(ArgType::HealthStartPeriod),
50
+ Argument::Create(ArgType::HealthTimeout),
51
Argument::Create(ArgType::Hostname),
52
Argument::Create(ArgType::Interactive),
53
Argument::Create(ArgType::Label, false, NO_LIMIT),
@@ -52,6 +57,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
57
Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
58
// Argument::Create(ArgType::NoDNS),
59
// Argument::Create(ArgType::Progress),
60
+ Argument::Create(ArgType::NoHealthcheck),
61
Argument::Create(ArgType::Publish, false, NO_LIMIT),
62
Argument::Create(ArgType::PublishAll),
63
// Argument::Create(ArgType::Pull),
src/windows/wslc/services/ContainerModel.h
+6
@@ -40,6 +40,12 @@ struct ContainerOptions
40
WSLCSignal StopSignal = WSLCSignalNone;
41
std::optional<int> StopTimeout{};
42
std::optional<int64_t> ShmSize{};
43
+ std::optional<std::string> HealthCmd{};
44
+ std::optional<int64_t> HealthInterval{}; // nanoseconds
45
+ std::optional<int64_t> HealthTimeout{}; // nanoseconds
46
+ std::optional<int64_t> HealthStartPeriod{}; // nanoseconds
47
+ std::optional<int> HealthRetries{};
48
+ bool NoHealthcheck = false;
49
bool Gpu = false;
50
std::vector<std::string> Ports;
51
std::vector<std::wstring> Volumes;
src/windows/wslc/services/ContainerService.cpp
+30
@@ -146,6 +146,36 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
146
containerLauncher.SetShmSize(options.ShmSize.value());
147
}
148
149
+ if (options.HealthCmd.has_value())
150
+ {
151
+ containerLauncher.SetHealthCmd(std::string(options.HealthCmd.value()));
152
+ }
153
+
154
+ if (options.HealthInterval.has_value())
155
+ {
156
+ containerLauncher.SetHealthInterval(options.HealthInterval.value());
157
+ }
158
+
159
+ if (options.HealthTimeout.has_value())
160
+ {
161
+ containerLauncher.SetHealthTimeout(options.HealthTimeout.value());
162
+ }
163
+
164
+ if (options.HealthStartPeriod.has_value())
165
+ {
166
+ containerLauncher.SetHealthStartPeriod(options.HealthStartPeriod.value());
167
+ }
168
+
169
+ if (options.HealthRetries.has_value())
170
+ {
171
+ containerLauncher.SetHealthRetries(options.HealthRetries.value());
172
+ }
173
+
174
+ if (options.NoHealthcheck)
175
+ {
176
+ containerLauncher.SetNoHealthcheck();
177
+ }
178
+
179
if (options.MemoryBytes.has_value())
180
{
181
containerLauncher.SetMemoryLimit(options.MemoryBytes.value());
src/windows/wslc/tasks/ContainerTasks.cpp
+30
@@ -453,6 +453,36 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
453
options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get<ArgType::ShmSize>());
454
}
455
456
+ if (context.Args.Contains(ArgType::HealthCmd))
457
+ {
458
+ options.HealthCmd = WideToMultiByte(context.Args.Get<ArgType::HealthCmd>());
459
+ }
460
+
461
+ if (context.Args.Contains(ArgType::HealthInterval))
462
+ {
463
+ options.HealthInterval = validation::GetDurationNanosFromString(context.Args.Get<ArgType::HealthInterval>());
464
+ }
465
+
466
+ if (context.Args.Contains(ArgType::HealthTimeout))
467
+ {
468
+ options.HealthTimeout = validation::GetDurationNanosFromString(context.Args.Get<ArgType::HealthTimeout>());
469
+ }
470
+
471
+ if (context.Args.Contains(ArgType::HealthStartPeriod))
472
+ {
473
+ options.HealthStartPeriod = validation::GetDurationNanosFromString(context.Args.Get<ArgType::HealthStartPeriod>());
474
+ }
475
+
476
+ if (context.Args.Contains(ArgType::HealthRetries))
477
+ {
478
+ options.HealthRetries = validation::GetIntegerFromString<int>(context.Args.Get<ArgType::HealthRetries>());
479
+ }
480
+
481
+ if (context.Args.Contains(ArgType::NoHealthcheck))
482
+ {
483
+ options.NoHealthcheck = true;
484
+ }
485
+
486
if (context.Args.Contains(ArgType::Memory))
487
{
488
options.MemoryBytes = validation::GetMemorySizeFromString(context.Args.Get<ArgType::Memory>());
src/windows/wslcsession/WSLCContainer.cpp
+73
@@ -1303,6 +1303,23 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1303
wslcInspect.State.StartedAt = dockerInspect.State.StartedAt;
1304
wslcInspect.State.FinishedAt = dockerInspect.State.FinishedAt;
1305
1306
+ if (dockerInspect.State.Health.has_value())
1307
+ {
1308
+ const auto& dockerHealth = dockerInspect.State.Health.value();
1309
+
1310
+ wslc_schema::Health health{};
1311
+ health.Status = dockerHealth.Status;
1312
+ health.FailingStreak = dockerHealth.FailingStreak;
1313
+
1314
+ health.Log.reserve(dockerHealth.Log.size());
1315
+ for (const auto& entry : dockerHealth.Log)
1316
+ {
1317
+ health.Log.push_back({entry.Start, entry.End, entry.ExitCode, entry.Output});
1318
+ }
1319
+
1320
+ wslcInspect.State.Health = std::move(health);
1321
+ }
1322
+
1323
wslcInspect.HostConfig.NetworkMode = dockerInspect.HostConfig.NetworkMode;
1324
wslcInspect.HostConfig.Memory = dockerInspect.HostConfig.Memory;
1325
wslcInspect.HostConfig.NanoCpus = dockerInspect.HostConfig.NanoCpus;
@@ -1323,6 +1340,20 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
1340
wslcInspect.Config.WorkingDir = dockerInspect.Config.WorkingDir;
1341
wslcInspect.Config.StopTimeout = dockerInspect.Config.StopTimeout;
1342
1343
+ if (dockerInspect.Config.Healthcheck.has_value())
1344
+ {
1345
+ const auto& dockerHealth = dockerInspect.Config.Healthcheck.value();
1346
+
1347
+ wslc_schema::HealthConfig health{};
1348
+ health.Test = dockerHealth.Test;
1349
+ health.Interval = dockerHealth.Interval;
1350
+ health.Timeout = dockerHealth.Timeout;
1351
+ health.StartPeriod = dockerHealth.StartPeriod;
1352
+ health.Retries = dockerHealth.Retries;
1353
+
1354
+ wslcInspect.Config.Healthcheck = std::move(health);
1355
+ }
1356
+
1357
// Map WSLC port mappings (Windows host ports only).
1358
for (const auto& e : m_mappedPorts)
1359
{
@@ -1521,6 +1552,48 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1552
1553
request.HostConfig.ShmSize = containerOptions.ShmSize;
1554
1555
+ if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsNoHealthCheck))
1556
+ {
1557
+ THROW_HR_IF_MSG(
1558
+ E_INVALIDARG,
1559
+ WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsHealthCheck),
1560
+ "WSLCContainerFlagsHealthCheck and WSLCContainerFlagsNoHealthCheck cannot be combined");
1561
+
1562
+ request.Healthcheck.emplace().Test = std::vector<std::string>{"NONE"};
1563
+ }
1564
+ else if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsHealthCheck))
1565
+ {
1566
+ common::docker_schema::HealthConfig health{};
1567
+
1568
+ if (containerOptions.HealthCmd != nullptr)
1569
+ {
1570
+ health.Test = std::vector<std::string>{"CMD-SHELL", containerOptions.HealthCmd};
1571
+ }
1572
+
1573
+ // N.B. '0' will use the default value from the image.
1574
+ if (containerOptions.HealthIntervalNs != 0)
1575
+ {
1576
+ health.Interval = containerOptions.HealthIntervalNs;
1577
+ }
1578
+
1579
+ if (containerOptions.HealthTimeoutNs != 0)
1580
+ {
1581
+ health.Timeout = containerOptions.HealthTimeoutNs;
1582
+ }
1583
+
1584
+ if (containerOptions.HealthStartPeriodNs != 0)
1585
+ {
1586
+ health.StartPeriod = containerOptions.HealthStartPeriodNs;
1587
+ }
1588
+
1589
+ if (containerOptions.HealthRetries != 0)
1590
+ {
1591
+ health.Retries = containerOptions.HealthRetries;
1592
+ }
1593
+
1594
+ request.Healthcheck = std::move(health);
1595
+ }
1596
+
1597
if (containerOptions.VolumesCount > 0)
1598
{
1599
THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes, "Volumes is null with VolumesCount=%lu", containerOptions.VolumesCount);
test/windows/WSLCTests.cpp
+159
-1
@@ -1696,6 +1696,130 @@ class WSLCTests
1696
}
1697
}
1698
1699
+ WSLC_TEST_METHOD(BuildImageHealthCheck)
1700
+ {
1701
+ auto contextDir = std::filesystem::current_path() / "build-context-healthcheck";
1702
+ std::filesystem::create_directories(contextDir);
1703
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1704
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-healthcheck:latest", WSLCDeleteImageFlagsForce).first);
1705
+ std::error_code ec;
1706
+ std::filesystem::remove_all(contextDir, ec);
1707
+ });
1708
+
1709
+ // Create an image with a healthcheck that only passes once a specific file exists.
1710
+ constexpr auto c_healthReadyFile = "/tmp/wslc-health-ready";
1711
+
1712
+ {
1713
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1714
+ dockerfile << "FROM debian:latest\n";
1715
+ dockerfile << "HEALTHCHECK --interval=1s --timeout=100ms --start-period=300s --retries=1000 CMD test -f "
1716
+ << c_healthReadyFile << "\n";
1717
+ dockerfile << "CMD [\"sleep\", \"99999\"]\n";
1718
+ }
1719
+
1720
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-healthcheck:latest"));
1721
+ ExpectImagePresent(*m_defaultSession, "wslc-test-healthcheck:latest");
1722
+
1723
+ auto waitForHealthStatus = [](auto& container, const std::string& expectedStatus, std::chrono::seconds timeout) {
1724
+ wsl::shared::retry::RetryWithTimeout<void>(
1725
+ [&]() {
1726
+ const auto inspect = container.Inspect();
1727
+ THROW_HR_IF_MSG(E_FAIL, !inspect.State.Health.has_value(), "container does not report a health status yet");
1728
+ THROW_HR_IF_MSG(
1729
+ E_FAIL,
1730
+ inspect.State.Health->Status != expectedStatus,
1731
+ "health status is '%hs', expected '%hs'",
1732
+ inspect.State.Health->Status.c_str(),
1733
+ expectedStatus.c_str());
1734
+ },
1735
+ std::chrono::milliseconds{100},
1736
+ timeout);
1737
+ };
1738
+
1739
+ // Validate that the image's default health check is inherited by a started container, and that its runtime
1740
+ // status stays "starting" until the health command passes, then deterministically becomes "healthy".
1741
+ {
1742
+ WSLCContainerLauncher launcher("wslc-test-healthcheck:latest", "wslc-healthcheck-test-default");
1743
+ auto container = launcher.Launch(*m_defaultSession);
1744
+
1745
+ auto inspect = container.Inspect();
1746
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
1747
+
1748
+ const auto& health = inspect.Config.Healthcheck.value();
1749
+ VERIFY_IS_TRUE(health.Test.has_value());
1750
+ const std::vector<std::string> expectedTest{"CMD-SHELL", std::string("test -f ") + c_healthReadyFile};
1751
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
1752
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.Interval.value_or(0));
1753
+ VERIFY_ARE_EQUAL(100'000'000LL, health.Timeout.value_or(0));
1754
+ VERIFY_ARE_EQUAL(300'000'000'000LL, health.StartPeriod.value_or(0));
1755
+
1756
+ // The health command fails while the file is absent, so the container stays "starting".
1757
+ waitForHealthStatus(container, "starting", 60s);
1758
+
1759
+ auto touchProcess = WSLCProcessLauncher({}, {"/usr/bin/touch", c_healthReadyFile}).Launch(container.Get());
1760
+ ValidateProcessOutput(touchProcess, {}, 0);
1761
+
1762
+ waitForHealthStatus(container, "healthy", 60s);
1763
+ }
1764
+
1765
+ // Validate that the image's default health check can be overridden, and that a failing (exit 1) check drives
1766
+ // the runtime status to "unhealthy".
1767
+ {
1768
+ WSLCContainerLauncher launcher("wslc-test-healthcheck:latest", "wslc-healthcheck-test-override");
1769
+ launcher.SetHealthCmd("exit 1");
1770
+ launcher.SetHealthInterval(1'000'000'000LL); // 1s
1771
+ launcher.SetHealthStartPeriod(1'000'000'000LL); // 1s
1772
+ launcher.SetHealthRetries(1);
1773
+ auto container = launcher.Launch(*m_defaultSession);
1774
+
1775
+ auto inspect = container.Inspect();
1776
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
1777
+
1778
+ const auto& health = inspect.Config.Healthcheck.value();
1779
+ VERIFY_IS_TRUE(health.Test.has_value());
1780
+ const std::vector<std::string> expectedTest{"CMD-SHELL", "exit 1"};
1781
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
1782
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.Interval.value_or(0));
1783
+ // The override must set an explicit start period: otherwise the engine merges the image's healthcheck
1784
+ // fields for any zero-valued field (see moby daemon merge()), inheriting the image's 300s start period,
1785
+ // during which failing checks keep the container "starting" instead of transitioning to "unhealthy".
1786
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value_or(0));
1787
+ VERIFY_ARE_EQUAL(1, health.Retries.value_or(0));
1788
+
1789
+ // Validate that the container transitions to "unhealthy" after the health command fails.
1790
+ waitForHealthStatus(container, "unhealthy", 60s);
1791
+ }
1792
+
1793
+ // Validate that WSLCContainerFlagsNoHealthCheck disables the image's default health check.
1794
+ {
1795
+ WSLCContainerLauncher launcher("wslc-test-healthcheck:latest", "wslc-healthcheck-test-disabled");
1796
+ launcher.SetNoHealthcheck();
1797
+ auto container = launcher.Launch(*m_defaultSession);
1798
+
1799
+ auto inspect = container.Inspect();
1800
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
1801
+
1802
+ const auto& health = inspect.Config.Healthcheck.value();
1803
+ VERIFY_IS_TRUE(health.Test.has_value());
1804
+ const std::vector<std::string> expectedTest{"NONE"};
1805
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
1806
+
1807
+ // A disabled health check is not monitored, so the container never reports a runtime health status.
1808
+ VERIFY_IS_FALSE(inspect.State.Health.has_value());
1809
+ }
1810
+
1811
+ // Validate that combining WSLCContainerFlagsNoHealthCheck with an explicit health check command is rejected.
1812
+ {
1813
+ WSLCContainerLauncher launcher("wslc-test-healthcheck:latest", "wslc-healthcheck-test-conflict");
1814
+ launcher.SetNoHealthcheck();
1815
+ launcher.SetHealthCmd("exit 0");
1816
+
1817
+ auto [result, container] = launcher.CreateNoThrow(*m_defaultSession);
1818
+ VERIFY_ARE_EQUAL(result, E_INVALIDARG);
1819
+ VERIFY_IS_FALSE(container.has_value());
1820
+ }
1821
+ }
1822
+
1823
WSLC_TEST_METHOD(BuildImageWithContext)
1824
{
1825
auto contextDir = std::filesystem::current_path() / "build-context-file";
@@ -5752,7 +5876,7 @@ class WSLCTests
5876
5877
// Invalid container flags are rejected with E_INVALIDARG.
5878
options.Image = "debian:latest";
5755
- options.Flags = static_cast<WSLCContainerFlags>(0x20);
5879
+ options.Flags = static_cast<WSLCContainerFlags>(0x80);
5880
VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateContainer(&options, nullptr, &container));
5881
5882
// Invalid init process flags are rejected with E_INVALIDARG.
@@ -6473,6 +6597,40 @@ class WSLCTests
6597
}
6598
}
6599
6600
+ // Validate that health check options are forwarded to the container configuration.
6601
+ {
6602
+ WSLCContainerLauncher launcher("debian:latest", "test-container-health", {"sleep", "99999"});
6603
+ launcher.SetHealthCmd("exit 0");
6604
+ launcher.SetHealthInterval(5'000'000'000LL); // 5s
6605
+ launcher.SetHealthTimeout(3'000'000'000LL); // 3s
6606
+ launcher.SetHealthStartPeriod(1'000'000'000LL); // 1s
6607
+ launcher.SetHealthRetries(2);
6608
+
6609
+ auto container = launcher.Create(*m_defaultSession);
6610
+
6611
+ auto inspect = container.Inspect();
6612
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
6613
+
6614
+ const auto& health = inspect.Config.Healthcheck.value();
6615
+ VERIFY_IS_TRUE(health.Test.has_value());
6616
+ const std::vector<std::string> expectedTest{"CMD-SHELL", "exit 0"};
6617
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
6618
+ VERIFY_ARE_EQUAL(5'000'000'000LL, health.Interval.value_or(0));
6619
+ VERIFY_ARE_EQUAL(3'000'000'000LL, health.Timeout.value_or(0));
6620
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value_or(0));
6621
+ VERIFY_ARE_EQUAL(2, health.Retries.value_or(0));
6622
+ }
6623
+
6624
+ // Validate that a container without health options reports no health check.
6625
+ {
6626
+ WSLCContainerLauncher launcher("debian:latest", "test-container-no-health", {"sleep", "99999"});
6627
+
6628
+ auto container = launcher.Create(*m_defaultSession);
6629
+
6630
+ auto inspect = container.Inspect();
6631
+ VERIFY_IS_FALSE(inspect.Config.Healthcheck.has_value());
6632
+ }
6633
+
6634
// Validate that Kill() works as expected
6635
{
6636
WSLCContainerLauncher launcher("debian:latest", "test-container-kill", {"sleep", "99999"}, {});
test/windows/wslc/CommandLineTestCases.h
+21
@@ -149,6 +149,27 @@ COMMAND_LINE_TEST_CASE(L"create --gpus all ubuntu", L"create", true)
149
COMMAND_LINE_TEST_CASE(L"container create --gpus all ubuntu sh", L"create", true)
150
COMMAND_LINE_TEST_CASE(L"create --gpus none ubuntu", L"create", false) // Only 'all' is supported
151
COMMAND_LINE_TEST_CASE(L"create --gpus", L"create", false) // Missing value for --gpus
152
+// Health check tests for container run
153
+COMMAND_LINE_TEST_CASE(L"run --health-cmd \"exit 0\" ubuntu", L"run", true)
154
+COMMAND_LINE_TEST_CASE(
155
+ L"run --health-interval 30s --health-timeout 5s --health-retries 3 --health-start-period 10s ubuntu", L"run", true)
156
+COMMAND_LINE_TEST_CASE(L"run --health-interval 1m30s ubuntu", L"run", true)
157
+COMMAND_LINE_TEST_CASE(L"run --health-interval notaduration ubuntu", L"run", false) // Invalid duration
158
+COMMAND_LINE_TEST_CASE(L"run --health-timeout -5s ubuntu", L"run", false) // Negative duration
159
+COMMAND_LINE_TEST_CASE(L"run --health-retries abc ubuntu", L"run", false) // Non-numeric retries
160
+COMMAND_LINE_TEST_CASE(L"run --health-retries -1 ubuntu", L"run", false) // Negative retries
161
+COMMAND_LINE_TEST_CASE(L"run --health-interval ubuntu", L"run", false) // Missing value for --health-interval
162
+COMMAND_LINE_TEST_CASE(L"run --health-cmd", L"run", false) // Missing value for --health-cmd
163
+// Health check tests for container create
164
+COMMAND_LINE_TEST_CASE(L"create --health-cmd \"exit 0\" ubuntu", L"create", true)
165
+COMMAND_LINE_TEST_CASE(
166
+ L"container create --health-cmd \"curl -f http://localhost/\" --health-interval 30s --health-timeout 5s --health-retries 3 "
167
+ L"--health-start-period 10s ubuntu",
168
+ L"create",
169
+ true)
170
+COMMAND_LINE_TEST_CASE(L"create --health-start-period 500ms ubuntu", L"create", true)
171
+COMMAND_LINE_TEST_CASE(L"create --health-timeout invalid ubuntu", L"create", false) // Invalid duration
172
+COMMAND_LINE_TEST_CASE(L"create --health-retries 2.5 ubuntu", L"create", false) // Non-integer retries
173
COMMAND_LINE_TEST_CASE(L"exec cont1 echo Hello", L"exec", true)
174
COMMAND_LINE_TEST_CASE(L"exec cont1", L"exec", false) // Missing required command argument
175
COMMAND_LINE_TEST_CASE(L"container exec -it cont1 sh -c \"echo a && echo b\"", L"exec", true) // docker exec example
test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp
+40
@@ -124,6 +124,46 @@ class WSLCCLIResourceLimitsParserUnitTests
124
VERIFY_NO_THROW(validation::ValidateUlimit({L"nofile=1024", L"core=-1"}, L"ulimit"));
125
VERIFY_THROWS(validation::ValidateUlimit({L"nofile=1024", L"bad"}, L"ulimit"), ArgumentException);
126
}
127
+
128
+ TEST_METHOD(Duration_Valid)
129
+ {
130
+ std::vector<std::pair<std::wstring, int64_t>> valid = {
131
+ {L"0", 0LL},
132
+ {L"0s", 0LL},
133
+ {L"1ns", 1LL},
134
+ {L"500ms", 500'000'000LL},
135
+ {L"30s", 30'000'000'000LL},
136
+ {L"1m", 60'000'000'000LL},
137
+ {L"1m30s", 90'000'000'000LL},
138
+ {L"1h", 3'600'000'000'000LL},
139
+ {L"1.5h", 5'400'000'000'000LL},
140
+ {L"2h45m", 9'900'000'000'000LL},
141
+ {L"100us", 100'000LL},
142
+ };
143
+
144
+ for (const auto& [input, expected] : valid)
145
+ {
146
+ const auto actual = validation::GetDurationNanosFromString(input, L"health-interval");
147
+ VERIFY_ARE_EQUAL(expected, actual);
148
+ }
149
+ }
150
+
151
+ TEST_METHOD(Duration_Invalid)
152
+ {
153
+ const std::vector<std::wstring> invalid = {
154
+ L"", L"-1", L"s", L"abc", L"30", L"30sec", L"30x", L"-30s", L"30 s", L"s", L"1.2.3s", L"9223372036854775808s"};
155
+
156
+ for (const auto& input : invalid)
157
+ {
158
+ VERIFY_THROWS(validation::GetDurationNanosFromString(input, L"health-interval"), ArgumentException);
159
+ }
160
+ }
161
+
162
+ TEST_METHOD(Duration_Validator)
163
+ {
164
+ VERIFY_NO_THROW(validation::ValidateDuration({L"30s", L"1m30s", L"0"}, L"health-interval"));
165
+ VERIFY_THROWS(validation::ValidateDuration({L"30s", L"bad"}, L"health-interval"), ArgumentException);
166
+ }
167
};
168
169
} // namespace WSLCCLIResourceLimitsParserUnitTests
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+134
-35
@@ -821,6 +821,97 @@ class WSLCE2EContainerCreateTests
821
}
822
}
823
824
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_HealthCheck)
825
+ {
826
+ // All health-check options are forwarded to the container configuration.
827
+ {
828
+ auto result = RunWslc(std::format(
829
+ LR"(container create --health-cmd "exit 0" --health-interval 5s --health-timeout 3s --health-retries 2 --health-start-period 1s --name {} {})",
830
+ WslcContainerName,
831
+ DebianImage.NameAndTag()));
832
+ result.Verify({.Stderr = L"", .ExitCode = 0});
833
+
834
+ const auto inspect = InspectContainer(WslcContainerName);
835
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
836
+
837
+ const auto& health = inspect.Config.Healthcheck.value();
838
+ VERIFY_IS_TRUE(health.Test.has_value());
839
+ const std::vector<std::string> expectedTest{"CMD-SHELL", "exit 0"};
840
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
841
+
842
+ // Durations are reported in nanoseconds.
843
+ VERIFY_IS_TRUE(health.Interval.has_value());
844
+ VERIFY_ARE_EQUAL(5'000'000'000LL, health.Interval.value());
845
+ VERIFY_IS_TRUE(health.Timeout.has_value());
846
+ VERIFY_ARE_EQUAL(3'000'000'000LL, health.Timeout.value());
847
+ VERIFY_IS_TRUE(health.StartPeriod.has_value());
848
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value());
849
+ VERIFY_IS_TRUE(health.Retries.has_value());
850
+ VERIFY_ARE_EQUAL(2, health.Retries.value());
851
+
852
+ EnsureContainerDoesNotExist(WslcContainerName);
853
+ }
854
+
855
+ // Only --health-cmd: the command is forwarded, other fields fall back to the default.
856
+ {
857
+ auto result = RunWslc(
858
+ std::format(LR"(container create --health-cmd "exit 1" --name {} {})", WslcContainerName, DebianImage.NameAndTag()));
859
+ result.Verify({.Stderr = L"", .ExitCode = 0});
860
+
861
+ const auto inspect = InspectContainer(WslcContainerName);
862
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
863
+
864
+ const auto& health = inspect.Config.Healthcheck.value();
865
+ VERIFY_IS_TRUE(health.Test.has_value());
866
+ const std::vector<std::string> expectedTest{"CMD-SHELL", "exit 1"};
867
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
868
+
869
+ EnsureContainerDoesNotExist(WslcContainerName);
870
+ }
871
+
872
+ // When no health option is specified, no health check is forwarded.
873
+ {
874
+ auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
875
+ result.Verify({.Stderr = L"", .ExitCode = 0});
876
+
877
+ const auto inspect = InspectContainer(WslcContainerName);
878
+ VERIFY_IS_FALSE(inspect.Config.Healthcheck.has_value());
879
+ EnsureContainerDoesNotExist(WslcContainerName);
880
+ }
881
+ }
882
+
883
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_HealthCheck_Invalid)
884
+ {
885
+ {
886
+ auto result = RunWslc(std::format(
887
+ L"container create --health-interval notaduration --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
888
+ result.Verify(
889
+ {.Stderr = L"Invalid health-interval argument value: 'notaduration'. Expected a duration (e.g. 30s, 1m30s)\r\n", .ExitCode = 1});
890
+ VerifyContainerIsNotListed(WslcContainerName);
891
+ }
892
+
893
+ {
894
+ auto result =
895
+ RunWslc(std::format(L"container create --health-retries abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
896
+ result.Verify({.Stderr = L"Invalid health-retries argument value: abc\r\n", .ExitCode = 1});
897
+ VerifyContainerIsNotListed(WslcContainerName);
898
+ }
899
+
900
+ {
901
+ auto result = RunWslc(std::format(
902
+ LR"(container create --no-healthcheck --health-cmd "exit 0" --name {} {})", WslcContainerName, DebianImage.NameAndTag()));
903
+ result.Verify({.Stderr = L"The --no-healthcheck option cannot be combined with other health check options.\r\n", .ExitCode = 1});
904
+ VerifyContainerIsNotListed(WslcContainerName);
905
+ }
906
+
907
+ {
908
+ auto result = RunWslc(std::format(
909
+ L"container create --no-healthcheck --health-interval 5s --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
910
+ result.Verify({.Stderr = L"The --no-healthcheck option cannot be combined with other health check options.\r\n", .ExitCode = 1});
911
+ VerifyContainerIsNotListed(WslcContainerName);
912
+ }
913
+ }
914
+
915
WSLC_TEST_METHOD(WSLCE2E_Container_Create_StopSignal_Invalid)
916
{
917
{
@@ -1260,47 +1351,55 @@ private:
1351
{
1352
std::wstringstream commands;
1353
commands << L"The following arguments are available:\r\n"
1263
- << L" image Image name\r\n"
1264
- << L" command The command to run\r\n"
1265
- << L" arguments Arguments to pass to container's init process\r\n\r\n";
1354
+ << L" image Image name\r\n"
1355
+ << L" command The command to run\r\n"
1356
+ << L" arguments Arguments to pass to container's init process\r\n\r\n";
1357
return commands.str();
1358
}
1359
1360
std::wstring GetAvailableOptions() const
1361
{
1362
std::wstringstream options;
1272
- options << L"The following options are available:\r\n" //
1273
- << L" --cidfile Write the container ID to the provided path\r\n"
1274
- << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
1275
- << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
1276
- << L" --dns-option Set DNS options\r\n"
1277
- << L" --dns-search Set DNS search domains\r\n"
1278
- << L" --domainname Container domain name\r\n"
1279
- << L" --entrypoint Specifies the container init process executable\r\n"
1280
- << L" -e,--env Key=Value pairs for environment variables\r\n"
1281
- << L" --env-file File containing key=value pairs of env variables\r\n"
1282
- << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
1283
- << L" -h,--hostname Container host name\r\n"
1284
- << L" -i,--interactive Attach to stdin and keep it open\r\n"
1285
- << L" -l,--label Set metadata on an object\r\n"
1286
- << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
1287
- << L" --name Name of the container\r\n"
1288
- << L" --network Connect a container to a network\r\n"
1289
- << L" --network-alias Add a network-scoped alias for the container\r\n"
1290
- << L" -p,--publish Publish a port from a container to host\r\n"
1291
- << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
1292
- << L" --rm Remove the container after it stops\r\n"
1293
- << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
1294
- << L" --stop-signal Signal to stop the container\r\n"
1295
- << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
1296
- << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
1297
- << L" -t,--tty Open a TTY with the container process.\r\n"
1298
- << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
1299
- << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
1300
- << L" -v,--volume Bind mount a volume to the container\r\n"
1301
- << L" -w,--workdir Working directory inside the container\r\n"
1302
- << L" -?,--help Shows help about the selected command\r\n"
1303
- << L"\r\n";
1363
+ options
1364
+ << L"The following options are available:\r\n" //
1365
+ << L" --cidfile Write the container ID to the provided path\r\n"
1366
+ << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
1367
+ << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
1368
+ << L" --dns-option Set DNS options\r\n"
1369
+ << L" --dns-search Set DNS search domains\r\n"
1370
+ << L" --domainname Container domain name\r\n"
1371
+ << L" --entrypoint Specifies the container init process executable\r\n"
1372
+ << L" -e,--env Key=Value pairs for environment variables\r\n"
1373
+ << L" --env-file File containing key=value pairs of env variables\r\n"
1374
+ << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
1375
+ << L" --health-cmd Command to run to check container health\r\n"
1376
+ << L" --health-interval Time between running the health check (e.g. 30s, 1m30s)\r\n"
1377
+ << L" --health-retries Consecutive failures needed to report the container as unhealthy\r\n"
1378
+ << L" --health-start-period Start period for the container to initialize before health-check countdown (e.g. 30s, "
1379
+ L"1m30s)\r\n"
1380
+ << L" --health-timeout Maximum time to allow one health check to run (e.g. 30s, 1m30s)\r\n"
1381
+ << L" -h,--hostname Container host name\r\n"
1382
+ << L" -i,--interactive Attach to stdin and keep it open\r\n"
1383
+ << L" -l,--label Set metadata on an object\r\n"
1384
+ << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
1385
+ << L" --name Name of the container\r\n"
1386
+ << L" --network Connect a container to a network\r\n"
1387
+ << L" --network-alias Add a network-scoped alias for the container\r\n"
1388
+ << L" --no-healthcheck Disable any container-specified health check\r\n"
1389
+ << L" -p,--publish Publish a port from a container to host\r\n"
1390
+ << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
1391
+ << L" --rm Remove the container after it stops\r\n"
1392
+ << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
1393
+ << L" --stop-signal Signal to stop the container\r\n"
1394
+ << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
1395
+ << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
1396
+ << L" -t,--tty Open a TTY with the container process.\r\n"
1397
+ << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
1398
+ << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
1399
+ << L" -v,--volume Bind mount a volume to the container\r\n"
1400
+ << L" -w,--workdir Working directory inside the container\r\n"
1401
+ << L" -?,--help Shows help about the selected command\r\n"
1402
+ << L"\r\n";
1403
return options.str();
1404
}
1405
};
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+140
-36
@@ -1116,6 +1116,102 @@ class WSLCE2EContainerRunTests
1116
}
1117
}
1118
1119
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthCheck)
1120
+ {
1121
+ // All health-check options are forwarded to the container configuration.
1122
+ {
1123
+ auto result = RunWslc(std::format(
1124
+ LR"(container run -d --health-cmd "exit 0" --health-interval 5s --health-timeout 3s --health-retries 2 --health-start-period 1s --name {} {} sleep infinity)",
1125
+ WslcContainerName,
1126
+ DebianImage.NameAndTag()));
1127
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1128
+
1129
+ const auto inspect = InspectContainer(WslcContainerName);
1130
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
1131
+
1132
+ const auto& health = inspect.Config.Healthcheck.value();
1133
+ VERIFY_IS_TRUE(health.Test.has_value());
1134
+ const std::vector<std::string> expectedTest{"CMD-SHELL", "exit 0"};
1135
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
1136
+
1137
+ // Durations are reported in nanoseconds.
1138
+ VERIFY_ARE_EQUAL(5'000'000'000LL, health.Interval.value_or(0));
1139
+ VERIFY_ARE_EQUAL(3'000'000'000LL, health.Timeout.value_or(0));
1140
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value_or(0));
1141
+ VERIFY_ARE_EQUAL(2, health.Retries.value_or(0));
1142
+ EnsureContainerDoesNotExist(WslcContainerName);
1143
+ }
1144
+
1145
+ // When no health option is specified, no health check is forwarded.
1146
+ {
1147
+ auto result =
1148
+ RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
1149
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1150
+
1151
+ const auto inspect = InspectContainer(WslcContainerName);
1152
+ VERIFY_IS_FALSE(inspect.Config.Healthcheck.has_value());
1153
+ EnsureContainerDoesNotExist(WslcContainerName);
1154
+ }
1155
+ }
1156
+
1157
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthCheck_Invalid)
1158
+ {
1159
+ auto result = RunWslc(
1160
+ std::format(L"container run --rm --health-timeout invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
1161
+ result.Verify({.Stderr = L"Invalid health-timeout argument value: 'invalid'. Expected a duration (e.g. 30s, 1m30s)\r\n", .ExitCode = 1});
1162
+ EnsureContainerDoesNotExist(WslcContainerName);
1163
+ }
1164
+
1165
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Healthy)
1166
+ {
1167
+ // A health check that always succeeds should drive the container to the "healthy" state.
1168
+ auto result = RunWslc(std::format(
1169
+ LR"(container run -d --health-cmd "exit 0" --health-interval 1s --health-timeout 3s --health-retries 1 --name {} {} sleep infinity)",
1170
+ WslcContainerName,
1171
+ DebianImage.NameAndTag()));
1172
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1173
+
1174
+ const auto health = WaitForContainerHealth(WslcContainerName, "healthy");
1175
+ VERIFY_ARE_EQUAL(0, health.FailingStreak);
1176
+ VERIFY_IS_FALSE(health.Log.empty());
1177
+ VERIFY_ARE_EQUAL(0, health.Log.back().ExitCode);
1178
+
1179
+ EnsureContainerDoesNotExist(WslcContainerName);
1180
+ }
1181
+
1182
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Unhealthy)
1183
+ {
1184
+ // A health check that always fails should drive the container to the "unhealthy" state.
1185
+ auto result = RunWslc(std::format(
1186
+ LR"(container run -d --health-cmd "exit 1" --health-interval 1s --health-timeout 3s --health-retries 1 --name {} {} sleep infinity)",
1187
+ WslcContainerName,
1188
+ DebianImage.NameAndTag()));
1189
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1190
+
1191
+ const auto health = WaitForContainerHealth(WslcContainerName, "unhealthy");
1192
+ VERIFY_IS_TRUE(health.FailingStreak >= 1);
1193
+ VERIFY_IS_FALSE(health.Log.empty());
1194
+ VERIFY_ARE_EQUAL(1, health.Log.back().ExitCode);
1195
+
1196
+ EnsureContainerDoesNotExist(WslcContainerName);
1197
+ }
1198
+
1199
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Timeout)
1200
+ {
1201
+ auto result = RunWslc(std::format(
1202
+ LR"(container run -d --health-cmd "sleep 30" --health-interval 1s --health-timeout 1s --health-retries 1 --name {} {} sleep infinity)",
1203
+ WslcContainerName,
1204
+ DebianImage.NameAndTag()));
1205
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1206
+
1207
+ const auto health = WaitForContainerHealth(WslcContainerName, "unhealthy");
1208
+ VERIFY_IS_TRUE(health.FailingStreak >= 1);
1209
+ VERIFY_IS_FALSE(health.Log.empty());
1210
+ VERIFY_ARE_EQUAL(-1, health.Log.back().ExitCode);
1211
+
1212
+ EnsureContainerDoesNotExist(WslcContainerName);
1213
+ }
1214
+
1215
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus)
1216
{
1217
auto result = RunWslc(std::format(L"container run --name {} --cpus 1.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
@@ -1264,9 +1360,9 @@ private:
1360
{
1361
std::wstringstream commands;
1362
commands << L"The following arguments are available:\r\n"
1267
- << L" image Image name\r\n"
1268
- << L" command The command to run\r\n"
1269
- << L" arguments Arguments to pass to container's init process\r\n"
1363
+ << L" image Image name\r\n"
1364
+ << L" command The command to run\r\n"
1365
+ << L" arguments Arguments to pass to container's init process\r\n"
1366
<< L"\r\n";
1367
return commands.str();
1368
}
@@ -1274,39 +1370,47 @@ private:
1370
std::wstring GetAvailableOptions() const
1371
{
1372
std::wstringstream options;
1277
- options << L"The following options are available:\r\n"
1278
- << L" --cidfile Write the container ID to the provided path\r\n"
1279
- << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
1280
- << L" -d,--detach Run container in detached mode\r\n"
1281
- << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
1282
- << L" --dns-option Set DNS options\r\n"
1283
- << L" --dns-search Set DNS search domains\r\n"
1284
- << L" --domainname Container domain name\r\n"
1285
- << L" --entrypoint Specifies the container init process executable\r\n"
1286
- << L" -e,--env Key=Value pairs for environment variables\r\n"
1287
- << L" --env-file File containing key=value pairs of env variables\r\n"
1288
- << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
1289
- << L" -h,--hostname Container host name\r\n"
1290
- << L" -i,--interactive Attach to stdin and keep it open\r\n"
1291
- << L" -l,--label Set metadata on an object\r\n"
1292
- << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
1293
- << L" --name Name of the container\r\n"
1294
- << L" --network Connect a container to a network\r\n"
1295
- << L" --network-alias Add a network-scoped alias for the container\r\n"
1296
- << L" -p,--publish Publish a port from a container to host\r\n"
1297
- << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
1298
- << L" --rm Remove the container after it stops\r\n"
1299
- << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
1300
- << L" --stop-signal Signal to stop the container\r\n"
1301
- << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
1302
- << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
1303
- << L" -t,--tty Open a TTY with the container process.\r\n"
1304
- << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
1305
- << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
1306
- << L" -v,--volume Bind mount a volume to the container\r\n"
1307
- << L" -w,--workdir Working directory inside the container\r\n"
1308
- << L" -?,--help Shows help about the selected command\r\n"
1309
- << L"\r\n";
1373
+ options
1374
+ << L"The following options are available:\r\n"
1375
+ << L" --cidfile Write the container ID to the provided path\r\n"
1376
+ << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
1377
+ << L" -d,--detach Run container in detached mode\r\n"
1378
+ << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
1379
+ << L" --dns-option Set DNS options\r\n"
1380
+ << L" --dns-search Set DNS search domains\r\n"
1381
+ << L" --domainname Container domain name\r\n"
1382
+ << L" --entrypoint Specifies the container init process executable\r\n"
1383
+ << L" -e,--env Key=Value pairs for environment variables\r\n"
1384
+ << L" --env-file File containing key=value pairs of env variables\r\n"
1385
+ << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
1386
+ << L" --health-cmd Command to run to check container health\r\n"
1387
+ << L" --health-interval Time between running the health check (e.g. 30s, 1m30s)\r\n"
1388
+ << L" --health-retries Consecutive failures needed to report the container as unhealthy\r\n"
1389
+ << L" --health-start-period Start period for the container to initialize before health-check countdown (e.g. 30s, "
1390
+ L"1m30s)\r\n"
1391
+ << L" --health-timeout Maximum time to allow one health check to run (e.g. 30s, 1m30s)\r\n"
1392
+ << L" -h,--hostname Container host name\r\n"
1393
+ << L" -i,--interactive Attach to stdin and keep it open\r\n"
1394
+ << L" -l,--label Set metadata on an object\r\n"
1395
+ << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
1396
+ << L" --name Name of the container\r\n"
1397
+ << L" --network Connect a container to a network\r\n"
1398
+ << L" --network-alias Add a network-scoped alias for the container\r\n"
1399
+ << L" --no-healthcheck Disable any container-specified health check\r\n"
1400
+ << L" -p,--publish Publish a port from a container to host\r\n"
1401
+ << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
1402
+ << L" --rm Remove the container after it stops\r\n"
1403
+ << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
1404
+ << L" --stop-signal Signal to stop the container\r\n"
1405
+ << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
1406
+ << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
1407
+ << L" -t,--tty Open a TTY with the container process.\r\n"
1408
+ << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
1409
+ << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
1410
+ << L" -v,--volume Bind mount a volume to the container\r\n"
1411
+ << L" -w,--workdir Working directory inside the container\r\n"
1412
+ << L" -?,--help Shows help about the selected command\r\n"
1413
+ << L"\r\n";
1414
return options.str();
1415
}
1416
};
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+28
@@ -288,6 +288,34 @@ wslc_schema::InspectContainer InspectContainer(const std::wstring& containerName
288
return inspectData[0];
289
}
290
291
+wslc_schema::Health WaitForContainerHealth(const std::wstring& containerName, const std::string_view& expectedStatus, std::chrono::milliseconds timeout)
292
+{
293
+ try
294
+ {
295
+ return wsl::shared::retry::RetryWithTimeout<wslc_schema::Health>(
296
+ [&]() {
297
+ const auto inspect = InspectContainer(containerName);
298
+ THROW_HR_IF(E_FAIL, !inspect.State.Health.has_value());
299
+ THROW_HR_IF(E_FAIL, inspect.State.Health->Status != expectedStatus);
300
+ return inspect.State.Health.value();
301
+ },
302
+ std::chrono::seconds(1),
303
+ timeout);
304
+ }
305
+ catch (...)
306
+ {
307
+ const auto inspect = InspectContainer(containerName);
308
+ const std::string actual = inspect.State.Health.has_value() ? inspect.State.Health->Status : "<none>";
309
+ VERIFY_FAIL(std::format(
310
+ L"Container '{}' did not reach health status '{}' (last status: '{}')",
311
+ containerName,
312
+ wsl::shared::string::MultiByteToWide(std::string(expectedStatus)),
313
+ wsl::shared::string::MultiByteToWide(actual))
314
+ .c_str());
315
+ throw;
316
+ }
317
+}
318
+
319
wslc_schema::InspectImage InspectImage(const std::wstring& imageName)
320
{
321
auto result = RunWslc(std::format(L"image inspect {}", imageName));
test/windows/wslc/e2e/WSLCE2EHelpers.h
+3
@@ -155,6 +155,9 @@ std::string SendUdpAndReceive(uint16_t hostPort, const std::string& payload, con
155
156
void WaitForContainerOutput(const std::wstring& containerName, std::string_view expected, std::chrono::milliseconds timeout = std::chrono::seconds(60));
157
158
+wsl::windows::common::wslc_schema::Health WaitForContainerHealth(
159
+ const std::wstring& containerName, const std::string_view& expectedStatus, std::chrono::milliseconds timeout = std::chrono::seconds(120));
160
+
161
// Default timeout of 0 will execute once.
162
template <typename IntervalRep, typename IntervalPeriod, typename TimeoutRep, typename TimeoutPeriod>
163
void VerifyContainerIsNotListed(