Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2575,22 +2575,37 @@ On first run, creates the file with all settings commented out at their defaults
<value>Signal to send (default: {})</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_SinceArgDescription" xml:space="preserve">
<value>Show logs since timestamp (e.g. unix timestamp)</value>
</data>
<data name="WSLCCLI_SourceArgDescription" xml:space="preserve">
<value>Current or existing image reference in the image-name[:tag] format</value>
</data>
<data name="WSLCCLI_TagArgDescription" xml:space="preserve">
<value>Tag for the built image</value>
</data>
<data name="WSLCCLI_TailArgDescription" xml:space="preserve">
<value>Number of lines to show from the end of the logs</value>
</data>
<data name="WSLCCLI_TargetArgDescription" xml:space="preserve">
<value>New image reference in the image-name[:tag] format</value>
</data>
<data name="WSLCCLI_TimeArgDescription" xml:space="preserve">
<value>Time in seconds to wait before executing (default 5)</value>
</data>
<data name="WSLCCLI_TimestampsArgDescription" xml:space="preserve">
<value>Show timestamps in log output</value>
</data>
<data name="WSLCCLI_TTYArgDescription" xml:space="preserve">
<value>Open a TTY with the container process.</value>
<comment>{Locked="TTY"}Command line arguments should not be translated</comment>
</data>
<data name="WSLCCLI_TypeArgDescription" xml:space="preserve">
<value>Type of the object to inspect</value>
</data>
<data name="WSLCCLI_UntilArgDescription" xml:space="preserve">
<value>Show logs before timestamp (e.g. unix timestamp)</value>
</data>
<data name="WSLCCLI_VerboseArgDescription" xml:space="preserve">
<value>Output verbose details</value>
</data>
Expand Down Expand Up @@ -2764,9 +2779,6 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_ObjectIdArgDescription" xml:space="preserve">
<value>Name or Id of any object type</value>
</data>
<data name="WSLCCLI_TypeArgDescription" xml:space="preserve">
<value>Type of the object to inspect</value>
</data>
<data name="WSLCCLI_InspectDesc" xml:space="preserve">
<value>Inspect objects.</value>
</data>
Expand Down
4 changes: 4 additions & 0 deletions src/windows/wslc/arguments/ArgumentDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,17 @@ _(Session, "session", NO_ALIAS, Kind::Value, L
_(SessionId, "session-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SessionIdPositionalArgDescription()) \
_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \
_(Signal, "signal", L"s", Kind::Value, Localization::WSLCCLI_SignalArgDescription(L"SIGKILL")) \
_(Since, "since", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SinceArgDescription()) \
_(Source, "source", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SourceArgDescription()) \
_(Tag, "tag", L"t", Kind::Value, Localization::WSLCCLI_TagArgDescription()) \
_(Tail, "tail", L"n", Kind::Value, Localization::WSLCCLI_TailArgDescription()) \
_(Target, "target", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_TargetArgDescription()) \
_(Time, "time", L"t", Kind::Value, Localization::WSLCCLI_TimeArgDescription()) \
_(Timestamps, "timestamps", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_TimestampsArgDescription()) \
_(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()) \
_(Until, "until", NO_ALIAS, Kind::Value, Localization::WSLCCLI_UntilArgDescription()) \
_(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()) \
Expand Down
4 changes: 4 additions & 0 deletions src/windows/wslc/commands/ContainerLogsCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ std::vector<Argument> ContainerLogsCommand::GetArguments() const
Argument::Create(ArgType::ContainerId, true),
Argument::Create(ArgType::Session),
Argument::Create(ArgType::Follow),
Argument::Create(ArgType::Tail),
Argument::Create(ArgType::Since),
Argument::Create(ArgType::Until),
Argument::Create(ArgType::Timestamps),
Comment on lines +33 to +36
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command adds --since/--until arguments, but the new E2E suite only covers --tail, --timestamps, and --follow. Add at least one end-to-end test that exercises --since and --until filtering (or validates their error handling) to ensure these options remain functional.

Copilot uses AI. Check for mistakes.
};
}

Expand Down
5 changes: 3 additions & 2 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ InspectContainer ContainerService::Inspect(Session& session, const std::string&
return wsl::shared::FromJson<InspectContainer>(output.get());
}

void ContainerService::Logs(Session& session, const std::string& id, bool follow)
void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail)
{
wil::com_ptr<IWSLCContainer> container;
THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
Expand All @@ -476,8 +476,9 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow
COMOutputHandle stderrHandle;
WSLCLogsFlags flags = WSLCLogsFlagsNone;
WI_SetFlagIf(flags, WSLCLogsFlagsFollow, follow);
WI_SetFlagIf(flags, WSLCLogsFlagsTimestamps, timestamps);

THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, 0, 0, 0));
THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, since, until, tail));

