diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index d08df400b..02f59cb9a 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2701,6 +2701,14 @@ On first run, creates the file with all settings commented out at their defaults
The command to run
+
+ Number of CPUs (e.g. 0.5, 1, 2.5)
+ {Locked="0.5"}{Locked="1"}{Locked="2.5"}Command line argument example values should not be translated
+
+
+ Invalid {} argument value: '{}'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="0.5"}{Locked="1"}{Locked="2"}
+
Delete containers even if they are running
@@ -2770,6 +2778,10 @@ On first run, creates the file with all settings commented out at their defaults
Show the latest created container (includes all states)
+
+ Memory limit (e.g. 512M, 1G)
+ {Locked="512M"}{Locked="1G"}Command line argument example values should not be translated
+
Container host name
@@ -2903,6 +2915,14 @@ On first run, creates the file with all settings commented out at their defaults
Mount tmpfs to the container at the given path
{Locked="tmpfs"}Command line arguments should not be translated
+
+ Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)
+ {Locked="-1"}{Locked="<name>=<soft>[:<hard>]"}Command line arguments should not be translated
+
+
+ Invalid {} argument value: '{}'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="-1"}{Locked="<name>=<soft>[:<hard>]"}
+
User ID for the process (name|uid|uid:gid)
{Locked="name|uid|uid:gid"}Command line arguments should not be translated
diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h
index 7e0a4df6e..cd4054a95 100644
--- a/src/windows/wslc/arguments/ArgumentDefinitions.h
+++ b/src/windows/wslc/arguments/ArgumentDefinitions.h
@@ -41,6 +41,7 @@ _(BuildTarget, "target", NO_ALIAS, Kind::Value, L
_(CIDFile, "cidfile", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CIDFileArgDescription()) \
_(Command, "command", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_CommandArgDescription()) \
_(ContainerId, "container-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ContainerIdArgDescription()) \
+_(Cpus, "cpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CpusArgDescription()) \
_(Force, "force", L"f", Kind::Flag, Localization::WSLCCLI_ForceArgDescription()) \
_(Detach, "detach", L"d", Kind::Flag, Localization::WSLCCLI_DetachArgDescription()) \
_(DNS, "dns", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSArgDescription()) \
@@ -72,6 +73,7 @@ _(Interactive, "interactive", L"i", Kind::Flag, L
_(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
_(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \
_(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
+_(Memory, "memory", L"m", Kind::Value, Localization::WSLCCLI_MemoryArgDescription()) \
_(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
_(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \
/*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
@@ -106,6 +108,7 @@ _(Time, "time", L"t", Kind::Value, L
_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \
_(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
_(Type, "type", L"t", Kind::Value, Localization::WSLCCLI_TypeArgDescription()) \
+_(Ulimit, "ulimit", NO_ALIAS, Kind::Value, Localization::WSLCCLI_UlimitArgDescription()) \
_(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \
_(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \
_(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp
index fda2fd219..1e54f7e2e 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.cpp
+++ b/src/windows/wslc/arguments/ArgumentValidation.cpp
@@ -11,6 +11,8 @@ Module Name:
Implementation of the Argument Validation.
--*/
+
+#include "precomp.h"
#include "Argument.h"
#include "ArgumentTypes.h"
#include "ArgumentValidation.h"
@@ -48,6 +50,18 @@ void Argument::Validate(const ArgMap& execArgs) const
validation::ValidateMemorySize(execArgs.GetAll(), m_name);
break;
+ case ArgType::Memory:
+ validation::ValidateMemorySize(execArgs.GetAll(), m_name);
+ break;
+
+ case ArgType::Cpus:
+ validation::ValidateNanoCpus(execArgs.GetAll(), m_name);
+ break;
+
+ case ArgType::Ulimit:
+ validation::ValidateUlimit(execArgs.GetAll(), m_name);
+ break;
+
case ArgType::Tail:
validation::ValidateIntegerFromString(
execArgs.GetAll(), m_name, [](auto value) { return value != 0; });
@@ -271,6 +285,83 @@ int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& a
return static_cast(parsed.value());
}
+void ValidateNanoCpus(const std::vector& values, const std::wstring& argName)
+{
+ for (const auto& value : values)
+ {
+ std::ignore = GetNanoCpusFromString(value, argName);
+ }
+}
+
+int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName)
+{
+ constexpr double NanosPerCpu = 1'000'000'000.0;
+ constexpr double MaxCpus = static_cast(std::numeric_limits::max()) / NanosPerCpu;
+
+ const std::string narrow = WideToMultiByte(input);
+ const char* begin = narrow.c_str();
+ const char* end = begin + narrow.size();
+
+ double cpus{};
+ const auto result = std::from_chars(begin, end, cpus, std::chars_format::fixed);
+ if (result.ec != std::errc() || result.ptr != end || cpus <= 0.0 || cpus > MaxCpus)
+ {
+ throw ArgumentException(Localization::WSLCCLI_InvalidCpusError(argName, input));
+ }
+
+ return static_cast(cpus * NanosPerCpu);
+}
+
+void ValidateUlimit(const std::vector& values, const std::wstring& argName)
+{
+ for (const auto& value : values)
+ {
+ std::ignore = ParseUlimit(value, argName);
+ }
+}
+
+std::tuple ParseUlimit(const std::wstring& input, const std::wstring& argName)
+{
+ // Accepts =[:]; if hard is omitted hard = soft. -1 means unlimited.
+ const auto equalsPos = input.find(L'=');
+ if (equalsPos == std::wstring::npos || equalsPos == 0)
+ {
+ throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
+ }
+
+ const std::wstring valuesPart = input.substr(equalsPos + 1);
+ const auto colonPos = valuesPart.find(L':');
+
+ auto parseLimit = [&](const std::wstring& limitStr) -> int64_t {
+ if (limitStr.empty())
+ {
+ throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
+ }
+
+ try
+ {
+ return GetIntegerFromString(limitStr, argName, [](int64_t v) { return v >= -1; });
+ }
+ catch (const ArgumentException&)
+ {
+ // Re-throw with the ulimit-specific error message so the user sees the full input.
+ throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
+ }
+ };
+
+ const int64_t soft = parseLimit(colonPos == std::wstring::npos ? valuesPart : valuesPart.substr(0, colonPos));
+ const int64_t hard = colonPos == std::wstring::npos ? soft : parseLimit(valuesPart.substr(colonPos + 1));
+
+ // This rejects "-1:1024" and "-1:" while allowing ":-1", "-1:-1", and "-1".
+ const bool invalidRange = (soft == -1) ? (hard != -1) : (hard != -1 && hard < soft);
+ if (invalidRange)
+ {
+ throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
+ }
+
+ return {WideToMultiByte(input.substr(0, equalsPos)), soft, hard};
+}
+
std::pair ParseLabel(const std::wstring& value)
{
std::pair result{};
diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h
index 563992246..2356ea2cb 100644
--- a/src/windows/wslc/arguments/ArgumentValidation.h
+++ b/src/windows/wslc/arguments/ArgumentValidation.h
@@ -17,6 +17,7 @@ Module Name:
#include "ContainerModel.h"
#include "InspectModel.h"
#include
+#include
#include
#include
#include
@@ -65,6 +66,12 @@ 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 ValidateNanoCpus(const std::vector& values, const std::wstring& argName);
+int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName = {});
+
+void ValidateUlimit(const std::vector& values, const std::wstring& argName);
+std::tuple ParseUlimit(const std::wstring& input, const std::wstring& argName = {});
+
void ValidateFormatTypeFromString(const std::vector& values, const std::wstring& argName);
FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {});
diff --git a/src/windows/wslc/commands/ContainerCreateCommand.cpp b/src/windows/wslc/commands/ContainerCreateCommand.cpp
index 676f8ac69..83e69c8e1 100644
--- a/src/windows/wslc/commands/ContainerCreateCommand.cpp
+++ b/src/windows/wslc/commands/ContainerCreateCommand.cpp
@@ -32,6 +32,7 @@ std::vector ContainerCreateCommand::GetArguments() const
Argument::Create(ArgType::Command),
Argument::Create(ArgType::ForwardArgs),
Argument::Create(ArgType::CIDFile),
+ Argument::Create(ArgType::Cpus),
Argument::Create(ArgType::DNS, false, NO_LIMIT),
// Argument::Create(ArgType::DNSDomain),
Argument::Create(ArgType::DNSOption, false, NO_LIMIT),
@@ -45,6 +46,7 @@ std::vector ContainerCreateCommand::GetArguments() const
Argument::Create(ArgType::Hostname),
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Label, false, NO_LIMIT),
+ Argument::Create(ArgType::Memory),
Argument::Create(ArgType::Name),
// Argument::Create(ArgType::NoDNS),
// Argument::Create(ArgType::Progress),
@@ -57,6 +59,7 @@ std::vector ContainerCreateCommand::GetArguments() const
Argument::Create(ArgType::StopSignal),
Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
Argument::Create(ArgType::TTY),
+ Argument::Create(ArgType::Ulimit, false, NO_LIMIT),
Argument::Create(ArgType::User),
Argument::Create(ArgType::Volume, false, NO_LIMIT),
// Argument::Create(ArgType::Virtual),
diff --git a/src/windows/wslc/commands/ContainerRunCommand.cpp b/src/windows/wslc/commands/ContainerRunCommand.cpp
index 49a54d249..88dc1a5b5 100644
--- a/src/windows/wslc/commands/ContainerRunCommand.cpp
+++ b/src/windows/wslc/commands/ContainerRunCommand.cpp
@@ -32,6 +32,7 @@ std::vector ContainerRunCommand::GetArguments() const
Argument::Create(ArgType::Command),
Argument::Create(ArgType::ForwardArgs),
Argument::Create(ArgType::CIDFile),
+ Argument::Create(ArgType::Cpus),
Argument::Create(ArgType::Detach),
Argument::Create(ArgType::DNS, false, NO_LIMIT),
// Argument::Create(ArgType::DNSDomain),
@@ -45,6 +46,7 @@ std::vector ContainerRunCommand::GetArguments() const
Argument::Create(ArgType::Hostname),
Argument::Create(ArgType::Interactive),
Argument::Create(ArgType::Label, false, NO_LIMIT),
+ Argument::Create(ArgType::Memory),
Argument::Create(ArgType::Name),
// Argument::Create(ArgType::NoDNS),
// Argument::Create(ArgType::Progress),
@@ -58,6 +60,7 @@ std::vector ContainerRunCommand::GetArguments() const
Argument::Create(ArgType::StopSignal),
Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
Argument::Create(ArgType::TTY),
+ Argument::Create(ArgType::Ulimit, false, NO_LIMIT),
Argument::Create(ArgType::User),
Argument::Create(ArgType::Volume, false, NO_LIMIT),
// Argument::Create(ArgType::Virtual),
diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h
index 94a283032..e5f876446 100644
--- a/src/windows/wslc/services/ContainerModel.h
+++ b/src/windows/wslc/services/ContainerModel.h
@@ -53,6 +53,9 @@ struct ContainerOptions
std::vector Tmpfs;
std::vector> Labels;
std::optional CidFile{};
+ std::optional MemoryBytes{};
+ std::optional NanoCpus{};
+ std::vector> Ulimits;
};
struct CreateContainerResult
diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp
index 0f2b2b776..7d9edd1d5 100644
--- a/src/windows/wslc/services/ContainerService.cpp
+++ b/src/windows/wslc/services/ContainerService.cpp
@@ -109,6 +109,21 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
containerLauncher.SetShmSize(options.ShmSize.value());
}
+ if (options.MemoryBytes.has_value())
+ {
+ containerLauncher.SetMemoryLimit(options.MemoryBytes.value());
+ }
+
+ if (options.NanoCpus.has_value())
+ {
+ containerLauncher.SetNanoCpus(options.NanoCpus.value());
+ }
+
+ for (const auto& [name, soft, hard] : options.Ulimits)
+ {
+ containerLauncher.AddUlimit(name, soft, hard);
+ }
+
if (!options.Entrypoint.empty())
{
auto entrypoints = options.Entrypoint;
diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp
index a98ddd9a0..86abac013 100644
--- a/src/windows/wslc/tasks/ContainerTasks.cpp
+++ b/src/windows/wslc/tasks/ContainerTasks.cpp
@@ -422,6 +422,24 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get());
}
+ if (context.Args.Contains(ArgType::Memory))
+ {
+ options.MemoryBytes = validation::GetMemorySizeFromString(context.Args.Get());
+ }
+
+ if (context.Args.Contains(ArgType::Cpus))
+ {
+ options.NanoCpus = validation::GetNanoCpusFromString(context.Args.Get());
+ }
+
+ if (context.Args.Contains(ArgType::Ulimit))
+ {
+ for (const auto& value : context.Args.GetAll())
+ {
+ options.Ulimits.emplace_back(validation::ParseUlimit(value));
+ }
+ }
+
if (context.Args.Contains(ArgType::Command))
{
options.Arguments.emplace_back(WideToMultiByte(context.Args.Get()));
diff --git a/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp b/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp
new file mode 100644
index 000000000..0943f8117
--- /dev/null
+++ b/test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp
@@ -0,0 +1,129 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WSLCCLIResourceLimitsParserUnitTests.cpp
+
+Abstract:
+
+ This file contains unit tests for WSLC CLI resource-limit (--cpus, --memory, --ulimit) parsing and validation.
+
+--*/
+
+#include "precomp.h"
+#include "windows/Common.h"
+#include "WSLCCLITestHelpers.h"
+#include "ArgumentValidation.h"
+
+using namespace wsl::windows::wslc;
+
+namespace WSLCCLIResourceLimitsParserUnitTests {
+
+class WSLCCLIResourceLimitsParserUnitTests
+{
+ WSLC_TEST_CLASS(WSLCCLIResourceLimitsParserUnitTests)
+
+ TEST_METHOD(NanoCpus_Valid)
+ {
+ // (input, expected nanoCpus)
+ std::vector> valid = {
+ {L"1", 1'000'000'000LL},
+ {L"2", 2'000'000'000LL},
+ {L"0.5", 500'000'000LL},
+ {L"1.5", 1'500'000'000LL},
+ {L"2.5", 2'500'000'000LL},
+ {L"0.001", 1'000'000LL},
+ };
+
+ for (const auto& [input, expected] : valid)
+ {
+ const auto actual = validation::GetNanoCpusFromString(input, L"cpus");
+ VERIFY_ARE_EQUAL(expected, actual);
+ }
+ }
+
+ TEST_METHOD(NanoCpus_Invalid)
+ {
+ // Each value should be rejected as an invalid --cpus value.
+ const std::vector invalid = {
+ L"",
+ L"0", // not positive
+ L"-1", // sign char rejected
+ L"-0.5", // sign char rejected
+ L"abc", // not numeric
+ L"1.5x", // trailing garbage
+ L" 1", // leading whitespace
+ L"1 ", // trailing whitespace
+ L"1e3", // exponent not allowed
+ L"+1", // sign char rejected
+ L"1.2.3", // multiple dots (rejected by strtod's strict end check)
+ L"99999999999" // overflow when multiplied by 1e9
+ };
+
+ for (const auto& input : invalid)
+ {
+ VERIFY_THROWS(validation::GetNanoCpusFromString(input, L"cpus"), ArgumentException);
+ }
+ }
+
+ TEST_METHOD(Ulimit_Valid)
+ {
+ // (input, expectedName, expectedSoft, expectedHard)
+ std::vector> valid = {
+ {L"nofile=1024", "nofile", 1024, 1024},
+ {L"nofile=1024:2048", "nofile", 1024, 2048},
+ {L"nproc=512:512", "nproc", 512, 512},
+ {L"core=-1", "core", -1, -1},
+ {L"core=-1:-1", "core", -1, -1},
+ {L"memlock=0", "memlock", 0, 0},
+ {L"stack=8192:-1", "stack", 8192, -1},
+ };
+
+ for (const auto& [input, expectedName, expectedSoft, expectedHard] : valid)
+ {
+ const auto [name, soft, hard] = validation::ParseUlimit(input, L"ulimit");
+ VERIFY_ARE_EQUAL(expectedName, name);
+ VERIFY_ARE_EQUAL(expectedSoft, soft);
+ VERIFY_ARE_EQUAL(expectedHard, hard);
+ }
+ }
+
+ TEST_METHOD(Ulimit_Invalid)
+ {
+ const std::vector invalid = {
+ L"",
+ L"=1024", // empty name
+ L"nofile=", // empty value
+ L"nofile", // missing '='
+ L"nofile=abc", // non-numeric soft
+ L"nofile=1024:", // empty hard
+ L"nofile=:1024", // empty soft
+ L"nofile=-2", // negative other than -1
+ L"nofile=1024:512", // hard < soft (and both positive)
+ L"nofile=-1:1024", // unlimited soft but limited hard
+ L"nofile=-1:9223372036854775807", // unlimited soft but finite (INT64_MAX) hard
+ L"nofile=1.5", // not integer
+ };
+
+ for (const auto& input : invalid)
+ {
+ VERIFY_THROWS(validation::ParseUlimit(input, L"ulimit"), ArgumentException);
+ }
+ }
+
+ TEST_METHOD(NanoCpus_Validator)
+ {
+ VERIFY_NO_THROW(validation::ValidateNanoCpus({L"0.5", L"1", L"2.5"}, L"cpus"));
+ VERIFY_THROWS(validation::ValidateNanoCpus({L"1", L"0"}, L"cpus"), ArgumentException);
+ }
+
+ TEST_METHOD(Ulimit_Validator)
+ {
+ 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);
+ }
+};
+
+} // namespace WSLCCLIResourceLimitsParserUnitTests
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
index 90f65edcd..95f602f49 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
@@ -828,6 +828,7 @@ class WSLCE2EContainerCreateTests
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"
@@ -839,6 +840,7 @@ class WSLCE2EContainerCreateTests
<< 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" -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"
@@ -848,6 +850,7 @@ class WSLCE2EContainerCreateTests
<< L" --stop-signal Signal to stop the container\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"
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
index 83dd35dfd..f3fbfe4bf 100644
--- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
@@ -731,6 +731,74 @@ class WSLCE2EContainerRunTests
}
}
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus)
+ {
+ auto result = RunWslc(std::format(L"container run --name {} --cpus 1.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_ARE_EQUAL(static_cast(1'500'000'000), inspect.HostConfig.NanoCpus);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory)
+ {
+ auto result = RunWslc(std::format(L"container run --name {} --memory 32M {} true", WslcContainerName, DebianImage.NameAndTag()));
+ // Note: stderr is not asserted here because some kernels emit a swap-limit warning
+ // ("Your kernel does not support swap limit capabilities...") when a memory limit is set.
+ result.Verify({.ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_ARE_EQUAL(static_cast(32) * 1024 * 1024, inspect.HostConfig.Memory);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit)
+ {
+ auto result = RunWslc(std::format(
+ L"container run --name {} --ulimit nofile=1024:2048 --ulimit nproc=512 {} true", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ const auto inspect = InspectContainer(WslcContainerName);
+ VERIFY_ARE_EQUAL(static_cast(2), inspect.HostConfig.Ulimits.size());
+
+ std::map> byName;
+ for (const auto& ul : inspect.HostConfig.Ulimits)
+ {
+ byName[ul.Name] = {ul.Soft, ul.Hard};
+ }
+
+ VERIFY_IS_TRUE(byName.contains("nofile"));
+ VERIFY_ARE_EQUAL(static_cast(1024), byName["nofile"].first);
+ VERIFY_ARE_EQUAL(static_cast(2048), byName["nofile"].second);
+
+ VERIFY_IS_TRUE(byName.contains("nproc"));
+ VERIFY_ARE_EQUAL(static_cast(512), byName["nproc"].first);
+ VERIFY_ARE_EQUAL(static_cast(512), byName["nproc"].second);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus_Invalid)
+ {
+ auto result = RunWslc(std::format(L"container run --rm --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)\r\n", .ExitCode = 1});
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory_Invalid)
+ {
+ auto result =
+ RunWslc(std::format(L"container run --rm --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit_Invalid)
+ {
+ auto result =
+ RunWslc(std::format(L"container run --rm --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
+ result.Verify(
+ {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected =[:] (use -1 for unlimited)\r\n", .ExitCode = 1});
+ EnsureContainerDoesNotExist(WslcContainerName);
+ }
+
WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopSignal_Invalid)
{
{
@@ -820,6 +888,7 @@ class WSLCE2EContainerRunTests
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"
@@ -832,6 +901,7 @@ class WSLCE2EContainerRunTests
<< 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" -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"
@@ -841,6 +911,7 @@ class WSLCE2EContainerRunTests
<< L" --stop-signal Signal to stop the container\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"