Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 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, rawjson) (default: auto)</value>
<comment>{Locked="auto"}{Locked="tty"}{Locked="plain"}{Locked="quiet"}{Locked="rawjson"}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
3 changes: 2 additions & 1 deletion src/windows/service/inc/WSLCShared.idl
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,10 @@ typedef enum _WSLCBuildImageFlags
WSLCBuildImageFlagsVerbose = 1, // Show all build progress including internal steps.
WSLCBuildImageFlagsNoCache = 2, // Do not use cache when building the image.
WSLCBuildImageFlagsPull = 4, // Always attempt to pull a newer version of the image.
WSLCBuildImageFlagsRawJson = 8, // Forward docker's raw --progress=rawjson output to the callback verbatim instead of parsing it into formatted progress messages.
} WSLCBuildImageFlags;

cpp_quote("#define WSLCBuildImageFlagsValid (WSLCBuildImageFlagsVerbose | WSLCBuildImageFlagsNoCache | WSLCBuildImageFlagsPull)")
cpp_quote("#define WSLCBuildImageFlagsValid (WSLCBuildImageFlagsVerbose | WSLCBuildImageFlagsNoCache | WSLCBuildImageFlagsPull | WSLCBuildImageFlagsRawJson)")

cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCBuildImageFlags);")

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
32 changes: 32 additions & 0 deletions src/windows/wslc/arguments/SpecParsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,38 @@ 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)
{
if (IsEqual(input, L"auto"))
{
return models::ProgressMode::Auto;
}
else if (IsEqual(input, L"tty"))
{
return models::ProgressMode::Tty;
}
else if (IsEqual(input, L"plain"))
{
return models::ProgressMode::Plain;
}
else if (IsEqual(input, L"quiet"))
{
return models::ProgressMode::Quiet;
}
else if (IsEqual(input, L"rawjson"))
{
return models::ProgressMode::RawJson;
}
else
{
throw ArgumentException(std::format(
L"Invalid {} value: {} is not a recognized progress type. Supported progress types are: auto, tty, plain, "
L"quiet, rawjson.",
argName,
input));
}
}

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"/"rawjson") 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
57 changes: 48 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 @@ -91,13 +108,34 @@ try
return S_OK;
}

// rawjson: the server forwards docker's raw progress JSON verbatim; print it as-is with no rendering.
if (m_mode == models::ProgressMode::RawJson)
{
m_terminal.Info(L"{}", MultiByteToWide(status));
return S_OK;
}

const std::string_view idView = (id != nullptr) ? id : std::string_view{};
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 +213,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 +235,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 +290,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
11 changes: 11 additions & 0 deletions src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ 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,
RawJson,
};

struct ContainerOptions
{
std::vector<std::string> Arguments;
Expand Down
14 changes: 13 additions & 1 deletion src/windows/wslc/tasks/ImageTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,20 @@ 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;
}

// rawjson is the only mode that changes what the server sends: it forwards docker's raw progress
// output verbatim instead of parsing it into formatted messages.
WI_SetFlagIf(flags, WSLCBuildImageFlagsRawJson, progressMode == ProgressMode::RawJson);

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
31 changes: 30 additions & 1 deletion src/windows/wslcsession/WSLCSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,7 @@ try
});

bool verbose = WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsVerbose);
bool rawJson = WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsRawJson);
std::string allOutput;
std::string pendingJson;
std::set<std::string> reportedSteps;
Expand Down Expand Up @@ -1413,6 +1414,27 @@ try
}
};

// rawjson mode: forward each complete JSON object docker writes to stderr verbatim (as newline-delimited
Comment thread
ggarzia-MSFT marked this conversation as resolved.
Outdated
// JSON) to the client, bypassing the parsing/formatting done by captureOutput.
auto rawJsonPassthrough = [&](const gsl::span<char>& content) {
pendingJson.append(content.begin(), content.end());

if (!nlohmann::json::accept(pendingJson))
{
// Not yet a complete object; keep accumulating. Drop leading non-JSON noise.
if (!pendingJson.empty() && pendingJson[0] != '{')
{
pendingJson.clear();
}

return;
}

pendingJson.push_back('\n');
reportProgress(pendingJson);
pendingJson.clear();
};

// With --progress=rawjson, docker writes progress to stderr and the final image ID to stdout on success (empty on
// failure).
//
Expand All @@ -1430,7 +1452,14 @@ try
buildProcess.GetStdHandle(1), [&](const auto& content) { allOutput.append(content.begin(), content.end()); }));
}

io.AddHandle(std::make_unique<io::LineBasedReadHandle>(buildProcess.GetStdHandle(2), captureOutput, false));
if (rawJson)
{
io.AddHandle(std::make_unique<io::LineBasedReadHandle>(buildProcess.GetStdHandle(2), rawJsonPassthrough, false));
}
else
{
io.AddHandle(std::make_unique<io::LineBasedReadHandle>(buildProcess.GetStdHandle(2), captureOutput, false));
}

// Handle cancellation within the IO loop (NeedNotComplete) so pipes keep draining.
bool cancelled = false;
Expand Down
Loading
Loading