Skip to content
Open
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
8 changes: 6 additions & 2 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3224,8 +3224,8 @@ On first run, creates the file with all settings commented out at their defaults
<comment>{Locked="DNS"}Command line arguments should not be translated</comment>
</data>
<data name="WSLCCLI_ProgressArgDescription" xml:space="preserve">
<value>Progress type (format: none|ansi) (default: ansi)</value>
<comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
<value>Set type of progress output (auto, tty, plain, quiet) (default: auto)</value>
<comment>{Locked="auto"}{Locked="tty"}{Locked="plain"}{Locked="quiet"}Command line arguments should not be translated</comment>
</data>
<data name="WSLCCLI_PullArgDescription" xml:space="preserve">
<value>Image pull policy (always|missing|never) (default: missing)</value>
Expand Down Expand Up @@ -3267,6 +3267,10 @@ On first run, creates the file with all settings commented out at their defaults
<value>Invalid {} value: {} is not a recognized pull policy. Supported pull policies are: {}.</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_InvalidProgressTypeError" xml:space="preserve">
<value>Invalid {} value: {} is not a recognized progress type. Supported progress types are: {}.</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_InvalidInspectError" xml:space="preserve">
<value>Invalid {} value: {} is not a recognized inspect type. Supported inspect types are: {}.</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/arguments/ArgumentConvertedTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ namespace wsl::windows::wslc::argument::details {
using FormatType = wsl::windows::wslc::models::FormatType;
using InspectType = wsl::windows::wslc::models::InspectType;
using JsonIndent = int;
using ProgressMode = wsl::windows::wslc::models::ProgressMode;
using PullPolicy = wsl::windows::wslc::models::PullPolicy;
using WSLCSignal = ::WSLCSignal;
using UlimitValue = std::tuple<std::string, int64_t, int64_t>;
Expand Down
2 changes: 1 addition & 1 deletion src/windows/wslc/arguments/ArgumentDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ _(Output, "output", L"o", Kind::Value,
_(Password, "password", L"p", Kind::Value, NoConversion, Localization::WSLCCLI_LoginPasswordArgDescription()) \
_(PasswordStdin, "password-stdin", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_LoginPasswordStdinArgDescription()) \
_(Path, "path", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_PathArgDescription()) \
/*_(Progress, "progress", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_ProgressArgDescription())*/ \
_(Progress, "progress", NO_ALIAS, Kind::Value, ProgressMode, Localization::WSLCCLI_ProgressArgDescription()) \
_(Publish, "publish", L"p", Kind::Value, NoConversion, Localization::WSLCCLI_PublishArgDescription()) \
_(PublishAll, "publish-all", L"P", Kind::Flag, NoConversion, Localization::WSLCCLI_PublishAllArgDescription()) \
_(Pull, "pull", NO_ALIAS, Kind::Value, PullPolicy, Localization::WSLCCLI_PullArgDescription()) \
Expand Down
4 changes: 4 additions & 0 deletions src/windows/wslc/arguments/ArgumentValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ void Argument::Validate(ArgMap& execArgs) const
CacheConverted<ArgType::Pull>(execArgs, m_name, validation::GetPullPolicyFromString);
break;

case ArgType::Progress:
CacheConverted<ArgType::Progress>(execArgs, m_name, validation::GetProgressModeFromString);
break;

case ArgType::Signal:
CacheConverted<ArgType::Signal>(execArgs, m_name, validation::GetWSLCSignalFromString);
break;
Expand Down
31 changes: 31 additions & 0 deletions src/windows/wslc/arguments/SpecParsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,37 @@ models::PullPolicy GetPullPolicyFromString(const std::wstring& input, const std:
throw ArgumentException(Localization::WSLCCLI_InvalidPullPolicyError(argName, input, supportedValues));
}

models::ProgressMode GetProgressModeFromString(const std::wstring& input, const std::wstring& argName)
{
static constexpr std::pair<std::wstring_view, models::ProgressMode> c_progressModes[] = {
{L"auto", models::ProgressMode::Auto},
{L"tty", models::ProgressMode::Tty},
{L"plain", models::ProgressMode::Plain},
{L"quiet", models::ProgressMode::Quiet},
};

for (const auto& [name, mode] : c_progressModes)
{
if (IsEqual(input, name))
{
return mode;
}
}

std::wstring supportedValues;
for (const auto& progressMode : c_progressModes)
{
if (!supportedValues.empty())
{
supportedValues += L", ";
}

supportedValues += progressMode.first;
}

throw ArgumentException(Localization::WSLCCLI_InvalidProgressTypeError(argName, input, supportedValues));
}

models::InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName)
{
if (IsEqual(input, L"image"))
Expand Down
3 changes: 3 additions & 0 deletions src/windows/wslc/arguments/SpecParsing.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ int GetInspectJsonIndentFromString(const std::wstring& input, const std::wstring
// Parses an image pull policy ("always"/"missing"/"never").
models::PullPolicy GetPullPolicyFromString(const std::wstring& input, const std::wstring& argName = {});

// Parses a build progress type ("auto"/"tty"/"plain"/"quiet") into a ProgressMode.
models::ProgressMode GetProgressModeFromString(const std::wstring& input, const std::wstring& argName = {});

// Parses an inspect target ("image"/"container"/"network"/"volume") into an InspectType.
models::InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName);

Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ImageBuildCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ std::vector<Argument> ImageBuildCommand::GetArguments() const
Argument::Create(ArgType::BuildLabel, false, Limit::Unlimited),
Argument::Create(ArgType::NoCache),
Argument::Create(ArgType::BuildOutput, false, std::nullopt, Localization::WSLCCLI_BuildOutputArgDescription()),
Argument::Create(ArgType::Progress),
Argument::Create(ArgType::Secret, false, Limit::Unlimited),
Argument::Create(ArgType::Tag, false, Limit::Unlimited),
Argument::Create(ArgType::Verbose),
Expand Down
50 changes: 41 additions & 9 deletions src/windows/wslc/services/BuildImageCallback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ bool BuildImageCallback::IsCancelled() const
return WaitForSingleObject(m_cancelEvent, 0) == WAIT_OBJECT_0;
}

const wsl::windows::common::vt::Sequence& BuildImageCallback::Color(const wsl::windows::common::vt::Sequence& sequence) const
{
static const wsl::windows::common::vt::Sequence empty{};
return m_color ? sequence : empty;
}

void BuildImageCallback::CaptureForReplay(std::string_view text)
{
m_allLines.emplace_back(text);
m_allLinesBytes += m_allLines.back().size();
while (m_allLinesBytes > c_maxAllLinesBytes && !m_allLines.empty())
{
m_allLinesBytes -= m_allLines.front().size();
m_allLines.pop_front();
}
}

void BuildImageCallback::CollapseWindow()
{
if (m_displayedLines > 0)
Expand Down Expand Up @@ -95,9 +112,23 @@ try
const bool isLog = (idView == "log");
const bool isPullProgress = (!idView.empty() && total > 0 && !isLog);

if (m_verbose || !m_isConsole)
// quiet: suppress live progress but retain everything plain would have printed, so the destructor
// can replay the failing step and its logs on build failure. Pull progress is excluded because it
// is rewritten in place rather than appended, and so is the only message with no trailing newline.
if (m_mode == models::ProgressMode::Quiet)
{
if (!isPullProgress)
{
CaptureForReplay(status);
}
return S_OK;
}

if (m_verbose || !m_renderInPlace)
{
// Skip pull progress updates when output is redirected, show only major steps
// Only major steps are reported here. Unlike docker's plain output, which appends
// throttled download lines, pull progress is omitted entirely: without in-place
// updates those lines are mostly noise.
if (!isPullProgress)
{
m_terminal.Info(L"{}", status);
Expand Down Expand Up @@ -175,8 +206,9 @@ try
wide.resize(bodyLength);

// Pass the color sequences as arguments (not baked into the string) so Terminal strips
// them when --no-color is set. The trailing newlines are emitted after the reset.
m_terminal.Info(L"{}{}{}{}", Format::Fg::BrightGreen, wide, Format::Default, newlines);
// them when --no-color is set. Color() additionally strips them outside Tty mode. The
// trailing newlines are emitted after the reset.
m_terminal.Info(L"{}{}{}{}", Color(Format::Fg::BrightGreen), wide, Color(Format::Default), newlines);
return S_OK;
}
CATCH_RETURN();
Expand All @@ -196,10 +228,10 @@ void BuildImageCallback::Redraw()
const int displayCount = completedCount + reservedLines;

// Build the frame body in one buffer to minimize console writes. The cursor moves,
// erases, and text lines it holds are non-color VT that only runs when a VT console is
// attached. The cursor hide/show wrapper and the dim intensity attribute are passed as
// Sequence arguments to Terminal (below) so it strips the color ones (Dim/Normal) when
// --no-color is set, while leaving the non-color cursor moves intact.
// erases, and text lines it holds are non-color VT. This only runs in Tty mode, where a
// VT console is attached. The cursor hide/show wrapper and the dim intensity attribute are
// passed as Sequence arguments to Terminal (below) so it strips the color ones (Dim/Normal)
// when --no-color is set, while leaving the non-color cursor moves intact.
//
// m_frameBuffer is a member so its backing allocation is reused across frames -
// it grows to the high-water mark and is never freed between redraws.
Expand Down Expand Up @@ -251,7 +283,7 @@ void BuildImageCallback::Redraw()
// Emit the frame as a single atomic write. Cursor Hide/Show are non-color and always
// rendered here (VT is on); Format::Dim/Normal are color sequences that Terminal strips
// under --no-color. The buffered body carries the cursor moves, erases, and text lines.
m_terminal.Info(L"{}{}{}{}{}", Cursor::Hide, Format::Dim, std::wstring_view{m_frameBuffer}, Format::Normal, Cursor::Show);
m_terminal.Info(L"{}{}{}{}{}", Cursor::Hide, Color(Format::Dim), std::wstring_view{m_frameBuffer}, Color(Format::Normal), Cursor::Show);
m_displayedLines = displayCount;
}

Expand Down
18 changes: 15 additions & 3 deletions src/windows/wslc/services/BuildImageCallback.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Module Name:

--*/
#pragma once
#include "ContainerModel.h"
#include "Terminal.h"
#include "SessionService.h"
#include "VTSupport.h"
Expand All @@ -24,8 +25,9 @@ class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback
{
public:
// The cancel event handle must remain valid for the lifetime of this callback.
BuildImageCallback(Terminal& terminal, HANDLE cancelEvent, bool verbose) :
m_terminal(terminal), m_verbose(verbose), m_cancelEvent(cancelEvent)
// Mode selects the rendering style (Auto is expected to already be resolved to Tty/Plain by the caller).
BuildImageCallback(Terminal& terminal, HANDLE cancelEvent, bool verbose, models::ProgressMode mode = models::ProgressMode::Tty) :
m_terminal(terminal), m_verbose(verbose), m_cancelEvent(cancelEvent), m_mode(mode), m_color(mode == models::ProgressMode::Tty)
{
}
~BuildImageCallback();
Expand All @@ -40,11 +42,21 @@ class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback
void Redraw();
void RedrawIfNeeded();
bool IsCancelled() const;
// Appends a log chunk to the error-replay buffer, enforcing the retained-bytes cap.
void CaptureForReplay(std::string_view text);
// Returns the sequence when color is enabled for this callback, else an empty (no-op) sequence so
// the Terminal emits nothing for it. Used to strip color from the sequences emitted in Tty mode.
const wsl::windows::common::vt::Sequence& Color(const wsl::windows::common::vt::Sequence& sequence) const;

Terminal& m_terminal;
const bool m_verbose;
const HANDLE m_cancelEvent;
bool m_isConsole = m_terminal.IsVTEnabled(Terminal::Level::Info);
const models::ProgressMode m_mode;
const bool m_color;
// In-place rendering (cursor moves, erases and redraws) is only used for Tty mode on a VT
// console. Plain mode appends one line at a time so its output carries no cursor control and
// is identical whether it goes to a console or a redirected stream.
bool m_renderInPlace = m_mode == models::ProgressMode::Tty && m_terminal.IsVTEnabled(Terminal::Level::Info);
std::deque<std::string> m_lines;
// Each entry already contains the trailing newline so the bytes match what's replayed.
// TODO: Track logs per step so the destructor can replay only the failing step's
Expand Down
10 changes: 10 additions & 0 deletions src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ enum class PullPolicy
Never,
};

// Progress output style for `wslc build`. Auto resolves to Tty when progress output is an
// interactive VT console and Plain otherwise.
enum class ProgressMode
{
Auto,
Tty,
Plain,
Quiet,
};

struct ContainerOptions
{
std::vector<std::string> Arguments;
Expand Down
10 changes: 9 additions & 1 deletion src/windows/wslc/tasks/ImageTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,16 @@ void BuildImage(CLIExecutionContext& context)
WI_SetFlagIf(flags, WSLCBuildImageFlagsNoCache, context.Args.GetValue<ArgType::NoCache>());
WI_SetFlagIf(flags, WSLCBuildImageFlagsPull, context.Args.GetValue<ArgType::BuildPull>());

auto progressMode = context.Args.GetValue<ArgType::Progress>(ProgressMode::Auto);

// Resolve Auto based on whether progress output (stderr) is an interactive VT console.
if (progressMode == ProgressMode::Auto)
{
progressMode = context.Terminal.IsVTEnabled(Terminal::Level::Info) ? ProgressMode::Tty : ProgressMode::Plain;
}

auto cancelEvent = context.CreateCancelEvent();
BuildImageCallback callback(context.Terminal, cancelEvent, context.Args.GetValue<ArgType::Verbose>());
BuildImageCallback callback(context.Terminal, cancelEvent, context.Args.GetValue<ArgType::Verbose>(), progressMode);
services::ImageService::Build(
session, contextPath, tags, buildArgs, labels, secrets, dockerfilePath, target, output, iidFilePath, flags, &callback, cancelEvent);
}
Expand Down
5 changes: 3 additions & 2 deletions src/windows/wslcsession/WSLCSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,8 @@ try

auto mountPath = mountInVm(Options->ContextPath, TRUE);

// Progress is requested as JSON so it can be parsed into the formatted progress messages sent to the
// client. The raw JSON is a docker implementation detail and is never forwarded.
std::vector<std::string> buildArgs{"/usr/bin/docker", "buildx", "build", "--builder", "default", "--progress=rawjson"};
if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsNoCache))
{
Expand Down Expand Up @@ -1413,8 +1415,7 @@ try
}
};

