@samitouri / QOSAMI-WSL / commits / e807fd7f

Add --cpus, --memory, and --ulimit options to wslc container create/run (#40727)

* Init resources * Fix test * Clang format * Simplify parse methods * Resolve copilot comment

AmirMS committed Jun 8, 2026 at 12:41 UTC e807fd7f41d254f2a73c98e109cc112e19e05b04
12 files changed +366
localization/strings/en-US/Resources.resw
+20
@@ -2708,6 +2708,14 @@ On first run, creates the file with all settings commented out at their defaults
2708 <data name="WSLCCLI_CommandArgDescription" xml:space="preserve">
2709 <value>The command to run</value>
2710 </data>
2711 + <data name="WSLCCLI_CpusArgDescription" xml:space="preserve">
2712 + <value>Number of CPUs (e.g. 0.5, 1, 2.5)</value>
2713 + <comment>{Locked="0.5"}{Locked="1"}{Locked="2.5"}Command line argument example values should not be translated</comment>
2714 + </data>
2715 + <data name="WSLCCLI_InvalidCpusError" xml:space="preserve">
2716 + <value>Invalid {} argument value: '{}'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)</value>
2717 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="0.5"}{Locked="1"}{Locked="2"}</comment>
2718 + </data>
2719 <data name="WSLCCLI_ForceArgDescription" xml:space="preserve">
2720 <value>Delete containers even if they are running</value>
2721 </data>
@@ -2777,6 +2785,10 @@ On first run, creates the file with all settings commented out at their defaults
2785 <data name="WSLCCLI_LatestArgDescription" xml:space="preserve">
2786 <value>Show the latest created container (includes all states)</value>
2787 </data>
2788 + <data name="WSLCCLI_MemoryArgDescription" xml:space="preserve">
2789 + <value>Memory limit (e.g. 512M, 1G)</value>
2790 + <comment>{Locked="512M"}{Locked="1G"}Command line argument example values should not be translated</comment>
2791 + </data>
2792 <data name="WSLCCLI_HostnameArgDescription" xml:space="preserve">
2793 <value>Container host name</value>
2794 </data>
@@ -2910,6 +2922,14 @@ On first run, creates the file with all settings commented out at their defaults
2922 <value>Mount tmpfs to the container at the given path</value>
2923 <comment>{Locked="tmpfs"}Command line arguments should not be translated</comment>
2924 </data>
2925 + <data name="WSLCCLI_UlimitArgDescription" xml:space="preserve">
2926 + <value>Ulimit options (format: &lt;name&gt;=&lt;soft&gt;[:&lt;hard&gt;], use -1 for unlimited)</value>
2927 + <comment>{Locked="-1"}{Locked="&lt;name&gt;=&lt;soft&gt;[:&lt;hard&gt;]"}Command line arguments should not be translated</comment>
2928 + </data>
2929 + <data name="WSLCCLI_InvalidUlimitError" xml:space="preserve">
2930 + <value>Invalid {} argument value: '{}'. Expected &lt;name&gt;=&lt;soft&gt;[:&lt;hard&gt;] (use -1 for unlimited)</value>
2931 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="-1"}{Locked="&lt;name&gt;=&lt;soft&gt;[:&lt;hard&gt;]"}</comment>
2932 + </data>
2933 <data name="WSLCCLI_UserArgDescription" xml:space="preserve">
2934 <value>User ID for the process (name|uid|uid:gid)</value>
2935 <comment>{Locked="name|uid|uid:gid"}Command line arguments should not be translated</comment>
src/windows/wslc/arguments/ArgumentDefinitions.h
+3
@@ -41,6 +41,7 @@ _(BuildTarget, "target", NO_ALIAS, Kind::Value, L
41 _(CIDFile, "cidfile", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CIDFileArgDescription()) \
42 _(Command, "command", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_CommandArgDescription()) \
43 _(ContainerId, "container-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ContainerIdArgDescription()) \
44 +_(Cpus, "cpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_CpusArgDescription()) \
45 _(Force, "force", L"f", Kind::Flag, Localization::WSLCCLI_ForceArgDescription()) \
46 _(Detach, "detach", L"d", Kind::Flag, Localization::WSLCCLI_DetachArgDescription()) \
47 _(DNS, "dns", NO_ALIAS, Kind::Value, Localization::WSLCCLI_DNSArgDescription()) \
@@ -72,6 +73,7 @@ _(Interactive, "interactive", L"i", Kind::Flag, L
73 _(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
74 _(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \
75 _(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
76 +_(Memory, "memory", L"m", Kind::Value, Localization::WSLCCLI_MemoryArgDescription()) \
77 _(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
78 _(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \
79 /*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
@@ -106,6 +108,7 @@ _(Time, "time", L"t", Kind::Value, L
108 _(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \
109 _(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
110 _(Type, "type", L"t", Kind::Value, Localization::WSLCCLI_TypeArgDescription()) \
111 +_(Ulimit, "ulimit", NO_ALIAS, Kind::Value, Localization::WSLCCLI_UlimitArgDescription()) \
112 _(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \
113 _(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \
114 _(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
src/windows/wslc/arguments/ArgumentValidation.cpp
+91
@@ -11,6 +11,8 @@ Abstract:
11 Implementation of the Argument Validation.
12
13 --*/
14 +
15 +#include "precomp.h"
16 #include "Argument.h"
17 #include "ArgumentTypes.h"
18 #include "ArgumentValidation.h"
@@ -48,6 +50,18 @@ void Argument::Validate(const ArgMap& execArgs) const
50 validation::ValidateMemorySize(execArgs.GetAll<ArgType::ShmSize>(), m_name);
51 break;
52
53 + case ArgType::Memory:
54 + validation::ValidateMemorySize(execArgs.GetAll<ArgType::Memory>(), m_name);
55 + break;
56 +
57 + case ArgType::Cpus:
58 + validation::ValidateNanoCpus(execArgs.GetAll<ArgType::Cpus>(), m_name);
59 + break;
60 +
61 + case ArgType::Ulimit:
62 + validation::ValidateUlimit(execArgs.GetAll<ArgType::Ulimit>(), m_name);
63 + break;
64 +
65 case ArgType::Tail:
66 validation::ValidateIntegerFromString<ULONGLONG>(
67 execArgs.GetAll<ArgType::Tail>(), m_name, [](auto value) { return value != 0; });
@@ -271,6 +285,83 @@ int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& a
285 return static_cast<int64_t>(parsed.value());
286 }
287
288 +void ValidateNanoCpus(const std::vector<std::wstring>& values, const std::wstring& argName)
289 +{
290 + for (const auto& value : values)
291 + {
292 + std::ignore = GetNanoCpusFromString(value, argName);
293 + }
294 +}
295 +
296 +int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName)
297 +{
298 + constexpr double NanosPerCpu = 1'000'000'000.0;
299 + constexpr double MaxCpus = static_cast<double>(std::numeric_limits<int64_t>::max()) / NanosPerCpu;
300 +
301 + const std::string narrow = WideToMultiByte(input);
302 + const char* begin = narrow.c_str();
303 + const char* end = begin + narrow.size();
304 +
305 + double cpus{};
306 + const auto result = std::from_chars(begin, end, cpus, std::chars_format::fixed);
307 + if (result.ec != std::errc() || result.ptr != end || cpus <= 0.0 || cpus > MaxCpus)
308 + {
309 + throw ArgumentException(Localization::WSLCCLI_InvalidCpusError(argName, input));
310 + }
311 +
312 + return static_cast<int64_t>(cpus * NanosPerCpu);
313 +}
314 +
315 +void ValidateUlimit(const std::vector<std::wstring>& values, const std::wstring& argName)
316 +{
317 + for (const auto& value : values)
318 + {
319 + std::ignore = ParseUlimit(value, argName);
320 + }
321 +}
322 +
323 +std::tuple<std::string, int64_t, int64_t> ParseUlimit(const std::wstring& input, const std::wstring& argName)
324 +{
325 + // Accepts <name>=<soft>[:<hard>]; if hard is omitted hard = soft. -1 means unlimited.
326 + const auto equalsPos = input.find(L'=');
327 + if (equalsPos == std::wstring::npos || equalsPos == 0)
328 + {
329 + throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
330 + }
331 +
332 + const std::wstring valuesPart = input.substr(equalsPos + 1);
333 + const auto colonPos = valuesPart.find(L':');
334 +
335 + auto parseLimit = [&](const std::wstring& limitStr) -> int64_t {
336 + if (limitStr.empty())
337 + {
338 + throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
339 + }
340 +
341 + try
342 + {
343 + return GetIntegerFromString<int64_t>(limitStr, argName, [](int64_t v) { return v >= -1; });
344 + }
345 + catch (const ArgumentException&)
346 + {
347 + // Re-throw with the ulimit-specific error message so the user sees the full input.
348 + throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
349 + }
350 + };
351 +
352 + const int64_t soft = parseLimit(colonPos == std::wstring::npos ? valuesPart : valuesPart.substr(0, colonPos));
353 + const int64_t hard = colonPos == std::wstring::npos ? soft : parseLimit(valuesPart.substr(colonPos + 1));
354 +
355 + // This rejects "-1:1024" and "-1:<finite>" while allowing "<finite>:-1", "-1:-1", and "-1".
356 + const bool invalidRange = (soft == -1) ? (hard != -1) : (hard != -1 && hard < soft);
357 + if (invalidRange)
358 + {
359 + throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
360 + }
361 +
362 + return {WideToMultiByte(input.substr(0, equalsPos)), soft, hard};
363 +}
364 +
365 std::pair<std::string, std::string> ParseLabel(const std::wstring& value)
366 {
367 std::pair<std::string, std::string> result{};
src/windows/wslc/arguments/ArgumentValidation.h
+7
@@ -17,6 +17,7 @@ Abstract:
17 #include "ContainerModel.h"
18 #include "InspectModel.h"
19 #include <string>
20 +#include <tuple>
21 #include <vector>
22 #include <charconv>
23 #include <wslc.h>
@@ -65,6 +66,12 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring
66 void ValidateMemorySize(const std::vector<std::wstring>& values, const std::wstring& argName);
67 int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName = {});
68
69 +void ValidateNanoCpus(const std::vector<std::wstring>& values, const std::wstring& argName);
70 +int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName = {});
71 +
72 +void ValidateUlimit(const std::vector<std::wstring>& values, const std::wstring& argName);
73 +std::tuple<std::string, int64_t, int64_t> ParseUlimit(const std::wstring& input, const std::wstring& argName = {});
74 +
75 void ValidateFormatTypeFromString(const std::vector<std::wstring>& values, const std::wstring& argName);
76 FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName = {});
77
src/windows/wslc/commands/ContainerCreateCommand.cpp
+3
@@ -32,6 +32,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
32 Argument::Create(ArgType::Command),
33 Argument::Create(ArgType::ForwardArgs),
34 Argument::Create(ArgType::CIDFile),
35 + Argument::Create(ArgType::Cpus),
36 Argument::Create(ArgType::DNS, false, NO_LIMIT),
37 // Argument::Create(ArgType::DNSDomain),
38 Argument::Create(ArgType::DNSOption, false, NO_LIMIT),
@@ -45,6 +46,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
46 Argument::Create(ArgType::Hostname),
47 Argument::Create(ArgType::Interactive),
48 Argument::Create(ArgType::Label, false, NO_LIMIT),
49 + Argument::Create(ArgType::Memory),
50 Argument::Create(ArgType::Name),
51 // Argument::Create(ArgType::NoDNS),
52 // Argument::Create(ArgType::Progress),
@@ -57,6 +59,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
59 Argument::Create(ArgType::StopSignal),
60 Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
61 Argument::Create(ArgType::TTY),
62 + Argument::Create(ArgType::Ulimit, false, NO_LIMIT),
63 Argument::Create(ArgType::User),
64 Argument::Create(ArgType::Volume, false, NO_LIMIT),
65 // Argument::Create(ArgType::Virtual),
src/windows/wslc/commands/ContainerRunCommand.cpp
+3
@@ -32,6 +32,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
32 Argument::Create(ArgType::Command),
33 Argument::Create(ArgType::ForwardArgs),
34 Argument::Create(ArgType::CIDFile),
35 + Argument::Create(ArgType::Cpus),
36 Argument::Create(ArgType::Detach),
37 Argument::Create(ArgType::DNS, false, NO_LIMIT),
38 // Argument::Create(ArgType::DNSDomain),
@@ -45,6 +46,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
46 Argument::Create(ArgType::Hostname),
47 Argument::Create(ArgType::Interactive),
48 Argument::Create(ArgType::Label, false, NO_LIMIT),
49 + Argument::Create(ArgType::Memory),
50 Argument::Create(ArgType::Name),
51 // Argument::Create(ArgType::NoDNS),
52 // Argument::Create(ArgType::Progress),
@@ -58,6 +60,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
60 Argument::Create(ArgType::StopSignal),
61 Argument::Create(ArgType::TMPFS, false, NO_LIMIT),
62 Argument::Create(ArgType::TTY),
63 + Argument::Create(ArgType::Ulimit, false, NO_LIMIT),
64 Argument::Create(ArgType::User),
65 Argument::Create(ArgType::Volume, false, NO_LIMIT),
66 // Argument::Create(ArgType::Virtual),
src/windows/wslc/services/ContainerModel.h
+3
@@ -53,6 +53,9 @@ struct ContainerOptions
53 std::vector<std::string> Tmpfs;
54 std::vector<std::pair<std::string, std::string>> Labels;
55 std::optional<std::wstring> CidFile{};
56 + std::optional<int64_t> MemoryBytes{};
57 + std::optional<int64_t> NanoCpus{};
58 + std::vector<std::tuple<std::string, int64_t, int64_t>> Ulimits;
59 };
60
61 struct CreateContainerResult
src/windows/wslc/services/ContainerService.cpp
+15
@@ -109,6 +109,21 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
109 containerLauncher.SetShmSize(options.ShmSize.value());
110 }
111
112 + if (options.MemoryBytes.has_value())
113 + {
114 + containerLauncher.SetMemoryLimit(options.MemoryBytes.value());
115 + }
116 +
117 + if (options.NanoCpus.has_value())
118 + {
119 + containerLauncher.SetNanoCpus(options.NanoCpus.value());
120 + }
121 +
122 + for (const auto& [name, soft, hard] : options.Ulimits)
123 + {
124 + containerLauncher.AddUlimit(name, soft, hard);
125 + }
126 +
127 if (!options.Entrypoint.empty())
128 {
129 auto entrypoints = options.Entrypoint;
src/windows/wslc/tasks/ContainerTasks.cpp
+18
@@ -422,6 +422,24 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
422 options.ShmSize = validation::GetMemorySizeFromString(context.Args.Get<ArgType::ShmSize>());
423 }
424
425 + if (context.Args.Contains(ArgType::Memory))
426 + {
427 + options.MemoryBytes = validation::GetMemorySizeFromString(context.Args.Get<ArgType::Memory>());
428 + }
429 +
430 + if (context.Args.Contains(ArgType::Cpus))
431 + {
432 + options.NanoCpus = validation::GetNanoCpusFromString(context.Args.Get<ArgType::Cpus>());
433 + }
434 +
435 + if (context.Args.Contains(ArgType::Ulimit))
436 + {
437 + for (const auto& value : context.Args.GetAll<ArgType::Ulimit>())
438 + {
439 + options.Ulimits.emplace_back(validation::ParseUlimit(value));
440 + }
441 + }
442 +
443 if (context.Args.Contains(ArgType::Command))
444 {
445 options.Arguments.emplace_back(WideToMultiByte(context.Args.Get<ArgType::Command>()));
test/windows/wslc/WSLCCLIResourceLimitsParserUnitTests.cpp new
+129
@@ -0,0 +1,129 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLIResourceLimitsParserUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for WSLC CLI resource-limit (--cpus, --memory, --ulimit) parsing and validation.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "WSLCCLITestHelpers.h"
18 +#include "ArgumentValidation.h"
19 +
20 +using namespace wsl::windows::wslc;
21 +
22 +namespace WSLCCLIResourceLimitsParserUnitTests {
23 +
24 +class WSLCCLIResourceLimitsParserUnitTests
25 +{
26 + WSLC_TEST_CLASS(WSLCCLIResourceLimitsParserUnitTests)
27 +
28 + TEST_METHOD(NanoCpus_Valid)
29 + {
30 + // (input, expected nanoCpus)
31 + std::vector<std::pair<std::wstring, int64_t>> valid = {
32 + {L"1", 1'000'000'000LL},
33 + {L"2", 2'000'000'000LL},
34 + {L"0.5", 500'000'000LL},
35 + {L"1.5", 1'500'000'000LL},
36 + {L"2.5", 2'500'000'000LL},
37 + {L"0.001", 1'000'000LL},
38 + };
39 +
40 + for (const auto& [input, expected] : valid)
41 + {
42 + const auto actual = validation::GetNanoCpusFromString(input, L"cpus");
43 + VERIFY_ARE_EQUAL(expected, actual);
44 + }
45 + }
46 +
47 + TEST_METHOD(NanoCpus_Invalid)
48 + {
49 + // Each value should be rejected as an invalid --cpus value.
50 + const std::vector<std::wstring> invalid = {
51 + L"",
52 + L"0", // not positive
53 + L"-1", // sign char rejected
54 + L"-0.5", // sign char rejected
55 + L"abc", // not numeric
56 + L"1.5x", // trailing garbage
57 + L" 1", // leading whitespace
58 + L"1 ", // trailing whitespace
59 + L"1e3", // exponent not allowed
60 + L"+1", // sign char rejected
61 + L"1.2.3", // multiple dots (rejected by strtod's strict end check)
62 + L"99999999999" // overflow when multiplied by 1e9
63 + };
64 +
65 + for (const auto& input : invalid)
66 + {
67 + VERIFY_THROWS(validation::GetNanoCpusFromString(input, L"cpus"), ArgumentException);
68 + }
69 + }
70 +
71 + TEST_METHOD(Ulimit_Valid)
72 + {
73 + // (input, expectedName, expectedSoft, expectedHard)
74 + std::vector<std::tuple<std::wstring, std::string, int64_t, int64_t>> valid = {
75 + {L"nofile=1024", "nofile", 1024, 1024},
76 + {L"nofile=1024:2048", "nofile", 1024, 2048},
77 + {L"nproc=512:512", "nproc", 512, 512},
78 + {L"core=-1", "core", -1, -1},
79 + {L"core=-1:-1", "core", -1, -1},
80 + {L"memlock=0", "memlock", 0, 0},
81 + {L"stack=8192:-1", "stack", 8192, -1},
82 + };
83 +
84 + for (const auto& [input, expectedName, expectedSoft, expectedHard] : valid)
85 + {
86 + const auto [name, soft, hard] = validation::ParseUlimit(input, L"ulimit");
87 + VERIFY_ARE_EQUAL(expectedName, name);
88 + VERIFY_ARE_EQUAL(expectedSoft, soft);
89 + VERIFY_ARE_EQUAL(expectedHard, hard);
90 + }
91 + }
92 +
93 + TEST_METHOD(Ulimit_Invalid)
94 + {
95 + const std::vector<std::wstring> invalid = {
96 + L"",
97 + L"=1024", // empty name
98 + L"nofile=", // empty value
99 + L"nofile", // missing '='
100 + L"nofile=abc", // non-numeric soft
101 + L"nofile=1024:", // empty hard
102 + L"nofile=:1024", // empty soft
103 + L"nofile=-2", // negative other than -1
104 + L"nofile=1024:512", // hard < soft (and both positive)
105 + L"nofile=-1:1024", // unlimited soft but limited hard
106 + L"nofile=-1:9223372036854775807", // unlimited soft but finite (INT64_MAX) hard
107 + L"nofile=1.5", // not integer
108 + };
109 +
110 + for (const auto& input : invalid)
111 + {
112 + VERIFY_THROWS(validation::ParseUlimit(input, L"ulimit"), ArgumentException);
113 + }
114 + }
115 +
116 + TEST_METHOD(NanoCpus_Validator)
117 + {
118 + VERIFY_NO_THROW(validation::ValidateNanoCpus({L"0.5", L"1", L"2.5"}, L"cpus"));
119 + VERIFY_THROWS(validation::ValidateNanoCpus({L"1", L"0"}, L"cpus"), ArgumentException);
120 + }
121 +
122 + TEST_METHOD(Ulimit_Validator)
123 + {
124 + VERIFY_NO_THROW(validation::ValidateUlimit({L"nofile=1024", L"core=-1"}, L"ulimit"));
125 + VERIFY_THROWS(validation::ValidateUlimit({L"nofile=1024", L"bad"}, L"ulimit"), ArgumentException);
126 + }
127 +};
128 +
129 +} // namespace WSLCCLIResourceLimitsParserUnitTests
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+3
@@ -828,6 +828,7 @@ private:
828 std::wstringstream options;
829 options << L"The following options are available:\r\n" //
830 << L" --cidfile Write the container ID to the provided path\r\n"
831 + << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
832 << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
833 << L" --dns-option Set DNS options\r\n"
834 << L" --dns-search Set DNS search domains\r\n"
@@ -839,6 +840,7 @@ private:
840 << L" -h,--hostname Container host name\r\n"
841 << L" -i,--interactive Attach to stdin and keep it open\r\n"
842 << L" -l,--label Set metadata on an object\r\n"
843 + << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
844 << L" --name Name of the container\r\n"
845 << L" -p,--publish Publish a port from a container to host\r\n"
846 << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
@@ -848,6 +850,7 @@ private:
850 << L" --stop-signal Signal to stop the container\r\n"
851 << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
852 << L" -t,--tty Open a TTY with the container process.\r\n"
853 + << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
854 << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
855 << L" -v,--volume Bind mount a volume to the container\r\n"
856 << L" -w,--workdir Working directory inside the container\r\n"
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+71
@@ -731,6 +731,74 @@ class WSLCE2EContainerRunTests
731 }
732 }
733
734 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus)
735 + {
736 + auto result = RunWslc(std::format(L"container run --name {} --cpus 1.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
737 + result.Verify({.Stderr = L"", .ExitCode = 0});
738 +
739 + const auto inspect = InspectContainer(WslcContainerName);
740 + VERIFY_ARE_EQUAL(static_cast<int64_t>(1'500'000'000), inspect.HostConfig.NanoCpus);
741 + }
742 +
743 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory)
744 + {
745 + auto result = RunWslc(std::format(L"container run --name {} --memory 32M {} true", WslcContainerName, DebianImage.NameAndTag()));
746 + // Note: stderr is not asserted here because some kernels emit a swap-limit warning
747 + // ("Your kernel does not support swap limit capabilities...") when a memory limit is set.
748 + result.Verify({.ExitCode = 0});
749 +
750 + const auto inspect = InspectContainer(WslcContainerName);
751 + VERIFY_ARE_EQUAL(static_cast<int64_t>(32) * 1024 * 1024, inspect.HostConfig.Memory);
752 + }
753 +
754 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit)
755 + {
756 + auto result = RunWslc(std::format(
757 + L"container run --name {} --ulimit nofile=1024:2048 --ulimit nproc=512 {} true", WslcContainerName, DebianImage.NameAndTag()));
758 + result.Verify({.Stderr = L"", .ExitCode = 0});
759 +
760 + const auto inspect = InspectContainer(WslcContainerName);
761 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), inspect.HostConfig.Ulimits.size());
762 +
763 + std::map<std::string, std::pair<int64_t, int64_t>> byName;
764 + for (const auto& ul : inspect.HostConfig.Ulimits)
765 + {
766 + byName[ul.Name] = {ul.Soft, ul.Hard};
767 + }
768 +
769 + VERIFY_IS_TRUE(byName.contains("nofile"));
770 + VERIFY_ARE_EQUAL(static_cast<int64_t>(1024), byName["nofile"].first);
771 + VERIFY_ARE_EQUAL(static_cast<int64_t>(2048), byName["nofile"].second);
772 +
773 + VERIFY_IS_TRUE(byName.contains("nproc"));
774 + VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].first);
775 + VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].second);
776 + }
777 +
778 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Cpus_Invalid)
779 + {
780 + auto result = RunWslc(std::format(L"container run --rm --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
781 + 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});
782 + EnsureContainerDoesNotExist(WslcContainerName);
783 + }
784 +
785 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Memory_Invalid)
786 + {
787 + auto result =
788 + RunWslc(std::format(L"container run --rm --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
789 + result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
790 + EnsureContainerDoesNotExist(WslcContainerName);
791 + }
792 +
793 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ulimit_Invalid)
794 + {
795 + auto result =
796 + RunWslc(std::format(L"container run --rm --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
797 + result.Verify(
798 + {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)\r\n", .ExitCode = 1});
799 + EnsureContainerDoesNotExist(WslcContainerName);
800 + }
801 +
802 WSLC_TEST_METHOD(WSLCE2E_Container_Run_StopSignal_Invalid)
803 {
804 {
@@ -820,6 +888,7 @@ private:
888 std::wstringstream options;
889 options << L"The following options are available:\r\n"
890 << L" --cidfile Write the container ID to the provided path\r\n"
891 + << L" --cpus Number of CPUs (e.g. 0.5, 1, 2.5)\r\n"
892 << L" -d,--detach Run container in detached mode\r\n"
893 << L" --dns IP address of the DNS nameserver in resolv.conf\r\n"
894 << L" --dns-option Set DNS options\r\n"
@@ -832,6 +901,7 @@ private:
901 << L" -h,--hostname Container host name\r\n"
902 << L" -i,--interactive Attach to stdin and keep it open\r\n"
903 << L" -l,--label Set metadata on an object\r\n"
904 + << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
905 << L" --name Name of the container\r\n"
906 << L" -p,--publish Publish a port from a container to host\r\n"
907 << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
@@ -841,6 +911,7 @@ private:
911 << L" --stop-signal Signal to stop the container\r\n"
912 << L" --tmpfs Mount tmpfs to the container at the given path\r\n"
913 << L" -t,--tty Open a TTY with the container process.\r\n"
914 + << L" --ulimit Ulimit options (format: <name>=<soft>[:<hard>], use -1 for unlimited)\r\n"
915 << L" -u,--user User ID for the process (name|uid|uid:gid)\r\n"
916 << L" -v,--volume Bind mount a volume to the container\r\n"
917 << L" -w,--workdir Working directory inside the container\r\n"