| 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 |