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