currently all loggers can always accept messages and never suggest flushing buffers. in the future this may change, and at that point we're already fully set up for it. local loggers should never keep asynchronous (i.e. network-backed) buffers, disk buffers are fine. networked loggers will require buffers and periodic flushes later. Change-Id: Ide2114f5bc17f4a1d289c92ed4f9381a1d59dacf
593 lines
18 KiB
C++
593 lines
18 KiB
C++
#include "lix/libmain/progress-bar.hh"
|
|
#include "lix/libutil/file-system.hh"
|
|
#include "lix/libutil/sync.hh"
|
|
#include "lix/libstore/names.hh"
|
|
#include "lix/libutil/terminal.hh"
|
|
#include "lix/libutil/strings.hh"
|
|
#include "lix/libutil/thread-name.hh"
|
|
|
|
#include <map>
|
|
#include <thread>
|
|
#include <sstream>
|
|
#include <iostream>
|
|
#include <chrono>
|
|
|
|
namespace nix {
|
|
|
|
// 100 years ought to be enough for anyone (yet sufficiently smaller than max() to not cause signed integer overflow).
|
|
constexpr const auto A_LONG_TIME = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
100 * 365 * std::chrono::seconds(86400)
|
|
);
|
|
|
|
using namespace std::literals::chrono_literals;
|
|
|
|
static std::string_view getS(const std::vector<Logger::Field> & fields, size_t n)
|
|
{
|
|
assert(n < fields.size());
|
|
assert(fields[n].type == Logger::Field::tString);
|
|
return fields[n].s;
|
|
}
|
|
|
|
static uint64_t getI(const std::vector<Logger::Field> & fields, size_t n)
|
|
{
|
|
assert(n < fields.size());
|
|
assert(fields[n].type == Logger::Field::tInt);
|
|
return fields[n].i;
|
|
}
|
|
|
|
static std::string_view storePathToName(std::string_view path)
|
|
{
|
|
auto base = baseNameOf(path);
|
|
auto i = base.find('-');
|
|
return i == std::string::npos ? base.substr(0, 0) : base.substr(i + 1);
|
|
}
|
|
|
|
ProgressBar::ProgressBar(bool isTTY)
|
|
: isTTY(isTTY)
|
|
{
|
|
resume();
|
|
}
|
|
|
|
ProgressBar::~ProgressBar()
|
|
{
|
|
pause();
|
|
}
|
|
|
|
void ProgressBar::pause()
|
|
{
|
|
if (!isTTY) return;
|
|
{
|
|
auto state(state_.lock());
|
|
state->paused++;
|
|
if (state->paused > 1) return; // recursive pause, the update thread is already gone
|
|
updateCV.notify_one();
|
|
quitCV.notify_one();
|
|
}
|
|
updateThread.join();
|
|
}
|
|
|
|
void ProgressBar::resetProgress()
|
|
{
|
|
auto state(state_.lock());
|
|
auto prevPaused = state->paused;
|
|
*state = ProgressBar::State {
|
|
.paused = prevPaused,
|
|
};
|
|
update(*state);
|
|
}
|
|
|
|
void ProgressBar::resume()
|
|
{
|
|
if (!isTTY) return;
|
|
auto state(state_.lock());
|
|
assert(state->paused > 0); // should be paused
|
|
state->paused--;
|
|
if (state->paused > 0) return; // recursive pause, wait for the parents to resume too
|
|
state->haveUpdate = true;
|
|
updateThread = std::thread([&]() {
|
|
setCurrentThreadName("progress bar");
|
|
auto state(state_.lock());
|
|
auto nextWakeup = A_LONG_TIME;
|
|
while (state->paused == 0) {
|
|
if (!state->haveUpdate)
|
|
state.wait_for(updateCV, nextWakeup);
|
|
eraseProgressDisplay(*state);
|
|
nextWakeup = restoreProgressDisplay(*state);
|
|
state.wait_for(quitCV, std::chrono::milliseconds(50));
|
|
}
|
|
eraseProgressDisplay(*state);
|
|
});
|
|
}
|
|
|
|
bool ProgressBar::isVerbose()
|
|
{
|
|
return printBuildLogs;
|
|
}
|
|
|
|
Logger::BufferState ProgressBar::log(Verbosity lvl, std::string_view s)
|
|
{
|
|
if (lvl > verbosity) {
|
|
return BufferState::HasSpace;
|
|
}
|
|
auto state(state_.lock());
|
|
return log(*state, lvl, s);
|
|
}
|
|
|
|
Logger::BufferState ProgressBar::logEI(const ErrorInfo & ei)
|
|
{
|
|
auto state(state_.lock());
|
|
|
|
std::stringstream oss;
|
|
showErrorInfo(oss, ei, loggerSettings.showTrace.get());
|
|
|
|
return log(*state, ei.level, oss.str());
|
|
}
|
|
|
|
Logger::BufferState ProgressBar::log(State & state, Verbosity lvl, std::string_view s)
|
|
{
|
|
if (state.paused == 0) eraseProgressDisplay(state);
|
|
writeLogsToStderr(filterANSIEscapes(s + ANSI_NORMAL "\n", !isTTY));
|
|
restoreProgressDisplay(state);
|
|
return BufferState::HasSpace;
|
|
}
|
|
|
|
Logger::BufferState ProgressBar::startActivityImpl(
|
|
ActivityId act,
|
|
Verbosity lvl,
|
|
ActivityType type,
|
|
const std::string & s,
|
|
const Fields & fields,
|
|
ActivityId parent
|
|
)
|
|
{
|
|
auto state(state_.lock());
|
|
|
|
if (lvl <= verbosity && !s.empty() && type != actBuildWaiting)
|
|
(void) log(*state, lvl, s + "...");
|
|
|
|
state->activities.emplace_back(ActInfo {
|
|
.s = s,
|
|
.type = type,
|
|
.parent = parent,
|
|
.startTime = std::chrono::steady_clock::now()
|
|
});
|
|
auto i = std::prev(state->activities.end());
|
|
state->its.emplace(act, i);
|
|
state->activitiesByType[type].its.emplace(act, i);
|
|
|
|
if (type == actBuild) {
|
|
std::string name(storePathToName(getS(fields, 0)));
|
|
if (name.ends_with(".drv"))
|
|
name = name.substr(0, name.size() - 4);
|
|
i->s = fmt("building " ANSI_BOLD "%s" ANSI_NORMAL, name);
|
|
auto machineName = getS(fields, 1);
|
|
if (machineName != "")
|
|
i->s += fmt(" on " ANSI_BOLD "%s" ANSI_NORMAL, machineName);
|
|
|
|
// Used to be curRound and nrRounds, but the
|
|
// implementation was broken for a long time.
|
|
if (getI(fields, 2) != 1 || getI(fields, 3) != 1) {
|
|
throw Error("log message indicated repeating builds, but this is not currently implemented");
|
|
}
|
|
i->name = DrvName(name).name;
|
|
}
|
|
|
|
if (type == actSubstitute) {
|
|
auto name = storePathToName(getS(fields, 0));
|
|
auto sub = getS(fields, 1);
|
|
i->s = fmt(
|
|
sub.starts_with("local")
|
|
? "copying " ANSI_BOLD "%s" ANSI_NORMAL " from %s"
|
|
: "fetching " ANSI_BOLD "%s" ANSI_NORMAL " from %s",
|
|
name, sub);
|
|
}
|
|
|
|
if (type == actPostBuildHook) {
|
|
auto name = storePathToName(getS(fields, 0));
|
|
if (name.ends_with(".drv"))
|
|
name = name.substr(0, name.size() - 4);
|
|
i->s = fmt("post-build " ANSI_BOLD "%s" ANSI_NORMAL, name);
|
|
i->name = DrvName(name).name;
|
|
}
|
|
|
|
if (type == actQueryPathInfo) {
|
|
auto name = storePathToName(getS(fields, 0));
|
|
i->s = fmt("querying " ANSI_BOLD "%s" ANSI_NORMAL " on %s", name, getS(fields, 1));
|
|
}
|
|
|
|
if ((type == actFileTransfer && hasAncestor(*state, actCopyPath, parent))
|
|
|| (type == actFileTransfer && hasAncestor(*state, actQueryPathInfo, parent))
|
|
|| (type == actCopyPath && hasAncestor(*state, actSubstitute, parent)))
|
|
i->visible = false;
|
|
|
|
update(*state);
|
|
return BufferState::HasSpace;
|
|
}
|
|
|
|
/* Check whether an activity has an ancestore with the specified
|
|
type. */
|
|
bool ProgressBar::hasAncestor(State & state, ActivityType type, ActivityId act)
|
|
{
|
|
while (act != 0) {
|
|
auto i = state.its.find(act);
|
|
if (i == state.its.end()) break;
|
|
if (i->second->type == type) return true;
|
|
act = i->second->parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Logger::BufferState ProgressBar::stopActivityImpl(ActivityId act)
|
|
{
|
|
auto state(state_.lock());
|
|
|
|
auto i = state->its.find(act);
|
|
if (i != state->its.end()) {
|
|
|
|
auto & actByType = state->activitiesByType[i->second->type];
|
|
actByType.done += i->second->done;
|
|
actByType.failed += i->second->failed;
|
|
|
|
for (auto & j : i->second->expectedByType)
|
|
state->activitiesByType[j.first].expected -= j.second;
|
|
|
|
actByType.its.erase(act);
|
|
state->activities.erase(i->second);
|
|
state->its.erase(i);
|
|
}
|
|
|
|
update(*state);
|
|
return BufferState::HasSpace;
|
|
}
|
|
|
|
Logger::BufferState
|
|
ProgressBar::resultImpl(ActivityId act, ResultType type, const std::vector<Field> & fields)
|
|
{
|
|
auto state(state_.lock());
|
|
|
|
if (type == resFileLinked) {
|
|
state->filesLinked++;
|
|
state->bytesLinked += getI(fields, 0);
|
|
update(*state);
|
|
}
|
|
|
|
else if (type == resBuildLogLine || type == resPostBuildLogLine) {
|
|
auto lastLine = chomp(getS(fields, 0));
|
|
if (!lastLine.empty()) {
|
|
auto i = state->its.find(act);
|
|
assert(i != state->its.end());
|
|
ActInfo info = *i->second;
|
|
if (printBuildLogs || type == resPostBuildLogLine) {
|
|
auto suffix = "> ";
|
|
if (type == resPostBuildLogLine) {
|
|
suffix = " (post)> ";
|
|
}
|
|
(void) log(
|
|
*state,
|
|
lvlInfo,
|
|
ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine
|
|
);
|
|
} else {
|
|
if (!printMultiline) {
|
|
state->activities.erase(i->second);
|
|
info.lastLine = lastLine;
|
|
state->activities.emplace_back(info);
|
|
i->second = std::prev(state->activities.end());
|
|
} else {
|
|
i->second->lastLine = lastLine;
|
|
}
|
|
update(*state);
|
|
}
|
|
}
|
|
}
|
|
|
|
else if (type == resUntrustedPath) {
|
|
state->untrustedPaths++;
|
|
update(*state);
|
|
}
|
|
|
|
else if (type == resCorruptedPath) {
|
|
state->corruptedPaths++;
|
|
update(*state);
|
|
}
|
|
|
|
else if (type == resSetPhase) {
|
|
auto i = state->its.find(act);
|
|
assert(i != state->its.end());
|
|
i->second->phase = getS(fields, 0);
|
|
update(*state);
|
|
}
|
|
|
|
else if (type == resProgress) {
|
|
auto i = state->its.find(act);
|
|
assert(i != state->its.end());
|
|
ActInfo & actInfo = *i->second;
|
|
actInfo.done = getI(fields, 0);
|
|
actInfo.expected = getI(fields, 1);
|
|
actInfo.running = getI(fields, 2);
|
|
actInfo.failed = getI(fields, 3);
|
|
update(*state);
|
|
}
|
|
|
|
else if (type == resSetExpected) {
|
|
auto i = state->its.find(act);
|
|
assert(i != state->its.end());
|
|
ActInfo & actInfo = *i->second;
|
|
auto type = (ActivityType) getI(fields, 0);
|
|
auto & j = actInfo.expectedByType[type];
|
|
state->activitiesByType[type].expected -= j;
|
|
j = getI(fields, 1);
|
|
state->activitiesByType[type].expected += j;
|
|
update(*state);
|
|
}
|
|
|
|
return BufferState::HasSpace;
|
|
}
|
|
|
|
void ProgressBar::update(State & state)
|
|
{
|
|
state.haveUpdate = true;
|
|
updateCV.notify_one();
|
|
}
|
|
|
|
void ProgressBar::eraseProgressDisplay(State & state)
|
|
{
|
|
if (state.paused == 0) {
|
|
writeLogsToStderr("\e[?2026h"); // begin synchronized update
|
|
}
|
|
if (printMultiline && (state.lastLines >= 1)) {
|
|
// FIXME: make sure this works on windows
|
|
writeLogsToStderr(fmt("\e[G\e[%dF\e[J", state.lastLines));
|
|
} else {
|
|
writeLogsToStderr("\r\e[K");
|
|
}
|
|
}
|
|
|
|
std::chrono::milliseconds ProgressBar::restoreProgressDisplay(State & state)
|
|
{
|
|
auto nextWakeup = A_LONG_TIME;
|
|
|
|
state.haveUpdate = false;
|
|
if (state.paused > 0) return nextWakeup; // when paused, the progress display should not actually be shown
|
|
|
|
auto windowSize = getWindowSize();
|
|
auto width = windowSize.second;
|
|
if (width <= 0) {
|
|
width = std::numeric_limits<decltype(width)>::max();
|
|
}
|
|
|
|
state.lastLines = 0;
|
|
|
|
std::string line;
|
|
std::string status = getStatus(state);
|
|
if (!status.empty()) {
|
|
line += '[';
|
|
line += status;
|
|
line += "]";
|
|
}
|
|
if (printMultiline && !line.empty()) {
|
|
writeLogsToStderr(filterANSIEscapes(line, false, width) + ANSI_NORMAL "\n");
|
|
state.lastLines++;
|
|
}
|
|
|
|
auto height = windowSize.first > 0 ? windowSize.first : 25;
|
|
auto moreActivities = 0;
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
std::string activity_line;
|
|
if (!state.activities.empty()) {
|
|
for (auto i = state.activities.begin(); i != state.activities.end(); ++i) {
|
|
if (!(i->visible && (!i->s.empty() || !i->lastLine.empty()))) {
|
|
continue;
|
|
}
|
|
/* Don't show activities until some time has
|
|
passed, to avoid displaying very short
|
|
activities. */
|
|
auto delay = std::chrono::milliseconds(10);
|
|
if (i->startTime + delay >= now) {
|
|
nextWakeup = std::min(
|
|
nextWakeup,
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
delay - (now - i->startTime)
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
|
|
activity_line = i->s;
|
|
|
|
if (!i->phase.empty()) {
|
|
activity_line += " (";
|
|
activity_line += i->phase;
|
|
activity_line += ")";
|
|
}
|
|
if (!i->lastLine.empty()) {
|
|
if (!i->s.empty())
|
|
activity_line += ": ";
|
|
activity_line += i->lastLine;
|
|
}
|
|
|
|
if (printMultiline) {
|
|
if (state.lastLines < (height -1)) {
|
|
writeLogsToStderr(filterANSIEscapes(activity_line, false, width) + ANSI_NORMAL "\n");
|
|
state.lastLines++;
|
|
} else moreActivities++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (printMultiline && moreActivities)
|
|
writeLogsToStderr(fmt("And %d more...", moreActivities));
|
|
|
|
if (!printMultiline) {
|
|
if (!line.empty()) {
|
|
line += " ";
|
|
}
|
|
line += activity_line;
|
|
if (!line.empty()) {
|
|
writeLogsToStderr(filterANSIEscapes(line, false, width) + ANSI_NORMAL);
|
|
}
|
|
}
|
|
|
|
writeLogsToStderr("\e[?2026l"); // end synchronized update
|
|
|
|
return nextWakeup;
|
|
}
|
|
|
|
std::string ProgressBar::getStatus(State & state)
|
|
{
|
|
constexpr auto MiB = 1024.0 * 1024.0;
|
|
|
|
std::string res;
|
|
|
|
auto renderActivity = [&](ActivityType type, const std::string & itemFmt, const std::string & numberFmt = "%d", double unit = 1) {
|
|
auto & act = state.activitiesByType[type];
|
|
uint64_t done = act.done, expected = act.done, running = 0, failed = act.failed;
|
|
for (auto & [actId, infoIt] : act.its) {
|
|
done += infoIt->done;
|
|
expected += infoIt->expected;
|
|
running += infoIt->running;
|
|
failed += infoIt->failed;
|
|
}
|
|
|
|
expected = std::max(expected, act.expected);
|
|
|
|
std::string rendered;
|
|
|
|
if (running || done || expected || failed) {
|
|
if (running) {
|
|
if (expected != 0) {
|
|
auto const runningPart = fmt(numberFmt, running / unit);
|
|
auto const donePart = fmt(numberFmt, done / unit);
|
|
auto const expectedPart = fmt(numberFmt, expected / unit);
|
|
rendered = fmt(
|
|
ANSI_BLUE "%s" ANSI_NORMAL "/" ANSI_GREEN "%s" ANSI_NORMAL "/%s",
|
|
runningPart,
|
|
donePart,
|
|
expectedPart
|
|
);
|
|
} else {
|
|
auto const runningPart = fmt(numberFmt, running / unit);
|
|
auto const donePart = fmt(numberFmt, done / unit);
|
|
rendered = fmt(
|
|
ANSI_BLUE "%s" ANSI_NORMAL "/" ANSI_GREEN "%s" ANSI_NORMAL,
|
|
runningPart,
|
|
donePart
|
|
);
|
|
}
|
|
} else if (expected != done) {
|
|
if (expected != 0) {
|
|
auto const donePart = fmt(numberFmt, done / unit);
|
|
auto const expectedPart = fmt(numberFmt, expected / unit);
|
|
rendered = fmt(
|
|
ANSI_GREEN "%s" ANSI_NORMAL "/%s",
|
|
donePart,
|
|
expectedPart
|
|
);
|
|
} else {
|
|
auto const donePart = fmt(numberFmt, done / unit);
|
|
rendered = fmt(ANSI_GREEN "%s" ANSI_NORMAL, donePart);
|
|
}
|
|
} else {
|
|
auto const donePart = fmt(numberFmt, done / unit);
|
|
|
|
// We only color if `done` is non-zero.
|
|
if (done) {
|
|
rendered = concatStrings(ANSI_GREEN, donePart, ANSI_NORMAL);
|
|
} else {
|
|
rendered = donePart;
|
|
}
|
|
}
|
|
rendered = fmt(itemFmt, rendered);
|
|
|
|
if (failed)
|
|
rendered += fmt(" (" ANSI_RED "%d failed" ANSI_NORMAL ")", failed / unit);
|
|
}
|
|
|
|
return rendered;
|
|
};
|
|
|
|
auto showActivity = [&](ActivityType type, const std::string & itemFmt, const std::string & numberFmt = "%d", double unit = 1) {
|
|
auto s = renderActivity(type, itemFmt, numberFmt, unit);
|
|
if (s.empty()) return;
|
|
if (!res.empty()) res += ", ";
|
|
res += s;
|
|
};
|
|
|
|
showActivity(actBuilds, "%s built");
|
|
|
|
auto s1 = renderActivity(actCopyPaths, "%s copied");
|
|
auto s2 = renderActivity(actCopyPath, "%s MiB", "%.1f", MiB);
|
|
|
|
if (!s1.empty() || !s2.empty()) {
|
|
if (!res.empty()) res += ", ";
|
|
if (s1.empty()) res += "0 copied"; else res += s1;
|
|
if (!s2.empty()) { res += " ("; res += s2; res += ')'; }
|
|
}
|
|
|
|
showActivity(actFileTransfer, "%s MiB DL", "%.1f", MiB);
|
|
|
|
{
|
|
auto s = renderActivity(actOptimiseStore, "%s paths optimised");
|
|
if (s != "") {
|
|
s += fmt(", %.1f MiB / %d inodes freed", state.bytesLinked / MiB, state.filesLinked);
|
|
if (!res.empty()) res += ", ";
|
|
res += s;
|
|
}
|
|
}
|
|
|
|
// FIXME: don't show "done" paths in green.
|
|
showActivity(actVerifyPaths, "%s paths verified");
|
|
|
|
if (state.corruptedPaths) {
|
|
if (!res.empty()) res += ", ";
|
|
res += fmt(ANSI_RED "%d corrupted" ANSI_NORMAL, state.corruptedPaths);
|
|
}
|
|
|
|
if (state.untrustedPaths) {
|
|
if (!res.empty()) res += ", ";
|
|
res += fmt(ANSI_RED "%d untrusted" ANSI_NORMAL, state.untrustedPaths);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void ProgressBar::writeToStdout(std::string_view s)
|
|
{
|
|
auto state(state_.lock());
|
|
if (state->paused == 0) eraseProgressDisplay(*state);
|
|
Logger::writeToStdout(s);
|
|
restoreProgressDisplay(*state);
|
|
}
|
|
|
|
std::optional<char> ProgressBar::ask(std::string_view msg)
|
|
{
|
|
auto state(state_.lock());
|
|
if (state->paused > 0 || !isatty(STDIN_FILENO)) return {};
|
|
eraseProgressDisplay(*state);
|
|
writeLogsToStderr("\e[?2026l"); // end synchronized update
|
|
std::cerr << msg;
|
|
auto s = trim(readLine(STDIN_FILENO));
|
|
writeLogsToStderr("\e[?2026h"); // begin synchronized update
|
|
if (s.size() != 1) return {};
|
|
restoreProgressDisplay(*state);
|
|
return s[0];
|
|
}
|
|
|
|
void ProgressBar::setPrintBuildLogs(bool printBuildLogs)
|
|
{
|
|
this->printBuildLogs = printBuildLogs;
|
|
}
|
|
|
|
void ProgressBar::setPrintMultiline(bool printMultiline)
|
|
{
|
|
this->printMultiline = printMultiline;
|
|
}
|
|
|
|
Logger * makeProgressBar()
|
|
{
|
|
return new ProgressBar(shouldANSI());
|
|
}
|
|
|
|
}
|