CLI: Update all non-help output to use Reporter (#41010)

* Update all non-help output to use Reporter

David Bennett committed Jul 17, 2026 at 15:20 UTC f642f80164ef138384551d8fdb021cdab693db5e
25 files changed +269 -217
src/windows/wslc/commands/RegistryCommand.cpp
+30 -20
@@ -25,30 +25,40 @@ using namespace wsl::shared;
25
26 namespace {
27
28 -auto MaskInput()
28 +std::wstring Prompt(wsl::windows::wslc::Reporter& reporter, const std::wstring& label, bool maskInput)
29 {
30 + // Write without a trailing newline so the cursor stays inline (matching Docker's behavior).
31 + reporter.Info(L"{}", label);
32 +
33 HANDLE input = GetStdHandle(STD_INPUT_HANDLE);
31 - DWORD mode = 0;
34 + DWORD previousMode = 0;
35 + const bool canMask = maskInput && (input != INVALID_HANDLE_VALUE) && GetConsoleMode(input, &previousMode);
36 +
37 + // Armed before echo is disabled and built from a direct lambda (no allocation, can't throw) so a
38 + // failure while masking still restores the console. Only acts once echo was actually disabled.
39 + bool echoDisabled = false;
40 + auto restoreConsole = wil::scope_exit([input, previousMode, &echoDisabled, &reporter]() {
41 + if (!echoDisabled)
42 + {
43 + return;
44 + }
45 +
46 + SetConsoleMode(input, previousMode);
47 + // Runs from a noexcept scope_exit destructor, possibly during unwinding, so swallow any
48 + // output failure to avoid std::terminate.
49 + try
50 + {
51 + reporter.Info(L"\n");
52 + }
53 + CATCH_LOG()
54 + });
55
33 - if ((input != INVALID_HANDLE_VALUE) && GetConsoleMode(input, &mode))
56 + if (canMask)
57 {
35 - THROW_IF_WIN32_BOOL_FALSE(SetConsoleMode(input, mode & ~ENABLE_ECHO_INPUT));
36 - return wil::scope_exit(std::function<void()>([input, mode] {
37 - SetConsoleMode(input, mode);
38 - std::wcerr << L'\n';
39 - }));
58 + THROW_IF_WIN32_BOOL_FALSE(SetConsoleMode(input, previousMode & ~ENABLE_ECHO_INPUT));
59 + echoDisabled = true;
60 }
61
42 - return wil::scope_exit(std::function<void()>([] {}));
43 -}
44 -
45 -std::wstring Prompt(const std::wstring& label, bool maskInput)
46 -{
47 - // Write without a trailing newline so the cursor stays inline (matching Docker's behavior).
48 - std::wcerr << label;
49 -
50 - auto restoreConsole = maskInput ? MaskInput() : wil::scope_exit(std::function<void()>([] {}));
51 -
62 std::wstring value;
63 std::getline(std::wcin, value);
64
@@ -127,7 +137,7 @@ void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const
137 // Prompt for username if not provided.
138 if (!context.Args.Contains(ArgType::Username))
139 {
130 - context.Args.Add(ArgType::Username, Prompt(Localization::WSLCCLI_LoginUsernamePrompt(), false));
140 + context.Args.Add(ArgType::Username, Prompt(context.Reporter, Localization::WSLCCLI_LoginUsernamePrompt(), false));
141 }
142
143 // Resolve password: --password, --password-stdin, or interactive prompt.
@@ -146,7 +156,7 @@ void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const
156 }
157 else
158 {
149 - context.Args.Add(ArgType::Password, Prompt(Localization::WSLCCLI_LoginPasswordPrompt(), true));
159 + context.Args.Add(ArgType::Password, Prompt(context.Reporter, Localization::WSLCCLI_LoginPasswordPrompt(), true));
160 }
161 }
162
src/windows/wslc/commands/RootCommand.cpp
+1 -1
@@ -105,7 +105,7 @@ void RootCommand::ExecuteInternal(CLIExecutionContext& context) const
105 {
106 if (context.Args.Contains(ArgType::Version))
107 {
108 - VersionCommand::PrintVersion();
108 + VersionCommand::PrintVersion(context.Reporter);
109 return;
110 }
111
src/windows/wslc/commands/SettingsCommand.cpp
+1 -1
@@ -83,7 +83,7 @@ std::wstring SettingsResetCommand::LongDescription() const
83 void SettingsResetCommand::ExecuteInternal(CLIExecutionContext& context) const
84 {
85 settings::User().Reset();
86 - PrintMessage(Localization::WSLCCLI_SettingsResetConfirm());
86 + context.Reporter.Output(L"{}\n", Localization::WSLCCLI_SettingsResetConfirm());
87 }
88
89 } // namespace wsl::windows::wslc
src/windows/wslc/commands/VersionCommand.cpp
+6 -4
@@ -11,8 +11,10 @@ Abstract:
11 Implementation of the version command.
12
13 --*/
14 +
15 #include "VersionCommand.h"
16 #include "ArgumentValidation.h"
17 +#include "CLIExecutionContext.h"
18 #include "JsonUtils.h"
19
20 using namespace wsl::shared;
@@ -38,9 +40,9 @@ std::wstring VersionCommand::LongDescription() const
40 return Localization::WSLCCLI_VersionLongDesc();
41 }
42
41 -void VersionCommand::PrintVersion()
43 +void VersionCommand::PrintVersion(Reporter& reporter)
44 {
43 - wsl::windows::common::wslutil::PrintMessage(std::format(L"{} {}", s_ExecutableName, WSL_PACKAGE_VERSION));
45 + reporter.Output(L"{} {}\n", s_ExecutableName, WSL_PACKAGE_VERSION);
46 }
47
48 void VersionCommand::ExecuteInternal(CLIExecutionContext& context) const
@@ -57,11 +59,11 @@ void VersionCommand::ExecuteInternal(CLIExecutionContext& context) const
59 {
60 nlohmann::json root;
61 root["Client"]["Version"] = std::string{WSL_PACKAGE_VERSION};
60 - wsl::windows::common::wslutil::PrintMessage(MultiByteToWide(root.dump(c_jsonPrettyPrintIndent)));
62 + context.Reporter.Output(L"{}\n", MultiByteToWide(root.dump(c_jsonPrettyPrintIndent)));
63 break;
64 }
65 case FormatType::Table:
64 - PrintVersion();
66 + PrintVersion(context.Reporter);
67 break;
68 default:
69 THROW_HR(E_UNEXPECTED);
src/windows/wslc/commands/VersionCommand.h
+3 -1
@@ -15,13 +15,15 @@ Abstract:
15 #include "Command.h"
16
17 namespace wsl::windows::wslc {
18 +struct Reporter;
19 struct VersionCommand final : public Command
20 {
21 constexpr static std::wstring_view CommandName = L"version";
22 VersionCommand(const std::wstring& parent) : Command(CommandName, parent)
23 {
24 }
24 - static void PrintVersion();
25 +
26 + static void PrintVersion(Reporter& reporter);
27 std::vector<Argument> GetArguments() const override;
28 std::wstring ShortDescription() const override;
29 std::wstring LongDescription() const override;
src/windows/wslc/services/BuildImageCallback.cpp
+19 -23
@@ -20,6 +20,9 @@ namespace wsl::windows::wslc::services {
20 using wsl::windows::common::string::MultiByteToWide;
21 using namespace wsl::windows::common::vt;
22
23 +// Fallback width used when the console width can't be queried.
24 +constexpr int c_fallbackConsoleWidth = 79;
25 +
26 BuildImageCallback::~BuildImageCallback()
27 try
28 {
@@ -38,18 +41,12 @@ try
41 {
42 for (const auto& line : m_allLines)
43 {
41 - WriteTerminal(MultiByteToWide(line));
44 + m_reporter.Info(L"{}", line);
45 }
46 }
47 }
48 CATCH_LOG()
49
47 -void BuildImageCallback::WriteTerminal(std::wstring_view content) const
48 -{
49 - DWORD written;
50 - LOG_IF_WIN32_BOOL_FALSE(WriteConsoleW(m_console, content.data(), static_cast<DWORD>(content.size()), &written, nullptr));
51 -}
52 -
50 bool BuildImageCallback::IsCancelled() const
51 {
52 return WaitForSingleObject(m_cancelEvent, 0) == WAIT_OBJECT_0;
@@ -60,7 +57,7 @@ void BuildImageCallback::CollapseWindow()
57 if (m_displayedLines > 0)
58 {
59 // Move cursor up to the start of the display area, then erase to end of screen.
63 - WriteTerminal(Cursor::Up(m_displayedLines) + Erase::ScreenForward);
60 + m_reporter.Info(L"{}{}", Cursor::Up(m_displayedLines), Erase::ScreenForward);
61 m_displayedLines = 0;
62 }
63
@@ -103,7 +100,7 @@ try
100 // Skip pull progress updates when output is redirected, show only major steps
101 if (!isPullProgress)
102 {
106 - wprintf(L"%hs", status);
103 + m_reporter.Info(L"{}", status);
104 }
105 return S_OK;
106 }
@@ -177,16 +174,16 @@ try
174 const auto newlines = wide.substr(bodyLength);
175 wide.resize(bodyLength);
176
180 - WriteTerminal(std::format(L"{}{}{}{}", Format::Fg::BrightGreen, wide, Format::Default, newlines));
177 + // Pass the color sequences as arguments (not baked into the string) so Reporter strips
178 + // them when --no-color is set. The trailing newlines are emitted after the reset.
179 + m_reporter.Info(L"{}{}{}{}", Format::Fg::BrightGreen, wide, Format::Default, newlines);
180 return S_OK;
181 }
182 CATCH_RETURN();
183
184 void BuildImageCallback::Redraw()
185 {
187 - CONSOLE_SCREEN_BUFFER_INFO info{};
188 - THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfo(m_console, &info));
189 - const int consoleWidth = std::max(0, static_cast<int>(info.srWindow.Right) - info.srWindow.Left);
186 + const int consoleWidth = m_reporter.GetConsoleWidth(Reporter::Level::Info).value_or(c_fallbackConsoleWidth);
187
188 const bool showPending = !m_pendingLine.empty();
189 const int pullCount = static_cast<int>(m_pullLines.size());
@@ -198,16 +195,15 @@ void BuildImageCallback::Redraw()
195 }
196 const int displayCount = completedCount + reservedLines;
197
201 - // Build the entire frame in one buffer to minimize console writes. Hide the cursor
202 - // during the redraw so the user doesn't see it bouncing through the cursor movement,
203 - // then show it again at the final position. The dim attribute (\033[2m) renders the
204 - // scrolling lines de-emphasized regardless of the user's theme.
198 + // Build the frame body in one buffer to minimize console writes. The cursor moves,
199 + // erases, and text lines it holds are non-color VT that only runs when a VT console is
200 + // attached. The cursor hide/show wrapper and the dim intensity attribute are passed as
201 + // Sequence arguments to Reporter (below) so it strips the color ones (Dim/Normal) when
202 + // --no-color is set, while leaving the non-color cursor moves intact.
203 //
204 // m_frameBuffer is a member so its backing allocation is reused across frames -
205 // it grows to the high-water mark and is never freed between redraws.
206 m_frameBuffer.clear();
209 - m_frameBuffer += Cursor::Hide;
210 - m_frameBuffer += Format::Dim;
207
208 // Move cursor to the start of the display area and erase from there to the end of
209 // the screen. \033[J handles the case where the new display is shorter than the
@@ -252,10 +248,10 @@ void BuildImageCallback::Redraw()
248 appendLine(line);
249 }
250
255 - m_frameBuffer += Format::Normal;
256 - m_frameBuffer += Cursor::Show;
257 -
258 - WriteTerminal(m_frameBuffer);
251 + // Emit the frame as a single atomic write. Cursor Hide/Show are non-color and always
252 + // rendered here (VT is on); Format::Dim/Normal are color sequences that Reporter strips
253 + // under --no-color. The buffered body carries the cursor moves, erases, and text lines.
254 + m_reporter.Info(L"{}{}{}{}{}", Cursor::Hide, Format::Dim, std::wstring_view{m_frameBuffer}, Format::Normal, Cursor::Show);
255 m_displayedLines = displayCount;
256 }
257
src/windows/wslc/services/BuildImageCallback.h
+5 -7
@@ -12,6 +12,7 @@ Abstract:
12
13 --*/
14 #pragma once
15 +#include "Reporter.h"
16 #include "SessionService.h"
17 #include "VTSupport.h"
18 #include <deque>
@@ -23,7 +24,8 @@ class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback
24 {
25 public:
26 // The cancel event handle must remain valid for the lifetime of this callback.
26 - BuildImageCallback(HANDLE cancelEvent, bool verbose) : m_verbose(verbose), m_cancelEvent(cancelEvent)
27 + BuildImageCallback(Reporter& reporter, HANDLE cancelEvent, bool verbose) :
28 + m_reporter(reporter), m_verbose(verbose), m_cancelEvent(cancelEvent)
29 {
30 }
31 ~BuildImageCallback();
@@ -37,16 +39,12 @@ private:
39 void CollapseWindow();
40 void Redraw();
41 void RedrawIfNeeded();
40 - // Use WriteConsoleW directly rather than wprintf: wprintf is noticeably slower for
41 - // the per-redraw scrolling display and produces visible flicker.
42 - void WriteTerminal(std::wstring_view content) const;
42 bool IsCancelled() const;
43
44 + Reporter& m_reporter;
45 const bool m_verbose;
46 const HANDLE m_cancelEvent;
47 - HANDLE m_console = GetStdHandle(STD_OUTPUT_HANDLE);
48 - bool m_isConsole = wsl::windows::common::wslutil::IsConsoleHandle(m_console);
49 - wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{m_console};
47 + bool m_isConsole = m_reporter.IsVTEnabled(Reporter::Level::Info);
48 std::deque<std::string> m_lines;
49 // Each entry already contains the trailing newline so the bytes match what's replayed.
50 // TODO: Track logs per step so the destructor can replay only the failing step's
src/windows/wslc/services/ConsoleService.cpp
+3 -2
@@ -173,13 +173,14 @@ void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_
173 io.Run({});
174 }
175
176 -int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process, bool triggerRefresh)
176 +int ConsoleService::AttachToCurrentConsole(
177 + Reporter& reporter, wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process, bool triggerRefresh)
178 {
179 if (WI_IsFlagSet(process.Flags(), WSLCProcessFlagsTty))
180 {
181 if (!RelayInteractiveTty(console, process, process.GetStdHandle(WSLCFDTty).get(), triggerRefresh))
182 {
182 - windows::common::wslutil::PrintMessage(L"[detached]", stderr);
183 + reporter.Info(L"[detached]\n");
184 return 0;
185 }
186 }
src/windows/wslc/services/ConsoleService.h
+2 -1
@@ -16,13 +16,14 @@ Abstract:
16 #include <wslc.h>
17 #include <WSLCContainerLauncher.h>
18 #include <ConsoleState.h>
19 +#include "Reporter.h"
20
21 namespace wsl::windows::wslc::services {
22 class ConsoleService
23 {
24 public:
25 static int AttachToCurrentConsole(
25 - wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process, bool triggerRefresh = false);
26 + Reporter& reporter, wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process, bool triggerRefresh = false);
27 static bool RelayInteractiveTty(
28 wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess& process, HANDLE tty, bool triggerRefresh = false);
29 static void RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr);
src/windows/wslc/services/ContainerService.cpp
+17 -17
@@ -31,7 +31,6 @@ Abstract:
31 namespace wsl::windows::wslc::services {
32 using wsl::windows::common::ClientRunningWSLCProcess;
33 using wsl::windows::common::wslc_schema::InspectContainer;
34 -using wsl::windows::common::wslutil::PrintMessage;
34 using namespace wsl::windows::common::wslutil;
35 using namespace wsl::shared;
36 using namespace wsl::windows::wslc::models;
@@ -43,7 +42,7 @@ static void SetContainerArguments(WSLCProcessOptions& options, std::vector<const
42 }
43
44 static wsl::windows::common::RunningWSLCContainer CreateInternal(
46 - Session& session, const std::string& image, const ContainerOptions& options, IWarningCallback* warningCallback = nullptr)
45 + Reporter& reporter, Session& session, const std::string& image, const ContainerOptions& options, IWarningCallback* warningCallback = nullptr)
46 {
47 auto processFlags = WSLCProcessFlagsNone;
48 WI_SetFlagIf(processFlags, WSLCProcessFlagsStdin, options.Interactive);
@@ -248,9 +247,10 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
247 if (result == WSLC_E_IMAGE_NOT_FOUND)
248 {
249 {
251 - // Attempt to pull the image if not found
252 - ImageProgressCallback callback;
253 - PrintMessage(Localization::WSLCCLI_ImageNotFoundPulling(wsl::shared::string::MultiByteToWide(image)), stderr);
250 + // Implicit pull for run/create: progress goes to Info (stderr), keeping stdout for the
251 + // container id/output.
252 + ImageProgressCallback callback(reporter, Reporter::Level::Info);
253 + reporter.Info(L"{}\n", Localization::WSLCCLI_ImageNotFoundPulling(wsl::shared::string::MultiByteToWide(image)));
254 ImageService imageService;
255 imageService.Pull(session, image, &callback);
256 }
@@ -324,7 +324,7 @@ std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp)
324 return pluralize(elapsed / SecondsPerYear, L"year", L"years");
325 }
326
327 -int ContainerService::Attach(Session& session, const std::string& id)
327 +int ContainerService::Attach(Reporter& reporter, Session& session, const std::string& id)
328 {
329 wil::com_ptr<IWSLCContainer> container;
330 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
@@ -355,7 +355,7 @@ int ContainerService::Attach(Session& session, const std::string& id)
355 wsl::windows::common::ConsoleState console;
356 if (!ConsoleService::RelayInteractiveTty(console, runningProcess, stdinLogs.Release().get(), true))
357 {
358 - wsl::windows::common::wslutil::PrintMessage(L"[detached]", stderr);
358 + reporter.Info(L"[detached]\n");
359 return 0; // Exit early if user detached
360 }
361 }
@@ -425,7 +425,7 @@ std::wstring ContainerService::FormatPorts(WSLCContainerState state, const std::
425 return result;
426 }
427
428 -int ContainerService::Run(Session& session, const std::string& image, ContainerOptions runOptions)
428 +int ContainerService::Run(Reporter& reporter, Session& session, const std::string& image, ContainerOptions runOptions)
429 {
430 // Reserve the CID file (fails if it already exists) before creating the container so a
431 // container isn't created when the caller-requested path can't be written. The file is
@@ -434,7 +434,7 @@ int ContainerService::Run(Session& session, const std::string& image, ContainerO
434 auto warningCallback = Microsoft::WRL::Make<WarningCallback>();
435
436 // Create the container
437 - auto runningContainer = CreateInternal(session, image, runOptions, warningCallback.Get());
437 + auto runningContainer = CreateInternal(reporter, session, image, runOptions, warningCallback.Get());
438 auto& container = runningContainer.Get();
439
440 WSLCContainerId containerId{};
@@ -465,18 +465,18 @@ int ContainerService::Run(Session& session, const std::string& image, ContainerO
465 // Handle attach if requested
466 if (attach)
467 {
468 - return ConsoleService::AttachToCurrentConsole(console, runningContainer.GetInitProcess());
468 + return ConsoleService::AttachToCurrentConsole(reporter, console, runningContainer.GetInitProcess());
469 }
470
471 - PrintMessage(L"%hs", stdout, containerId);
471 + reporter.Output(L"{}\n", wsl::shared::string::MultiByteToWide(containerId));
472 return 0;
473 }
474
475 -CreateContainerResult ContainerService::Create(Session& session, const std::string& image, ContainerOptions runOptions)
475 +CreateContainerResult ContainerService::Create(Reporter& reporter, Session& session, const std::string& image, ContainerOptions runOptions)
476 {
477 CidFile cidFile(runOptions.CidFile);
478 auto warningCallback = Microsoft::WRL::Make<WarningCallback>();
479 - auto runningContainer = CreateInternal(session, image, runOptions, warningCallback.Get());
479 + auto runningContainer = CreateInternal(reporter, session, image, runOptions, warningCallback.Get());
480 runningContainer.SetDeleteOnClose(false);
481 auto& container = runningContainer.Get();
482 WSLCContainerId id{};
@@ -485,7 +485,7 @@ CreateContainerResult ContainerService::Create(Session& session, const std::stri
485 return {.Id = id};
486 }
487
488 -int ContainerService::Start(Session& session, const std::string& id, bool attach)
488 +int ContainerService::Start(Reporter& reporter, Session& session, const std::string& id, bool attach)
489 {
490 wil::com_ptr<IWSLCContainer> container;
491 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
@@ -512,7 +512,7 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach
512 THROW_IF_FAILED(process->GetFlags(&processFlags));
513 ClientRunningWSLCProcess runningProcess(std::move(process), processFlags);
514
515 - return ConsoleService::AttachToCurrentConsole(console, std::move(runningProcess), true);
515 + return ConsoleService::AttachToCurrentConsole(reporter, console, std::move(runningProcess), true);
516 }
517
518 void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options)
@@ -583,7 +583,7 @@ std::vector<ContainerInformation> ContainerService::List(
583 return result;
584 }
585
586 -int ContainerService::Exec(Session& session, const std::string& id, ContainerOptions options)
586 +int ContainerService::Exec(Reporter& reporter, Session& session, const std::string& id, ContainerOptions options)
587 {
588 wil::com_ptr<IWSLCContainer> container;
589 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
@@ -611,7 +611,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt
611 processLauncher.SetWorkingDirectory(std::move(options.WorkingDirectory));
612 }
613
614 - return ConsoleService::AttachToCurrentConsole(console, processLauncher.Launch(*container));
614 + return ConsoleService::AttachToCurrentConsole(reporter, console, processLauncher.Launch(*container));
615 }
616
617 InspectContainer ContainerService::Inspect(Session& session, const std::string& id)
src/windows/wslc/services/ContainerService.h
+6 -5
@@ -14,6 +14,7 @@ Abstract:
14 #pragma once
15 #include "SessionModel.h"
16 #include "ContainerModel.h"
17 +#include "Reporter.h"
18 #include <docker_schema.h>
19 #include <wslc_schema.h>
20
@@ -23,17 +24,17 @@ struct ContainerService
24 static std::wstring ContainerStateToString(WSLCContainerState state, ULONGLONG stateChangedAt = 0);
25 static std::wstring FormatRelativeTime(ULONGLONG timestamp);
26 static std::wstring FormatPorts(WSLCContainerState state, const std::vector<models::PortInformation>& ports);
26 - static int Attach(models::Session& session, const std::string& id);
27 - static int Run(models::Session& session, const std::string& image, models::ContainerOptions options);
28 - static models::CreateContainerResult Create(models::Session& session, const std::string& image, models::ContainerOptions options);
29 - static int Start(models::Session& session, const std::string& id, bool attach = false);
27 + static int Attach(Reporter& reporter, models::Session& session, const std::string& id);
28 + static int Run(Reporter& reporter, models::Session& session, const std::string& image, models::ContainerOptions options);
29 + static models::CreateContainerResult Create(Reporter& reporter, models::Session& session, const std::string& image, models::ContainerOptions options);
30 + static int Start(Reporter& reporter, models::Session& session, const std::string& id, bool attach = false);
31 static void Stop(models::Session& session, const std::string& id, models::StopContainerOptions options);
32 static void Kill(models::Session& session, const std::string& id, WSLCSignal signal = WSLCSignalSIGKILL);
33 static void Delete(models::Session& session, const std::string& id, bool force);
34 static std::vector<models::ContainerInformation> List(
35 models::Session& session, bool all = false, int limit = -1, const std::vector<std::pair<std::string, std::string>>& filters = {});
36
36 - static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
37 + static int Exec(Reporter& reporter, models::Session& session, const std::string& id, models::ContainerOptions options);
38 static void Export(models::Session& session, const std::string& id, const std::wstring& outputPath);
39 static void Export(models::Session& session, const std::string& id, HANDLE outputHandle);
40 static void CopyToContainer(models::Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize);
src/windows/wslc/services/ImageProgressCallback.cpp
+48 -28
@@ -21,23 +21,24 @@ namespace wsl::windows::wslc::services {
21 using namespace wsl::shared;
22 using namespace wsl::windows::common::vt;
23
24 -void ImageProgressCallback::WriteTerminal(std::wstring_view content) const
25 -{
26 - DWORD written;
27 - LOG_IF_WIN32_BOOL_FALSE(WriteConsoleW(m_console, content.data(), static_cast<DWORD>(content.size()), &written, nullptr));
28 -}
24 +// Fallback width for the in-place progress display when the console width can't be queried. This
25 +// value already includes the autowrap guard (visible width minus one) so a wrapped line can't
26 +// corrupt the cursor-based rendering.
27 +constexpr int c_fallbackConsoleWidth = 79;
28
29 auto ImageProgressCallback::MoveToLine(int line)
30 {
31 if (line > 0)
32 {
34 - WriteTerminal(Cursor::Up(line).Get());
33 + m_reporter.Write(m_level, L"{}", Cursor::Up(line));
34 }
35
37 - return wil::scope_exit([line = line, this]() {
36 + // scope_exit is noexcept and may fire during unwinding; scope_exit_log swallows output
37 + // failures so a throw here can't call std::terminate.
38 + return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [line = line, this]() {
39 if (line > 1)
40 {
40 - WriteTerminal(Cursor::Down(line - 1).Get());
41 + m_reporter.Write(m_level, L"{}", Cursor::Down(line - 1));
42 }
43 });
44 }
@@ -46,32 +47,57 @@ HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG cu
47 {
48 try
49 {
49 - if (!m_terminalMode.IsConsole())
50 + // status is [unique] in the IDL, so it may be null; normalize before either path uses it.
51 + status = (status != nullptr) ? status : "";
52 +
53 + // The in-place progress display needs cursor movement, so when output is redirected fall
54 + // back to a log stream: one line per new status, deduping the repeated byte-progress
55 + // callbacks that share a status text.
56 + if (!m_vtEnabled)
57 {
58 + if (id == nullptr || *id == '\0')
59 + {
60 + m_reporter.Write(m_level, L"{}\n", status);
61 + }
62 + else
63 + {
64 + auto [it, inserted] = m_lastStatusById.try_emplace(id, status);
65 + if (inserted || it->second != status)
66 + {
67 + it->second = status;
68 + m_reporter.Write(m_level, L"{}: {}\n", id, status);
69 + }
70 + }
71 +
72 return S_OK;
73 }
74
75 + // Hide the cursor while rendering so it doesn't bounce through the movements; scope_exit_log
76 + // restores it on every exit path and can't call std::terminate during unwinding.
77 + m_reporter.Write(m_level, L"{}", Cursor::Hide);
78 + auto showCursor = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { m_reporter.Write(m_level, L"{}", Cursor::Show); });
79 +
80 if (id == nullptr || *id == '\0') // Print all 'global' statuses on their own line
81 {
56 - WriteTerminal(std::format(L"{}\n", status));
82 + m_reporter.Write(m_level, L"{}\n", status);
83 m_currentLine++;
84 return S_OK;
85 }
86
61 - auto info = Info();
87 + const int visibleWidth = m_reporter.GetConsoleWidth(m_level).value_or(c_fallbackConsoleWidth);
88
89 auto it = m_statuses.find(id);
90 if (it == m_statuses.end())
91 {
92 // If this is the first time we see this ID, create a new line for it.
93 m_statuses.emplace(id, m_currentLine);
68 - WriteTerminal(GenerateStatusLine(status, id, current, total, info) + L'\n');
94 + m_reporter.Write(m_level, L"{}\n", GenerateStatusLine(status, id, current, total, visibleWidth));
95 m_currentLine++;
96 }
97 else
98 {
99 auto revert = MoveToLine(m_currentLine - it->second);
74 - WriteTerminal(GenerateStatusLine(status, id, current, total, info) + L'\n');
100 + m_reporter.Write(m_level, L"{}\n", GenerateStatusLine(status, id, current, total, visibleWidth));
101 }
102
103 return S_OK;
@@ -79,15 +105,12 @@ HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG cu
105 CATCH_RETURN();
106 }
107
82 -CONSOLE_SCREEN_BUFFER_INFO ImageProgressCallback::Info()
108 +std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, int visibleWidth)
109 {
84 - CONSOLE_SCREEN_BUFFER_INFO info{};
85 - THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info));
86 - return info;
87 -}
110 + // status/id are [unique] in the IDL and may be null; treat null as empty before formatting.
111 + const char* const safeStatus = (status != nullptr) ? status : "";
112 + const char* const safeId = (id != nullptr) ? id : "";
113
89 -std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info)
90 -{
114 std::wstring line;
115 if (total != 0)
116 {
@@ -121,21 +144,19 @@ std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id,
144 progress += std::format(L"/{}", wsl::shared::string::FormatBytes(total));
145 }
146
124 - line = std::format(L"{}: {} [{}] {}", id, status, bar, progress);
147 + line = std::format(L"{}: {} [{}] {}", safeId, safeStatus, bar, progress);
148 }
149 else if (current != 0)
150 {
128 - line = std::format(L"{}: {} {}", id, status, wsl::shared::string::FormatBytes(current));
151 + line = std::format(L"{}: {} {}", safeId, safeStatus, wsl::shared::string::FormatBytes(current));
152 }
153 else
154 {
132 - line = std::format(L"{}: {}", id, status);
155 + line = std::format(L"{}: {}", safeId, safeStatus);
156 }
157
135 - // Use the visible window width (not the buffer width) to prevent wrapping.
136 - const auto visibleWidth = std::max(0, static_cast<int>(info.srWindow.Right) - info.srWindow.Left + 1);
137 -
138 - // Truncate to console width to prevent wrapping that would break cursor repositioning.
158 + // Truncate to the console width to prevent wrapping that breaks cursor repositioning, then pad
159 + // to erase any previously written characters on the line.
160 if (line.size() > static_cast<size_t>(visibleWidth))
161 {
162 line.resize(visibleWidth);
@@ -148,7 +169,6 @@ std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id,
169 }
170 }
171
151 - // Erase any previously written char on that line.
172 line.resize(visibleWidth, L' ');
173
174 return line;
src/windows/wslc/services/ImageProgressCallback.h
+14 -6
@@ -12,6 +12,7 @@ Abstract:
12
13 --*/
14 #pragma once
15 +#include "Reporter.h"
16 #include "SessionService.h"
17 #include "VTSupport.h"
18 #include <map>
@@ -24,17 +25,24 @@ class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallbac
25 : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback, IFastRundown>
26 {
27 public:
28 + // level selects the target stream: Output (stdout) for standalone pull/push, Info (stderr) for
29 + // the implicit pull during run/create, matching Docker.
30 + ImageProgressCallback(Reporter& reporter, Reporter::Level level) : m_reporter(reporter), m_level(level)
31 + {
32 + }
33 HRESULT OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) override;
34
35 private:
36 auto MoveToLine(int line);
31 - static CONSOLE_SCREEN_BUFFER_INFO Info();
32 - void WriteTerminal(std::wstring_view content) const;
33 - std::wstring GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info);
37 + std::wstring GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, int visibleWidth);
38 + Reporter& m_reporter;
39 + // Declared before m_vtEnabled, whose initializer reads it.
40 + const Reporter::Level m_level;
41 std::map<std::string, int> m_statuses;
42 + // Last status text per id; used only when redirected to dedupe repeated byte-progress callbacks.
43 + std::map<std::string, std::string> m_lastStatusById;
44 int m_currentLine = 0;
36 - HANDLE m_console = GetStdHandle(STD_OUTPUT_HANDLE);
37 - wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{m_console};
38 - wsl::windows::common::vt::ChangeTerminalMode m_terminalMode{m_console, false};
45 + // The progress display only renders on a VT console.
46 + bool m_vtEnabled = m_reporter.IsVTEnabled(m_level);
47 };
48 } // namespace wsl::windows::wslc::services
src/windows/wslc/services/SessionService.cpp
+10 -11
@@ -64,7 +64,7 @@ Session SessionService::OpenOrCreateDefaultSession()
64 return Session(std::move(session));
65 }
66
67 -int SessionService::Attach(const Session& session)
67 +int SessionService::Attach(Reporter& reporter, const Session& session)
68 {
69 // Configure console for interactive usage.
70 wsl::windows::common::ConsoleState console{};
@@ -113,12 +113,12 @@ int SessionService::Attach(const Session& session)
113
114 auto exitCode = process.GetExitCode();
115
116 - wslutil::PrintMessage(wsl::shared::Localization::MessageWslcShellExited(string::MultiByteToWide(shell), static_cast<int>(exitCode)), stdout);
116 + reporter.Output(L"{}\n", wsl::shared::Localization::MessageWslcShellExited(string::MultiByteToWide(shell), static_cast<int>(exitCode)));
117
118 return static_cast<int>(exitCode);
119 }
120
121 -int SessionService::Enter(const std::wstring& storagePath, const std::wstring& displayName)
121 +int SessionService::Enter(Reporter& reporter, const std::wstring& storagePath, const std::wstring& displayName)
122 {
123 THROW_HR_IF(E_INVALIDARG, storagePath.empty());
124 THROW_HR_IF(E_INVALIDARG, displayName.empty());
@@ -129,7 +129,7 @@ int SessionService::Enter(const std::wstring& storagePath, const std::wstring& d
129 auto warningCallback = Microsoft::WRL::Make<WarningCallback>();
130 THROW_IF_FAILED(sessionManager->EnterSession(displayName.c_str(), storagePath.c_str(), warningCallback.Get(), &session));
131 wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
132 - wsl::windows::common::wslutil::PrintMessage(Localization::MessageWslcCreatedSession(displayName), stderr);
132 + reporter.Info(L"{}\n", Localization::MessageWslcCreatedSession(displayName));
133
134 const std::string shell = "/bin/sh";
135 wsl::windows::common::WSLCProcessLauncher launcher{shell, {shell, "--login"}, {"TERM=xterm-256color"}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin};
@@ -138,7 +138,7 @@ int SessionService::Enter(const std::wstring& storagePath, const std::wstring& d
138 const auto windowSize = console.GetWindowSize();
139 launcher.SetTtySize(windowSize.Y, windowSize.X);
140
141 - return ConsoleService::AttachToCurrentConsole(console, launcher.Launch(*session.get()));
141 + return ConsoleService::AttachToCurrentConsole(reporter, console, launcher.Launch(*session.get()));
142 }
143
144 std::vector<SessionInformation> SessionService::List()
@@ -161,7 +161,7 @@ std::vector<SessionInformation> SessionService::List()
161 return result;
162 }
163
164 -int SessionService::Run(const Session& session, const std::vector<std::string>& arguments)
164 +int SessionService::Run(Reporter& reporter, const Session& session, const std::vector<std::string>& arguments)
165 {
166 WI_ASSERT(!arguments.empty());
167
@@ -175,10 +175,10 @@ int SessionService::Run(const Session& session, const std::vector<std::string>&
175 THROW_IF_FAILED(result);
176
177 wsl::windows::common::ConsoleState console{};
178 - return ConsoleService::AttachToCurrentConsole(console, std::move(process.value()));
178 + return ConsoleService::AttachToCurrentConsole(reporter, console, std::move(process.value()));
179 }
180
181 -int SessionService::TerminateSession(const Session& session)
181 +int SessionService::TerminateSession(Reporter& reporter, const Session& session)
182 {
183 HRESULT hr = session.Get()->Terminate();
184 if (FAILED(hr))
@@ -188,12 +188,11 @@ int SessionService::TerminateSession(const Session& session)
188 wil::unique_cotaskmem_string displayName;
189 if (SUCCEEDED(session.Get()->GetDisplayName(&displayName)) && displayName)
190 {
191 - wslutil::PrintMessage(
192 - Localization::MessageErrorCode(Localization::MessageWslcTerminateSessionFailed(displayName.get()), errorString), stderr);
191 + reporter.Error(L"{}\n", Localization::MessageErrorCode(Localization::MessageWslcTerminateSessionFailed(displayName.get()), errorString));
192 }
193 else
194 {
196 - wslutil::PrintMessage(Localization::MessageErrorCode(Localization::MessageWslcTerminateDefaultSessionFailed(), errorString), stderr);
195 + reporter.Error(L"{}\n", Localization::MessageErrorCode(Localization::MessageWslcTerminateDefaultSessionFailed(), errorString));
196 }
197 return 1;
198 }
src/windows/wslc/services/SessionService.h
+5 -4
@@ -14,6 +14,7 @@ Abstract:
14 #pragma once
15
16 #include "SessionModel.h"
17 +#include "Reporter.h"
18 #include <wslc.h>
19
20 namespace wsl::windows::wslc::services {
@@ -26,8 +27,8 @@ struct SessionInformation
27
28 struct SessionService
29 {
29 - static int Attach(const wsl::windows::wslc::models::Session& session);
30 - static int Enter(const std::wstring& storagePath, const std::wstring& displayName);
30 + static int Attach(Reporter& reporter, const wsl::windows::wslc::models::Session& session);
31 + static int Enter(Reporter& reporter, const std::wstring& storagePath, const std::wstring& displayName);
32 static std::vector<SessionInformation> List();
33 // Opens an existing session by name. Throws if not found.
34 static wsl::windows::wslc::models::Session OpenSession(const std::wstring& name);
@@ -36,8 +37,8 @@ struct SessionService
37 // Opens or creates the default session.
38 static wsl::windows::wslc::models::Session OpenOrCreateDefaultSession();
39 // Runs the given command and arguments in a session without a TTY, resolving the executable from PATH.
39 - static int Run(const wsl::windows::wslc::models::Session& session, const std::vector<std::string>& arguments);
40 - static int TerminateSession(const wsl::windows::wslc::models::Session& session);
40 + static int Run(Reporter& reporter, const wsl::windows::wslc::models::Session& session, const std::vector<std::string>& arguments);
41 + static int TerminateSession(Reporter& reporter, const wsl::windows::wslc::models::Session& session);
42
43 private:
44 // Common open-only session lookup with unified error handling.
src/windows/wslc/tasks/ContainerTasks.cpp
+30 -20
@@ -136,7 +136,7 @@ nlohmann::json ComputeContainerStatsJson(const wsl::windows::common::docker_sche
136
137 namespace wsl::windows::wslc::task {
138
139 -static bool TryInspectContainer(Session& session, const std::string& containerId, std::optional<wslc_schema::InspectContainer>& inspectData)
139 +static bool TryInspectContainer(Reporter& reporter, Session& session, const std::string& containerId, std::optional<wslc_schema::InspectContainer>& inspectData)
140 {
141 try
142 {
@@ -147,7 +147,7 @@ static bool TryInspectContainer(Session& session, const std::string& containerId
147 {
148 if (ex.GetErrorCode() == WSLC_E_CONTAINER_NOT_FOUND)
149 {
150 - PrintMessage(Localization::MessageWslcContainerNotFound(containerId.c_str()), stderr);
150 + reporter.Error(L"{}\n", Localization::MessageWslcContainerNotFound(containerId.c_str()));
151 return false;
152 }
153
@@ -158,7 +158,7 @@ static bool TryInspectContainer(Session& session, const std::string& containerId
158 void AttachContainer::operator()(CLIExecutionContext& context) const
159 {
160 WI_ASSERT(context.Data.Contains(Data::Session));
161 - context.ExitCode = ContainerService::Attach(context.Data.Get<Data::Session>(), WideToMultiByte(m_containerId));
161 + context.ExitCode = ContainerService::Attach(context.Reporter, context.Data.Get<Data::Session>(), WideToMultiByte(m_containerId));
162 }
163
164 void CreateContainer(CLIExecutionContext& context)
@@ -167,8 +167,11 @@ void CreateContainer(CLIExecutionContext& context)
167 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
168 WI_ASSERT(context.Data.Contains(Data::ContainerOptions));
169 auto result = ContainerService::Create(
170 - context.Data.Get<Data::Session>(), WideToMultiByte(context.Args.Get<ArgType::ImageId>()), context.Data.Get<Data::ContainerOptions>());
171 - PrintMessage(MultiByteToWide(result.Id));
170 + context.Reporter,
171 + context.Data.Get<Data::Session>(),
172 + WideToMultiByte(context.Args.Get<ArgType::ImageId>()),
173 + context.Data.Get<Data::ContainerOptions>());
174 + context.Reporter.Output(L"{}\n", MultiByteToWide(result.Id));
175 }
176
177 void ExecContainer(CLIExecutionContext& context)
@@ -177,7 +180,10 @@ void ExecContainer(CLIExecutionContext& context)
180 WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
181 WI_ASSERT(context.Data.Contains(Data::ContainerOptions));
182 context.ExitCode = ContainerService::Exec(
180 - context.Data.Get<Data::Session>(), WideToMultiByte(context.Args.Get<ArgType::ContainerId>()), context.Data.Get<Data::ContainerOptions>());
183 + context.Reporter,
184 + context.Data.Get<Data::Session>(),
185 + WideToMultiByte(context.Args.Get<ArgType::ContainerId>()),
186 + context.Data.Get<Data::ContainerOptions>());
187 }
188
189 void GetContainers(CLIExecutionContext& context)
@@ -222,7 +228,7 @@ void InspectContainers(CLIExecutionContext& context)
228 for (const auto& id : containerIds)
229 {
230 std::optional<wslc_schema::InspectContainer> inspectData;
225 - if (TryInspectContainer(session, WideToMultiByte(id), inspectData))
231 + if (TryInspectContainer(context.Reporter, session, WideToMultiByte(id), inspectData))
232 {
233 result.push_back(*inspectData);
234 }
@@ -233,7 +239,7 @@ void InspectContainers(CLIExecutionContext& context)
239 }
240
241 auto json = ToJson(result, c_jsonPrettyPrintIndent);
236 - PrintMessage(MultiByteToWide(json));
242 + context.Reporter.Output(L"{}\n", MultiByteToWide(json));
243 }
244
245 void KillContainers(CLIExecutionContext& context)
@@ -250,7 +256,7 @@ void KillContainers(CLIExecutionContext& context)
256 for (const auto& id : containerIds)
257 {
258 ContainerService::Kill(session, WideToMultiByte(id), signal);
253 - PrintMessage(id);
259 + context.Reporter.Output(L"{}\n", id);
260 }
261 }
262
@@ -547,7 +553,7 @@ void ListContainers(CLIExecutionContext& context)
553 // Print only the container ids
554 for (const auto& container : containers)
555 {
550 - PrintMessage(MultiByteToWide(container.Id));
556 + context.Reporter.Output(L"{}\n", MultiByteToWide(container.Id));
557 }
558
559 return;
@@ -564,7 +570,7 @@ void ListContainers(CLIExecutionContext& context)
570 case FormatType::Json:
571 {
572 auto json = ToJson(containers, c_jsonPrettyPrintIndent);
567 - PrintMessage(MultiByteToWide(json));
573 + context.Reporter.Output(L"{}\n", MultiByteToWide(json));
574 break;
575 }
576 case FormatType::Table:
@@ -621,7 +627,7 @@ void RemoveContainers(CLIExecutionContext& context)
627 for (const auto& id : containerIds)
628 {
629 ContainerService::Delete(session, WideToMultiByte(id), force);
624 - PrintMessage(id);
630 + context.Reporter.Output(L"{}\n", id);
631 }
632 }
633
@@ -631,7 +637,10 @@ void RunContainer(CLIExecutionContext& context)
637 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
638 WI_ASSERT(context.Data.Contains(Data::ContainerOptions));
639 context.ExitCode = ContainerService::Run(
634 - context.Data.Get<Data::Session>(), WideToMultiByte(context.Args.Get<ArgType::ImageId>()), context.Data.Get<Data::ContainerOptions>());
640 + context.Reporter,
641 + context.Data.Get<Data::Session>(),
642 + WideToMultiByte(context.Args.Get<ArgType::ImageId>()),
643 + context.Data.Get<Data::ContainerOptions>());
644 }
645
646 void SetContainerOptionsFromArgs(CLIExecutionContext& context)
@@ -970,7 +979,7 @@ void ShowContainerStats(CLIExecutionContext& context)
979 {
980 case FormatType::Json:
981 {
973 - PrintMessage(MultiByteToWide(statsJson.dump(c_jsonPrettyPrintIndent)));
982 + context.Reporter.Output(L"{}\n", MultiByteToWide(statsJson.dump(c_jsonPrettyPrintIndent)));
983 break;
984 }
985 case FormatType::Table:
@@ -1029,11 +1038,11 @@ void StartContainer(CLIExecutionContext& context)
1038 WI_ASSERT(context.Args.Contains(ArgType::ContainerId));
1039 const auto& containerId = context.Args.Get<ArgType::ContainerId>();
1040 const bool attach = context.Args.Contains(ArgType::Attach);
1032 - context.ExitCode = ContainerService::Start(context.Data.Get<Data::Session>(), WideToMultiByte(containerId), attach);
1041 + context.ExitCode = ContainerService::Start(context.Reporter, context.Data.Get<Data::Session>(), WideToMultiByte(containerId), attach);
1042
1043 if (!attach)
1044 {
1036 - PrintMessage(containerId);
1045 + context.Reporter.Output(L"{}\n", containerId);
1046 }
1047 }
1048
@@ -1056,7 +1065,7 @@ void StopContainers(CLIExecutionContext& context)
1065 for (const auto& id : containersToStop)
1066 {
1067 ContainerService::Stop(context.Data.Get<Data::Session>(), WideToMultiByte(id), options);
1059 - PrintMessage(id);
1068 + context.Reporter.Output(L"{}\n", id);
1069 }
1070 }
1071
@@ -1101,10 +1110,11 @@ void PruneContainers(CLIExecutionContext& context)
1110
1111 for (const auto& containerId : result.PrunedContainers)
1112 {
1104 - PrintMessage(MultiByteToWide(containerId));
1113 + context.Reporter.Output(L"{}\n", MultiByteToWide(containerId));
1114 }
1115
1107 - PrintMessage(L"");
1108 - PrintMessage(Localization::WSLCCLI_ContainerPruneSpaceReclaimedBytes(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
1116 + context.Reporter.Output(L"\n");
1117 + context.Reporter.Output(
1118 + L"{}\n", Localization::WSLCCLI_ContainerPruneSpaceReclaimedBytes(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
1119 }
1120 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/ImageTasks.cpp
+13 -14
@@ -71,7 +71,7 @@ namespace {
71
72 } // namespace
73
74 -static bool TryInspectImage(Session& session, const std::string& imageId, std::optional<wslc_schema::InspectImage>& inspectData)
74 +static bool TryInspectImage(Reporter& reporter, Session& session, const std::string& imageId, std::optional<wslc_schema::InspectImage>& inspectData)
75 {
76 try
77 {
@@ -82,7 +82,7 @@ static bool TryInspectImage(Session& session, const std::string& imageId, std::o
82 {
83 if (ex.GetErrorCode() == WSLC_E_IMAGE_NOT_FOUND)
84 {
85 - PrintMessage(Localization::MessageWslcImageNotFound(imageId.c_str()), stderr);
85 + reporter.Error(L"{}\n", Localization::MessageWslcImageNotFound(imageId.c_str()));
86 return false;
87 }
88
@@ -117,15 +117,13 @@ void BuildImage(CLIExecutionContext& context)
117 target = context.Args.Get<ArgType::BuildTarget>();
118 }
119
120 - PrintMessage(std::format(L"Building image from directory: {}\n", contextPath), stdout);
121 -
120 WSLCBuildImageFlags flags = WSLCBuildImageFlagsNone;
121 WI_SetFlagIf(flags, WSLCBuildImageFlagsVerbose, context.Args.Contains(ArgType::Verbose));
122 WI_SetFlagIf(flags, WSLCBuildImageFlagsNoCache, context.Args.Contains(ArgType::NoCache));
123 WI_SetFlagIf(flags, WSLCBuildImageFlagsPull, context.Args.Contains(ArgType::BuildPull));
124
125 auto cancelEvent = context.CreateCancelEvent();
128 - BuildImageCallback callback(cancelEvent, context.Args.Contains(ArgType::Verbose));
126 + BuildImageCallback callback(context.Reporter, cancelEvent, context.Args.Contains(ArgType::Verbose));
127 services::ImageService::Build(session, contextPath, tags, buildArgs, labels, dockerfilePath, target, flags, &callback, cancelEvent);
128 }
129
@@ -179,7 +177,7 @@ void ListImages(CLIExecutionContext& context)
177 case FormatType::Json:
178 {
179 auto json = ToJson(images, c_jsonPrettyPrintIndent);
182 - PrintMessage(MultiByteToWide(json));
180 + context.Reporter.Output(L"{}\n", MultiByteToWide(json));
181 break;
182 }
183 case FormatType::Table:
@@ -227,7 +225,7 @@ void PullImage(CLIExecutionContext& context)
225 auto& session = context.Data.Get<Data::Session>();
226 auto& imageId = context.Args.Get<ArgType::ImageId>();
227
230 - ImageProgressCallback callback;
228 + ImageProgressCallback callback(context.Reporter, Reporter::Level::Output);
229 services::ImageService::Pull(session, WideToMultiByte(imageId), &callback);
230 }
231
@@ -238,7 +236,7 @@ void PushImage(CLIExecutionContext& context)
236 auto& session = context.Data.Get<Data::Session>();
237 auto& imageId = context.Args.Get<ArgType::ImageId>();
238
241 - ImageProgressCallback callback;
239 + ImageProgressCallback callback(context.Reporter, Reporter::Level::Output);
240 services::ImageService::Push(session, WideToMultiByte(imageId), &callback);
241 }
242
@@ -304,7 +302,7 @@ void InspectImages(CLIExecutionContext& context)
302 for (const auto& id : imageIds)
303 {
304 std::optional<wslc_schema::InspectImage> inspectData;
307 - if (TryInspectImage(session, WideToMultiByte(id), inspectData))
305 + if (TryInspectImage(context.Reporter, session, WideToMultiByte(id), inspectData))
306 {
307 result.push_back(*inspectData);
308 }
@@ -315,7 +313,7 @@ void InspectImages(CLIExecutionContext& context)
313 }
314
315 auto json = ToJson(result, c_jsonPrettyPrintIndent);
318 - PrintMessage(MultiByteToWide(json));
316 + context.Reporter.Output(L"{}\n", MultiByteToWide(json));
317 }
318
319 void SaveImage(CLIExecutionContext& context)
@@ -383,15 +381,16 @@ void PruneImages(CLIExecutionContext& context)
381
382 for (const auto& image : result.UntaggedImages)
383 {
386 - PrintMessage(Localization::WSLCCLI_ImagePruneUntagged(image));
384 + context.Reporter.Output(L"{}\n", Localization::WSLCCLI_ImagePruneUntagged(image));
385 }
386
387 for (const auto& image : result.DeletedImages)
388 {
391 - PrintMessage(Localization::WSLCCLI_ImagePruneDeleted(image));
389 + context.Reporter.Output(L"{}\n", Localization::WSLCCLI_ImagePruneDeleted(image));
390 }
391
394 - PrintMessage(L"");
395 - PrintMessage(Localization::WSLCCLI_ImagePruneSpaceReclaimedBytes(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
392 + context.Reporter.Output(L"\n");
393 + context.Reporter.Output(
394 + L"{}\n", Localization::WSLCCLI_ImagePruneSpaceReclaimedBytes(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
395 }
396 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/InspectTasks.cpp
+2 -2
@@ -107,12 +107,12 @@ void Inspect(CLIExecutionContext& context)
107 }
108 else
109 {
110 - PrintMessage(Localization::WSLCCLI_ObjectNotFoundError(objectId), stderr);
110 + context.Reporter.Error(L"{}\n", Localization::WSLCCLI_ObjectNotFoundError(objectId));
111 context.ExitCode = 1;
112 }
113 }
114
115 // Always print the array, even if it's empty or an error was encountered
116 - PrintMessage(MultiByteToWide(array.dump(c_jsonPrettyPrintIndent)));
116 + context.Reporter.Output(L"{}\n", MultiByteToWide(array.dump(c_jsonPrettyPrintIndent)));
117 }
118 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/NetworkTasks.cpp
+12 -12
@@ -30,7 +30,7 @@ using namespace wsl::windows::wslc::services;
30
31 namespace wsl::windows::wslc::task {
32
33 -static bool TryInspectNetwork(Session& session, const std::string& networkName, std::optional<wslc_schema::Network>& inspectData)
33 +static bool TryInspectNetwork(Reporter& reporter, Session& session, const std::string& networkName, std::optional<wslc_schema::Network>& inspectData)
34 {
35 try
36 {
@@ -41,7 +41,7 @@ static bool TryInspectNetwork(Session& session, const std::string& networkName,
41 {
42 if (ex.GetErrorCode() == WSLC_E_NETWORK_NOT_FOUND)
43 {
44 - PrintMessage(Localization::MessageWslcNetworkNotFound(networkName.c_str()), stderr);
44 + reporter.Error(L"{}\n", Localization::MessageWslcNetworkNotFound(networkName.c_str()));
45 return false;
46 }
47
@@ -49,7 +49,7 @@ static bool TryInspectNetwork(Session& session, const std::string& networkName,
49 }
50 }
51
52 -static bool TryDeleteNetwork(Session& session, const std::string& networkName, bool force)
52 +static bool TryDeleteNetwork(Reporter& reporter, Session& session, const std::string& networkName, bool force)
53 {
54 try
55 {
@@ -62,7 +62,7 @@ static bool TryDeleteNetwork(Session& session, const std::string& networkName, b
62 {
63 if (!force)
64 {
65 - PrintMessage(Localization::MessageWslcNetworkNotFound(networkName.c_str()), stderr);
65 + reporter.Error(L"{}\n", Localization::MessageWslcNetworkNotFound(networkName.c_str()));
66 }
67
68 return false;
@@ -108,7 +108,7 @@ void CreateNetwork(CLIExecutionContext& context)
108 }
109
110 NetworkService::Create(context.Data.Get<Data::Session>(), options);
111 - PrintMessage(MultiByteToWide(options.Name));
111 + context.Reporter.Output(L"{}\n", MultiByteToWide(options.Name));
112 }
113
114 void DeleteNetworks(CLIExecutionContext& context)
@@ -119,9 +119,9 @@ void DeleteNetworks(CLIExecutionContext& context)
119 const bool force = context.Args.Contains(ArgType::Force);
120 for (const auto& name : networkNames)
121 {
122 - if (TryDeleteNetwork(session, WideToMultiByte(name), force))
122 + if (TryDeleteNetwork(context.Reporter, session, WideToMultiByte(name), force))
123 {
124 - PrintMessage(name);
124 + context.Reporter.Output(L"{}\n", name);
125 }
126 else if (!force)
127 {
@@ -146,7 +146,7 @@ void InspectNetworks(CLIExecutionContext& context)
146 for (const auto& name : networkNames)
147 {
148 std::optional<wslc_schema::Network> inspectData;
149 - if (TryInspectNetwork(session, WideToMultiByte(name), inspectData))
149 + if (TryInspectNetwork(context.Reporter, session, WideToMultiByte(name), inspectData))
150 {
151 result.push_back(*inspectData);
152 }
@@ -157,7 +157,7 @@ void InspectNetworks(CLIExecutionContext& context)
157 }
158
159 auto json = ToJson(result, c_jsonPrettyPrintIndent);
160 - PrintMessage(MultiByteToWide(json));
160 + context.Reporter.Output(L"{}\n", MultiByteToWide(json));
161 }
162
163 void ListNetworks(CLIExecutionContext& context)
@@ -169,7 +169,7 @@ void ListNetworks(CLIExecutionContext& context)
169 {
170 for (const auto& network : networks)
171 {
172 - PrintMessage(MultiByteToWide(network.Name));
172 + context.Reporter.Output(L"{}\n", MultiByteToWide(network.Name));
173 }
174
175 return;
@@ -186,7 +186,7 @@ void ListNetworks(CLIExecutionContext& context)
186 case FormatType::Json:
187 {
188 auto json = ToJson(networks, c_jsonPrettyPrintIndent);
189 - PrintMessage(MultiByteToWide(json));
189 + context.Reporter.Output(L"{}\n", MultiByteToWide(json));
190 break;
191 }
192 case FormatType::Table:
@@ -224,7 +224,7 @@ void PruneNetworks(CLIExecutionContext& context)
224
225 for (const auto& networkName : result.PrunedNetworks)
226 {
227 - PrintMessage(Localization::WSLCCLI_NetworkPruneDeleted(MultiByteToWide(networkName)));
227 + context.Reporter.Output(L"{}\n", Localization::WSLCCLI_NetworkPruneDeleted(MultiByteToWide(networkName)));
228 }
229 }
230
src/windows/wslc/tasks/RegistryTasks.cpp
+2 -2
@@ -46,7 +46,7 @@ void Login(CLIExecutionContext& context)
46 auto [credUsername, credSecret] = RegistryService::Authenticate(session, serverAddress, username, password);
47 RegistryService::Store(serverAddress, credUsername, credSecret);
48
49 - PrintMessage(Localization::WSLCCLI_LoginSucceeded());
49 + context.Reporter.Output(L"{}\n", Localization::WSLCCLI_LoginSucceeded());
50 }
51
52 void Logout(CLIExecutionContext& context)
@@ -60,7 +60,7 @@ void Logout(CLIExecutionContext& context)
60
61 RegistryService::Erase(serverAddress);
62
63 - PrintMessage(Localization::WSLCCLI_LogoutSucceeded(MultiByteToWide(serverAddress)));
63 + context.Reporter.Output(L"{}\n", Localization::WSLCCLI_LogoutSucceeded(MultiByteToWide(serverAddress)));
64 }
65
66 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/SessionTasks.cpp
+5 -5
@@ -30,7 +30,7 @@ namespace wsl::windows::wslc::task {
30 void AttachToSession(CLIExecutionContext& context)
31 {
32 auto& session = context.Data.Get<Data::Session>();
33 - context.ExitCode = SessionService::Attach(session);
33 + context.ExitCode = SessionService::Attach(context.Reporter, session);
34 }
35
36 void OpenSessionIfSpecified(CLIExecutionContext& context)
@@ -70,7 +70,7 @@ void ListSessions(CLIExecutionContext& context)
70 if (context.Args.Contains(ArgType::Verbose))
71 {
72 const wchar_t* plural = sessions.size() == 1 ? L"" : L"s";
73 - PrintMessage(std::format(L"[wslc] Found {} session{}", sessions.size(), plural), stdout);
73 + context.Reporter.Output(L"[wslc] Found {} session{}\n", sessions.size(), plural);
74 }
75
76 TableOutput<3> table(
@@ -92,7 +92,7 @@ void ListSessions(CLIExecutionContext& context)
92 void TerminateSession(CLIExecutionContext& context)
93 {
94 auto& session = context.Data.Get<Data::Session>();
95 - context.ExitCode = SessionService::TerminateSession(session);
95 + context.ExitCode = SessionService::TerminateSession(context.Reporter, session);
96 }
97
98 void RunInSession(CLIExecutionContext& context)
@@ -109,7 +109,7 @@ void RunInSession(CLIExecutionContext& context)
109 }
110 }
111
112 - context.ExitCode = SessionService::Run(session, arguments);
112 + context.ExitCode = SessionService::Run(context.Reporter, session, arguments);
113 }
114
115 void EnterSession(CLIExecutionContext& context)
@@ -128,7 +128,7 @@ void EnterSession(CLIExecutionContext& context)
128 sessionName = wsl::shared::string::GuidToString<wchar_t>(guid, wsl::shared::string::GuidToStringFlags::None);
129 }
130
131 - context.ExitCode = SessionService::Enter(storagePath.wstring(), sessionName);
131 + context.ExitCode = SessionService::Enter(context.Reporter, storagePath.wstring(), sessionName);
132 }
133
134 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/VolumeTasks.cpp
+14 -14
@@ -30,7 +30,7 @@ using namespace wsl::windows::wslc::services;
30
31 namespace wsl::windows::wslc::task {
32
33 -static bool TryInspectVolume(Session& session, const std::string& volumeName, std::optional<wslc_schema::InspectVolume>& inspectData)
33 +static bool TryInspectVolume(Reporter& reporter, Session& session, const std::string& volumeName, std::optional<wslc_schema::InspectVolume>& inspectData)
34 {
35 try
36 {
@@ -41,7 +41,7 @@ static bool TryInspectVolume(Session& session, const std::string& volumeName, st
41 {
42 if (ex.GetErrorCode() == WSLC_E_VOLUME_NOT_FOUND)
43 {
44 - PrintMessage(Localization::MessageWslcVolumeNotFound(volumeName.c_str()), stderr);
44 + reporter.Error(L"{}\n", Localization::MessageWslcVolumeNotFound(volumeName.c_str()));
45 return false;
46 }
47
@@ -49,7 +49,7 @@ static bool TryInspectVolume(Session& session, const std::string& volumeName, st
49 }
50 }
51
52 -static bool TryDeleteVolume(Session& session, const std::string& volumeName, bool force)
52 +static bool TryDeleteVolume(Reporter& reporter, Session& session, const std::string& volumeName, bool force)
53 {
54 try
55 {
@@ -62,7 +62,7 @@ static bool TryDeleteVolume(Session& session, const std::string& volumeName, boo
62 {
63 if (!force)
64 {
65 - PrintMessage(Localization::MessageWslcVolumeNotFound(volumeName.c_str()), stderr);
65 + reporter.Error(L"{}\n", Localization::MessageWslcVolumeNotFound(volumeName.c_str()));
66 }
67
68 return false;
@@ -100,7 +100,7 @@ void CreateVolume(CLIExecutionContext& context)
100 }
101
102 auto result = VolumeService::Create(context.Data.Get<Data::Session>(), options);
103 - PrintMessage(MultiByteToWide(result.Name));
103 + context.Reporter.Output(L"{}\n", MultiByteToWide(result.Name));
104 }
105
106 void DeleteVolumes(CLIExecutionContext& context)
@@ -111,9 +111,9 @@ void DeleteVolumes(CLIExecutionContext& context)
111 const bool force = context.Args.Contains(ArgType::Force);
112 for (const auto& name : volumeNames)
113 {
114 - if (TryDeleteVolume(session, WideToMultiByte(name), force))
114 + if (TryDeleteVolume(context.Reporter, session, WideToMultiByte(name), force))
115 {
116 - PrintMessage(name);
116 + context.Reporter.Output(L"{}\n", name);
117 }
118 else if (!force)
119 {
@@ -138,7 +138,7 @@ void InspectVolumes(CLIExecutionContext& context)
138 for (const auto& name : volumeNames)
139 {
140 std::optional<wslc_schema::InspectVolume> inspectData;
141 - if (TryInspectVolume(session, WideToMultiByte(name), inspectData))
141 + if (TryInspectVolume(context.Reporter, session, WideToMultiByte(name), inspectData))
142 {
143 result.push_back(*inspectData);
144 }
@@ -149,7 +149,7 @@ void InspectVolumes(CLIExecutionContext& context)
149 }
150
151 auto json = ToJson(result, c_jsonPrettyPrintIndent);
152 - PrintMessage(MultiByteToWide(json));
152 + context.Reporter.Output(L"{}\n", MultiByteToWide(json));
153 }
154
155 void ListVolumes(CLIExecutionContext& context)
@@ -161,7 +161,7 @@ void ListVolumes(CLIExecutionContext& context)
161 {
162 for (const auto& volume : volumes)
163 {
164 - PrintMessage(MultiByteToWide(volume.Name));
164 + context.Reporter.Output(L"{}\n", MultiByteToWide(volume.Name));
165 }
166
167 return;
@@ -178,7 +178,7 @@ void ListVolumes(CLIExecutionContext& context)
178 case FormatType::Json:
179 {
180 auto json = ToJson(volumes, c_jsonPrettyPrintIndent);
181 - PrintMessage(MultiByteToWide(json));
181 + context.Reporter.Output(L"{}\n", MultiByteToWide(json));
182 break;
183 }
184 case FormatType::Table:
@@ -217,10 +217,10 @@ void PruneVolumes(CLIExecutionContext& context)
217
218 for (const auto& volumeName : result.PrunedVolumes)
219 {
220 - PrintMessage(Localization::WSLCCLI_VolumePruneDeleted(MultiByteToWide(volumeName)));
220 + context.Reporter.Output(L"{}\n", Localization::WSLCCLI_VolumePruneDeleted(MultiByteToWide(volumeName)));
221 }
222
223 - PrintMessage(L"");
224 - PrintMessage(Localization::WSLCCLI_VolumePruneSpaceReclaimed(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
223 + context.Reporter.Output(L"\n");
224 + context.Reporter.Output(L"{}\n", Localization::WSLCCLI_VolumePruneSpaceReclaimed(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
225 }
226 } // namespace wsl::windows::wslc::task
test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp
+13 -13
@@ -71,7 +71,7 @@ class WSLCE2EImageBuildTests
71
72 auto buildResult = RunWslc(
73 std::format(L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), BuiltImage.NameAndTag()));
74 - buildResult.Verify({.Stderr = L"", .ExitCode = 0});
74 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
75
76 auto inspectData = InspectImage(BuiltImage.NameAndTag());
77 VERIFY_IS_TRUE(inspectData.RepoTags.has_value());
@@ -110,7 +110,7 @@ class WSLCE2EImageBuildTests
110 dockerfilePath.wstring(),
111 BuiltImageTag1.NameAndTag(),
112 BuiltImageTag2.NameAndTag()));
113 - buildResult.Verify({.Stderr = L"", .ExitCode = 0});
113 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
114
115 // Verify both tags are present by inspecting each one
116 auto inspectData1 = InspectImage(BuiltImageTag1.NameAndTag());
@@ -148,13 +148,13 @@ class WSLCE2EImageBuildTests
148
149 // Build with --pull --verbose. When --pull causes docker to resolve the base image
150 // from the registry, the FROM step includes a @sha256: digest (e.g.
151 - // "FROM docker.io/library/debian:latest@sha256:..."). Without --pull, no digest appears.
151 + // "FROM docker.io/library/debian:latest@sha256:..."). Build progress goes to stderr.
152 auto buildResult = RunWslc(std::format(
153 L"build \"{}\" -f \"{}\" -t {} --pull --verbose", contextDir.wstring(), dockerfilePath.wstring(), BuiltImagePull.NameAndTag()));
154 - buildResult.Verify({.Stderr = L"", .ExitCode = 0});
154 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
155
156 - VERIFY_IS_TRUE(buildResult.Stdout.has_value());
157 - VERIFY_IS_TRUE(buildResult.Stdout->find(L"@sha256:") != std::wstring::npos);
156 + VERIFY_IS_TRUE(buildResult.Stderr.has_value());
157 + VERIFY_IS_TRUE(buildResult.Stderr->find(L"@sha256:") != std::wstring::npos);
158 }
159
160 WSLC_TEST_METHOD(WSLCE2E_Image_Build_Target_Success)
@@ -180,7 +180,7 @@ class WSLCE2EImageBuildTests
180
181 auto buildResult = RunWslc(std::format(
182 L"build \"{}\" -f \"{}\" -t {} --target build-stage", contextDir.wstring(), dockerfilePath.wstring(), BuiltImageTarget.NameAndTag()));
183 - buildResult.Verify({.Stderr = L"", .ExitCode = 0});
183 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
184
185 auto inspectData = InspectImage(BuiltImageTarget.NameAndTag());
186 VERIFY_IS_TRUE(inspectData.RepoTags.has_value());
@@ -214,7 +214,7 @@ class WSLCE2EImageBuildTests
214 contextDir.wstring(),
215 dockerfilePath.wstring(),
216 BuiltImageLabel.NameAndTag()));
217 - buildResult.Verify({.Stderr = L"", .ExitCode = 0});
217 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
218
219 auto inspectData = InspectImage(BuiltImageLabel.NameAndTag());
220 VERIFY_IS_TRUE(inspectData.Config.has_value());
@@ -250,7 +250,7 @@ class WSLCE2EImageBuildTests
250 contextDir.wstring(),
251 dockerfilePath.wstring(),
252 BuiltImageLabelOverride.NameAndTag()));
253 - buildResult.Verify({.Stderr = L"", .ExitCode = 0});
253 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
254
255 auto inspectData = InspectImage(BuiltImageLabelOverride.NameAndTag());
256 VERIFY_IS_TRUE(inspectData.Config.has_value());
@@ -345,19 +345,19 @@ class WSLCE2EImageBuildTests
345
346 // Seed the cache.
347 auto firstBuild = RunWslc(buildCmd);
348 - firstBuild.Verify({.Stderr = L"", .ExitCode = 0});
348 + firstBuild.Verify({.Stdout = L"", .ExitCode = 0});
349 const auto firstId = InspectImage(BuiltImageNoCache.NameAndTag()).Id;
350 VERIFY_ARE_NOT_EQUAL(std::string{}, firstId);
351
352 // A repeated build without --no-cache should hit the cache and produce the same id.
353 auto cachedBuild = RunWslc(buildCmd);
354 - cachedBuild.Verify({.Stderr = L"", .ExitCode = 0});
354 + cachedBuild.Verify({.Stdout = L"", .ExitCode = 0});
355 const auto cachedId = InspectImage(BuiltImageNoCache.NameAndTag()).Id;
356 VERIFY_ARE_EQUAL(firstId, cachedId, L"Repeated build without --no-cache should reuse the cached layer");
357
358 // --no-cache must re-run the non-deterministic step, producing a new id.
359 auto noCacheBuild = RunWslc(buildCmd + L" --no-cache");
360 - noCacheBuild.Verify({.Stderr = L"", .ExitCode = 0});
360 + noCacheBuild.Verify({.Stdout = L"", .ExitCode = 0});
361 const auto noCacheId = InspectImage(BuiltImageNoCache.NameAndTag()).Id;
362 VERIFY_ARE_NOT_EQUAL(firstId, noCacheId, L"--no-cache must rebuild the non-deterministic RUN step");
363 }
@@ -382,7 +382,7 @@ private:
382 WriteTestFileContent(testRoot / fileName, "FROM debian:latest\nCMD [\"echo\", \"build-ok\"]\n");
383
384 auto buildResult = RunWslc(std::format(L"build \"{}\" -t {}", testRoot.wstring(), image.NameAndTag()));
385 - buildResult.Verify({.Stderr = L"", .ExitCode = 0});
385 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
386
387 auto inspectData = InspectImage(image.NameAndTag());
388 VERIFY_IS_TRUE(inspectData.RepoTags.has_value());
test/windows/wslc/e2e/WSLCE2EImageInspectTests.cpp
+1 -1
@@ -104,7 +104,7 @@ class WSLCE2EImageInspectTests
104
105 auto buildResult = RunWslc(std::format(
106 L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), BuiltExposeImage.NameAndTag()));
107 - buildResult.Verify({.Stderr = L"", .ExitCode = 0});
107 + buildResult.Verify({.Stdout = L"", .ExitCode = 0});
108
109 auto inspectData = InspectImage(BuiltExposeImage.NameAndTag());
110 VERIFY_IS_TRUE(inspectData.Config.has_value());
test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp
+7 -3
@@ -70,15 +70,19 @@ class WSLCE2EPushPullTests
70
71 auto tagCleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImage)); });
72
73 - // Push should succeed.
73 + // Standalone push/pull send progress to stdout (Docker parity), leaving stderr empty.
74 auto result = RunWslc(std::format(L"push {}", registryImage));
75 - result.Verify({.ExitCode = 0});
75 + result.Verify({.Stderr = L"", .ExitCode = 0});
76 + VERIFY_IS_TRUE(result.Stdout.has_value());
77 + VERIFY_IS_FALSE(result.Stdout->empty());
78
79 // Delete the local copy and pull it back.
80 RunWslcAndVerify(std::format(L"image delete --force {}", registryImage), {.ExitCode = 0});
81
82 result = RunWslc(std::format(L"pull {}", registryImage));
83 result.Verify({.Stderr = L"", .ExitCode = 0});
84 + VERIFY_IS_TRUE(result.Stdout.has_value());
85 + VERIFY_IS_FALSE(result.Stdout->empty());
86
87 // Verify the image is now present.
88 auto registryRepo = registryImage.substr(0, registryImage.rfind(L':'));
@@ -93,7 +97,7 @@ class WSLCE2EPushPullTests
97 {
98 auto result = RunWslc(L"push does-not-exist:latest");
99 auto errorMessage = L"An image does not exist locally with the tag: does-not-exist\r\nError code: E_FAIL\r\n";
96 - result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
100 + result.Verify({.Stderr = errorMessage, .ExitCode = 1});
101 }
102
103 WSLC_TEST_METHOD(WSLCE2E_Image_Pull_NonExistentImage)