@samitouri / QOSAMI-WSL / commits / 6fce9369

Add -n option 'wslc logs' (#40408)

* Add -n option 'wslc logs' * Add test coverage * Format * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Apply PR feedback * Format --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Blue committed May 5, 2026 at 11:09 UTC 6fce9369abcdeecacd32ff5349f2531c68f154a5
11 files changed +118 -11
localization/strings/en-US/Resources.resw
+7
@@ -2646,6 +2646,9 @@ On first run, creates the file with all settings commented out at their defaults
2646 <data name="WSLCCLI_TagArgDescription" xml:space="preserve">
2647 <value>Tag for the built image</value>
2648 </data>
2649 + <data name="WSLCCLI_TailArgDescription" xml:space="preserve">
2650 + <value>Number of lines to show from the end of the logs</value>
2651 + </data>
2652 <data name="WSLCCLI_TargetArgDescription" xml:space="preserve">
2653 <value>New image reference in the image-name[:tag] format</value>
2654 </data>
@@ -2737,6 +2740,10 @@ On first run, creates the file with all settings commented out at their defaults
2740 <value>Invalid {} value: {} is not a recognized signal name or number (Example: SIGKILL, kill, or 9).</value>
2741 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2742 </data>
2743 + <data name="WSLCCLI_InvalidIntegerArgumentError" xml:space="preserve">
2744 + <value>Invalid {} argument value: {}</value>
2745 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2746 + </data>
2747 <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
2748 <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
2749 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/wslc/arguments/ArgumentDefinitions.h
+1
@@ -89,6 +89,7 @@ _(SessionId, "session-id", NO_ALIAS, Kind::Positional, L
89 _(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \
90 _(Signal, "signal", L"s", Kind::Value, Localization::WSLCCLI_SignalArgDescription(L"SIGKILL")) \
91 _(Source, "source", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SourceArgDescription()) \
92 +_(Tail, "tail", L"n", Kind::Value, Localization::WSLCCLI_TailArgDescription()) \
93 _(Tag, "tag", L"t", Kind::Value, Localization::WSLCCLI_TagArgDescription()) \
94 _(Target, "target", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_TargetArgDescription()) \
95 _(Time, "time", L"t", Kind::Value, Localization::WSLCCLI_TimeArgDescription()) \
src/windows/wslc/arguments/ArgumentValidation.cpp
+5
@@ -40,6 +40,11 @@ void Argument::Validate(const ArgMap& execArgs) const
40 validation::ValidateWSLCSignalFromString(execArgs.GetAll<ArgType::Signal>(), m_name);
41 break;
42
43 + case ArgType::Tail:
44 + validation::ValidateIntegerFromString<ULONGLONG>(
45 + execArgs.GetAll<ArgType::Tail>(), m_name, [](auto value) { return value != 0; });
46 + break;
47 +
48 case ArgType::Time:
49 validation::ValidateIntegerFromString<LONGLONG>(execArgs.GetAll<ArgType::Time>(), m_name);
50 break;
src/windows/wslc/arguments/ArgumentValidation.h
+11 -7
@@ -19,25 +19,29 @@ Abstract:
19 #include <string>
20 #include <vector>
21 #include <charconv>
22 -#include <format>
22 #include <wslc.h>
23 #include <string.hpp>
24 +#include "Localization.h"
25
26 using namespace wsl::windows::wslc::models;
27
28 namespace wsl::windows::wslc::validation {
29
30 template <typename T>
31 -void ValidateIntegerFromString(const std::vector<std::wstring>& values, const std::wstring& argName)
31 +void ValidateIntegerFromString(
32 + const std::vector<std::wstring>& values, const std::wstring& argName, const std::function<bool(T)>& validate = [](T) {
33 + return true;
34 + })
35 {
36 for (const auto& value : values)
37 {
35 - std::ignore = GetIntegerFromString<T>(value, argName);
38 + std::ignore = GetIntegerFromString<T>(value, argName, validate);
39 }
40 }
41
42 template <typename T>
40 -T GetIntegerFromString(const std::wstring& value, const std::wstring& argName = {})
43 +T GetIntegerFromString(
44 + const std::wstring& value, const std::wstring& argName = {}, const std::function<bool(T)>& validate = [](T) { return true; })
45 {
46 std::string narrowValue = wsl::windows::common::string::WideToMultiByte(value);
47
@@ -47,9 +51,9 @@ T GetIntegerFromString(const std::wstring& value, const std::wstring& argName =
51 auto result = std::from_chars(begin, end, convertedValue);
52
53 // Reject conversion errors and partial parses (e.g. "1.5", "9abc")
50 - if (result.ec != std::errc() || result.ptr != end)
54 + if (result.ec != std::errc() || result.ptr != end || !validate(convertedValue))
55 {
52 - throw ArgumentException(std::format(L"Invalid {} argument value: {}", argName, value));
56 + throw ArgumentException(wsl::shared::Localization::WSLCCLI_InvalidIntegerArgumentError(argName, value));
57 }
58
59 return convertedValue;
@@ -66,4 +70,4 @@ InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstri
70 void ValidateGpus(const std::vector<std::wstring>& values, const std::wstring& argName);
71 void ValidateVolumeMount(const std::vector<std::wstring>& values);
72
69 -} // namespace wsl::windows::wslc::validation
\ No newline at end of file
73 +} // namespace wsl::windows::wslc::validation
src/windows/wslc/commands/ContainerLogsCommand.cpp
+1
@@ -30,6 +30,7 @@ std::vector<Argument> ContainerLogsCommand::GetArguments() const
30 Argument::Create(ArgType::ContainerId, true),
31 Argument::Create(ArgType::Session),
32 Argument::Create(ArgType::Follow),
33 + Argument::Create(ArgType::Tail),
34 };
35 }
36
src/windows/wslc/services/ContainerService.cpp
+2 -2
@@ -473,7 +473,7 @@ InspectContainer ContainerService::Inspect(Session& session, const std::string&
473 return wsl::shared::FromJson<InspectContainer>(output.get());
474 }
475
476 -void ContainerService::Logs(Session& session, const std::string& id, bool follow)
476 +void ContainerService::Logs(Session& session, const std::string& id, bool follow, ULONGLONG tail)
477 {
478 wil::com_ptr<IWSLCContainer> container;
479 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
@@ -483,7 +483,7 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow
483 WSLCLogsFlags flags = WSLCLogsFlagsNone;
484 WI_SetFlagIf(flags, WSLCLogsFlagsFollow, follow);
485
486 - THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, 0, 0, 0));
486 + THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, 0, 0, tail));
487
488 wsl::windows::common::relay::MultiHandleWait io;
489 io.AddHandle(std::make_unique<wsl::windows::common::relay::RelayHandle<wsl::windows::common::relay::ReadHandle>>(
src/windows/wslc/services/ContainerService.h
+1 -1
@@ -32,6 +32,6 @@ struct ContainerService
32 static std::vector<models::ContainerInformation> List(models::Session& session);
33 static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
34 static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
35 - static void Logs(models::Session& session, const std::string& id, bool follow);
35 + static void Logs(models::Session& session, const std::string& id, bool follow, ULONGLONG tail = 0);
36 };
37 } // namespace wsl::windows::wslc::services
src/windows/wslc/tasks/ContainerTasks.cpp
+8 -1
@@ -435,6 +435,13 @@ void ViewContainerLogs(CLIExecutionContext& context)
435 auto& session = context.Data.Get<Data::Session>();
436 auto containerId = context.Args.Get<ArgType::ContainerId>();
437 bool follow = context.Args.Contains(ArgType::Follow);
438 - ContainerService::Logs(session, WideToMultiByte(containerId), follow);
438 +
439 + ULONGLONG tail = 0;
440 + if (context.Args.Contains(ArgType::Tail))
441 + {
442 + tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>());
443 + }
444 +
445 + ContainerService::Logs(session, WideToMultiByte(containerId), follow, tail);
446 }
447 } // namespace wsl::windows::wslc::task
test/windows/wslc/CommandLineTestCases.h
+12
@@ -151,6 +151,18 @@ COMMAND_LINE_TEST_CASE(L"container logs cont1", L"logs", true)
151 COMMAND_LINE_TEST_CASE(L"container logs --follow cont1", L"logs", true)
152 COMMAND_LINE_TEST_CASE(L"container logs cont1 -f", L"logs", true)
153 COMMAND_LINE_TEST_CASE(L"container logs", L"logs", false)
154 +COMMAND_LINE_TEST_CASE(L"container logs --tail 10 cont1", L"logs", true)
155 +COMMAND_LINE_TEST_CASE(L"container logs -n 10 cont1", L"logs", true)
156 +COMMAND_LINE_TEST_CASE(L"container logs cont1 --tail 10", L"logs", true)
157 +COMMAND_LINE_TEST_CASE(L"container logs --tail=10 cont1", L"logs", true)
158 +COMMAND_LINE_TEST_CASE(L"container logs -n=10 cont1", L"logs", true)
159 +COMMAND_LINE_TEST_CASE(L"container logs --follow --tail 5 cont1", L"logs", true)
160 +COMMAND_LINE_TEST_CASE(L"container logs --tail 0 cont1", L"logs", false)
161 +COMMAND_LINE_TEST_CASE(L"container logs --tail abc cont1", L"logs", false)
162 +COMMAND_LINE_TEST_CASE(L"container logs -n abc cont1", L"logs", false)
163 +COMMAND_LINE_TEST_CASE(L"container logs -n=abc cont1", L"logs", false)
164 +COMMAND_LINE_TEST_CASE(L"container logs --tail", L"logs", false)
165 +COMMAND_LINE_TEST_CASE(L"container logs -n", L"logs", false)
166
167 // Image command
168 COMMAND_LINE_TEST_CASE(L"image build C:\\context", L"build", true)
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+5
@@ -115,6 +115,11 @@ class WSLCCLIArgumentUnitTests
115 VERIFY_NO_THROW(validation::ValidateIntegerFromString<LONGLONG>({L"1234", L"-1234567890123"}, L"testArg"));
116 VERIFY_THROWS(validation::ValidateIntegerFromString<LONGLONG>({L"1234", L"-92233720369999854775808"}, L"testArg"), ArgumentException);
117
118 + // Verify --tail validation rejects 0 (mirrors ArgType::Tail validation)
119 + VERIFY_THROWS(validation::ValidateIntegerFromString<ULONGLONG>({L"0"}, L"tail", [](auto value) { return value != 0; }), ArgumentException);
120 + VERIFY_NO_THROW(validation::ValidateIntegerFromString<ULONGLONG>({L"10"}, L"tail", [](auto value) { return value != 0; }));
121 + VERIFY_NO_THROW(validation::ValidateIntegerFromString<ULONGLONG>({L"1"}, L"tail", [](auto value) { return value != 0; }));
122 +
123 // Verify WSLCSignal conversion
124 auto validSignal = validation::GetWSLCSignalFromString(L"SIGTERM");
125 VERIFY_ARE_EQUAL(validSignal, WSLCSignalSIGTERM);
test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp new
+65
@@ -0,0 +1,65 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerLogsTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for WSLC container logs.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +
21 +class WSLCE2EContainerLogsTests
22 +{
23 + WSLC_TEST_CLASS(WSLCE2EContainerLogsTests)
24 +
25 + TEST_CLASS_SETUP(ClassSetup)
26 + {
27 + EnsureImageIsLoaded(DebianImage);
28 + return true;
29 + }
30 +
31 + TEST_CLASS_CLEANUP(ClassCleanup)
32 + {
33 + EnsureContainerDoesNotExist(WslcContainerName);
34 + EnsureImageIsDeleted(DebianImage);
35 + return true;
36 + }
37 +
38 + TEST_METHOD_SETUP(TestMethodSetup)
39 + {
40 + EnsureContainerDoesNotExist(WslcContainerName);
41 + return true;
42 + }
43 +
44 + WSLC_TEST_METHOD(WSLCE2E_Container_Logs_Tail)
45 + {
46 + // Run a container that outputs two lines
47 + auto result = RunWslc(std::format(
48 + L"container run --name {} {} sh -c \"echo line1 && echo line2\"", WslcContainerName, DebianImage.NameAndTag()));
49 + result.Verify({.Stdout = L"line1\nline2\n", .Stderr = L"", .ExitCode = 0});
50 +
51 + // Verify --tail 1 only shows the last line
52 + result = RunWslc(std::format(L"container logs --tail 1 {}", WslcContainerName));
53 + result.Verify({.Stdout = L"line2\n", .Stderr = L"", .ExitCode = 0});
54 +
55 + // Verify -n 2 shows both lines
56 + result = RunWslc(std::format(L"container logs -n 2 {}", WslcContainerName));
57 + result.Verify({.Stdout = L"line1\nline2\n", .Stderr = L"", .ExitCode = 0});
58 + }
59 +
60 +private:
61 + const std::wstring WslcContainerName = L"wslc-test-logs";
62 + const TestImage& DebianImage = DebianTestImage();
63 +};
64 +
65 +} // namespace WSLCE2ETests