diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index de258d29d..1283a6188 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2575,22 +2575,37 @@ On first run, creates the file with all settings commented out at their defaults Signal to send (default: {}) {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Show logs since timestamp (e.g. unix timestamp) + Current or existing image reference in the image-name[:tag] format Tag for the built image + + Number of lines to show from the end of the logs + New image reference in the image-name[:tag] format Time in seconds to wait before executing (default 5) + + Show timestamps in log output + Open a TTY with the container process. {Locked="TTY"}Command line arguments should not be translated + + Type of the object to inspect + + + Show logs before timestamp (e.g. unix timestamp) + Output verbose details @@ -2764,9 +2779,6 @@ On first run, creates the file with all settings commented out at their defaults Name or Id of any object type - - Type of the object to inspect - Inspect objects. diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index a7bfe4e9b..dda890786 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -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()) \ diff --git a/src/windows/wslc/commands/ContainerLogsCommand.cpp b/src/windows/wslc/commands/ContainerLogsCommand.cpp index da4796f30..350a0f508 100644 --- a/src/windows/wslc/commands/ContainerLogsCommand.cpp +++ b/src/windows/wslc/commands/ContainerLogsCommand.cpp @@ -30,6 +30,10 @@ std::vector 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), }; } diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 90af89e87..b281f2a0d 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -467,7 +467,7 @@ InspectContainer ContainerService::Inspect(Session& session, const std::string& return wsl::shared::FromJson(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 container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -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>( diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h index aeddaff9f..6292b7ab6 100644 --- a/src/windows/wslc/services/ContainerService.h +++ b/src/windows/wslc/services/ContainerService.h @@ -32,6 +32,6 @@ struct ContainerService static std::vector 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 diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 804f54282..a3102e2c3 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -392,6 +392,26 @@ void ViewContainerLogs(CLIExecutionContext& context) auto& session = context.Data.Get(); auto containerId = context.Args.Get(); 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())); + } + + ULONGLONG since = 0; + if (context.Args.Contains(ArgType::Since)) + { + since = std::stoull(WideToMultiByte(context.Args.Get())); + } + + ULONGLONG until = 0; + if (context.Args.Contains(ArgType::Until)) + { + until = std::stoull(WideToMultiByte(context.Args.Get())); + } + + ContainerService::Logs(session, WideToMultiByte(containerId), follow, timestamps, since, until, tail); } } // namespace wsl::windows::wslc::task diff --git a/test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp new file mode 100644 index 000000000..478da8469 --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp @@ -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 [] \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