1 #include "progress-bar.hh"
2 #include "util.hh"
3 #include "sync.hh"
4 #include "store-api.hh"
5 #include "names.hh"
6
7 #include <atomic>
8 #include <map>
9 #include <thread>
10
11 namespace nix {
12
getS(const std::vector<Logger::Field> & fields,size_t n)13 static std::string getS(const std::vector<Logger::Field> & fields, size_t n)
14 {
15 assert(n < fields.size());
16 assert(fields[n].type == Logger::Field::tString);
17 return fields[n].s;
18 }
19
getI(const std::vector<Logger::Field> & fields,size_t n)20 static uint64_t getI(const std::vector<Logger::Field> & fields, size_t n)
21 {
22 assert(n < fields.size());
23 assert(fields[n].type == Logger::Field::tInt);
24 return fields[n].i;
25 }
26
27 class ProgressBar : public Logger
28 {
29 private:
30
31 struct ActInfo
32 {
33 std::string s, lastLine, phase;
34 ActivityType type = actUnknown;
35 uint64_t done = 0;
36 uint64_t expected = 0;
37 uint64_t running = 0;
38 uint64_t failed = 0;
39 std::map<ActivityType, uint64_t> expectedByType;
40 bool visible = true;
41 ActivityId parent;
42 std::optional<std::string> name;
43 };
44
45 struct ActivitiesByType
46 {
47 std::map<ActivityId, std::list<ActInfo>::iterator> its;
48 uint64_t done = 0;
49 uint64_t expected = 0;
50 uint64_t failed = 0;
51 };
52
53 struct State
54 {
55 std::list<ActInfo> activities;
56 std::map<ActivityId, std::list<ActInfo>::iterator> its;
57
58 std::map<ActivityType, ActivitiesByType> activitiesByType;
59
60 uint64_t filesLinked = 0, bytesLinked = 0;
61
62 uint64_t corruptedPaths = 0, untrustedPaths = 0;
63
64 bool active = true;
65 bool haveUpdate = true;
66 };
67
68 Sync<State> state_;
69
70 std::thread updateThread;
71
72 std::condition_variable quitCV, updateCV;
73
74 bool printBuildLogs;
75 bool isTTY;
76
77 public:
78
ProgressBar(bool printBuildLogs,bool isTTY)79 ProgressBar(bool printBuildLogs, bool isTTY)
80 : printBuildLogs(printBuildLogs)
81 , isTTY(isTTY)
82 {
83 state_.lock()->active = isTTY;
84 updateThread = std::thread([&]() {
85 auto state(state_.lock());
86 while (state->active) {
87 if (!state->haveUpdate)
88 state.wait(updateCV);
89 draw(*state);
90 state.wait_for(quitCV, std::chrono::milliseconds(50));
91 }
92 });
93 }
94
~ProgressBar()95 ~ProgressBar()
96 {
97 stop();
98 updateThread.join();
99 }
100
stop()101 void stop()
102 {
103 auto state(state_.lock());
104 if (!state->active) return;
105 state->active = false;
106 std::string status = getStatus(*state);
107 writeToStderr("\r\e[K");
108 if (status != "")
109 writeToStderr("[" + status + "]\n");
110 updateCV.notify_one();
111 quitCV.notify_one();
112 }
113
log(Verbosity lvl,const FormatOrString & fs)114 void log(Verbosity lvl, const FormatOrString & fs) override
115 {
116 auto state(state_.lock());
117 log(*state, lvl, fs.s);
118 }
119
log(State & state,Verbosity lvl,const std::string & s)120 void log(State & state, Verbosity lvl, const std::string & s)
121 {
122 if (state.active) {
123 writeToStderr("\r\e[K" + filterANSIEscapes(s, !isTTY) + ANSI_NORMAL "\n");
124 draw(state);
125 } else {
126 auto s2 = s + ANSI_NORMAL "\n";
127 if (!isTTY) s2 = filterANSIEscapes(s2, true);
128 writeToStderr(s2);
129 }
130 }
131
startActivity(ActivityId act,Verbosity lvl,ActivityType type,const std::string & s,const Fields & fields,ActivityId parent)132 void startActivity(ActivityId act, Verbosity lvl, ActivityType type,
133 const std::string & s, const Fields & fields, ActivityId parent) override
134 {
135 auto state(state_.lock());
136
137 if (lvl <= verbosity && !s.empty())
138 log(*state, lvl, s + "...");
139
140 state->activities.emplace_back(ActInfo());
141 auto i = std::prev(state->activities.end());
142 i->s = s;
143 i->type = type;
144 i->parent = parent;
145 state->its.emplace(act, i);
146 state->activitiesByType[type].its.emplace(act, i);
147
148 if (type == actBuild) {
149 auto name = storePathToName(getS(fields, 0));
150 if (hasSuffix(name, ".drv"))
151 name.resize(name.size() - 4);
152 i->s = fmt("building " ANSI_BOLD "%s" ANSI_NORMAL, name);
153 auto machineName = getS(fields, 1);
154 if (machineName != "")
155 i->s += fmt(" on " ANSI_BOLD "%s" ANSI_NORMAL, machineName);
156 auto curRound = getI(fields, 2);
157 auto nrRounds = getI(fields, 3);
158 if (nrRounds != 1)
159 i->s += fmt(" (round %d/%d)", curRound, nrRounds);
160 i->name = DrvName(name).name;
161 }
162
163 if (type == actSubstitute) {
164 auto name = storePathToName(getS(fields, 0));
165 auto sub = getS(fields, 1);
166 i->s = fmt(
167 hasPrefix(sub, "local")
168 ? "copying " ANSI_BOLD "%s" ANSI_NORMAL " from %s"
169 : "fetching " ANSI_BOLD "%s" ANSI_NORMAL " from %s",
170 name, sub);
171 }
172
173 if (type == actPostBuildHook) {
174 auto name = storePathToName(getS(fields, 0));
175 if (hasSuffix(name, ".drv"))
176 name.resize(name.size() - 4);
177 i->s = fmt("post-build " ANSI_BOLD "%s" ANSI_NORMAL, name);
178 i->name = DrvName(name).name;
179 }
180
181 if (type == actQueryPathInfo) {
182 auto name = storePathToName(getS(fields, 0));
183 i->s = fmt("querying " ANSI_BOLD "%s" ANSI_NORMAL " on %s", name, getS(fields, 1));
184 }
185
186 if ((type == actDownload && hasAncestor(*state, actCopyPath, parent))
187 || (type == actDownload && hasAncestor(*state, actQueryPathInfo, parent))
188 || (type == actCopyPath && hasAncestor(*state, actSubstitute, parent)))
189 i->visible = false;
190
191 update(*state);
192 }
193
194 /* Check whether an activity has an ancestore with the specified
195 type. */
hasAncestor(State & state,ActivityType type,ActivityId act)196 bool hasAncestor(State & state, ActivityType type, ActivityId act)
197 {
198 while (act != 0) {
199 auto i = state.its.find(act);
200 if (i == state.its.end()) break;
201 if (i->second->type == type) return true;
202 act = i->second->parent;
203 }
204 return false;
205 }
206
stopActivity(ActivityId act)207 void stopActivity(ActivityId act) override
208 {
209 auto state(state_.lock());
210
211 auto i = state->its.find(act);
212 if (i != state->its.end()) {
213
214 auto & actByType = state->activitiesByType[i->second->type];
215 actByType.done += i->second->done;
216 actByType.failed += i->second->failed;
217
218 for (auto & j : i->second->expectedByType)
219 state->activitiesByType[j.first].expected -= j.second;
220
221 actByType.its.erase(act);
222 state->activities.erase(i->second);
223 state->its.erase(i);
224 }
225
226 update(*state);
227 }
228
result(ActivityId act,ResultType type,const std::vector<Field> & fields)229 void result(ActivityId act, ResultType type, const std::vector<Field> & fields) override
230 {
231 auto state(state_.lock());
232
233 if (type == resFileLinked) {
234 state->filesLinked++;
235 state->bytesLinked += getI(fields, 0);
236 update(*state);
237 }
238
239 else if (type == resBuildLogLine || type == resPostBuildLogLine) {
240 auto lastLine = trim(getS(fields, 0));
241 if (!lastLine.empty()) {
242 auto i = state->its.find(act);
243 assert(i != state->its.end());
244 ActInfo info = *i->second;
245 if (printBuildLogs) {
246 auto suffix = "> ";
247 if (type == resPostBuildLogLine) {
248 suffix = " (post)> ";
249 }
250 log(*state, lvlInfo, ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine);
251 } else {
252 state->activities.erase(i->second);
253 info.lastLine = lastLine;
254 state->activities.emplace_back(info);
255 i->second = std::prev(state->activities.end());
256 update(*state);
257 }
258 }
259 }
260
261 else if (type == resUntrustedPath) {
262 state->untrustedPaths++;
263 update(*state);
264 }
265
266 else if (type == resCorruptedPath) {
267 state->corruptedPaths++;
268 update(*state);
269 }
270
271 else if (type == resSetPhase) {
272 auto i = state->its.find(act);
273 assert(i != state->its.end());
274 i->second->phase = getS(fields, 0);
275 update(*state);
276 }
277
278 else if (type == resProgress) {
279 auto i = state->its.find(act);
280 assert(i != state->its.end());
281 ActInfo & actInfo = *i->second;
282 actInfo.done = getI(fields, 0);
283 actInfo.expected = getI(fields, 1);
284 actInfo.running = getI(fields, 2);
285 actInfo.failed = getI(fields, 3);
286 update(*state);
287 }
288
289 else if (type == resSetExpected) {
290 auto i = state->its.find(act);
291 assert(i != state->its.end());
292 ActInfo & actInfo = *i->second;
293 auto type = (ActivityType) getI(fields, 0);
294 auto & j = actInfo.expectedByType[type];
295 state->activitiesByType[type].expected -= j;
296 j = getI(fields, 1);
297 state->activitiesByType[type].expected += j;
298 update(*state);
299 }
300 }
301
update(State & state)302 void update(State & state)
303 {
304 state.haveUpdate = true;
305 updateCV.notify_one();
306 }
307
draw(State & state)308 void draw(State & state)
309 {
310 state.haveUpdate = false;
311 if (!state.active) return;
312
313 std::string line;
314
315 std::string status = getStatus(state);
316 if (!status.empty()) {
317 line += '[';
318 line += status;
319 line += "]";
320 }
321
322 if (!state.activities.empty()) {
323 if (!status.empty()) line += " ";
324 auto i = state.activities.rbegin();
325
326 while (i != state.activities.rend() && (!i->visible || (i->s.empty() && i->lastLine.empty())))
327 ++i;
328
329 if (i != state.activities.rend()) {
330 line += i->s;
331 if (!i->phase.empty()) {
332 line += " (";
333 line += i->phase;
334 line += ")";
335 }
336 if (!i->lastLine.empty()) {
337 if (!i->s.empty()) line += ": ";
338 line += i->lastLine;
339 }
340 }
341 }
342
343 auto width = getWindowSize().second;
344 if (width <= 0) width = std::numeric_limits<decltype(width)>::max();
345
346 writeToStderr("\r" + filterANSIEscapes(line, false, width) + "\e[K");
347 }
348
getStatus(State & state)349 std::string getStatus(State & state)
350 {
351 auto MiB = 1024.0 * 1024.0;
352
353 std::string res;
354
355 auto renderActivity = [&](ActivityType type, const std::string & itemFmt, const std::string & numberFmt = "%d", double unit = 1) {
356 auto & act = state.activitiesByType[type];
357 uint64_t done = act.done, expected = act.done, running = 0, failed = act.failed;
358 for (auto & j : act.its) {
359 done += j.second->done;
360 expected += j.second->expected;
361 running += j.second->running;
362 failed += j.second->failed;
363 }
364
365 expected = std::max(expected, act.expected);
366
367 std::string s;
368
369 if (running || done || expected || failed) {
370 if (running)
371 if (expected != 0)
372 s = fmt(ANSI_BLUE + numberFmt + ANSI_NORMAL "/" ANSI_GREEN + numberFmt + ANSI_NORMAL "/" + numberFmt,
373 running / unit, done / unit, expected / unit);
374 else
375 s = fmt(ANSI_BLUE + numberFmt + ANSI_NORMAL "/" ANSI_GREEN + numberFmt + ANSI_NORMAL,
376 running / unit, done / unit);
377 else if (expected != done)
378 if (expected != 0)
379 s = fmt(ANSI_GREEN + numberFmt + ANSI_NORMAL "/" + numberFmt,
380 done / unit, expected / unit);
381 else
382 s = fmt(ANSI_GREEN + numberFmt + ANSI_NORMAL, done / unit);
383 else
384 s = fmt(done ? ANSI_GREEN + numberFmt + ANSI_NORMAL : numberFmt, done / unit);
385 s = fmt(itemFmt, s);
386
387 if (failed)
388 s += fmt(" (" ANSI_RED "%d failed" ANSI_NORMAL ")", failed / unit);
389 }
390
391 return s;
392 };
393
394 auto showActivity = [&](ActivityType type, const std::string & itemFmt, const std::string & numberFmt = "%d", double unit = 1) {
395 auto s = renderActivity(type, itemFmt, numberFmt, unit);
396 if (s.empty()) return;
397 if (!res.empty()) res += ", ";
398 res += s;
399 };
400
401 showActivity(actBuilds, "%s built");
402
403 auto s1 = renderActivity(actCopyPaths, "%s copied");
404 auto s2 = renderActivity(actCopyPath, "%s MiB", "%.1f", MiB);
405
406 if (!s1.empty() || !s2.empty()) {
407 if (!res.empty()) res += ", ";
408 if (s1.empty()) res += "0 copied"; else res += s1;
409 if (!s2.empty()) { res += " ("; res += s2; res += ')'; }
410 }
411
412 showActivity(actDownload, "%s MiB DL", "%.1f", MiB);
413
414 {
415 auto s = renderActivity(actOptimiseStore, "%s paths optimised");
416 if (s != "") {
417 s += fmt(", %.1f MiB / %d inodes freed", state.bytesLinked / MiB, state.filesLinked);
418 if (!res.empty()) res += ", ";
419 res += s;
420 }
421 }
422
423 // FIXME: don't show "done" paths in green.
424 showActivity(actVerifyPaths, "%s paths verified");
425
426 if (state.corruptedPaths) {
427 if (!res.empty()) res += ", ";
428 res += fmt(ANSI_RED "%d corrupted" ANSI_NORMAL, state.corruptedPaths);
429 }
430
431 if (state.untrustedPaths) {
432 if (!res.empty()) res += ", ";
433 res += fmt(ANSI_RED "%d untrusted" ANSI_NORMAL, state.untrustedPaths);
434 }
435
436 return res;
437 }
438 };
439
startProgressBar(bool printBuildLogs)440 void startProgressBar(bool printBuildLogs)
441 {
442 logger = new ProgressBar(
443 printBuildLogs,
444 isatty(STDERR_FILENO) && getEnv("TERM", "dumb") != "dumb");
445 }
446
stopProgressBar()447 void stopProgressBar()
448 {
449 auto progressBar = dynamic_cast<ProgressBar *>(logger);
450 if (progressBar) progressBar->stop();
451
452 }
453
454 }
455