master
cpp 174 lines 5.64 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 ImageProgressCallback.cpp
8
9 Abstract:
10
11 This file contains the ImageProgressCallback Implementation.
12
13 --*/
14
15 #include "precomp.h"
16 #include "ImageProgressCallback.h"
17 #include "ImageService.h"
18 #include <format>
19
20 namespace wsl::windows::wslc::services {
21 using namespace wsl::shared;
22 using namespace wsl::windows::common::vt;
23 using wsl::windows::common::string::FormatHumanReadableSize;
24
25 constexpr uint32_t c_progressPrecision = 4;
26
27 auto ImageProgressCallback::MoveToLine(int line)
28 {
29 if (line > 0)
30 {
31 m_terminal.Write(m_level, L"{}", Cursor::Up(line));
32 }
33
34 // scope_exit is noexcept and may fire during unwinding; scope_exit_log swallows output
35 // failures so a throw here can't call std::terminate.
36 return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [line = line, this]() {
37 if (line > 1)
38 {
39 m_terminal.Write(m_level, L"{}", Cursor::Down(line - 1));
40 }
41 });
42 }
43
44 HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total)
45 {
46 try
47 {
48 // status is [unique] in the IDL, so it may be null; normalize before either path uses it.
49 status = (status != nullptr) ? status : "";
50
51 // The in-place progress display needs cursor movement, so when output is redirected fall
52 // back to a log stream: one line per new status, deduping the repeated byte-progress
53 // callbacks that share a status text.
54 if (!m_vtEnabled)
55 {
56 if (id == nullptr || *id == '\0')
57 {
58 m_terminal.Write(m_level, L"{}\n", status);
59 }
60 else
61 {
62 auto [it, inserted] = m_lastStatusById.try_emplace(id, status);
63 if (inserted || it->second != status)
64 {
65 it->second = status;
66 m_terminal.Write(m_level, L"{}: {}\n", id, status);
67 }
68 }
69
70 return S_OK;
71 }
72
73 // Hide the cursor while rendering so it doesn't bounce through the movements; scope_exit_log
74 // restores it on every exit path and can't call std::terminate during unwinding.
75 m_terminal.Write(m_level, L"{}", Cursor::Hide);
76 auto showCursor = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { m_terminal.Write(m_level, L"{}", Cursor::Show); });
77
78 if (id == nullptr || *id == '\0') // Print all 'global' statuses on their own line
79 {
80 m_terminal.Write(m_level, L"{}\n", status);
81 m_currentLine++;
82 return S_OK;
83 }
84
85 const int visibleWidth = m_terminal.GetConsoleWidth(m_level).value_or(c_fallbackConsoleWidth);
86
87 auto it = m_statuses.find(id);
88 if (it == m_statuses.end())
89 {
90 // If this is the first time we see this ID, create a new line for it.
91 m_statuses.emplace(id, m_currentLine);
92 m_terminal.Write(m_level, L"{}\n", GenerateStatusLine(status, id, current, total, visibleWidth));
93 m_currentLine++;
94 }
95 else
96 {
97 auto revert = MoveToLine(m_currentLine - it->second);
98 m_terminal.Write(m_level, L"{}\n", GenerateStatusLine(status, id, current, total, visibleWidth));
99 }
100
101 return S_OK;
102 }
103 CATCH_RETURN();
104 }
105
106 std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, int visibleWidth)
107 {
108 // status/id are [unique] in the IDL and may be null; treat null as empty before formatting.
109 const char* const safeStatus = (status != nullptr) ? status : "";
110 const char* const safeId = (id != nullptr) ? id : "";
111
112 std::wstring line;
113 if (total != 0)
114 {
115 constexpr int c_progressBarWidth = 30;
116
117 int filled = 0;
118 if (current >= total)
119 {
120 filled = c_progressBarWidth;
121 }
122 else
123 {
124 auto ratio = static_cast<long double>(current) / static_cast<long double>(total);
125 filled = static_cast<int>(ratio * c_progressBarWidth);
126 }
127
128 filled = std::clamp(filled, 0, c_progressBarWidth);
129
130 std::wstring bar;
131 bar.reserve(c_progressBarWidth);
132 bar.append(filled, L'=');
133 bar.append(L">");
134 bar.resize(c_progressBarWidth, L' ');
135
136 // Docker's reported total is an estimate of the compressed layer size, so the actual bytes
137 // transferred can exceed it. Drop the total in that case to avoid displaying a count over 100%.
138 auto progress = FormatHumanReadableSize(current, c_progressPrecision);
139
140 if (current <= total)
141 {
142 progress += std::format(L"/{}", FormatHumanReadableSize(total, c_progressPrecision));
143 }
144
145 line = std::format(L"{}: {} [{}] {}", safeId, safeStatus, bar, progress);
146 }
147 else if (current != 0)
148 {
149 line = std::format(L"{}: {} {}", safeId, safeStatus, FormatHumanReadableSize(current, c_progressPrecision));
150 }
151 else
152 {
153 line = std::format(L"{}: {}", safeId, safeStatus);
154 }
155
156 // Truncate to the console width to prevent wrapping that breaks cursor repositioning, then pad
157 // to erase any previously written characters on the line.
158 if (line.size() > static_cast<size_t>(visibleWidth))
159 {
160 line.resize(visibleWidth);
161
162 // Avoid splitting a surrogate pair — if the last code unit is a high surrogate,
163 // drop it so we don't emit an invalid UTF-16 sequence.
164 if (!line.empty() && IS_HIGH_SURROGATE(line.back()))
165 {
166 line.pop_back();
167 }
168 }
169
170 line.resize(visibleWidth, L' ');
171
172 return line;
173 }
174 } // namespace wsl::windows::wslc::services