diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index d95f6a646..fccec5f14 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2355,6 +2355,10 @@ For privacy information about this product please visit https://aka.ms/privacy.< Unsupported network mode: '{}' {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Invalid stop timeout value: {} + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Additional networks are not allowed when the primary network mode is 'host' or 'none'. {Locked="host"}{Locked="none"}Command line arguments, file names and string inserts should not be translated @@ -2920,6 +2924,9 @@ On first run, creates the file with all settings commented out at their defaults Signal to stop the container + + Timeout (in seconds) to stop the container before killing it (-1 for no timeout) + Size of /dev/shm (e.g. 64M, 1G) {Locked="/dev/shm"}Command line arguments should not be translated diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index e022d7976..cdbb50a0c 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -129,6 +129,11 @@ void WSLCContainerLauncher::SetDefaultStopSignal(WSLCSignal Signal) m_stopSignal = Signal; } +void WSLCContainerLauncher::SetStopTimeout(LONG Timeout) +{ + m_stopTimeout = Timeout; +} + void WSLCContainerLauncher::SetShmSize(int64_t ShmSize) { m_shmSize = ShmSize; @@ -296,6 +301,12 @@ std::pair> WSLCContainerLauncher::C options.PortsCount = static_cast(m_ports.size()); options.StopSignal = m_stopSignal; options.Flags = m_containerFlags; + if (m_stopTimeout.has_value()) + { + options.StopTimeout = m_stopTimeout.value(); + WI_SetFlag(options.Flags, WSLCContainerFlagsStopTimeout); + } + options.ShmSize = m_shmSize; if (!entrypointStorage.empty()) diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h index fa01c59a2..0cde0d926 100644 --- a/src/windows/common/WSLCContainerLauncher.h +++ b/src/windows/common/WSLCContainerLauncher.h @@ -75,6 +75,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher void SetName(std::string&& Name); void SetEntrypoint(std::vector&& entrypoint); void SetDefaultStopSignal(WSLCSignal Signal); + void SetStopTimeout(LONG Timeout); void SetShmSize(int64_t ShmSize); void SetContainerFlags(WSLCContainerFlags Flags); void SetHostname(std::string&& Hostname); @@ -103,6 +104,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher std::string m_networkMode; std::vector m_entrypoint; WSLCSignal m_stopSignal = WSLCSignalNone; + std::optional m_stopTimeout; int64_t m_shmSize = 0; WSLCContainerFlags m_containerFlags = WSLCContainerFlagsNone; std::string m_hostname; diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h index a7e998bc6..fcc796a7a 100644 --- a/src/windows/inc/docker_schema.h +++ b/src/windows/inc/docker_schema.h @@ -297,6 +297,7 @@ struct CreateContainer std::string Hostname; std::string Domainname; std::optional StopSignal; + std::optional StopTimeout; std::optional WorkingDir; std::optional> Cmd; std::optional> Entrypoint; @@ -307,7 +308,7 @@ struct CreateContainer NetworkingConfig NetworkingConfig; NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE( - CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, WorkingDir, User, Hostname, Domainname, Labels, NetworkingConfig); + CreateContainer, Image, Cmd, Tty, OpenStdin, StdinOnce, Entrypoint, Env, ExposedPorts, HostConfig, StopSignal, StopTimeout, WorkingDir, User, Hostname, Domainname, Labels, NetworkingConfig); }; struct ContainerInspectState @@ -329,8 +330,10 @@ struct ContainerConfig std::optional> Env; std::optional> Cmd; std::optional> Entrypoint; + std::optional StopSignal; + std::optional StopTimeout; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image, User, WorkingDir, Env, Cmd, Entrypoint); + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Image, User, WorkingDir, Env, Cmd, Entrypoint, StopSignal, StopTimeout); }; struct InspectMount diff --git a/src/windows/inc/wslc_schema.h b/src/windows/inc/wslc_schema.h index db4354ba3..a936a3355 100644 --- a/src/windows/inc/wslc_schema.h +++ b/src/windows/inc/wslc_schema.h @@ -76,8 +76,9 @@ struct ContainerConfig std::optional> Entrypoint; std::string User; std::string WorkingDir; + std::optional StopTimeout; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir); + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerConfig, Env, Cmd, Entrypoint, User, WorkingDir, StopTimeout); }; struct InspectEndpointSettings diff --git a/src/windows/service/inc/WSLCShared.idl b/src/windows/service/inc/WSLCShared.idl index d02849aa0..39e22eb76 100644 --- a/src/windows/service/inc/WSLCShared.idl +++ b/src/windows/service/inc/WSLCShared.idl @@ -98,9 +98,10 @@ typedef enum _WSLCContainerFlags WSLCContainerFlagsGpu = 2, // Enable GPU access. 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). } WSLCContainerFlags; -cpp_quote("#define WSLCContainerFlagsValid (WSLCContainerFlagsRm | WSLCContainerFlagsGpu | WSLCContainerFlagsInit | WSLCContainerFlagsPublishAll)") +cpp_quote("#define WSLCContainerFlagsValid (WSLCContainerFlagsRm | WSLCContainerFlagsGpu | WSLCContainerFlagsInit | WSLCContainerFlagsPublishAll | WSLCContainerFlagsStopTimeout)") cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCContainerFlags);") diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 147b44ce4..9fc0fa4e5 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -44,6 +44,8 @@ cpp_quote("#define WSLC_CONTAINER_ID_LENGTH 64") cpp_quote("#define WSLC_MAX_BINDING_ADDRESS_LENGTH 45") cpp_quote("#define WSLC_MAX_SAVE_IMAGES_COUNT 256") cpp_quote("#define WSLC_EPHEMERAL_PORT 0") +cpp_quote("#define WSLC_STOP_TIMEOUT_DEFAULT LONG_MIN") // Pass to Stop() to use the default container stop timeout +cpp_quote("#define WSLC_STOP_TIMEOUT_NONE -1") // Wait forever for the container to stop. typedef struct _WSLCVersion { @@ -263,6 +265,9 @@ typedef struct _WSLCContainerOptions LONGLONG NanoCpus; [unique, size_is(UlimitsCount)] const WSLCUlimit* Ulimits; ULONG UlimitsCount; + + // Ignored unless WSLCContainerFlagsStopTimeout is set in Flags. + LONG StopTimeout; } WSLCContainerOptions; typedef char WSLCContainerId[WSLC_CONTAINER_ID_LENGTH + 1] ; @@ -726,7 +731,7 @@ interface IWSLCSessionManager : IUnknown HRESULT OpenSession([in] ULONG Id, [out] IWSLCSession** Session); HRESULT OpenSessionByName([in, unique] LPCWSTR DisplayName, [out] IWSLCSession** Session); } - + // Ensure wslcsdk.h and wslcsdk.idl are also updated. cpp_quote("#define WSLC_E_BASE (0x0600)") cpp_quote("#define WSLC_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 1) /* 0x80040601 */") diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index 200022bcb..c43bb3c91 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -103,6 +103,7 @@ _(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L _(Signal, "signal", L"s", Kind::Value, Localization::WSLCCLI_SignalArgDescription()) \ _(Source, "source", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SourceArgDescription()) \ _(StopSignal, "stop-signal", NO_ALIAS, Kind::Value, Localization::WSLCCLI_StopSignalArgDescription()) \ +_(StopTimeout, "stop-timeout", NO_ALIAS, Kind::Value, Localization::WSLCCLI_StopTimeoutArgDescription()) \ _(Tail, "tail", L"n", Kind::Value, Localization::WSLCCLI_TailArgDescription()) \ _(Tag, "tag", L"t", Kind::Value, Localization::WSLCCLI_TagArgDescription()) \ _(Target, "target", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_TargetArgDescription()) \ diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index 735196f01..fd78795f8 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -48,6 +48,10 @@ void Argument::Validate(const ArgMap& execArgs) const validation::ValidateWSLCSignalFromString(execArgs.GetAll(), m_name); break; + case ArgType::StopTimeout: + validation::ValidateIntegerFromString(execArgs.GetAll(), m_name); + break; + case ArgType::ShmSize: validation::ValidateMemorySize(execArgs.GetAll(), m_name); break; diff --git a/src/windows/wslc/commands/ContainerCreateCommand.cpp b/src/windows/wslc/commands/ContainerCreateCommand.cpp index 1997e8f27..c21d0ecec 100644 --- a/src/windows/wslc/commands/ContainerCreateCommand.cpp +++ b/src/windows/wslc/commands/ContainerCreateCommand.cpp @@ -58,6 +58,7 @@ std::vector ContainerCreateCommand::GetArguments() const // Argument::Create(ArgType::Scheme), Argument::Create(ArgType::ShmSize), Argument::Create(ArgType::StopSignal), + Argument::Create(ArgType::StopTimeout), Argument::Create(ArgType::TMPFS, false, NO_LIMIT), Argument::Create(ArgType::TTY), Argument::Create(ArgType::Ulimit, false, NO_LIMIT), diff --git a/src/windows/wslc/commands/ContainerRunCommand.cpp b/src/windows/wslc/commands/ContainerRunCommand.cpp index c754e4c80..990b5dd8d 100644 --- a/src/windows/wslc/commands/ContainerRunCommand.cpp +++ b/src/windows/wslc/commands/ContainerRunCommand.cpp @@ -59,6 +59,7 @@ std::vector ContainerRunCommand::GetArguments() const // Argument::Create(ArgType::Scheme), Argument::Create(ArgType::ShmSize), Argument::Create(ArgType::StopSignal), + Argument::Create(ArgType::StopTimeout), Argument::Create(ArgType::TMPFS, false, NO_LIMIT), Argument::Create(ArgType::TTY), Argument::Create(ArgType::Ulimit, false, NO_LIMIT), diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index da9d5dbc6..eb7bd2f1a 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -38,6 +38,7 @@ struct ContainerOptions bool TTY = false; bool PublishAll = false; WSLCSignal StopSignal = WSLCSignalNone; + std::optional StopTimeout{}; std::optional ShmSize{}; bool Gpu = false; std::vector Ports; @@ -67,10 +68,8 @@ struct CreateContainerResult struct StopContainerOptions { - static constexpr LONG DefaultTimeout = -1; - WSLCSignal Signal = WSLCSignalNone; - LONG Timeout = DefaultTimeout; + LONG Timeout = WSLC_STOP_TIMEOUT_DEFAULT; }; struct PruneContainersResult diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 529c1039f..9a85ebf98 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -136,6 +136,11 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal( containerLauncher.SetDefaultStopSignal(options.StopSignal); } + if (options.StopTimeout.has_value()) + { + containerLauncher.SetStopTimeout(options.StopTimeout.value()); + } + if (options.ShmSize.has_value()) { containerLauncher.SetShmSize(options.ShmSize.value()); diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index bbd61025a..ad7d112d8 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -443,6 +443,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context) options.StopSignal = validation::GetWSLCSignalFromString(context.Args.Get()); } + if (context.Args.Contains(ArgType::StopTimeout)) + { + options.StopTimeout = validation::GetIntegerFromString(context.Args.Get()); + } + if (context.Args.Contains(ArgType::ShmSize)) { options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get()); diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 9c855a6d6..98b9a3e45 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -302,7 +302,7 @@ void DockerHTTPClient::StartContainer(const std::string& Id, const std::optional Transaction(verb::post, url); } -void DockerHTTPClient::StopContainer(const std::string& Id, std::optional Signal, std::optional TimeoutSeconds) +void DockerHTTPClient::StopContainer(const std::string& Id, std::optional Signal, std::optional TimeoutSeconds) { auto url = URL::Create("/containers/{}/stop", Id); if (Signal.has_value()) diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index fd24a490b..3e837be72 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -126,7 +126,7 @@ class DockerHTTPClient bool all = false, int limit = -1, const std::map>& filters = {}); common::docker_schema::CreatedContainer CreateContainer(const common::docker_schema::CreateContainer& Request, const std::optional& Name); void StartContainer(const std::string& Id, const std::optional& DetachKeys); - void StopContainer(const std::string& Id, std::optional Signal, std::optional TimeoutSeconds); + void StopContainer(const std::string& Id, std::optional Signal, std::optional TimeoutSeconds); void DeleteContainer(const std::string& Id, bool Force, bool DeleteVolumes = false); void SignalContainer(const std::string& Id, std::optional Signal); common::docker_schema::InspectContainer InspectContainer(const std::string& Id); diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 4da373ed4..6618fceea 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -67,6 +67,14 @@ using WslcInspectContainer = wsl::windows::common::wslc_schema::InspectContainer namespace { +void ValidateStopTimeout(LONG TimeoutSeconds) +{ + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, + Localization::MessageWslcInvalidStopTimeout(TimeoutSeconds), + TimeoutSeconds < 0 && TimeoutSeconds != WSLC_STOP_TIMEOUT_NONE && TimeoutSeconds != WSLC_STOP_TIMEOUT_DEFAULT); +} + std::vector StringArrayToVector(const WSLCStringArray& array) { if (array.Count == 0) @@ -944,6 +952,8 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) SignalArg = Signal; } + ValidateStopTimeout(TimeoutSeconds); + // Don't wait for the container to stop if we're not sending SIGKILL, since it may not stop the container. // N.B. If the signal was SIGTERM for instance, we'll receive the stop notification via OnEvent(). bool waitForStop = !Kill || (SignalArg.value_or(WSLCSignalSIGKILL) == WSLCSignalSIGKILL); @@ -961,10 +971,10 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) } else { - std::optional TimeoutArg; - if (TimeoutSeconds >= 0) + std::optional TimeoutArg; + if (TimeoutSeconds != WSLC_STOP_TIMEOUT_DEFAULT) { - TimeoutArg = static_cast(TimeoutSeconds); + TimeoutArg = TimeoutSeconds; } m_dockerClient.StopContainer(m_id, SignalArg, TimeoutArg); @@ -1305,6 +1315,7 @@ WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspec wslcInspect.Config.Entrypoint = dockerInspect.Config.Entrypoint; wslcInspect.Config.User = dockerInspect.Config.User; wslcInspect.Config.WorkingDir = dockerInspect.Config.WorkingDir; + wslcInspect.Config.StopTimeout = dockerInspect.Config.StopTimeout; // Map WSLC port mappings (Windows host ports only). for (const auto& e : m_mappedPorts) @@ -1419,6 +1430,14 @@ std::unique_ptr WSLCContainerImpl::Create( request.StopSignal = std::to_string(containerOptions.StopSignal); } + if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsStopTimeout)) + { + ValidateStopTimeout(containerOptions.StopTimeout); + + THROW_HR_IF(E_INVALIDARG, containerOptions.StopTimeout == WSLC_STOP_TIMEOUT_DEFAULT); + request.StopTimeout = static_cast(containerOptions.StopTimeout); + } + if (containerOptions.InitProcessOptions.CurrentDirectory != nullptr) { request.WorkingDir = containerOptions.InitProcessOptions.CurrentDirectory; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 8baad20ab..09baf22a9 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -5572,7 +5572,7 @@ class WSLCTests // Invalid container flags are rejected with E_INVALIDARG. options.Image = "debian:latest"; - options.Flags = static_cast(0x10); + options.Flags = static_cast(0x20); VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateContainer(&options, nullptr, &container)); // Invalid init process flags are rejected with E_INVALIDARG. @@ -5647,7 +5647,7 @@ class WSLCTests VERIFY_ARE_EQUAL(process.Wait(), WSLCSignalSIGHUP + 128); } - // Validate that the default stop signal can be overriden. + // Validate that the default stop signal can be overridden. { WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-2", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin); launcher.SetDefaultStopSignal(WSLCSignalSIGHUP); @@ -6232,6 +6232,67 @@ class WSLCTests expectContainerList({}); } + // test StopContainer with custom timeouts. + // N.B. We can't validate the actual timeouts since the tests environment will affect container stop times. + { + { + // Create a container with a no stop timeout. + WSLCContainerLauncher launcher("debian:latest", "test-container-stop-timeout-1", {"sleep", "99999"}); + launcher.SetStopTimeout(WSLC_STOP_TIMEOUT_NONE); + + auto container = launcher.Launch(*m_defaultSession); + + auto inspect = container.Inspect(); + VERIFY_ARE_EQUAL(inspect.Config.StopTimeout.value_or(0), WSLC_STOP_TIMEOUT_NONE); + + // Validate that passing '0' as the stop timeout overrides the default + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalNone, 0)); + } + + { + // Create a container with an instant stop timeout. + WSLCContainerLauncher launcher("debian:latest", "test-container-stop-timeout-2", {"sleep", "99999"}); + launcher.SetStopTimeout(0); + + auto container = launcher.Create(*m_defaultSession); + + auto inspect = container.Inspect(); + VERIFY_ARE_EQUAL(inspect.Config.StopTimeout.value_or(-1), 0); + } + + { + // Create a container with an short stop timeout. + WSLCContainerLauncher launcher("debian:latest", "test-container-stop-timeout-3", {"sleep", "99999"}); + launcher.SetStopTimeout(1); + + auto container = launcher.Launch(*m_defaultSession); + + auto inspect = container.Inspect(); + VERIFY_ARE_EQUAL(inspect.Config.StopTimeout.value_or(0), 1); + + auto initProcess = container.GetInitProcess(); + std::thread stopThread([&]() { VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalNone, -1)); }); + + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + // TODO: calling Kill() here hangs since Stop() holds the container lock. + // Update this once fixed to: + // LOG_IF_FAILED(container.Get().Kill(WSLCSignalSIGKILL)); + + LOG_IF_FAILED(initProcess.Get().Signal(WSLCSignalSIGKILL)); + + if (stopThread.joinable()) + { + stopThread.join(); + } + }); + + // Wait for at least 2 seconds for the stop to complete to prove that the default 1 second timeout was correctly overriden. + auto waitResult = WaitForSingleObject(stopThread.native_handle(), 2000); + + VERIFY_ARE_EQUAL(waitResult, WAIT_TIMEOUT); + } + } + // Validate that Kill() works as expected { WSLCContainerLauncher launcher("debian:latest", "test-container-kill", {"sleep", "99999"}, {}); diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp index ca3bb555b..1c7bd9007 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp @@ -726,6 +726,72 @@ class WSLCE2EContainerCreateTests VERIFY_ARE_EQUAL(ExpectedExitCode, inspect.State.ExitCode); } + WSLC_TEST_METHOD(WSLCE2E_Container_Create_StopTimeout) + { + // A positive value is forwarded to the container configuration. + { + constexpr int ExpectedStopTimeout = 30; + auto result = RunWslc(std::format( + L"container create --stop-timeout {} --name {} {}", ExpectedStopTimeout, WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value()); + VERIFY_ARE_EQUAL(ExpectedStopTimeout, inspect.Config.StopTimeout.value()); + EnsureContainerDoesNotExist(WslcContainerName); + } + + // A value of 0 (stop the container immediately) is a valid, explicit timeout. + { + auto result = + RunWslc(std::format(L"container create --stop-timeout 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value()); + VERIFY_ARE_EQUAL(0, inspect.Config.StopTimeout.value()); + EnsureContainerDoesNotExist(WslcContainerName); + } + + // A value of -1 means "no timeout"; it is a valid, explicit value forwarded to the configuration. + { + auto result = + RunWslc(std::format(L"container create --stop-timeout -1 --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value()); + VERIFY_ARE_EQUAL(-1, inspect.Config.StopTimeout.value()); + EnsureContainerDoesNotExist(WslcContainerName); + } + + // When --stop-timeout is not specified, no timeout is forwarded to the container configuration. + { + 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.StopTimeout.has_value()); + } + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Create_StopTimeout_Invalid) + { + { + auto result = + RunWslc(std::format(L"container create --stop-timeout abc --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"Invalid stop-timeout argument value: abc\r\n", .ExitCode = 1}); + VerifyContainerIsNotListed(WslcContainerName); + } + + { + auto result = + RunWslc(std::format(L"container create --stop-timeout -2 --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"Invalid stop timeout value: -2\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + VerifyContainerIsNotListed(WslcContainerName); + } + } + WSLC_TEST_METHOD(WSLCE2E_Container_Create_ShmSize) { auto result = RunWslc( @@ -1225,6 +1291,7 @@ class WSLCE2EContainerCreateTests << 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" diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp index 03f19ecda..290ed3bc4 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp @@ -971,6 +971,76 @@ class WSLCE2EContainerRunTests VERIFY_ARE_EQUAL(ExpectedExitCode, inspect.State.ExitCode); } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopTimeout) + { + // A positive value is forwarded to the container configuration. + { + constexpr int ExpectedStopTimeout = 25; + auto result = RunWslc(std::format( + L"container run -d --stop-timeout {} --name {} {} sleep infinity", + ExpectedStopTimeout, + WslcContainerName, + DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value()); + VERIFY_ARE_EQUAL(ExpectedStopTimeout, inspect.Config.StopTimeout.value()); + EnsureContainerDoesNotExist(WslcContainerName); + } + + // A value of 0 (stop the container immediately) is a valid, explicit timeout. + { + auto result = RunWslc(std::format( + L"container run -d --stop-timeout 0 --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value()); + VERIFY_ARE_EQUAL(0, inspect.Config.StopTimeout.value()); + EnsureContainerDoesNotExist(WslcContainerName); + } + + // A value of -1 means "no timeout"; it is a valid, explicit value forwarded to the configuration. + { + auto result = RunWslc(std::format( + L"container run -d --stop-timeout -1 --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + const auto inspect = InspectContainer(WslcContainerName); + VERIFY_IS_TRUE(inspect.Config.StopTimeout.has_value()); + VERIFY_ARE_EQUAL(-1, inspect.Config.StopTimeout.value()); + EnsureContainerDoesNotExist(WslcContainerName); + } + + // When --stop-timeout is not specified, no timeout is forwarded to the container configuration. + { + 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.StopTimeout.has_value()); + } + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopTimeout_Invalid) + { + { + auto result = + RunWslc(std::format(L"container run --rm --stop-timeout abc --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"Invalid stop-timeout argument value: abc\r\n", .ExitCode = 1}); + EnsureContainerDoesNotExist(WslcContainerName); + } + + { + auto result = + RunWslc(std::format(L"container run --rm --stop-timeout -2 --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"Invalid stop timeout value: -2\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1}); + EnsureContainerDoesNotExist(WslcContainerName); + } + } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_ShmSize) { auto result = RunWslc(std::format(L"container run --rm --shm-size 128M {} df -h /dev/shm", DebianImage.NameAndTag())); @@ -1177,6 +1247,7 @@ class WSLCE2EContainerRunTests << 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" diff --git a/test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp index 0cef70d0d..52ac84538 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerStopTests.cpp @@ -237,25 +237,6 @@ class WSLCE2EContainerStopTests } } - WSLC_TEST_METHOD(WSLCE2E_Container_Stop_ValidTimeoutNegativeOne) - { - // Run a container in the background - auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); - result.Verify({.Stderr = L"", .ExitCode = 0}); - const auto containerId = result.GetStdoutOneLine(); - VERIFY_IS_FALSE(containerId.empty()); - - // Verify container is running - VerifyContainerIsListed(containerId, L"running"); - - // -1 is a valid timeout value - result = RunWslc(std::format(L"container stop {} -t -1", containerId)); - result.Verify({.Stderr = L"", .ExitCode = 0}); - - // Verify the container is no longer running - VerifyContainerIsListed(containerId, L"exited"); - } - private: const std::wstring WslcContainerName = L"wslc-test-container"; const std::wstring WslcContainerName2 = L"wslc-test-container-2";