Improved details for build and pull (#40596)
* Improved details for build and pull
Craig Loewen committed
May 28, 2026 at 12:25 UTC
6b39b6dad37d0eadf4fbf5f614a5cb98d3696b82
6 files changed
+189
-71
src/shared/inc/stringshared.h
+24
@@ -825,6 +825,30 @@ struct CaseInsensitiveCompare
825
}
826
};
827
828
+inline std::wstring FormatBytes(uint64_t bytes)
829
+{
830
+ constexpr double c_kB = 1000.0;
831
+ constexpr double c_MB = 1000.0 * 1000.0;
832
+ constexpr double c_GB = 1000.0 * 1000.0 * 1000.0;
833
+
834
+ if (bytes >= static_cast<uint64_t>(c_GB))
835
+ {
836
+ return std::format(L"{:.2f} GB", bytes / c_GB);
837
+ }
838
+ else if (bytes >= static_cast<uint64_t>(c_MB))
839
+ {
840
+ return std::format(L"{:.2f} MB", bytes / c_MB);
841
+ }
842
+ else if (bytes >= static_cast<uint64_t>(c_kB))
843
+ {
844
+ return std::format(L"{:.2f} KB", bytes / c_kB);
845
+ }
846
+ else
847
+ {
848
+ return std::format(L"{} B", bytes);
849
+ }
850
+}
851
+
852
} // namespace wsl::shared::string
853
854
template <>
src/windows/inc/docker_schema.h
+3
-1
@@ -584,8 +584,10 @@ struct BuildKitStatus
584
{
585
std::string id;
586
std::string vertex;
587
+ int64_t current{};
588
+ int64_t total{};
589
588
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(BuildKitStatus, id, vertex);
590
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(BuildKitStatus, id, vertex, current, total);
591
};
592
593
struct BuildKitLog
src/windows/wslc/services/BuildImageCallback.cpp
+100
-60
@@ -19,6 +19,15 @@ namespace wsl::windows::wslc::services {
19
20
using wsl::windows::common::string::MultiByteToWide;
21
22
+namespace {
23
+ constexpr std::wstring_view c_escapeMoveCursorUpAndClear = L"\033[{}A\033[J";
24
+ constexpr std::wstring_view c_escapeBrightGreen = L"\033[92m";
25
+ constexpr std::wstring_view c_escapeResetAttributes = L"\033[0m";
26
+ constexpr std::wstring_view c_escapeHideCursorDim = L"\033[?25l\033[2m";
27
+ constexpr std::wstring_view c_escapeClearLineAndNewline = L"\033[K\n";
28
+ constexpr std::wstring_view c_escapeUndimShowCursor = L"\033[22m\033[?25h";
29
+} // namespace
30
+
31
BuildImageCallback::~BuildImageCallback()
32
try
33
{
@@ -58,15 +67,26 @@ void BuildImageCallback::CollapseWindow()
67
{
68
if (m_displayedLines > 0)
69
{
61
- WriteTerminal(std::format(L"\033[{}A\033[J", m_displayedLines));
70
+ WriteTerminal(std::format(c_escapeMoveCursorUpAndClear, m_displayedLines));
71
m_displayedLines = 0;
72
}
73
74
m_lines.clear();
75
m_pendingLine.clear();
76
+ m_pullLines.clear();
77
}
78
69
-HRESULT BuildImageCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG /*current*/, ULONGLONG /*total*/)
79
+void BuildImageCallback::RedrawIfNeeded()
80
+{
81
+ auto now = std::chrono::steady_clock::now();
82
+ if (now - m_lastRedraw >= c_redrawInterval)
83
+ {
84
+ Redraw();
85
+ m_lastRedraw = now;
86
+ }
87
+}
88
+
89
+HRESULT BuildImageCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total)
90
try
91
{
92
if (status == nullptr || *status == '\0')
@@ -81,78 +101,90 @@ try
101
return S_OK;
102
}
103
104
+ const std::string_view idView = (id != nullptr) ? id : std::string_view{};
105
+ const bool isLog = (idView == "log");
106
+ const bool isPullProgress = (!idView.empty() && total > 0 && !isLog);
107
+
108
if (m_verbose || !m_isConsole)
109
{
86
- wprintf(L"%hs", status);
110
+ // Skip pull progress updates when output is redirected, show only major steps
111
+ if (!isPullProgress)
112
+ {
113
+ wprintf(L"%hs", status);
114
+ }
115
return S_OK;
116
}
117
90
- // Match the specific "log" sentinel sent by WSLCSession::BuildImage rather than
91
- // accepting any non-empty id, so future or unrelated id usage defaults to permanent.
92
- const bool isLog = (id != nullptr && std::string_view{id} == "log");
93
-
94
- if (!isLog)
118
+ // Pull/download progress: update the per-entry map so Redraw can show each entry
119
+ // on a single line that updates in place.
120
+ if (isPullProgress)
121
{
96
- // Permanent line: collapse the scrolling window then print directly.
97
- CollapseWindow();
98
- WriteTerminal(MultiByteToWide(status));
122
+ m_pullLines[id] = status;
123
+ RedrawIfNeeded();
124
+
125
return S_OK;
126
}
127
102
- // Log line: add to the scrolling window.
103
- for (const char* p = status; *p != '\0'; ++p)
128
+ if (isLog)
129
{
105
- if (*p == '\n')
130
+ // Log line: add to the scrolling window.
131
+ for (const char* p = status; *p != '\0'; ++p)
132
{
107
- // Store with the trailing newline so the byte count matches what is replayed.
108
- // Cap retained log output to avoid unbounded growth on very long builds.
109
- m_allLines.push_back(m_pendingLine + '\n');
110
- m_allLinesBytes += m_allLines.back().size();
111
- while (m_allLinesBytes > c_maxAllLinesBytes && !m_allLines.empty())
133
+ if (*p == '\n')
134
{
113
- m_allLinesBytes -= m_allLines.front().size();
114
- m_allLines.pop_front();
115
- }
135
+ // Store with the trailing newline so the byte count matches what is replayed.
136
+ // Cap retained log output to avoid unbounded growth on very long builds.
137
+ m_allLines.push_back(m_pendingLine + '\n');
138
+ m_allLinesBytes += m_allLines.back().size();
139
+ while (m_allLinesBytes > c_maxAllLinesBytes && !m_allLines.empty())
140
+ {
141
+ m_allLinesBytes -= m_allLines.front().size();
142
+ m_allLines.pop_front();
143
+ }
144
117
- m_lines.push_back(std::move(m_pendingLine));
118
- m_pendingLine.clear();
119
- if (m_lines.size() > c_maxDisplayLines)
120
- {
121
- m_lines.pop_front();
145
+ m_lines.push_back(std::move(m_pendingLine));
146
+ m_pendingLine.clear();
147
+ if (m_lines.size() > c_maxDisplayLines)
148
+ {
149
+ m_lines.pop_front();
150
+ }
151
}
123
- }
124
- else if (*p == '\r')
125
- {
126
- // \r\n is a line ending; standalone \r overwrites the current line.
127
- if (*(p + 1) != '\n')
152
+ else if (*p == '\r')
153
{
129
- // Flush a throttled redraw before clearing so \r-based progress
130
- // updates are visible even when batched in a single OnProgress call.
131
- auto now = std::chrono::steady_clock::now();
132
- if (!m_pendingLine.empty() && now - m_lastRedraw >= c_redrawInterval)
154
+ // \r\n is a line ending; standalone \r overwrites the current line.
155
+ if (*(p + 1) != '\n')
156
{
134
- Redraw();
135
- m_lastRedraw = now;
157
+ // Flush a throttled redraw before clearing so \r-based progress
158
+ // updates are visible even when batched in a single OnProgress call.
159
+ if (!m_pendingLine.empty())
160
+ {
161
+ RedrawIfNeeded();
162
+ }
163
+ m_pendingLine.clear();
164
}
137
- m_pendingLine.clear();
165
+ }
166
+ else
167
+ {
168
+ m_pendingLine += *p;
169
}
170
}
140
- else
141
- {
142
- m_pendingLine += *p;
143
- }
144
- }
171
146
- // Throttle redraws to avoid blocking the server's IO loop with console writes
147
- // during rapid output. Lines accumulate in the deque immediately; the display
148
- // catches up at ~20fps.
149
- auto now = std::chrono::steady_clock::now();
150
- if (now - m_lastRedraw >= c_redrawInterval)
151
- {
152
- Redraw();
153
- m_lastRedraw = now;
172
+ // Throttle redraws to avoid blocking the server's IO loop with console writes
173
+ // during rapid output. Lines accumulate in the deque immediately; the display
174
+ // catches up at ~20fps.
175
+ RedrawIfNeeded();
176
+
177
+ return S_OK;
178
}
179
180
+ // Else is a build step
181
+ CollapseWindow();
182
+ auto wide = MultiByteToWide(status);
183
+ const auto bodyLength = wide.find_last_not_of(L"\r\n") + 1;
184
+ const auto newlines = wide.substr(bodyLength);
185
+ wide.resize(bodyLength);
186
+
187
+ WriteTerminal(std::format(L"{}{}{}{}", c_escapeBrightGreen, wide, c_escapeResetAttributes, newlines));
188
return S_OK;
189
}
190
CATCH_RETURN();
@@ -167,27 +199,29 @@ void BuildImageCallback::Redraw()
199
// to std::wstring::resize).
200
const SHORT consoleWidth = std::max<SHORT>(0, info.srWindow.Right - info.srWindow.Left);
201
170
- // Determine how many completed lines to show, leaving room for the pending line.
202
+ // Determine how many completed lines to show, leaving room for the pending line and pull progress.
203
const bool showPending = !m_pendingLine.empty();
204
+ const SHORT pullCount = static_cast<SHORT>(m_pullLines.size());
205
SHORT completedCount = static_cast<SHORT>(m_lines.size());
173
- if (showPending && completedCount >= c_maxDisplayLines)
206
+ const SHORT reservedLines = (showPending ? 1 : 0) + pullCount;
207
+ if (completedCount + reservedLines > c_maxDisplayLines)
208
{
175
- completedCount = c_maxDisplayLines - 1;
209
+ completedCount = std::max<SHORT>(0, c_maxDisplayLines - reservedLines);
210
}
177
- const SHORT displayCount = completedCount + (showPending ? 1 : 0);
211
+ const SHORT displayCount = completedCount + reservedLines;
212
213
// Build the entire frame in one buffer to minimize console writes. Hide the cursor
214
// during the redraw so the user doesn't see it bouncing through the cursor movement,
215
// then show it again at the final position. The dim attribute (\033[2m) renders the
216
// scrolling lines de-emphasized regardless of the user's theme.
183
- std::wstring buffer = L"\033[?25l\033[2m";
217
+ std::wstring buffer{c_escapeHideCursorDim};
218
219
// Move cursor to the start of the display area and erase from there to the end of
220
// the screen. \033[J handles the case where the new display is shorter than the
221
// previous one (e.g. when \r clears the pending line without a replacement).
222
if (m_displayedLines > 0)
223
{
190
- buffer += std::format(L"\033[{}A\033[J", m_displayedLines);
224
+ buffer += std::format(c_escapeMoveCursorUpAndClear, m_displayedLines);
225
}
226
227
auto appendLine = [&](const std::string& line) {
@@ -197,7 +231,7 @@ void BuildImageCallback::Redraw()
231
wline.resize(consoleWidth);
232
}
233
buffer += wline;
200
- buffer += L"\033[K\n";
234
+ buffer += c_escapeClearLineAndNewline;
235
};
236
237
// Print completed lines (skip older ones if we need room for the pending line).
@@ -217,7 +251,13 @@ void BuildImageCallback::Redraw()
251
appendLine(m_pendingLine);
252
}
253
220
- buffer += L"\033[22m\033[?25h";
254
+ // Render per-entry pull progress (each entry updates in place via the map).
255
+ for (const auto& [key, line] : m_pullLines)
256
+ {
257
+ appendLine(line);
258
+ }
259
+
260
+ buffer += c_escapeUndimShowCursor;
261
262
WriteTerminal(buffer);
263
m_displayedLines = displayCount;
src/windows/wslc/services/BuildImageCallback.h
+4
@@ -15,6 +15,7 @@ Abstract:
15
#include "ChangeTerminalMode.h"
16
#include "SessionService.h"
17
#include <deque>
18
+#include <map>
19
20
namespace wsl::windows::wslc::services {
21
class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback
@@ -35,6 +36,7 @@ private:
36
37
void CollapseWindow();
38
void Redraw();
39
+ 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;
@@ -54,6 +56,8 @@ private:
56
std::string m_pendingLine;
57
SHORT m_displayedLines = 0;
58
std::chrono::steady_clock::time_point m_lastRedraw{};
59
+ // Per-entry pull progress lines, keyed by entry id. Updated in place by Redraw. std::map so order is consistent.
60
+ std::map<std::string, std::string> m_pullLines;
61
// Captured at construction so the destructor can detect destruction during exception unwinding.
62
int m_uncaughtExceptions = std::uncaught_exceptions();
63
};
src/windows/wslc/services/ImageProgressCallback.cpp
+41
-6
@@ -84,23 +84,58 @@ std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id,
84
std::wstring line;
85
if (total != 0)
86
{
87
- line = std::format(L"{} '{}': {}%", status, id, current * 100 / total);
87
+ constexpr int c_progressBarWidth = 30;
88
+
89
+ int filled = 0;
90
+ if (current >= total)
91
+ {
92
+ filled = c_progressBarWidth;
93
+ }
94
+ else
95
+ {
96
+ auto ratio = static_cast<long double>(current) / static_cast<long double>(total);
97
+ filled = static_cast<int>(ratio * c_progressBarWidth);
98
+ }
99
+
100
+ filled = std::clamp(filled, 0, c_progressBarWidth);
101
+
102
+ std::wstring bar;
103
+ bar.reserve(c_progressBarWidth);
104
+ bar.append(filled, L'=');
105
+ bar.append(L">");
106
+ bar.resize(c_progressBarWidth, L' ');
107
+
108
+ line = std::format(
109
+ L"{}: {} [{}] {}/{}", id, status, bar, wsl::shared::string::FormatBytes(current), wsl::shared::string::FormatBytes(total));
110
}
111
else if (current != 0)
112
{
91
- line = std::format(L"{} '{}': {}s", status, id, current);
113
+ line = std::format(L"{}: {} {}s", id, status, current);
114
}
115
else
116
{
95
- line = std::format(L"{} '{}'", status, id);
117
+ line = std::format(L"{}: {}", id, status);
118
}
119
98
- // Erase any previously written char on that line.
99
- while (line.size() < info.dwSize.X)
120
+ // Use the visible window width (not the buffer width) to prevent wrapping.
121
+ const auto visibleWidth = std::max<SHORT>(0, info.srWindow.Right - info.srWindow.Left + 1);
122
+
123
+ // Truncate to console width to prevent wrapping that would break cursor repositioning.
124
+ if (line.size() > static_cast<size_t>(visibleWidth))
125
{
101
- line += L' ';
126
+ line.resize(visibleWidth);
127
+
128
+ // Avoid splitting a surrogate pair — if the last code unit is a high surrogate,
129
+ // drop it so we don't emit an invalid UTF-16 sequence.
130
+ if (!line.empty() && IS_HIGH_SURROGATE(line.back()))
131
+ {
132
+ line.pop_back();
133
+ }
134
}
135
136
+ // Erase any previously written char on that line.
137
+ line.resize(visibleWidth, L' ');
138
+
139
return line;
140
}
141
} // namespace wsl::windows::wslc::services
src/windows/wslcsession/WSLCSession.cpp
+17
-4
@@ -827,10 +827,10 @@ try
827
return " [" + name + "] ";
828
};
829
830
- auto reportProgress = [&](const std::string& message, const char* id = "") {
830
+ auto reportProgress = [&](const std::string& message, const char* id = "", ULONGLONG current = 0, ULONGLONG total = 0) {
831
if (ProgressCallback != nullptr)
832
{
833
- THROW_IF_FAILED(ProgressCallback->OnProgress(message.c_str(), id, 0, 0));
833
+ THROW_IF_FAILED(ProgressCallback->OnProgress(message.c_str(), id, current, total));
834
}
835
};
836
@@ -924,8 +924,21 @@ try
924
925
for (const auto& entry : status.statuses)
926
{
927
- if (auto it = digestToStageName.find(entry.vertex);
928
- it != digestToStageName.end() && !entry.id.empty() && reportedSteps.insert(entry.id).second)
927
+ auto it = digestToStageName.find(entry.vertex);
928
+ if (it == digestToStageName.end() || entry.id.empty())
929
+ {
930
+ continue;
931
+ }
932
+
933
+ if (entry.total > 0)
934
+ {
935
+ auto currentBytes = static_cast<ULONGLONG>(std::max<int64_t>(entry.current, 0));
936
+ auto totalBytes = static_cast<ULONGLONG>(std::max<int64_t>(entry.total, 0));
937
+ auto current = wsl::shared::string::FormatBytes(currentBytes);
938
+ auto total = wsl::shared::string::FormatBytes(totalBytes);
939
+ reportProgress(std::format("{}{} {} / {}", logPrefix(it->second), entry.id, current, total), entry.id.c_str(), currentBytes, totalBytes);
940
+ }
941
+ else if (reportedSteps.insert(entry.id).second)
942
{
943
flushLine();
944
reportProgress(logPrefix(it->second) + entry.id + "\n");