Skip to content
7 changes: 7 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2355,6 +2355,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<value>Unsupported network mode: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="MessageWslcInvalidStopTimeout" xml:space="preserve">
<value>Invalid stop timeout value: {}</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="MessageWslcAdditionalNetworksRequirePrimary" xml:space="preserve">
<value>Additional networks are not allowed when the primary network mode is 'host' or 'none'.</value>
<comment>{Locked="host"}{Locked="none"}Command line arguments, file names and string inserts should not be translated</comment>
Expand Down Expand Up @@ -2920,6 +2924,9 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_StopSignalArgDescription" xml:space="preserve">
<value>Signal to stop the container</value>
</data>
<data name="WSLCCLI_StopTimeoutArgDescription" xml:space="preserve">
<value>Timeout (in seconds) to stop the container before killing it (-1 for no timeout)</value>
</data>
<data name="WSLCCLI_ShmSizeArgDescription" xml:space="preserve">
<value>Size of /dev/shm (e.g. 64M, 1G)</value>
<comment>{Locked="/dev/shm"}Command line arguments should not be translated</comment>
Expand Down
11 changes: 11 additions & 0 deletions src/windows/common/WSLCContainerLauncher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -296,6 +301,12 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
options.PortsCount = static_cast<ULONG>(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())
Expand Down
2 changes: 2 additions & 0 deletions src/windows/common/WSLCContainerLauncher.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
void SetName(std::string&& Name);
void SetEntrypoint(std::vector<std::string>&& entrypoint);
void SetDefaultStopSignal(WSLCSignal Signal);
void SetStopTimeout(LONG Timeout);
void SetShmSize(int64_t ShmSize);
void SetContainerFlags(WSLCContainerFlags Flags);
void SetHostname(std::string&& Hostname);
Expand Down Expand Up @@ -103,6 +104,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
std::string m_networkMode;
std::vector<std::string> m_entrypoint;
WSLCSignal m_stopSignal = WSLCSignalNone;
std::optional<LONG> m_stopTimeout;
int64_t m_shmSize = 0;
WSLCContainerFlags m_containerFlags = WSLCContainerFlagsNone;
std::string m_hostname;
Expand Down
7 changes: 5 additions & 2 deletions src/windows/inc/docker_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ struct CreateContainer
std::string Hostname;
std::string Domainname;
std::optional<std::string> StopSignal;
std::optional<int> StopTimeout;
std::optional<std::string> WorkingDir;
std::optional<std::vector<std::string>> Cmd;
std::optional<std::vector<std::string>> Entrypoint;
Expand All @@ -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
Expand All @@ -329,8 +330,10 @@ struct ContainerConfig
std::optional<std::vector<std::string>> Env;
std::optional<std::vector<std::string>> Cmd;
std::optional<std::vector<std::string>> Entrypoint;
std::optional<std::string> StopSignal;
std::optional<int> 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
Expand Down
3 changes: 2 additions & 1 deletion src/windows/inc/wslc_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ struct ContainerConfig
std::optional<std::vector<std::string>> Entrypoint;
std::string User;
std::string WorkingDir;
std::optional<int> 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
Expand Down
3 changes: 2 additions & 1 deletion src/windows/service/inc/WSLCShared.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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);")

Expand Down
7 changes: 6 additions & 1 deletion src/windows/service/inc/wslc.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Comment thread
OneBlue marked this conversation as resolved.
} WSLCContainerOptions;

typedef char WSLCContainerId[WSLC_CONTAINER_ID_LENGTH + 1] ;
Expand Down Expand Up @@ -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 */")
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/arguments/ArgumentDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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()) \
Expand Down
4 changes: 4 additions & 0 deletions src/windows/wslc/arguments/ArgumentValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ void Argument::Validate(const ArgMap& execArgs) const
validation::ValidateWSLCSignalFromString(execArgs.GetAll<ArgType::StopSignal>(), m_name);
break;

case ArgType::StopTimeout:
validation::ValidateIntegerFromString<int>(execArgs.GetAll<ArgType::StopTimeout>(), m_name);
break;

