commands: RunPager RAII -> withPager wrapper

RunPager is weird and confusing in that it replaces what stdout *is*
depending on environmental conditions. this has not caused problems,
but it's easy to imagine situations in which it would (eg if the stl
decided to capture the stdout fd by duplicating it). using a wrapper
for this also makes clear *what* actually goes into the pager; while
the previous contract was semi-reasonable it was also very implicit,
and with the proliferation of functions we had that printed directly
to stdout it would have been easy to send wrong output to the pager.

using a wrapper also makes process management much easier because we
do not have to rely on destructors to always produce correct output.

Change-Id: Ifd3760940af1ec719fe856158c913cbb9a5bf270
This commit is contained in:
eldritch horrors
2026-01-18 19:17:11 +00:00
parent 46b86ffa49
commit a7bc1be03f
8 changed files with 140 additions and 89 deletions
+23 -16
View File
@@ -1133,12 +1133,10 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
return;
}
RunPager pager;
{
withPager([&](Pager & pager) {
Table table;
std::ostringstream dummy;
XMLWriter xml(true, *(xmlOutput ? &cout : &dummy));
std::ostringstream xmlStream;
XMLWriter xml(true, xmlStream);
XMLOpenElement xmlRoot(xml, "items");
for (auto & i : elems) {
@@ -1369,9 +1367,11 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs)
}
if (!xmlOutput) {
std::cout << printTable(table);
}
pager << printTable(table);
} else {
pager << xmlStream.str();
}
});
}
static void opSwitchProfile(Globals & globals, Strings opFlags, Strings opArgs)
@@ -1424,20 +1424,27 @@ static void opListGenerations(Globals & globals, Strings opFlags, Strings opArgs
auto [gens, curGen] = findGenerations(globals.profile);
RunPager pager;
withPager([&](Pager & pager) {
for (auto & i : gens) {
tm t;
if (!localtime_r(&i.creationTime, &t)) throw Error("cannot convert time");
logger->cout("%|4| %|4|-%|02|-%|02| %|02|:%|02|:%|02| %||",
i.number,
t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
t.tm_hour, t.tm_min, t.tm_sec,
i.number == curGen ? "(current)" : "");
if (!localtime_r(&i.creationTime, &t)) {
throw Error("cannot convert time");
}
pager << fmt(
"%|4| %|4|-%|02|-%|02| %|02|:%|02|:%|02| %||\n",
i.number,
t.tm_year + 1900,
t.tm_mon + 1,
t.tm_mday,
t.tm_hour,
t.tm_min,
t.tm_sec,
i.number == curGen ? "(current)" : ""
);
}
});
}
static void opDeleteGenerations(Globals & globals, Strings opFlags, Strings opArgs)
{
if (opFlags.size() > 0)
+23 -21
View File
@@ -25,7 +25,9 @@
#include <iostream>
#include <algorithm>
#include <ostream>
#include <ranges>
#include <sstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
@@ -375,17 +377,14 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
if (!query) query = qOutputs;
RunPager pager;
{
withPager([&](Pager & pager) {
switch (*query) {
case qOutputs: {
for (auto & i : opArgs) {
auto outputs =
aio.blockOn(maybeUseOutputs(store, store->followLinksToStorePath(i), true, forceRealise));
for (auto & outputPath : outputs) {
cout << fmt("%1%\n", store->printStorePath(outputPath));
pager << fmt("%1%\n", store->printStorePath(outputPath));
}
}
break;
@@ -420,7 +419,7 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
}
auto sorted = aio.blockOn(store->topoSortPaths(paths));
for (StorePaths::reverse_iterator i = sorted.rbegin(); i != sorted.rend(); ++i) {
cout << fmt("%s\n", store->printStorePath(*i));
pager << fmt("%s\n", store->printStorePath(*i));
}
break;
}
@@ -428,7 +427,7 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
case qDeriver:
for (auto & i : opArgs) {
auto info = aio.blockOn(store->queryPathInfo(store->followLinksToStorePath(i)));
cout << fmt(
pager << fmt(
"%s\n", info->deriver ? store->printStorePath(*info->deriver) : "unknown-deriver"
);
}
@@ -444,7 +443,7 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
}
auto sorted = aio.blockOn(store->topoSortPaths(result));
for (StorePaths::reverse_iterator i = sorted.rbegin(); i != sorted.rend(); ++i) {
cout << fmt("%s\n", store->printStorePath(*i));
pager << fmt("%s\n", store->printStorePath(*i));
}
break;
}
@@ -461,7 +460,7 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
bindingName
);
}
cout << fmt("%s\n", j->second);
pager << fmt("%s\n", j->second);
}
break;
@@ -475,9 +474,9 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
auto info = aio.blockOn(store->queryPathInfo(j));
if (query == qHash) {
assert(info->narHash.type == HashType::SHA256);
cout << fmt("%s\n", info->narHash.to_string(HashFormat::Base32));
pager << fmt("%s\n", info->narHash.to_string(HashFormat::Base32));
} else if (query == qSize) {
cout << fmt("%d\n", info->narSize);
pager << fmt("%d\n", info->narSize);
}
}
}
@@ -486,7 +485,9 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
case qTree: {
StorePathSet done;
for (auto & i : opArgs) {
printTree(std::cout, store, aio, store->followLinksToStorePath(i), "", "", done);
std::stringstream tmp;
printTree(tmp, store, aio, store->followLinksToStorePath(i), "", "", done);
pager << tmp.str();
}
break;
}
@@ -501,7 +502,7 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
roots.insert(j);
}
}
std::cout << aio.blockOn(printDotGraph(ref<Store>::unsafeFromPtr(store), std::move(roots)));
pager << aio.blockOn(printDotGraph(ref<Store>::unsafeFromPtr(store), std::move(roots)));
break;
}
@@ -515,13 +516,13 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
roots.insert(j);
}
}
std::cout << aio.blockOn(printGraphML(ref<Store>::unsafeFromPtr(store), std::move(roots)));
pager << aio.blockOn(printGraphML(ref<Store>::unsafeFromPtr(store), std::move(roots)));
break;
}
case qResolve: {
for (auto & i : opArgs) {
cout << fmt("%s\n", store->printStorePath(store->followLinksToStorePath(i)));
pager << fmt("%s\n", store->printStorePath(store->followLinksToStorePath(i)));
}
break;
}
@@ -547,7 +548,7 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
for (auto & [target, links] : roots) {
if (referrers.find(target) != referrers.end()) {
for (auto & link : links) {
cout << fmt("%1% -> %2%\n", link, gcStore.printStorePath(target));
pager << fmt("%1% -> %2%\n", link, gcStore.printStorePath(target));
}
}
}
@@ -557,7 +558,7 @@ opQuery(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
default:
abort();
}
}
});
}
static void
@@ -593,15 +594,16 @@ opReadLog(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, Stri
auto & logStore = require<LogStore>(*store);
RunPager pager;
withPager([&](Pager & pager) {
for (auto & i : opArgs) {
auto path = logStore.followLinksToStorePath(i);
auto log = aio.blockOn(logStore.getBuildLog(path));
if (!log)
if (!log) {
throw Error("build log of derivation '%s' is not available", logStore.printStorePath(path));
std::cout << *log;
}
pager << *log;
}
});
}
static void
+4 -5
View File
@@ -903,14 +903,12 @@ void NixRepl::initBuiltinCommands()
subs.push_front(repl.evaluator.store);
bool foundLog = false;
RunPager pager;
withPager([&](Pager & pager) {
for (auto & sub : subs) {
auto * logSubP = dynamic_cast<LogStore *>(&*sub);
if (!logSubP) {
printInfo(
"Skipped '%s' which does not support retrieving build logs", sub->getUri()
);
printInfo("Skipped '%s' which does not support retrieving build logs", sub->getUri());
continue;
}
auto & logSub = *logSubP;
@@ -918,7 +916,7 @@ void NixRepl::initBuiltinCommands()
auto log = repl.state.aio.blockOn(logSub.getBuildLog(drvPath));
if (log) {
printInfo("got build log for '%s' from '%s'", drvPathRaw, logSub.getUri());
logger->writeToStdout(*log);
pager << *log;
foundLog = true;
break;
}
@@ -927,6 +925,7 @@ void NixRepl::initBuiltinCommands()
if (!foundLog) {
throw Error("build log of '%s' is not available", drvPathRaw);
}
});
return ProcessLineResult::PromptAgain;
},
+17
View File
@@ -419,6 +419,23 @@ RunPager::~RunPager()
}
}
void withPager(kj::Function<void(Pager &)> fn)
{
struct PagerImpl : Pager
{
Pager & operator<<(std::string_view data) override
{
std::cout.write(data.data(), data.size());
return *this;
}
};
RunPager wrapper;
PagerImpl pager;
fn(pager);
std::cout.flush();
}
PrintFreed::~PrintFreed()
{
// When in dry-run mode, print the paths on stdout
+26
View File
@@ -10,6 +10,8 @@
#include "lix/libutil/processes.hh"
#include "lix/libutil/strings.hh"
#include <kj/function.h>
namespace nix {
int handleExceptions(const std::string & programName, std::function<int()> fun);
@@ -86,6 +88,30 @@ private:
int std_out;
};
/**
* Represents a running pager if paging is available, or stdout if not.
*/
class Pager
{
protected:
Pager() = default;
public:
/**
* Writes some data to the pager (or stdout). Only the provided data
* is written, with no newlines are added or any formatting applied.
*/
virtual Pager & operator<<(std::string_view data) = 0;
};
/**
* Starts a pager if standard output is a terminal and $PAGER is set. The
* pager is provided as an argument to the callback; only data written to
* that object will be sent to the pager program. If no pager is started,
* e.g. if $PAGER is cleared, the pager object writes directly to stdout.
*/
void withPager(kj::Function<void(Pager &)> fn);
/* GC helpers. */
std::string showBytes(uint64_t bytes);
+6 -3
View File
@@ -43,7 +43,7 @@ struct CmdLog : InstallableCommand
},
}, b.path.raw());
RunPager pager;
withPager([&](Pager & pager) {
for (auto & sub : subs) {
auto * logSubP = dynamic_cast<LogStore *>(&*sub);
if (!logSubP) {
@@ -53,14 +53,17 @@ struct CmdLog : InstallableCommand
auto & logSub = *logSubP;
auto log = aio().blockOn(logSub.getBuildLog(path));
if (!log) continue;
if (!log) {
continue;
}
logger->pause();
printInfo("got build log for '%s' from '%s'", installable->what(), logSub.getUri());
writeFull(STDOUT_FILENO, *log);
pager << *log;
return;
}
throw Error("build log of '%s' is not available", installable->what());
});
}
};
+1 -2
View File
@@ -381,8 +381,7 @@ static void showHelp(AsyncIoRoot & aio, std::vector<std::string> subcommand, Nix
auto markdown =
state->forceString(attr->value, noPos, "while evaluating the lowdown help text");
RunPager pager;
std::cout << renderMarkdownToTerminal(markdown) << "\n";
withPager([&](Pager & pager) { pager << renderMarkdownToTerminal(markdown) << "\n"; });
}
static NixArgs & getNixArgs(Command & cmd)
+3 -5
View File
@@ -101,13 +101,11 @@ struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions
closure (i.e., that have a non-infinite distance to
'dependency'). Print every edge on a path between `package`
and `dependency`. */
RunPager pager;
logger->cout(
"%s",
aio().blockOn(
withPager([&](Pager & pager) {
pager << aio().blockOn(
genGraphString(packagePath, dependencyPath, graphData, *store, all, precise)
)
);
});
}
};