@samitouri / QOSAMI-WSL / commits / b3decbdb

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(