wsl::windows::common::relay::MultiHandleWait io;
io.AddHandle(std::make_unique<wsl::windows::common::relay::RelayHandle<wsl::windows::common::relay::ReadHandle>>(
Expand Down
2 changes: 1 addition & 1 deletion src/windows/wslc/services/ContainerService.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ struct ContainerService
static std::vector<models::ContainerInformation> List(models::Session& session);
static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
static void Logs(models::Session& session, const std::string& id, bool follow);
static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps = false, ULONGLONG since = 0, ULONGLONG until = 0, ULONGLONG tail = 0);
};
} // namespace wsl::windows::wslc::services
22 changes: 21 additions & 1 deletion src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,26 @@ void ViewContainerLogs(CLIExecutionContext& context)
auto& session = context.Data.Get<Data::Session>();
auto containerId = context.Args.Get<ArgType::ContainerId>();
bool follow = context.Args.Contains(ArgType::Follow);
ContainerService::Logs(session, WideToMultiByte(containerId), follow);
bool timestamps = context.Args.Contains(ArgType::Timestamps);

ULONGLONG tail = 0;
if (context.Args.Contains(ArgType::Tail))
{
tail = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Tail>()));
}

ULONGLONG since = 0;
if (context.Args.Contains(ArgType::Since))
{
since = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Since>()));
}

ULONGLONG until = 0;
if (context.Args.Contains(ArgType::Until))
{
until = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Until>()));
Comment on lines +400 to +412
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing --tail with std::stoull can throw std::invalid_argument/std::out_of_range, which will bypass the command argument error path and end up as a generic caught exception. Use the existing argument validation helpers (e.g., validation::GetIntegerFromString<ULONGLONG>(..., L"tail")) or convert failures into an ArgumentException so users get a clear, consistent CLI error for invalid values.

Suggested change
tail = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Tail>()));
}
ULONGLONG since = 0;
if (context.Args.Contains(ArgType::Since))
{
since = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Since>()));
}
ULONGLONG until = 0;
if (context.Args.Contains(ArgType::Until))
{
until = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Until>()));
tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>(), L"tail");
}
ULONGLONG since = 0;
if (context.Args.Contains(ArgType::Since))
{
since = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Since>(), L"since");
}
ULONGLONG until = 0;
if (context.Args.Contains(ArgType::Until))
{
until = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Until>(), L"until");

Copilot uses AI. Check for mistakes.
Comment on lines +400 to +412
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--since is parsed with std::stoull, which can throw and produce a non-user-friendly error path. Prefer validation::GetIntegerFromString<ULONGLONG> (or a dedicated validator in Argument::Validate) so invalid timestamps are rejected with a consistent CLI ArgumentException message.

Suggested change
tail = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Tail>()));
}
ULONGLONG since = 0;
if (context.Args.Contains(ArgType::Since))
{
since = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Since>()));
}
ULONGLONG until = 0;
if (context.Args.Contains(ArgType::Until))
{
until = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Until>()));
tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>());
}
ULONGLONG since = 0;
if (context.Args.Contains(ArgType::Since))
{
since = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Since>());
}
ULONGLONG until = 0;
if (context.Args.Contains(ArgType::Until))
{
until = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Until>());

Copilot uses AI. Check for mistakes.
Comment on lines +400 to +412
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as --since: parsing --until via std::stoull can throw and skip the CLI's normal argument-error handling. Use the repo's integer parsing/validation helpers so bad values surface as a clear user-facing argument error.

Suggested change
tail = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Tail>()));
}
ULONGLONG since = 0;
if (context.Args.Contains(ArgType::Since))
{
since = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Since>()));
}
ULONGLONG until = 0;
if (context.Args.Contains(ArgType::Until))
{
until = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Until>()));
tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>());
}
ULONGLONG since = 0;
if (context.Args.Contains(ArgType::Since))
{
since = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Since>());
}
ULONGLONG until = 0;
if (context.Args.Contains(ArgType::Until))
{
until = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Until>());

Copilot uses AI. Check for mistakes.
}

ContainerService::Logs(session, WideToMultiByte(containerId), follow, timestamps, since, until, tail);
}
} // namespace wsl::windows::wslc::task
170 changes: 170 additions & 0 deletions test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*++

Copyright (c) Microsoft. All rights reserved.

Module Name:

WSLCE2EContainerLogsTests.cpp

Abstract:

This file contains end-to-end tests for WSLC.
--*/

#include "precomp.h"
#include "windows/Common.h"
#include "WSLCExecutor.h"
#include "WSLCE2EHelpers.h"