case ArgType::ShmSize:
validation::ValidateMemorySize(execArgs.GetAll<ArgType::ShmSize>(), m_name);
break;
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ContainerCreateCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ std::vector<Argument> 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),
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ContainerRunCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ std::vector<Argument> 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),
Expand Down
5 changes: 2 additions & 3 deletions src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ struct ContainerOptions
bool TTY = false;
bool PublishAll = false;
WSLCSignal StopSignal = WSLCSignalNone;
std::optional<int> StopTimeout{};
std::optional<int64_t> ShmSize{};
bool Gpu = false;
std::vector<std::string> Ports;
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
5 changes: 5 additions & 0 deletions src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
options.StopSignal = validation::GetWSLCSignalFromString(context.Args.Get<ArgType::StopSignal>());
}

if (context.Args.Contains(ArgType::StopTimeout))
{
options.StopTimeout = validation::GetIntegerFromString<int>(context.Args.Get<ArgType::StopTimeout>());
}

if (context.Args.Contains(ArgType::ShmSize))
{
options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get<ArgType::ShmSize>());
Expand Down
2 changes: 1 addition & 1 deletion src/windows/wslcsession/DockerHTTPClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<WSLCSignal> Signal, std::optional<ULONG> TimeoutSeconds)
void DockerHTTPClient::StopContainer(const std::string& Id, std::optional<WSLCSignal> Signal, std::optional<LONG> TimeoutSeconds)
{
auto url = URL::Create("/containers/{}/stop", Id);
if (Signal.has_value())
Expand Down
2 changes: 1 addition & 1 deletion src/windows/wslcsession/DockerHTTPClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class DockerHTTPClient
bool all = false, int limit = -1, const std::map<std::string, std::vector<std::string>>& filters = {});
common::docker_schema::CreatedContainer CreateContainer(const common::docker_schema::CreateContainer& Request, const std::optional<std::string>& Name);
void StartContainer(const std::string& Id, const std::optional<std::string>& DetachKeys);
void StopContainer(const std::string& Id, std::optional<WSLCSignal> Signal, std::optional<ULONG> TimeoutSeconds);
void StopContainer(const std::string& Id, std::optional<WSLCSignal> Signal, std::optional<LONG> TimeoutSeconds);
void DeleteContainer(const std::string& Id, bool Force, bool DeleteVolumes = false);
void SignalContainer(const std::string& Id, std::optional<WSLCSignal> Signal);
common::docker_schema::InspectContainer InspectContainer(const std::string& Id);
Expand Down
25 changes: 22 additions & 3 deletions src/windows/wslcsession/WSLCContainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> StringArrayToVector(const WSLCStringArray& array)
{
if (array.Count == 0)
Expand Down Expand Up @@ -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);
Expand All @@ -961,10 +971,10 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
}
else
{
std::optional<ULONG> TimeoutArg;
if (TimeoutSeconds >= 0)
std::optional<LONG> TimeoutArg;
if (TimeoutSeconds != WSLC_STOP_TIMEOUT_DEFAULT)
{
TimeoutArg = static_cast<ULONG>(TimeoutSeconds);
TimeoutArg = TimeoutSeconds;
}

m_dockerClient.StopContainer(m_id, SignalArg, TimeoutArg);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1419,6 +1430,14 @@ std::unique_ptr<WSLCContainerImpl> 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<int>(containerOptions.StopTimeout);
Comment on lines +1435 to +1438
}
Comment thread
OneBlue marked this conversation as resolved.

if (containerOptions.InitProcessOptions.CurrentDirectory != nullptr)
{
request.WorkingDir = containerOptions.InitProcessOptions.CurrentDirectory;
Expand Down
65 changes: 63 additions & 2 deletions test/windows/WSLCTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5572,7 +5572,7 @@ class WSLCTests

// Invalid container flags are rejected with E_INVALIDARG.
options.Image = "debian:latest";
options.Flags = static_cast<WSLCContainerFlags>(0x10);
options.Flags = static_cast<WSLCContainerFlags>(0x20);
VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateContainer(&options, nullptr, &container));

// Invalid init process flags are rejected with E_INVALIDARG.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Comment thread
OneBlue marked this conversation as resolved.
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"}, {});
Expand Down
Loading