diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index 9be32f8f6..70c1f6568 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2849,6 +2849,24 @@ On first run, creates the file with all settings commented out at their defaults
Invalid {} value: '{}'. Only 'all' is supported.
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+ Command to run to check container health
+
+
+ Time between running the health check (e.g. 30s, 1m30s)
+ {Locked="30s"}{Locked="1m30s"}Command line argument example values should not be translated
+
+
+ Consecutive failures needed to report the container as unhealthy
+
+
+ Start period for the container to initialize before health-check countdown (e.g. 30s, 1m30s)
+ {Locked="30s"}{Locked="1m30s"}Command line argument example values should not be translated
+
+
+ Maximum time to allow one health check to run (e.g. 30s, 1m30s)
+ {Locked="30s"}{Locked="1m30s"}Command line argument example values should not be translated
+
Delete images even if they are being used
@@ -2906,6 +2924,9 @@ On first run, creates the file with all settings commented out at their defaults
Do not use cache when building the image
+
+ Disable any container-specified health check
+
Do not delete untagged parents
@@ -3086,6 +3107,14 @@ On first run, creates the file with all settings commented out at their defaults
Invalid {} argument value: '{}'. Expected a memory size (e.g. 256M, 1G)
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="256M"}{Locked="1G"}
+
+ Invalid {} argument value: '{}'. Expected a duration (e.g. 30s, 1m30s)
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="30s"}{Locked="1m30s"}
+
+
+ The --no-healthcheck option cannot be combined with other health check options.
+ {Locked="--no-healthcheck "}Command line arguments, file names and string inserts should not be translated
+
CID file '{}' already exists
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp
index cdbb50a0c..77b98cdd4 100644
--- a/src/windows/common/WSLCContainerLauncher.cpp
+++ b/src/windows/common/WSLCContainerLauncher.cpp
@@ -139,6 +139,36 @@ void WSLCContainerLauncher::SetShmSize(int64_t ShmSize)
m_shmSize = ShmSize;
}
+void WSLCContainerLauncher::SetHealthCmd(std::string&& HealthCmd)
+{
+ m_healthCmd = std::move(HealthCmd);
+}
+
+void WSLCContainerLauncher::SetHealthInterval(int64_t Nanoseconds)
+{
+ m_healthInterval = Nanoseconds;
+}
+
+void WSLCContainerLauncher::SetHealthTimeout(int64_t Nanoseconds)
+{
+ m_healthTimeout = Nanoseconds;
+}
+
+void WSLCContainerLauncher::SetHealthStartPeriod(int64_t Nanoseconds)
+{
+ m_healthStartPeriod = Nanoseconds;
+}
+
+void WSLCContainerLauncher::SetHealthRetries(LONG Retries)
+{
+ m_healthRetries = Retries;
+}
+
+void WSLCContainerLauncher::SetNoHealthcheck()
+{
+ WI_SetFlag(m_containerFlags, WSLCContainerFlagsNoHealthCheck);
+}
+
void WSLCContainerLauncher::SetEntrypoint(std::vector&& entrypoint)
{
m_entrypoint = std::move(entrypoint);
@@ -309,6 +339,21 @@ std::pair> WSLCContainerLauncher::C
options.ShmSize = m_shmSize;
+ if (m_healthCmd.has_value() || m_healthInterval.has_value() || m_healthTimeout.has_value() ||
+ m_healthStartPeriod.has_value() || m_healthRetries.has_value())
+ {
+ if (m_healthCmd.has_value())
+ {
+ options.HealthCmd = m_healthCmd->c_str();
+ }
+
+ options.HealthIntervalNs = m_healthInterval.value_or(0);
+ options.HealthTimeoutNs = m_healthTimeout.value_or(0);
+ options.HealthStartPeriodNs = m_healthStartPeriod.value_or(0);
+ options.HealthRetries = m_healthRetries.value_or(0);
+ WI_SetFlag(options.Flags, WSLCContainerFlagsHealthCheck);
+ }
+
if (!entrypointStorage.empty())
{
options.Entrypoint = {entrypointStorage.data(), static_cast(entrypointStorage.size())};
diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h
index 0cde0d926..0e9a63f42 100644
--- a/src/windows/common/WSLCContainerLauncher.h
+++ b/src/windows/common/WSLCContainerLauncher.h
@@ -77,6 +77,12 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
void SetDefaultStopSignal(WSLCSignal Signal);
void SetStopTimeout(LONG Timeout);
void SetShmSize(int64_t ShmSize);
+ void SetHealthCmd(std::string&& HealthCmd);
+ void SetHealthInterval(int64_t Nanoseconds);
+ void SetHealthTimeout(int64_t Nanoseconds);
+ void SetHealthStartPeriod(int64_t Nanoseconds);
+ void SetHealthRetries(LONG Retries);
+ void SetNoHealthcheck();
void SetContainerFlags(WSLCContainerFlags Flags);
void SetHostname(std::string&& Hostname);
void SetDomainname(std::string&& Domainame);
@@ -106,6 +112,11 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
WSLCSignal m_stopSignal = WSLCSignalNone;
std::optional m_stopTimeout;
int64_t m_shmSize = 0;
+ std::optional m_healthCmd;
+ std::optional m_healthInterval;
+ std::optional m_healthTimeout;
+ std::optional m_healthStartPeriod;
+ std::optional m_healthRetries;
WSLCContainerFlags m_containerFlags = WSLCContainerFlagsNone;
std::string m_hostname;
std::string m_domainname;
diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h
index a73f52727..87b383a82 100644
--- a/src/windows/inc/docker_schema.h
+++ b/src/windows/inc/docker_schema.h
@@ -284,6 +284,17 @@ struct NetworkSettings
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkSettings, Networks);
};
+struct HealthConfig
+{
+ std::optional> Test;
+ std::optional Interval;
+ std::optional Timeout;
+ std::optional StartPeriod;
+ std::optional Retries;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HealthConfig, Test, Interval, Timeout, StartPeriod, Retries);
+};
+
struct CreateContainer
{
using TResponse = CreatedContainer;
@@ -306,11 +317,31 @@ struct CreateContainer
std::vector Env;
std::map ExposedPorts;
std::map Labels;
+ std::optional Healthcheck;
HostConfig HostConfig;
NetworkingConfig NetworkingConfig;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(
- CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, StopTimeout, WorkingDir, User, Hostname, Domainname, Labels, NetworkingConfig);
+ CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, StopTimeout, WorkingDir, User, Hostname, Domainname, Labels, Healthcheck, NetworkingConfig);
+};
+
+struct HealthcheckResult
+{
+ std::string Start;
+ std::string End;
+ int ExitCode{};
+ std::string Output;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HealthcheckResult, Start, End, ExitCode, Output);
+};
+
+struct Health
+{
+ std::string Status;
+ int FailingStreak{};
+ std::vector Log;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Health, Status, FailingStreak, Log);
};
struct ContainerInspectState
@@ -320,8 +351,9 @@ struct ContainerInspectState
int ExitCode{};
std::string StartedAt;
std::string FinishedAt;
+ std::optional Health;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt, Health);
};
struct ContainerConfig
@@ -334,8 +366,9 @@ struct ContainerConfig
std::optional> Entrypoint;
std::optional StopSignal;
std::optional StopTimeout;
+ std::optional Healthcheck;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image, User, WorkingDir, Env, Cmd, Entrypoint, StopSignal, StopTimeout);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image, User, WorkingDir, Env, Cmd, Entrypoint, StopSignal, StopTimeout, Healthcheck);
};
struct InspectMount
diff --git a/src/windows/inc/wslc_schema.h b/src/windows/inc/wslc_schema.h
index 896158b4b..ccbd20a1f 100644
--- a/src/windows/inc/wslc_schema.h
+++ b/src/windows/inc/wslc_schema.h
@@ -39,6 +39,25 @@ struct InspectMount
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectMount, Type, Source, Destination, ReadWrite);
};
+struct HealthcheckResult
+{
+ std::string Start;
+ std::string End;
+ int ExitCode{};
+ std::string Output;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HealthcheckResult, Start, End, ExitCode, Output);
+};
+
+struct Health
+{
+ std::string Status;
+ int FailingStreak{};
+ std::vector Log;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Health, Status, FailingStreak, Log);
+};
+
struct ContainerInspectState
{
std::string Status;
@@ -46,8 +65,9 @@ struct ContainerInspectState
int ExitCode{};
std::string StartedAt;
std::string FinishedAt;
+ std::optional Health;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerInspectState, Status, Running, ExitCode, StartedAt, FinishedAt, Health);
};
struct Ulimit
@@ -69,6 +89,17 @@ struct InspectHostConfig
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectHostConfig, NetworkMode, Memory, NanoCpus, Ulimits);
};
+struct HealthConfig
+{
+ std::optional> Test;
+ std::optional Interval;
+ std::optional Timeout;
+ std::optional StartPeriod;
+ std::optional Retries;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HealthConfig, Test, Interval, Timeout, StartPeriod, Retries);
+};
+
struct ContainerConfig
{
std::optional> Env;
@@ -77,8 +108,9 @@ struct ContainerConfig
std::string User;
std::string WorkingDir;
std::optional StopTimeout;
+ std::optional Healthcheck;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout, Healthcheck);
};
struct InspectEndpointSettings
diff --git a/src/windows/service/inc/WSLCShared.idl b/src/windows/service/inc/WSLCShared.idl
index 14ab5ce62..1aee8ccd1 100644
--- a/src/windows/service/inc/WSLCShared.idl
+++ b/src/windows/service/inc/WSLCShared.idl
@@ -99,9 +99,11 @@ typedef enum _WSLCContainerFlags
WSLCContainerFlagsInit = 4, // Run the container under an init process.
WSLCContainerFlagsPublishAll = 8, // Publish all exposed ports.
WSLCContainerFlagsStopTimeout = 16, // The StopTimeout field is set and should be honored (otherwise StopTimeout is ignored).
+ WSLCContainerFlagsHealthCheck = 32, // The Health* fields are set and should be honored (otherwise they are ignored).
+ WSLCContainerFlagsNoHealthCheck = 64, // Disable any container/image-specified health check.
} WSLCContainerFlags;
-cpp_quote("#define WSLCContainerFlagsValid (WSLCContainerFlagsRm | WSLCContainerFlagsGpu | WSLCContainerFlagsInit | WSLCContainerFlagsPublishAll | WSLCContainerFlagsStopTimeout)")
+cpp_quote("#define WSLCContainerFlagsValid (WSLCContainerFlagsRm | WSLCContainerFlagsGpu | WSLCContainerFlagsInit | WSLCContainerFlagsPublishAll | WSLCContainerFlagsStopTimeout | WSLCContainerFlagsHealthCheck | WSLCContainerFlagsNoHealthCheck)")
cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCContainerFlags);")
diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl
index 7639c4517..6e516f9d6 100644
--- a/src/windows/service/inc/wslc.idl
+++ b/src/windows/service/inc/wslc.idl
@@ -285,6 +285,12 @@ typedef struct _WSLCContainerOptions
// Ignored unless WSLCContainerFlagsStopTimeout is set in Flags.
LONG StopTimeout;
+
+ [unique] LPCSTR HealthCmd;
+ LONGLONG HealthIntervalNs;
+ LONGLONG HealthTimeoutNs;
+ LONGLONG HealthStartPeriodNs;
+ LONG HealthRetries;
} WSLCContainerOptions;
typedef char WSLCContainerId[WSLC_CONTAINER_ID_LENGTH + 1] ;
diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h
index b6d621799..d281c6a5d 100644
--- a/src/windows/wslc/arguments/ArgumentDefinitions.h
+++ b/src/windows/wslc/arguments/ArgumentDefinitions.h
@@ -64,6 +64,11 @@ _(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, L
_(Gateway, "gateway", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkGatewayArgDescription()) \
_(Gpus, "gpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GpusArgDescription()) \
/*_(GroupId, "groupid", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GroupIdArgDescription())*/ \
+_(HealthCmd, "health-cmd", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthCmdArgDescription()) \
+_(HealthInterval, "health-interval", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthIntervalArgDescription()) \
+_(HealthRetries, "health-retries", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthRetriesArgDescription()) \
+_(HealthStartPeriod, "health-start-period", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthStartPeriodArgDescription()) \
+_(HealthTimeout, "health-timeout", NO_ALIAS, Kind::Value, Localization::WSLCCLI_HealthTimeoutArgDescription()) \
_(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, Localization::WSLCCLI_HelpArgDescription()) \
_(Hostname, "hostname", L"h", Kind::Value, Localization::WSLCCLI_HostnameArgDescription()) \
_(ImageForce, "force", L"f", Kind::Flag, Localization::WSLCCLI_ImageForceArgDescription()) \
@@ -83,6 +88,7 @@ _(NetworkName, "network-name", NO_ALIAS, Kind::Positional, L
/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
_(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \
_(NoColor, "no-color", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoColorArgDescription()) \
+_(NoHealthcheck, "no-healthcheck", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoHealthcheckArgDescription()) \
_(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoPruneArgDescription()) \
_(NoTrunc, "no-trunc", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoTruncArgDescription()) \
_(ObjectId, "object-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ObjectIdArgDescription()) \
diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp
index a2e949e74..4b2995e8e 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.cpp
+++ b/src/windows/wslc/arguments/ArgumentValidation.cpp
@@ -19,8 +19,10 @@ Module Name:
#include "ContainerModel.h"
#include "Exceptions.h"
#include "Localization.h"
+#include
#include
#include
+#include
#include
#include
#include
@@ -56,6 +58,31 @@ void Argument::Validate(const ArgMap& execArgs) const
validation::ValidateMemorySize(execArgs.GetAll(), m_name);
break;
+ case ArgType::HealthInterval:
+ validation::ValidateDuration(execArgs.GetAll(), m_name);
+ break;
+
+ case ArgType::HealthTimeout:
+ validation::ValidateDuration(execArgs.GetAll(), m_name);
+ break;
+
+ case ArgType::HealthStartPeriod:
+ validation::ValidateDuration(execArgs.GetAll(), m_name);
+ break;
+
+ case ArgType::HealthRetries:
+ validation::ValidateIntegerFromString(
+ execArgs.GetAll(), m_name, [](int value) { return value >= 0; });
+ break;
+
+ case ArgType::NoHealthcheck:
+ if (execArgs.Contains(ArgType::HealthCmd) || execArgs.Contains(ArgType::HealthInterval) || execArgs.Contains(ArgType::HealthTimeout) ||
+ execArgs.Contains(ArgType::HealthStartPeriod) || execArgs.Contains(ArgType::HealthRetries))
+ {
+ throw ArgumentException(Localization::WSLCCLI_NoHealthcheckConflictError());
+ }
+ break;
+
case ArgType::Memory:
validation::ValidateMemorySize(execArgs.GetAll(), m_name);
break;
@@ -417,6 +444,144 @@ int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& a
return static_cast(parsed.value());
}
+// Parses duration string into nanoseconds.
+static std::optional TryParseDuration(const std::string& input)
+{
+ if (input.empty())
+ {
+ return std::nullopt;
+ }
+
+ size_t pos = 0;
+ bool negative = false;
+ if (input[pos] == '+' || input[pos] == '-')
+ {
+ negative = input[pos] == '-';
+ pos++;
+ }
+
+ // Special case: a bare "0" (with optional sign) is a valid zero duration.
+ if (input.substr(pos) == "0")
+ {
+ return 0;
+ }
+
+ // Accumulate in a long double so fractional units (e.g. "1.5h") are handled, then round.
+ long double totalNanos = 0.0L;
+ bool sawValue = false;
+
+ while (pos < input.size())
+ {
+ // Parse the numeric part (integer and/or fraction).
+ const size_t numberStart = pos;
+ while (pos < input.size() && (std::isdigit(static_cast(input[pos])) || input[pos] == '.'))
+ {
+ pos++;
+ }
+
+ const std::string numberStr = input.substr(numberStart, pos - numberStart);
+ if (numberStr.empty() || numberStr == "." || std::count(numberStr.begin(), numberStr.end(), '.') > 1)
+ {
+ return std::nullopt;
+ }
+
+ // Parse the unit (everything up to the next digit or '.').
+ const size_t unitStart = pos;
+ while (pos < input.size() && !std::isdigit(static_cast(input[pos])) && input[pos] != '.')
+ {
+ pos++;
+ }
+
+ const std::string unit = input.substr(unitStart, pos - unitStart);
+
+ long double multiplier{};
+ if (unit == "ns")
+ {
+ multiplier = 1.0L;
+ }
+ else if (unit == "us" || unit == "\xC2\xB5s" /* µs (U+00B5) */ || unit == "\xCE\xBCs" /* μs (U+03BC) */)
+ {
+ multiplier = 1000L;
+ }
+ else if (unit == "ms")
+ {
+ multiplier = 1000000L;
+ }
+ else if (unit == "s")
+ {
+ multiplier = 1000000000L;
+ }
+ else if (unit == "m")
+ {
+ multiplier = 60000000000L;
+ }
+ else if (unit == "h")
+ {
+ multiplier = 3600000000000L;
+ }
+ else
+ {
+ return std::nullopt;
+ }
+
+ long double value{};
+ try
+ {
+ auto [ptr, ec] = std::from_chars(numberStr.data(), numberStr.data() + numberStr.size(), value, std::chars_format::fixed);
+ if (ptr != numberStr.data() + numberStr.size() || ec != std::errc())
+ {
+ return std::nullopt;
+ }
+ }
+ catch (...)
+ {
+ return std::nullopt;
+ }
+
+ totalNanos += value * multiplier;
+ sawValue = true;
+ }
+
+ if (!sawValue)
+ {
+ return std::nullopt;
+ }
+
+ if (negative)
+ {
+ totalNanos = -totalNanos;
+ }
+
+ if (totalNanos > static_cast(std::numeric_limits::max()) ||
+ totalNanos < static_cast(std::numeric_limits::min()))
+ {
+ return std::nullopt;
+ }
+
+ return static_cast(std::llroundl(totalNanos));
+}
+
+void ValidateDuration(const std::vector& values, const std::wstring& argName)
+{
+ for (const auto& value : values)
+ {
+ std::ignore = GetDurationNanosFromString(value, argName);
+ }
+}
+
+int64_t GetDurationNanosFromString(const std::wstring& input, const std::wstring& argName)
+{
+ const std::string narrow = WideToMultiByte(input);
+ const auto parsed = TryParseDuration(narrow);
+
+ if (!parsed.has_value() || parsed.value() < 0)
+ {
+ throw ArgumentException(Localization::WSLCCLI_InvalidDurationError(argName, input));
+ }
+
+ return parsed.value();
+}
+
void ValidateNanoCpus(const std::vector& values, const std::wstring& argName)
{
for (const auto& value : values)
diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h
index 83e0f4eb4..b25a807e0 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.h
+++ b/src/windows/wslc/arguments/ArgumentValidation.h
@@ -66,6 +66,9 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring
void ValidateMemorySize(const std::vector& values, const std::wstring& argName);
int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName = {});
+void ValidateDuration(const std::vector& values, const std::wstring& argName);
+int64_t GetDurationNanosFromString(const std::wstring& input, const std::wstring& argName = {});
+
void ValidateTimestamp(const std::vector& values, const std::wstring& argName);
ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName = {});
void ValidateNanoCpus(const std::vector& values, const std::wstring& argName);
diff --git a/src/windows/wslc/commands/ContainerCreateCommand.cpp b/src/windows/wslc/commands/ContainerCreateCommand.cpp
index c21d0ecec..114d54dfc 100644
--- a/src/windows/wslc/commands/ContainerCreateCommand.cpp
+++ b/src/windows/wslc/commands/ContainerCreateCommand.cpp
@@ -43,6 +43,11 @@ std::vector ContainerCreateCommand::GetArguments() const
Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
// Argument::Create(ArgType::GroupId),
Argument::Create(ArgType::Gpus),
+ Argument::Create(ArgType::HealthCmd),
+ Argument::Create(ArgType::HealthInterval),
+ Argument::Create(ArgType::HealthRetries),
+ Argument::Create(ArgType::HealthStartPeriod),
+ Argument::Create(ArgType::HealthTimeout),
Argument::Create(ArgType::Hostname),
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Label, false, NO_LIMIT),
@@ -52,6 +57,7 @@ std::vector ContainerCreateCommand::GetArguments() const
Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
// Argument::Create(ArgType::NoDNS),
// Argument::Create(ArgType::Progress),
+ Argument::Create(ArgType::NoHealthcheck),
Argument::Create(ArgType::Publish, false, NO_LIMIT),
Argument::Create(ArgType::PublishAll),
Argument::Create(ArgType::Remove),
diff --git a/src/windows/wslc/commands/ContainerRunCommand.cpp b/src/windows/wslc/commands/ContainerRunCommand.cpp
index 990b5dd8d..1dba19881 100644
--- a/src/windows/wslc/commands/ContainerRunCommand.cpp
+++ b/src/windows/wslc/commands/ContainerRunCommand.cpp
@@ -43,6 +43,11 @@ std::vector ContainerRunCommand::GetArguments() const
Argument::Create(ArgType::Env, false, NO_LIMIT),
Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
Argument::Create(ArgType::Gpus),
+ Argument::Create(ArgType::HealthCmd),
+ Argument::Create(ArgType::HealthInterval),
+ Argument::Create(ArgType::HealthRetries),
+ Argument::Create(ArgType::HealthStartPeriod),
+ Argument::Create(ArgType::HealthTimeout),
Argument::Create(ArgType::Hostname),
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Label, false, NO_LIMIT),
@@ -52,6 +57,7 @@ std::vector ContainerRunCommand::GetArguments() const
Argument::Create(ArgType::NetworkAlias, false, NO_LIMIT),
// Argument::Create(ArgType::NoDNS),
// Argument::Create(ArgType::Progress),
+ Argument::Create(ArgType::NoHealthcheck),
Argument::Create(ArgType::Publish, false, NO_LIMIT),
Argument::Create(ArgType::PublishAll),
// Argument::Create(ArgType::Pull),
diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h
index eb7bd2f1a..4d8ff2cc5 100644
--- a/src/windows/wslc/services/ContainerModel.h
+++ b/src/windows/wslc/services/ContainerModel.h
@@ -40,6 +40,12 @@ struct ContainerOptions
WSLCSignal StopSignal = WSLCSignalNone;
std::optional StopTimeout{};
std::optional ShmSize{};
+ std::optional HealthCmd{};
+ std::optional HealthInterval{}; // nanoseconds
+ std::optional HealthTimeout{}; // nanoseconds
+ std::optional HealthStartPeriod{}; // nanoseconds
+ std::optional HealthRetries{};
+ bool NoHealthcheck = false;
bool Gpu = false;
std::vector Ports;
std::vector Volumes;
diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp
index 9a85ebf98..b0e66fd47 100644
--- a/src/windows/wslc/services/ContainerService.cpp
+++ b/src/windows/wslc/services/ContainerService.cpp
@@ -146,6 +146,36 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
containerLauncher.SetShmSize(options.ShmSize.value());
}
+ if (options.HealthCmd.has_value())
+ {
+ containerLauncher.SetHealthCmd(std::string(options.HealthCmd.value()));
+ }
+
+ if (options.HealthInterval.has_value())
+ {
+ containerLauncher.SetHealthInterval(options.HealthInterval.value());
+ }
+
+ if (options.HealthTimeout.has_value())
+ {
+ containerLauncher.SetHealthTimeout(options.HealthTimeout.value());
+ }
+
+ if (options.HealthStartPeriod.has_value())
+ {
+ containerLauncher.SetHealthStartPeriod(options.HealthStartPeriod.value());
+ }
+
+ if (options.HealthRetries.has_value())
+ {
+ containerLauncher.SetHealthRetries(options.HealthRetries.value());
+ }
+
+ if (options.NoHealthcheck)
+ {
+ containerLauncher.SetNoHealthcheck();
+ }
+
if (options.MemoryBytes.has_value())
{
containerLauncher.SetMemoryLimit(options.MemoryBytes.value());
diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp
index ad7d112d8..9e5146005 100644
--- a/src/windows/wslc/tasks/ContainerTasks.cpp
+++ b/src/windows/wslc/tasks/ContainerTasks.cpp
@@ -453,6 +453,36 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get());
}
+ if (context.Args.Contains(ArgType::HealthCmd))
+ {
+ options.HealthCmd = WideToMultiByte(context.Args.Get());
+ }
+
+ if (context.Args.Contains(ArgType::HealthInterval))
+ {
+ options.HealthInterval = validation::GetDurationNanosFromString(context.Args.Get());
+ }
+
+ if (context.Args.Contains(ArgType::HealthTimeout))
+ {
+ options.HealthTimeout = validation::GetDurationNanosFromString(context.Args.Get());
+ }
+
+ if (context.Args.Contains(ArgType::HealthStartPeriod))
+ {
+ options.HealthStartPeriod = validation::GetDurationNanosFromString(context.Args.Get());
+ }
+
+ if (context.Args.Contains(ArgType::HealthRetries))
+ {
+ options.HealthRetries = validation::GetIntegerFromString(context.Args.Get());
+ }
+
+ if (context.Args.Contains(ArgType::NoHealthcheck))
+ {
+ options.NoHealthcheck = true;
+ }
+
if (context.Args.Contains(ArgType::Memory))
{
options.MemoryBytes = validation::GetMemorySizeFromString(context.Args.Get());
diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp
index 674b0b0cd..ff1e79f5e 100644
--- a/src/windows/wslcsession/WSLCContainer.cpp
+++ b/src/windows/wslcsession/WSLCContainer.cpp
@@ -1297,6 +1297,23 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
wslcInspect.State.StartedAt = dockerInspect.State.StartedAt;
wslcInspect.State.FinishedAt = dockerInspect.State.FinishedAt;
+ if (dockerInspect.State.Health.has_value())
+ {
+ const auto& dockerHealth = dockerInspect.State.Health.value();
+
+ wslc_schema::Health health{};
+ health.Status = dockerHealth.Status;
+ health.FailingStreak = dockerHealth.FailingStreak;
+
+ health.Log.reserve(dockerHealth.Log.size());
+ for (const auto& entry : dockerHealth.Log)
+ {
+ health.Log.push_back({entry.Start, entry.End, entry.ExitCode, entry.Output});
+ }
+
+ wslcInspect.State.Health = std::move(health);
+ }
+
wslcInspect.HostConfig.NetworkMode = dockerInspect.HostConfig.NetworkMode;
wslcInspect.HostConfig.Memory = dockerInspect.HostConfig.Memory;
wslcInspect.HostConfig.NanoCpus = dockerInspect.HostConfig.NanoCpus;
@@ -1317,6 +1334,20 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec
wslcInspect.Config.WorkingDir = dockerInspect.Config.WorkingDir;
wslcInspect.Config.StopTimeout = dockerInspect.Config.StopTimeout;
+ if (dockerInspect.Config.Healthcheck.has_value())
+ {
+ const auto& dockerHealth = dockerInspect.Config.Healthcheck.value();
+
+ wslc_schema::HealthConfig health{};
+ health.Test = dockerHealth.Test;
+ health.Interval = dockerHealth.Interval;
+ health.Timeout = dockerHealth.Timeout;
+ health.StartPeriod = dockerHealth.StartPeriod;
+ health.Retries = dockerHealth.Retries;
+
+ wslcInspect.Config.Healthcheck = std::move(health);
+ }
+
// Map WSLC port mappings (Windows host ports only).
for (const auto& e : m_mappedPorts)
{
@@ -1515,6 +1546,48 @@ std::unique_ptr WSLCContainerImpl::Create(
request.HostConfig.ShmSize = containerOptions.ShmSize;
+ if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsNoHealthCheck))
+ {
+ THROW_HR_IF_MSG(
+ E_INVALIDARG,
+ WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsHealthCheck),
+ "WSLCContainerFlagsHealthCheck and WSLCContainerFlagsNoHealthCheck cannot be combined");
+
+ request.Healthcheck.emplace().Test = std::vector{"NONE"};
+ }
+ else if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsHealthCheck))
+ {
+ common::docker_schema::HealthConfig health{};
+
+ if (containerOptions.HealthCmd != nullptr)
+ {
+ health.Test = std::vector{"CMD-SHELL", containerOptions.HealthCmd};
+ }
+
+ // N.B. '0' will use the default value from the image.
+ if (containerOptions.HealthIntervalNs != 0)
+ {
+ health.Interval = containerOptions.HealthIntervalNs;
+ }
+
+ if (containerOptions.HealthTimeoutNs != 0)
+ {
+ health.Timeout = containerOptions.HealthTimeoutNs;
+ }
+
+ if (containerOptions.HealthStartPeriodNs != 0)
+ {
+ health.StartPeriod = containerOptions.HealthStartPeriodNs;
+ }
+
+ if (containerOptions.HealthRetries != 0)
+ {
+ health.Retries = containerOptions.HealthRetries;
+ }
+
+ request.Healthcheck = std::move(health);
+ }
+
if (containerOptions.VolumesCount > 0)
{
THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes, "Volumes is null with VolumesCount=%lu", containerOptions.VolumesCount);
diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp
index 94fb0ed26..07761bd21 100644
--- a/test/windows/WSLCTests.cpp
+++ b/test/windows/WSLCTests.cpp
@@ -1696,6 +1696,130 @@ class WSLCTests
}
}
+ WSLC_TEST_METHOD(BuildImageHealthCheck)
+ {
+ auto contextDir = std::filesystem::current_path() / "build-context-healthcheck";
+ std::filesystem::create_directories(contextDir);
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-healthcheck:latest", WSLCDeleteImageFlagsForce).first);
+ std::error_code ec;
+ std::filesystem::remove_all(contextDir, ec);
+ });
+
+ // Create an image with a healthcheck that only passes once a specific file exists.
+ constexpr auto c_healthReadyFile = "/tmp/wslc-health-ready";
+
+ {
+ std::ofstream dockerfile(contextDir / "Dockerfile");
+ dockerfile << "FROM debian:latest\n";
+ dockerfile << "HEALTHCHECK --interval=1s --timeout=100ms --start-period=300s --retries=1000 CMD test -f "
+ << c_healthReadyFile << "\n";
+ dockerfile << "CMD [\"sleep\", \"99999\"]\n";
+ }
+
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-healthcheck:latest"));
+ ExpectImagePresent(*m_defaultSession, "wslc-test-healthcheck:latest");
+
+ auto waitForHealthStatus = [](auto& container, const std::string& expectedStatus, std::chrono::seconds timeout) {
+ wsl::shared::retry::RetryWithTimeout(
+ [&]() {
+ const auto inspect = container.Inspect();
+ THROW_HR_IF_MSG(E_FAIL, !inspect.State.Health.has_value(), "container does not report a health status yet");
+ THROW_HR_IF_MSG(
+ E_FAIL,
+ inspect.State.Health->Status != expectedStatus,
+ "health status is '%hs', expected '%hs'",
+ inspect.State.Health->Status.c_str(),
+ expectedStatus.c_str());
+ },
+ std::chrono::milliseconds{100},
+ timeout);
+ };
+
+ // Validate that the image's default health check is inherited by a started container, and that its runtime
+ // status stays "starting" until the health command passes, then deterministically becomes "healthy".
+ {
+ WSLCContainerLauncher launcher("wslc-test-healthcheck:latest", "wslc-healthcheck-test-default");
+ auto container = launcher.Launch(*m_defaultSession);
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
+
+ const auto& health = inspect.Config.Healthcheck.value();
+ VERIFY_IS_TRUE(health.Test.has_value());
+ const std::vector expectedTest{"CMD-SHELL", std::string("test -f ") + c_healthReadyFile};
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.Interval.value_or(0));
+ VERIFY_ARE_EQUAL(100'000'000LL, health.Timeout.value_or(0));
+ VERIFY_ARE_EQUAL(300'000'000'000LL, health.StartPeriod.value_or(0));
+
+ // The health command fails while the file is absent, so the container stays "starting".
+ waitForHealthStatus(container, "starting", 60s);
+
+ auto touchProcess = WSLCProcessLauncher({}, {"/usr/bin/touch", c_healthReadyFile}).Launch(container.Get());
+ ValidateProcessOutput(touchProcess, {}, 0);
+
+ waitForHealthStatus(container, "healthy", 60s);
+ }
+
+ // Validate that the image's default health check can be overridden, and that a failing (exit 1) check drives
+ // the runtime status to "unhealthy".
+ {
+ WSLCContainerLauncher launcher("wslc-test-healthcheck:latest", "wslc-healthcheck-test-override");
+ launcher.SetHealthCmd("exit 1");
+ launcher.SetHealthInterval(1'000'000'000LL); // 1s
+ launcher.SetHealthStartPeriod(1'000'000'000LL); // 1s
+ launcher.SetHealthRetries(1);
+ auto container = launcher.Launch(*m_defaultSession);
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
+
+ const auto& health = inspect.Config.Healthcheck.value();
+ VERIFY_IS_TRUE(health.Test.has_value());
+ const std::vector expectedTest{"CMD-SHELL", "exit 1"};
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.Interval.value_or(0));
+ // The override must set an explicit start period: otherwise the engine merges the image's healthcheck
+ // fields for any zero-valued field (see moby daemon merge()), inheriting the image's 300s start period,
+ // during which failing checks keep the container "starting" instead of transitioning to "unhealthy".
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value_or(0));
+ VERIFY_ARE_EQUAL(1, health.Retries.value_or(0));
+
+ // Validate that the container transitions to "unhealthy" after the health command fails.
+ waitForHealthStatus(container, "unhealthy", 60s);
+ }
+
+ // Validate that WSLCContainerFlagsNoHealthCheck disables the image's default health check.
+ {
+ WSLCContainerLauncher launcher("wslc-test-healthcheck:latest", "wslc-healthcheck-test-disabled");
+ launcher.SetNoHealthcheck();
+ auto container = launcher.Launch(*m_defaultSession);
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
+
+ const auto& health = inspect.Config.Healthcheck.value();
+ VERIFY_IS_TRUE(health.Test.has_value());
+ const std::vector expectedTest{"NONE"};
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
+
+ // A disabled health check is not monitored, so the container never reports a runtime health status.
+ VERIFY_IS_FALSE(inspect.State.Health.has_value());
+ }
+
+ // Validate that combining WSLCContainerFlagsNoHealthCheck with an explicit health check command is rejected.
+ {
+ WSLCContainerLauncher launcher("wslc-test-healthcheck:latest", "wslc-healthcheck-test-conflict");
+ launcher.SetNoHealthcheck();
+ launcher.SetHealthCmd("exit 0");
+
+ auto [result, container] = launcher.CreateNoThrow(*m_defaultSession);
+ VERIFY_ARE_EQUAL(result, E_INVALIDARG);
+ VERIFY_IS_FALSE(container.has_value());
+ }
+ }
+
WSLC_TEST_METHOD(BuildImageWithContext)
{
auto contextDir = std::filesystem::current_path() / "build-context-file";
@@ -5752,7 +5876,7 @@ class WSLCTests
// Invalid container flags are rejected with E_INVALIDARG.
options.Image = "debian:latest";
- options.Flags = static_cast(0x20);
+ options.Flags = static_cast(0x80);
VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateContainer(&options, nullptr, &container));
// Invalid init process flags are rejected with E_INVALIDARG.
@@ -6473,6 +6597,40 @@ class WSLCTests
}
}
+ // Validate that health check options are forwarded to the container configuration.
+ {
+ WSLCContainerLauncher launcher("debian:latest", "test-container-health", {"sleep", "99999"});
+ launcher.SetHealthCmd("exit 0");
+ launcher.SetHealthInterval(5'000'000'000LL); // 5s
+ launcher.SetHealthTimeout(3'000'000'000LL); // 3s
+ launcher.SetHealthStartPeriod(1'000'000'000LL); // 1s
+ launcher.SetHealthRetries(2);
+
+ auto container = launcher.Create(*m_defaultSession);
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
+
+ const auto& health = inspect.Config.Healthcheck.value();
+ VERIFY_IS_TRUE(health.Test.has_value());
+ const std::vector expectedTest{"CMD-SHELL", "exit 0"};
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
+ VERIFY_ARE_EQUAL(5'000'000'000LL, health.Interval.value_or(0));
+ VERIFY_ARE_EQUAL(3'000'000'000LL, health.Timeout.value_or(0));
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value_or(0));
+ VERIFY_ARE_EQUAL(2, health.Retries.value_or(0));
+ }
+
+ // Validate that a container without health options reports no health check.
+ {
+ WSLCContainerLauncher launcher("debian:latest", "test-container-no-health", {"sleep", "99999"});
+
+ auto container = launcher.Create(*m_defaultSession);
+
+ auto inspect = container.Inspect();
+ VERIFY_IS_FALSE(inspect.Config.Healthcheck.has_value());
+ }
+
// Validate that Kill() works as expected
{
WSLCContainerLauncher launcher("debian:latest", "test-container-kill", {"sleep", "99999"}, {});
diff --git a/test/windows/wslc/CommandLineTestCases.h b/test/windows/wslc/CommandLineTestCases.h
index 6ac8329e4..7f8cbebee 100644
--- a/test/windows/wslc/CommandLineTestCases.h
+++ b/test/windows/wslc/CommandLineTestCases.h
@@ -149,6 +149,27 @@ COMMAND_LINE_TEST_CASE(L"create --gpus all ubuntu", L"create", true)
COMMAND_LINE_TEST_CASE(L"container create --gpus all ubuntu sh", L"create", true)
COMMAND_LINE_TEST_CASE(L"create --gpus none ubuntu", L"create", false) // Only 'all' is supported
COMMAND_LINE_TEST_CASE(L"create --gpus", L"create", false) // Missing value for --gpus
+// Health check tests for container run
+COMMAND_LINE_TEST_CASE(L"run --health-cmd \"exit 0\" ubuntu", L"run", true)
+COMMAND_LINE_TEST_CASE(
+ L"run --health-interval 30s --health-timeout 5s --health-retries 3 --health-start-period 10s ubuntu", L"run", true)
+COMMAND_LINE_TEST_CASE(L"run --health-interval 1m30s ubuntu", L"run", true)
+COMMAND_LINE_TEST_CASE(L"run --health-interval notaduration ubuntu", L"run", false) // Invalid duration
+COMMAND_LINE_TEST_CASE(L"run --health-timeout -5s ubuntu", L"run", false) // Negative duration
+COMMAND_LINE_TEST_CASE(L"run --health-retries abc ubuntu", L"run", false) // Non-numeric retries
+COMMAND_LINE_TEST_CASE(L"run --health-retries -1 ubuntu", L"run", false) // Negative retries
+COMMAND_LINE_TEST_CASE(L"run --health-interval ubuntu", L"run", false) // Missing value for --health-interval
+COMMAND_LINE_TEST_CASE(L"run --health-cmd", L"run", false) // Missing value for --health-cmd
+// Health check tests for container create
+COMMAND_LINE_TEST_CASE(L"create --health-cmd \"exit 0\" ubuntu", L"create", true)
+COMMAND_LINE_TEST_CASE(
+ L"container create --health-cmd \"curl -f http://localhost/\" --health-interval 30s --health-timeout 5s --health-retries 3 "
+ L"--health-start-period 10s ubuntu",
+ L"create",
+ true)
+COMMAND_LINE_TEST_CASE(L"create --health-start-period 500ms ubuntu", L"create", true)
+COMMAND_LINE_TEST_CASE(L"create --health-timeout invalid ubuntu", L"create", false) // Invalid duration
+COMMAND_LINE_TEST_CASE(L"create --health-retries 2.5 ubuntu", L"create", false) // Non-integer retries
COMMAND_LINE_TEST_CASE(L"exec cont1 echo Hello", L"exec", true)
COMMAND_LINE_TEST_CASE(L"exec cont1", L"exec", false) // Missing required command argument
COMMAND_LINE_TEST_CASE(L"container exec -it cont1 sh -c \"echo a && echo b\"", L"exec", true) // docker exec example
diff --git a/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp b/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp
index 0943f8117..a8a530875 100644
--- a/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp
@@ -124,6 +124,46 @@ class WSLCCLIResourceLimitsParserUnitTests
VERIFY_NO_THROW(validation::ValidateUlimit({L"nofile=1024", L"core=-1"}, L"ulimit"));
VERIFY_THROWS(validation::ValidateUlimit({L"nofile=1024", L"bad"}, L"ulimit"), ArgumentException);
}
+
+ TEST_METHOD(Duration_Valid)
+ {
+ std::vector> valid = {
+ {L"0", 0LL},
+ {L"0s", 0LL},
+ {L"1ns", 1LL},
+ {L"500ms", 500'000'000LL},
+ {L"30s", 30'000'000'000LL},
+ {L"1m", 60'000'000'000LL},
+ {L"1m30s", 90'000'000'000LL},
+ {L"1h", 3'600'000'000'000LL},
+ {L"1.5h", 5'400'000'000'000LL},
+ {L"2h45m", 9'900'000'000'000LL},
+ {L"100us", 100'000LL},
+ };
+
+ for (const auto& [input, expected] : valid)
+ {
+ const auto actual = validation::GetDurationNanosFromString(input, L"health-interval");
+ VERIFY_ARE_EQUAL(expected, actual);
+ }
+ }
+
+ TEST_METHOD(Duration_Invalid)
+ {
+ const std::vector invalid = {
+ 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"};
+
+ for (const auto& input : invalid)
+ {
+ VERIFY_THROWS(validation::GetDurationNanosFromString(input, L"health-interval"), ArgumentException);
+ }
+ }
+
+ TEST_METHOD(Duration_Validator)
+ {
+ VERIFY_NO_THROW(validation::ValidateDuration({L"30s", L"1m30s", L"0"}, L"health-interval"));
+ VERIFY_THROWS(validation::ValidateDuration({L"30s", L"bad"}, L"health-interval"), ArgumentException);
+ }
};
} // namespace WSLCCLIResourceLimitsParserUnitTests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
index 33e6c89ab..7de665c8e 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
@@ -821,6 +821,97 @@ class WSLCE2EContainerCreateTests
}
}
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_HealthCheck)
+ {
+ // All health-check options are forwarded to the container configuration.
+ {
+ auto result = RunWslc(std::format(
+ LR"(container create --health-cmd "exit 0" --health-interval 5s --health-timeout 3s --health-retries 2 --health-start-period 1s --name {} {})",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
+
+ const auto& health = inspect.Config.Healthcheck.value();
+ VERIFY_IS_TRUE(health.Test.has_value());
+ const std::vector expectedTest{"CMD-SHELL", "exit 0"};
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
+
+ // Durations are reported in nanoseconds.
+ VERIFY_IS_TRUE(health.Interval.has_value());
+ VERIFY_ARE_EQUAL(5'000'000'000LL, health.Interval.value());
+ VERIFY_IS_TRUE(health.Timeout.has_value());
+ VERIFY_ARE_EQUAL(3'000'000'000LL, health.Timeout.value());
+ VERIFY_IS_TRUE(health.StartPeriod.has_value());
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value());
+ VERIFY_IS_TRUE(health.Retries.has_value());
+ VERIFY_ARE_EQUAL(2, health.Retries.value());
+
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ // Only --health-cmd: the command is forwarded, other fields fall back to the default.
+ {
+ auto result = RunWslc(
+ std::format(LR"(container create --health-cmd "exit 1" --name {} {})", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
+
+ const auto& health = inspect.Config.Healthcheck.value();
+ VERIFY_IS_TRUE(health.Test.has_value());
+ const std::vector expectedTest{"CMD-SHELL", "exit 1"};
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
+
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ // When no health option is specified, no health check is forwarded.
+ {
+ auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_IS_FALSE(inspect.Config.Healthcheck.has_value());
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_HealthCheck_Invalid)
+ {
+ {
+ auto result = RunWslc(std::format(
+ L"container create --health-interval notaduration --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify(
+ {.Stderr = L"Invalid health-interval argument value: 'notaduration'. Expected a duration (e.g. 30s, 1m30s)\r\n", .ExitCode = 1});
+ VerifyContainerIsNotListed(WslcContainerName);
+ }
+
+ {
+ auto result =
+ RunWslc(std::format(L"container create --health-retries abc --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"Invalid health-retries argument value: abc\r\n", .ExitCode = 1});
+ VerifyContainerIsNotListed(WslcContainerName);
+ }
+
+ {
+ auto result = RunWslc(std::format(
+ LR"(container create --no-healthcheck --health-cmd "exit 0" --name {} {})", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"The --no-healthcheck option cannot be combined with other health check options.\r\n", .ExitCode = 1});
+ VerifyContainerIsNotListed(WslcContainerName);
+ }
+
+ {
+ auto result = RunWslc(std::format(
+ L"container create --no-healthcheck --health-interval 5s --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"The --no-healthcheck option cannot be combined with other health check options.\r\n", .ExitCode = 1});
+ VerifyContainerIsNotListed(WslcContainerName);
+ }
+ }
+
WSLC_TEST_METHOD(WSLCE2E_Container_Create_StopSignal_Invalid)
{
{
@@ -1260,47 +1351,55 @@ class WSLCE2EContainerCreateTests
{
std::wstringstream commands;
commands << L"The following arguments are available:\r\n"
- << L" image Image name\r\n"
- << L" command The command to run\r\n"
- << L" arguments Arguments to pass to container's init process\r\n\r\n";
+ << L" image Image name\r\n"
+ << L" command The command to run\r\n"
+ << L" arguments Arguments to pass to container's init process\r\n\r\n";
return commands.str();
}
std::wstring GetAvailableOptions() const
{
std::wstringstream options;
- options << L"The following options are available:\r\n" //
- << L" --cidfile Write the container ID to the provided path\r\n"
- << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
- << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
- << L" --dns-option Set DNS options\r\n"
- << L" --dns-search Set DNS search domains\r\n"
- << L" --domainname Container domain name\r\n"
- << L" --entrypoint Specifies the container init process executable\r\n"
- << L" -e,--env Key=Value pairs for environment variables\r\n"
- << L" --env-file File containing key=value pairs of env variables\r\n"
- << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
- << L" -h,--hostname Container host name\r\n"
- << L" -i,--interactive Attach to stdin and keep it open\r\n"
- << L" -l,--label Set metadata on an object\r\n"
- << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
- << L" --name Name of the container\r\n"
- << L" --network Connect a container to a network\r\n"
- << L" --network-alias Add a network-scoped alias for the container\r\n"
- << L" -p,--publish Publish a port from a container to host\r\n"
- << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
- << L" --rm Remove the container after it stops\r\n"
- << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
- << L" --stop-signal Signal to stop the container\r\n"
- << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
- << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
- << L" -t,--tty Open a TTY with the container process.\r\n"
- << L" --ulimit Ulimit options (format: =[:], use -1 for unlimited)\r\n"
- << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
- << L" -v,--volume Bind mount a volume to the container\r\n"
- << L" -w,--workdir Working directory inside the container\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
+ options
+ << L"The following options are available:\r\n" //
+ << L" --cidfile Write the container ID to the provided path\r\n"
+ << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
+ << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
+ << L" --dns-option Set DNS options\r\n"
+ << L" --dns-search Set DNS search domains\r\n"
+ << L" --domainname Container domain name\r\n"
+ << L" --entrypoint Specifies the container init process executable\r\n"
+ << L" -e,--env Key=Value pairs for environment variables\r\n"
+ << L" --env-file File containing key=value pairs of env variables\r\n"
+ << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
+ << L" --health-cmd Command to run to check container health\r\n"
+ << L" --health-interval Time between running the health check (e.g. 30s, 1m30s)\r\n"
+ << L" --health-retries Consecutive failures needed to report the container as unhealthy\r\n"
+ << L" --health-start-period Start period for the container to initialize before health-check countdown (e.g. 30s, "
+ L"1m30s)\r\n"
+ << L" --health-timeout Maximum time to allow one health check to run (e.g. 30s, 1m30s)\r\n"
+ << L" -h,--hostname Container host name\r\n"
+ << L" -i,--interactive Attach to stdin and keep it open\r\n"
+ << L" -l,--label Set metadata on an object\r\n"
+ << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
+ << L" --name Name of the container\r\n"
+ << L" --network Connect a container to a network\r\n"
+ << L" --network-alias Add a network-scoped alias for the container\r\n"
+ << L" --no-healthcheck Disable any container-specified health check\r\n"
+ << L" -p,--publish Publish a port from a container to host\r\n"
+ << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
+ << L" --rm Remove the container after it stops\r\n"
+ << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
+ << L" --stop-signal Signal to stop the container\r\n"
+ << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
+ << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
+ << L" -t,--tty Open a TTY with the container process.\r\n"
+ << L" --ulimit Ulimit options (format: =[:], use -1 for unlimited)\r\n"
+ << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
+ << L" -v,--volume Bind mount a volume to the container\r\n"
+ << L" -w,--workdir Working directory inside the container\r\n"
+ << L" -?,--help Shows help about the selected command\r\n"
+ << L"\r\n";
return options.str();
}
};
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
index 5021e614f..4ad50632f 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
@@ -1116,6 +1116,102 @@ class WSLCE2EContainerRunTests
}
}
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthCheck)
+ {
+ // All health-check options are forwarded to the container configuration.
+ {
+ auto result = RunWslc(std::format(
+ LR"(container run -d --health-cmd "exit 0" --health-interval 5s --health-timeout 3s --health-retries 2 --health-start-period 1s --name {} {} sleep infinity)",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_IS_TRUE(inspect.Config.Healthcheck.has_value());
+
+ const auto& health = inspect.Config.Healthcheck.value();
+ VERIFY_IS_TRUE(health.Test.has_value());
+ const std::vector expectedTest{"CMD-SHELL", "exit 0"};
+ VERIFY_ARE_EQUAL(expectedTest, health.Test.value());
+
+ // Durations are reported in nanoseconds.
+ VERIFY_ARE_EQUAL(5'000'000'000LL, health.Interval.value_or(0));
+ VERIFY_ARE_EQUAL(3'000'000'000LL, health.Timeout.value_or(0));
+ VERIFY_ARE_EQUAL(1'000'000'000LL, health.StartPeriod.value_or(0));
+ VERIFY_ARE_EQUAL(2, health.Retries.value_or(0));
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ // When no health option is specified, no health check is forwarded.
+ {
+ auto result =
+ RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_IS_FALSE(inspect.Config.Healthcheck.has_value());
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthCheck_Invalid)
+ {
+ auto result = RunWslc(
+ std::format(L"container run --rm --health-timeout invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"Invalid health-timeout argument value: 'invalid'. Expected a duration (e.g. 30s, 1m30s)\r\n", .ExitCode = 1});
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Healthy)
+ {
+ // A health check that always succeeds should drive the container to the "healthy" state.
+ auto result = RunWslc(std::format(
+ LR"(container run -d --health-cmd "exit 0" --health-interval 1s --health-timeout 3s --health-retries 1 --name {} {} sleep infinity)",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto health = WaitForContainerHealth(WslcContainerName, "healthy");
+ VERIFY_ARE_EQUAL(0, health.FailingStreak);
+ VERIFY_IS_FALSE(health.Log.empty());
+ VERIFY_ARE_EQUAL(0, health.Log.back().ExitCode);
+
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Unhealthy)
+ {
+ // A health check that always fails should drive the container to the "unhealthy" state.
+ auto result = RunWslc(std::format(
+ LR"(container run -d --health-cmd "exit 1" --health-interval 1s --health-timeout 3s --health-retries 1 --name {} {} sleep infinity)",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto health = WaitForContainerHealth(WslcContainerName, "unhealthy");
+ VERIFY_IS_TRUE(health.FailingStreak >= 1);
+ VERIFY_IS_FALSE(health.Log.empty());
+ VERIFY_ARE_EQUAL(1, health.Log.back().ExitCode);
+
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_HealthStatus_Timeout)
+ {
+ auto result = RunWslc(std::format(
+ LR"(container run -d --health-cmd "sleep 30" --health-interval 1s --health-timeout 1s --health-retries 1 --name {} {} sleep infinity)",
+ WslcContainerName,
+ DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto health = WaitForContainerHealth(WslcContainerName, "unhealthy");
+ VERIFY_IS_TRUE(health.FailingStreak >= 1);
+ VERIFY_IS_FALSE(health.Log.empty());
+ VERIFY_ARE_EQUAL(-1, health.Log.back().ExitCode);
+
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus)
{
auto result = RunWslc(std::format(L"container run --name {} --cpus 1.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
@@ -1264,9 +1360,9 @@ class WSLCE2EContainerRunTests
{
std::wstringstream commands;
commands << L"The following arguments are available:\r\n"
- << L" image Image name\r\n"
- << L" command The command to run\r\n"
- << L" arguments Arguments to pass to container's init process\r\n"
+ << L" image Image name\r\n"
+ << L" command The command to run\r\n"
+ << L" arguments Arguments to pass to container's init process\r\n"
<< L"\r\n";
return commands.str();
}
@@ -1274,39 +1370,47 @@ class WSLCE2EContainerRunTests
std::wstring GetAvailableOptions() const
{
std::wstringstream options;
- options << L"The following options are available:\r\n"
- << L" --cidfile Write the container ID to the provided path\r\n"
- << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
- << L" -d,--detach Run container in detached mode\r\n"
- << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
- << L" --dns-option Set DNS options\r\n"
- << L" --dns-search Set DNS search domains\r\n"
- << L" --domainname Container domain name\r\n"
- << L" --entrypoint Specifies the container init process executable\r\n"
- << L" -e,--env Key=Value pairs for environment variables\r\n"
- << L" --env-file File containing key=value pairs of env variables\r\n"
- << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
- << L" -h,--hostname Container host name\r\n"
- << L" -i,--interactive Attach to stdin and keep it open\r\n"
- << L" -l,--label Set metadata on an object\r\n"
- << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
- << L" --name Name of the container\r\n"
- << L" --network Connect a container to a network\r\n"
- << L" --network-alias Add a network-scoped alias for the container\r\n"
- << L" -p,--publish Publish a port from a container to host\r\n"
- << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
- << L" --rm Remove the container after it stops\r\n"
- << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
- << L" --stop-signal Signal to stop the container\r\n"
- << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
- << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
- << L" -t,--tty Open a TTY with the container process.\r\n"
- << L" --ulimit Ulimit options (format: =[:], use -1 for unlimited)\r\n"
- << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
- << L" -v,--volume Bind mount a volume to the container\r\n"
- << L" -w,--workdir Working directory inside the container\r\n"
- << L" -?,--help Shows help about the selected command\r\n"
- << L"\r\n";
+ options
+ << L"The following options are available:\r\n"
+ << L" --cidfile Write the container ID to the provided path\r\n"
+ << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
+ << L" -d,--detach Run container in detached mode\r\n"
+ << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
+ << L" --dns-option Set DNS options\r\n"
+ << L" --dns-search Set DNS search domains\r\n"
+ << L" --domainname Container domain name\r\n"
+ << L" --entrypoint Specifies the container init process executable\r\n"
+ << L" -e,--env Key=Value pairs for environment variables\r\n"
+ << L" --env-file File containing key=value pairs of env variables\r\n"
+ << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
+ << L" --health-cmd Command to run to check container health\r\n"
+ << L" --health-interval Time between running the health check (e.g. 30s, 1m30s)\r\n"
+ << L" --health-retries Consecutive failures needed to report the container as unhealthy\r\n"
+ << L" --health-start-period Start period for the container to initialize before health-check countdown (e.g. 30s, "
+ L"1m30s)\r\n"
+ << L" --health-timeout Maximum time to allow one health check to run (e.g. 30s, 1m30s)\r\n"
+ << L" -h,--hostname Container host name\r\n"
+ << L" -i,--interactive Attach to stdin and keep it open\r\n"
+ << L" -l,--label Set metadata on an object\r\n"
+ << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
+ << L" --name Name of the container\r\n"
+ << L" --network Connect a container to a network\r\n"
+ << L" --network-alias Add a network-scoped alias for the container\r\n"
+ << L" --no-healthcheck Disable any container-specified health check\r\n"
+ << L" -p,--publish Publish a port from a container to host\r\n"
+ << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
+ << L" --rm Remove the container after it stops\r\n"
+ << L" --shm-size Size of /dev/shm (e.g. 64M, 1G)\r\n"
+ << L" --stop-signal Signal to stop the container\r\n"
+ << L" --stop-timeout Timeout (in seconds) to stop the container before killing it (-1 for no timeout)\r\n"
+ << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
+ << L" -t,--tty Open a TTY with the container process.\r\n"
+ << L" --ulimit Ulimit options (format: =[:], use -1 for unlimited)\r\n"
+ << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
+ << L" -v,--volume Bind mount a volume to the container\r\n"
+ << L" -w,--workdir Working directory inside the container\r\n"
+ << L" -?,--help Shows help about the selected command\r\n"
+ << L"\r\n";
return options.str();
}
};
diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp
index 5d0b228d6..d460aaa6f 100644
--- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp
@@ -288,6 +288,34 @@ wslc_schema::InspectContainer InspectContainer(const std::wstring& containerName
return inspectData[0];
}
+wslc_schema::Health WaitForContainerHealth(const std::wstring& containerName, const std::string_view& expectedStatus, std::chrono::milliseconds timeout)
+{
+ try
+ {
+ return wsl::shared::retry::RetryWithTimeout(
+ [&]() {
+ const auto inspect = InspectContainer(containerName);
+ THROW_HR_IF(E_FAIL, !inspect.State.Health.has_value());
+ THROW_HR_IF(E_FAIL, inspect.State.Health->Status != expectedStatus);
+ return inspect.State.Health.value();
+ },
+ std::chrono::seconds(1),
+ timeout);
+ }
+ catch (...)
+ {
+ const auto inspect = InspectContainer(containerName);
+ const std::string actual = inspect.State.Health.has_value() ? inspect.State.Health->Status : "";
+ VERIFY_FAIL(std::format(
+ L"Container '{}' did not reach health status '{}' (last status: '{}')",
+ containerName,
+ wsl::shared::string::MultiByteToWide(std::string(expectedStatus)),
+ wsl::shared::string::MultiByteToWide(actual))
+ .c_str());
+ throw;
+ }
+}
+
wslc_schema::InspectImage InspectImage(const std::wstring& imageName)
{
auto result = RunWslc(std::format(L"image inspect {}", imageName));
diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h
index d81fa2b33..ab38b2663 100644
--- a/test/windows/wslc/e2e/WSLCE2EHelpers.h
+++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h
@@ -155,6 +155,9 @@ std::string SendUdpAndReceive(uint16_t hostPort, const std::string& payload, con
void WaitForContainerOutput(const std::wstring& containerName, std::string_view expected, std::chrono::milliseconds timeout = std::chrono::seconds(60));
+wsl::windows::common::wslc_schema::Health WaitForContainerHealth(
+ const std::wstring& containerName, const std::string_view& expectedStatus, std::chrono::milliseconds timeout = std::chrono::seconds(120));
+
// Default timeout of 0 will execute once.
template
void VerifyContainerIsNotListed(