master
cpp 287 lines 9.42 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 BuildImageCallback.cpp
8
9 Abstract:
10
11 This file contains the BuildImageCallback Implementation.
12
13 --*/
14
15 #include "precomp.h"
16 #include "BuildImageCallback.h"
17
18 namespace wsl::windows::wslc::services {
19
20 using wsl::windows::common::string::MultiByteToWide;
21 using namespace wsl::windows::common::vt;
22
23 BuildImageCallback::~BuildImageCallback()
24 try
25 {
26 // Capture any partial line so it's included in the error replay below; otherwise
27 // CollapseWindow() would discard it.
28 if (!m_pendingLine.empty())
29 {
30 m_pendingLine += '\n';
31 m_allLines.push_back(std::move(m_pendingLine));
32 }
33
34 CollapseWindow();
35
36 // On build error (not cancellation), replay the full log output so the user can see what went wrong.
37 if (!IsCancelled() && std::uncaught_exceptions() > m_uncaughtExceptions && !m_allLines.empty())
38 {
39 for (const auto& line : m_allLines)
40 {
41 m_terminal.Info(L"{}", line);
42 }
43 }
44 }
45 CATCH_LOG()
46
47 bool BuildImageCallback::IsCancelled() const
48 {
49 return WaitForSingleObject(m_cancelEvent, 0) == WAIT_OBJECT_0;
50 }
51
52 const wsl::windows::common::vt::Sequence& BuildImageCallback::Color(const wsl::windows::common::vt::Sequence& sequence) const
53 {
54 static const wsl::windows::common::vt::Sequence empty{};
55 return m_color ? sequence : empty;
56 }
57
58 void BuildImageCallback::CaptureForReplay(std::string_view text)
59 {
60 m_allLines.emplace_back(text);
61 m_allLinesBytes += m_allLines.back().size();
62 while (m_allLinesBytes > c_maxAllLinesBytes && !m_allLines.empty())
63 {
64 m_allLinesBytes -= m_allLines.front().size();
65 m_allLines.pop_front();
66 }
67 }
68
69 void BuildImageCallback::CollapseWindow()
70 {
71 if (m_displayedLines > 0)
72 {
73 // Move cursor up to the start of the display area, then erase to end of screen.
74 m_terminal.Info(L"{}{}", Cursor::Up(m_displayedLines), Erase::ScreenForward);
75 m_displayedLines = 0;
76 }
77
78 m_lines.clear();
79 m_pendingLine.clear();
80 m_pullLines.clear();
81 }
82
83 void BuildImageCallback::RedrawIfNeeded()
84 {
85 auto now = std::chrono::steady_clock::now();
86 if (now - m_lastRedraw >= c_redrawInterval)
87 {
88 Redraw();
89 m_lastRedraw = now;
90 }
91 }
92
93 HRESULT BuildImageCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total)
94 try
95 {
96 if (status == nullptr || *status == '\0')
97 {
98 return S_OK;
99 }
100
101 // When cancellation is pending, skip all processing so the server's IO loop can
102 // return to its event wait and detect the cancel event promptly.
103 if (IsCancelled())
104 {
105 return S_OK;
106 }
107
108 const std::string_view idView = (id != nullptr) ? id : std::string_view{};
109 const bool isLog = (idView == "log");
110 const bool isPullProgress = (!idView.empty() && total > 0 && !isLog);
111
112 // quiet: suppress live progress but retain everything plain would have printed, so the destructor
113 // can replay the failing step and its logs on build failure. Pull progress is excluded because it
114 // is rewritten in place rather than appended, and so is the only message with no trailing newline.
115 if (m_mode == models::ProgressMode::Quiet)
116 {
117 if (!isPullProgress)
118 {
119 CaptureForReplay(status);
120 }
121 return S_OK;
122 }
123
124 if (m_verbose || !m_renderInPlace)
125 {
126 // Only major steps are reported here. Unlike docker's plain output, which appends
127 // throttled download lines, pull progress is omitted entirely: without in-place
128 // updates those lines are mostly noise.
129 if (!isPullProgress)
130 {
131 m_terminal.Info(L"{}", status);
132 }
133 return S_OK;
134 }
135
136 // Pull/download progress: update the per-entry map so Redraw can show each entry
137 // on a single line that updates in place.
138 if (isPullProgress)
139 {
140 m_pullLines[id] = status;
141 RedrawIfNeeded();
142
143 return S_OK;
144 }
145
146 if (isLog)
147 {
148 // Log line: add to the scrolling window.
149 for (const char* p = status; *p != '\0'; ++p)
150 {
151 if (*p == '\n')
152 {
153 // Store with the trailing newline so the byte count matches what is replayed.
154 // Cap retained log output to avoid unbounded growth on very long builds.
155 m_allLines.push_back(m_pendingLine + '\n');
156 m_allLinesBytes += m_allLines.back().size();
157 while (m_allLinesBytes > c_maxAllLinesBytes && !m_allLines.empty())
158 {
159 m_allLinesBytes -= m_allLines.front().size();
160 m_allLines.pop_front();
161 }
162
163 m_lines.push_back(std::move(m_pendingLine));
164 m_pendingLine.clear();
165 if (m_lines.size() > c_maxDisplayLines)
166 {
167 m_lines.pop_front();
168 }
169 }
170 else if (*p == '\r')
171 {
172 // \r\n is a line ending; standalone \r overwrites the current line.
173 if (*(p + 1) != '\n')
174 {
175 // Flush a throttled redraw before clearing so \r-based progress
176 // updates are visible even when batched in a single OnProgress call.
177 if (!m_pendingLine.empty())
178 {
179 RedrawIfNeeded();
180 }
181 m_pendingLine.clear();
182 }
183 }
184 else
185 {
186 m_pendingLine += *p;
187 }
188 }
189
190 // Throttle redraws to avoid blocking the server's IO loop with console writes
191 // during rapid output. Lines accumulate in the deque immediately; the display
192 // catches up at ~20fps.
193 RedrawIfNeeded();
194
195 return S_OK;
196 }
197
198 // Else is a build step
199 CollapseWindow();
200 auto wide = MultiByteToWide(status);
201 const auto bodyLength = wide.find_last_not_of(L"\r\n") + 1;
202 const auto newlines = wide.substr(bodyLength);
203 wide.resize(bodyLength);
204
205 // Pass the color sequences as arguments (not baked into the string) so Terminal strips
206 // them when --no-color is set. Color() additionally strips them outside Tty mode. The
207 // trailing newlines are emitted after the reset.
208 m_terminal.Info(L"{}{}{}{}", Color(Format::Fg::BrightGreen), wide, Color(Format::Default), newlines);
209 return S_OK;
210 }
211 CATCH_RETURN();
212
213 void BuildImageCallback::Redraw()
214 {
215 const int consoleWidth = m_terminal.GetConsoleWidth(Terminal::Level::Info).value_or(c_fallbackConsoleWidth);
216
217 const bool showPending = !m_pendingLine.empty();
218 const int pullCount = static_cast<int>(m_pullLines.size());
219 int completedCount = static_cast<int>(m_lines.size());
220 const int reservedLines = (showPending ? 1 : 0) + pullCount;
221 if (completedCount + reservedLines > c_maxDisplayLines)
222 {
223 completedCount = std::max(0, c_maxDisplayLines - reservedLines);
224 }
225 const int displayCount = completedCount + reservedLines;
226
227 // Build the frame body in one buffer to minimize console writes. The cursor moves,
228 // erases, and text lines it holds are non-color VT. This only runs in Tty mode, where a
229 // VT console is attached. The cursor hide/show wrapper and the dim intensity attribute are
230 // passed as Sequence arguments to Terminal (below) so it strips the color ones (Dim/Normal)
231 // when --no-color is set, while leaving the non-color cursor moves intact.
232 //
233 // m_frameBuffer is a member so its backing allocation is reused across frames -
234 // it grows to the high-water mark and is never freed between redraws.
235 m_frameBuffer.clear();
236
237 // Move cursor to the start of the display area and erase from there to the end of
238 // the screen. \033[J handles the case where the new display is shorter than the
239 // previous one (e.g. when \r clears the pending line without a replacement).
240 if (m_displayedLines > 0)
241 {
242 m_frameBuffer += Cursor::Up(m_displayedLines);
243 m_frameBuffer += Erase::ScreenForward;
244 }
245
246 auto appendLine = [&](const std::string& line) {
247 auto wline = MultiByteToWide(line);
248 if (wline.size() > static_cast<size_t>(consoleWidth))
249 {
250 wline.resize(static_cast<size_t>(consoleWidth));
251 }
252 m_frameBuffer += std::move(wline);
253 m_frameBuffer += Erase::LineForward;
254 m_frameBuffer += L'\n';
255 };
256
257 // Print completed lines (skip older ones if we need room for the pending line).
258 auto it = m_lines.begin();
259 if (completedCount < static_cast<int>(m_lines.size()))
260 {
261 std::advance(it, m_lines.size() - completedCount);
262 }
263 for (; it != m_lines.end(); ++it)
264 {
265 appendLine(*it);
266 }
267
268 // Print the in-progress line (e.g. \r-based progress updates).
269 if (showPending)
270 {
271 appendLine(m_pendingLine);
272 }
273
274 // Render per-entry pull progress (each entry updates in place via the map).
275 for (const auto& [key, line] : m_pullLines)
276 {
277 appendLine(line);
278 }
279
280 // Emit the frame as a single atomic write. Cursor Hide/Show are non-color and always
281 // rendered here (VT is on); Format::Dim/Normal are color sequences that Terminal strips
282 // under --no-color. The buffered body carries the cursor moves, erases, and text lines.
283 m_terminal.Info(L"{}{}{}{}{}", Cursor::Hide, Color(Format::Dim), std::wstring_view{m_frameBuffer}, Color(Format::Normal), Cursor::Show);
284 m_displayedLines = displayCount;
285 }
286
287 } // namespace wsl::windows::wslc::services