// With --progress=rawjson, docker writes progress to stderr and the final image ID to stdout on success (empty on
// failure).
// Docker writes progress to stderr and the final image ID to stdout on success (empty on failure).
//
// For dest=- the exporter tarball is written to stdout, so it is relayed to the client handle as the
// build runs. RelayHandle is an overlapped handle, so a slow client only marks the relay pending and
Expand Down
42 changes: 41 additions & 1 deletion test/windows/wslc/ParserTestCases.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum class ArgumentSet
{
Run,
List,
Build,
// RootCommand globals; parsed in optionsOnly mode (stops at first positional).
Globals,
};
Expand Down Expand Up @@ -73,6 +74,25 @@ inline std::vector<wsl::windows::wslc::Argument> GetArgumentsForSet(ArgumentSet
Argument::Create(ArgType::Verbose),
};

case ArgumentSet::Build:
// Mirrors ImageBuildCommand::GetArguments() so the parser tests exercise the
// real `wslc build` option set (notably the --progress value option).
return {
Argument::Create(ArgType::Path, true), // Required positional (build context path)
Argument::Create(ArgType::BuildArg, false, Limit::Unlimited),
Argument::Create(ArgType::BuildPull),
Argument::Create(ArgType::BuildTarget),
Argument::Create(ArgType::File),
Argument::Create(ArgType::Label, false, Limit::Unlimited),
Argument::Create(ArgType::NoCache),
Argument::Create(ArgType::Output),
Argument::Create(ArgType::Progress),
Argument::Create(ArgType::Secret, false, Limit::Unlimited),
Argument::Create(ArgType::Tag, false, Limit::Unlimited),
Argument::Create(ArgType::Verbose),
Argument::Create(ArgType::Help),
};