namespace WSLCE2ETests {
using namespace wsl::shared;

class WSLCE2EContainerLogsTests
{
WSLC_TEST_CLASS(WSLCE2EContainerLogsTests)

TEST_CLASS_SETUP(ClassSetup)
{
EnsureImageIsLoaded(DebianImage);
return true;
}

TEST_CLASS_CLEANUP(ClassCleanup)
{
EnsureContainerDoesNotExist(WslcContainerName);
EnsureImageIsDeleted(DebianImage);
return true;
}

TEST_METHOD_SETUP(MethodSetup)
{
EnsureContainerDoesNotExist(WslcContainerName);
return true;
}

WSLC_TEST_METHOD(WSLCE2E_Container_Logs_HelpCommand)
{
auto result = RunWslc(L"container logs --help");
result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0});
}

WSLC_TEST_METHOD(WSLCE2E_Container_Logs_MissingContainerId)
{
auto result = RunWslc(L"container logs");
result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1});
}

WSLC_TEST_METHOD(WSLCE2E_Container_Logs_Success)
{
auto result = RunWslc(std::format(
L"container run --name {} {} sh -c \"echo hello && echo world\"", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

result = RunWslc(std::format(L"container logs {}", WslcContainerName));
result.Verify({.Stderr = L"", .ExitCode = 0});
auto lines = result.GetStdoutLines();
VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), L"hello"));
VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), L"world"));
}

WSLC_TEST_METHOD(WSLCE2E_Container_Logs_TailOption)
{
auto result = RunWslc(std::format(
L"container run --name {} {} sh -c \"echo line1 && echo line2 && echo line3\"", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

result = RunWslc(std::format(L"container logs --tail 1 {}", WslcContainerName));
result.Verify({.Stderr = L"", .ExitCode = 0});
auto lines = result.GetStdoutLines();
VERIFY_ARE_EQUAL(1u, lines.size());
VERIFY_ARE_EQUAL(L"line3", lines[0]);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Logs_TimestampsOption)
{
auto result =
RunWslc(std::format(L"container run --name {} {} sh -c \"echo hello\"", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

result = RunWslc(std::format(L"container logs --timestamps {}", WslcContainerName));
result.Verify({.Stderr = L"", .ExitCode = 0});

auto lines = result.GetStdoutLines();
VERIFY_IS_TRUE(lines.size() >= 1u);
// Timestamps are prepended in ISO 8601 / RFC 3339 format, verify by checking for 'T' separator
VERIFY_IS_TRUE(lines[0].find(L"T") != std::wstring::npos);
VERIFY_IS_TRUE(lines[0].find(L"hello") != std::wstring::npos);
}

WSLC_TEST_METHOD(WSLCE2E_Container_Logs_FollowOption)
{
// Run a detached container that outputs lines with a delay between them
auto result = RunWslc(std::format(
L"container run -d --name {} {} sh -c \"echo first && sleep 2 && echo second\"", WslcContainerName, DebianImage.NameAndTag()));
result.Verify({.Stderr = L"", .ExitCode = 0});

// Start following logs interactively — this blocks until the container exits
auto session = RunWslcInteractive(std::format(L"container logs --follow {}", WslcContainerName));
VERIFY_IS_TRUE(session.IsRunning(), L"Follow session should be running");

// Expect the first line of output
session.ExpectStdout("first\n");

// Expect the second line which comes after the sleep
session.ExpectStdout("second\n");

// The container exits after printing second, so follow should terminate
auto exitCode = session.Wait(30000);
VERIFY_ARE_EQUAL(0, exitCode);
}

private:
const std::wstring WslcContainerName = L"wslc-e2e-container-logs";
const TestImage& DebianImage = DebianTestImage();

std::wstring GetHelpMessage() const
{
std::wstringstream output;
output << GetWslcHeader() //
<< GetDescription() //
<< GetUsage() //
<< GetAvailableCommands() //
<< GetAvailableOptions();
return output.str();
}

std::wstring GetDescription() const
{
return Localization::WSLCCLI_ContainerLogsLongDesc() + L"\r\n\r\n";
}

std::wstring GetUsage() const
{
return L"Usage: wslc container logs [<options>] <container-id>\r\n\r\n";
}

std::wstring GetAvailableCommands() const
{
std::wstringstream commands;
commands << L"The following arguments are available:\r\n" //
<< L" container-id Container name or id\r\n" //
<< L"\r\n";
return commands.str();
}

std::wstring GetAvailableOptions() const
{
std::wstringstream options;
options << L"The following options are available:\r\n" //
<< L" --session Specify the session to use\r\n" //
<< L" -f,--follow Follow log output\r\n" //
<< L" -n,--tail Number of lines to show from the end of the logs\r\n" //
<< L" --since Show logs since timestamp (e.g. unix timestamp)\r\n" //
<< L" --until Show logs before timestamp (e.g. unix timestamp)\r\n" //
<< L" --timestamps Show timestamps in log output\r\n" //
<< L" -?,--help Shows help about the selected command\r\n" //
<< L"\r\n";
return options.str();
}
};
} // namespace WSLCE2ETests
Loading