case ArgumentSet::Globals:
// Synthetic stand-in for what Main.cpp passes as cliGlobals to the
// first (optionsOnly) parse pass. Decoupled from RootCommand so the
Expand Down Expand Up @@ -222,5 +242,25 @@ WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --quiet --session foo image1)") \
WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --session foo -q image1)") \
/* Docker-style idempotency: duplicate global flags collapse to a single entry. */ \
WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc --quiet --quiet)") \
WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc -q -q system list)")
WSLC_PARSER_TEST_CASE(Globals, true, LR"(wslc -q -q system list)") \
\
/* `wslc build` --progress option (Build set mirrors ImageBuildCommand). The build \
* context path is the required positional; --progress takes one of auto/tty/plain/ \
* quiet and is validated by Argument::Validate via GetProgressModeFromString. */ \
/* Valid modes, separated and adjoined value forms, and case-sensitivity. */ \
WSLC_PARSER_TEST_CASE(Build, true, LR"(wslc . --progress=auto)") \
WSLC_PARSER_TEST_CASE(Build, true, LR"(wslc . --progress auto)") \
WSLC_PARSER_TEST_CASE(Build, true, LR"(wslc --progress=tty .)") \
WSLC_PARSER_TEST_CASE(Build, true, LR"(wslc . --progress=plain)") \
WSLC_PARSER_TEST_CASE(Build, true, LR"(wslc . --progress quiet)") \
/* Values are case-sensitive (lowercase only), matching Docker and --format. */ \
WSLC_PARSER_TEST_CASE(Build, false, LR"(wslc . --progress=TTY)") \
/* A build with no --progress at all is valid (the option is optional). */ \
WSLC_PARSER_TEST_CASE(Build, true, LR"(wslc .)") \
/* Invalid / unrecognized modes, empty value, and missing value at end of input. */ \
WSLC_PARSER_TEST_CASE(Build, false, LR"(wslc . --progress=fancy)") \
WSLC_PARSER_TEST_CASE(Build, false, LR"(wslc . --progress bogus)") \
WSLC_PARSER_TEST_CASE(Build, false, LR"(wslc . --progress=json)") \
WSLC_PARSER_TEST_CASE(Build, false, LR"(wslc . --progress=)") \
WSLC_PARSER_TEST_CASE(Build, false, LR"(wslc . --progress)")
// clang-format on
8 changes: 8 additions & 0 deletions test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ class WSLCCLIArgumentUnitTests
VERIFY_ARE_EQUAL(pullPolicy, PullPolicy::Never);
VERIFY_THROWS(validation::GetPullPolicyFromString(L"invalid"), ArgumentException);

// Verify build progress mode
VERIFY_ARE_EQUAL(validation::GetProgressModeFromString(L"auto"), ProgressMode::Auto);
VERIFY_ARE_EQUAL(validation::GetProgressModeFromString(L"tty"), ProgressMode::Tty);
VERIFY_ARE_EQUAL(validation::GetProgressModeFromString(L"plain"), ProgressMode::Plain);
VERIFY_ARE_EQUAL(validation::GetProgressModeFromString(L"quiet"), ProgressMode::Quiet);
VERIFY_THROWS(validation::GetProgressModeFromString(L"TTY"), ArgumentException); // Case-sensitive: only lowercase accepted
VERIFY_THROWS(validation::GetProgressModeFromString(L"fancy"), ArgumentException);

// Verify GPU device argument
VERIFY_NO_THROW(validation::ValidateGpus({L"all"}, L"gpusArg"));
VERIFY_THROWS(validation::ValidateGpus({L"none"}, L"gpusArg"), ArgumentException);
Expand Down
Loading