summaryrefslogtreecommitdiff
path: root/apt-private
diff options
context:
space:
mode:
authorJulian Andres Klode <jak@debian.org>2024-12-16 17:05:40 +0100
committerJulian Andres Klode <jak@debian.org>2024-12-18 10:25:24 +0100
commit6e260e26e4cd671f781151e24d89a17b29a55530 (patch)
tree0ae3b4f9122e5168df4d4d09102a88196d6baaf2 /apt-private
parent19000fab15ab0b30c95a1bf6e034aa3c6bdc36cb (diff)
Introduce automatic pager for read commands
Automatically show the output of `show`, `policy`, `list`, `search`, `showsrc` in a pager. The pager setup is inspired by git's pager setup. Notably, the pager is found using APT_PAGER and PAGER variables. We wait for the pager to be setup somewhat correctly by using a notify pipe to figure out whether execvp() was succesful - then the pipe will read EOF as the other end got closed by CLOEXEC during exec - or not, then the pipe will contain an errno. We set up the correct handlers for signals and exit to close the fds and wait for the pager. Notably inside the signal handler we cannot flush our streams, only close them, so there is some duplication. We call the InitOutputPager() function from inside the various Do...() functions rather than setting it up generally in InitOutput(). Doing so allows us to first render the progress without a pager, and then setup the pager for the content only which improves user experience. When we setup a pager we also take care to disable standard input, as we should not be prompting users while a pager is running (the pager will be reading from the tty directly). We do this by dup2-ing() a /dev/null over it; if we just close()d the fd, another open() might reuse the fd number and problems could occur.
Diffstat (limited to 'apt-private')
-rw-r--r--apt-private/private-cmndline.cc1
-rw-r--r--apt-private/private-list.cc4
-rw-r--r--apt-private/private-main.cc5
-rw-r--r--apt-private/private-output.cc168
-rw-r--r--apt-private/private-output.h2
-rw-r--r--apt-private/private-search.cc3
-rw-r--r--apt-private/private-show.cc10
7 files changed, 189 insertions, 4 deletions
diff --git a/apt-private/private-cmndline.cc b/apt-private/private-cmndline.cc
index 5d4eeb5cb..cbf787aa9 100644
--- a/apt-private/private-cmndline.cc
+++ b/apt-private/private-cmndline.cc
@@ -509,6 +509,7 @@ static void BinarySpecificConfiguration(char const * const Binary) /*{{{*/
_config->CndSet("Binary::apt::APT::Keep-Downloaded-Packages", false);
_config->CndSet("Binary::apt::APT::Get::Update::InteractiveReleaseInfoChanges", true);
_config->CndSet("Binary::apt::APT::Cmd::Pattern-Only", true);
+ _config->CndSet("Binary::apt::Pager", true);
if (isatty(STDIN_FILENO))
_config->CndSet("Binary::apt::Dpkg::Lock::Timeout", -1);
diff --git a/apt-private/private-list.cc b/apt-private/private-list.cc
index eee657c46..6536c8df7 100644
--- a/apt-private/private-list.cc
+++ b/apt-private/private-list.cc
@@ -131,6 +131,10 @@ bool DoList(CommandLine &Cmd)
GetLocalitySortedVersionSet(CacheFile, &bag, matcher, &progress);
bool const ShowAllVersions = _config->FindB("APT::Cmd::All-Versions", false);
std::map<std::string, std::string> output_map;
+
+ if (not InitOutputPager())
+ return false;
+
for (LocalitySortedVersionSet::iterator V = bag.begin(); V != bag.end(); ++V)
{
std::stringstream outs;
diff --git a/apt-private/private-main.cc b/apt-private/private-main.cc
index 39ad07b36..7b4f05a50 100644
--- a/apt-private/private-main.cc
+++ b/apt-private/private-main.cc
@@ -6,6 +6,7 @@
#include <apt-pkg/strutl.h>
#include <apt-private/private-main.h>
+#include <apt-private/private-output.h>
#include <iostream>
#include <locale>
@@ -78,8 +79,8 @@ void CheckIfCalledByScript(int argc, const char *argv[]) /*{{{*/
{
if (unlikely(argc < 1)) return;
- if(!isatty(STDOUT_FILENO) &&
- _config->FindB("Apt::Cmd::Disable-Script-Warning", false) == false)
+ if (not IsStdoutAtty() &&
+ _config->FindB("Apt::Cmd::Disable-Script-Warning", false) == false)
{
// NOTE: CLI interface is redundant on the I/interface, this is
// intentional to make it easier to read.
diff --git a/apt-private/private-output.cc b/apt-private/private-output.cc
index 79a97128c..e3f795cf3 100644
--- a/apt-private/private-output.cc
+++ b/apt-private/private-output.cc
@@ -6,6 +6,7 @@
#include <apt-pkg/configuration.h>
#include <apt-pkg/depcache.h>
#include <apt-pkg/error.h>
+#include <apt-pkg/fileutl.h>
#include <apt-pkg/pkgcache.h>
#include <apt-pkg/pkgrecords.h>
#include <apt-pkg/policy.h>
@@ -18,11 +19,13 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
+#include <fcntl.h>
#include <iomanip>
#include <iostream>
#include <langinfo.h>
#include <regex.h>
#include <sys/ioctl.h>
+#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
@@ -41,6 +44,14 @@ std::ofstream devnull("/dev/null");
unsigned int ScreenWidth = 80 - 1; /* - 1 for the cursor */
+static pid_t Pager = -1;
+static constexpr std::array<int, 5> PagerSignalsToHandle = {
+ SIGINT,
+ SIGHUP,
+ SIGTERM,
+ SIGQUIT,
+ SIGPIPE,
+};
// SigWinch - Window size change signal handler /*{{{*/
// ---------------------------------------------------------------------
@@ -56,9 +67,162 @@ static void SigWinch(int)
#endif
}
/*}}}*/
+
+bool IsStdoutAtty()
+{
+ static bool is = isatty(STDOUT_FILENO);
+ return is;
+}
+
+static void WaitPager()
+{
+ if (Pager != -1)
+ {
+ c0out.flush();
+ c1out.flush();
+ c2out.flush();
+ std::cerr.flush();
+ std::cout.flush();
+ fflush(stdout);
+ fflush(stderr);
+ close(STDOUT_FILENO);
+ close(STDERR_FILENO);
+ waitpid(Pager, nullptr, 0);
+ Pager = -1;
+ }
+}
+
+static void SignalPager(int signo)
+{
+ if (Pager != -1)
+ {
+ // We can't flush from inside the signal handler.
+ close(STDOUT_FILENO);
+ close(STDERR_FILENO);
+ waitpid(Pager, nullptr, 0);
+ Pager = -1;
+ for (auto signalToHandle : PagerSignalsToHandle)
+ signal(signalToHandle, SIG_DFL);
+ raise(signo);
+ }
+}
+
+static std::string DeterminePager()
+{
+ if (not IsStdoutAtty() || not _config->FindB("Pager", false))
+ return "";
+ if (auto pager = getenv("APT_PAGER"); pager)
+ return pager;
+ if (auto pager = getenv("PAGER"); pager)
+ return pager;
+ return DEFAULT_PAGER;
+}
+
+bool InitOutputPager()
+{
+ // InitOutputPager() behaves like a singleton, do not run twice-
+ static bool alreadyRan = false;
+ if (alreadyRan)
+ return true;
+ alreadyRan = true;
+
+ auto pager = DeterminePager();
+ if (pager.empty() || pager == "cat")
+ return true;
+
+ auto pagerEnv = VectorizeString(PAGER_ENV, '\n');
+
+ int pipefds[2] = {-1, -1};
+ int notifyPipe[2] = {-1, -1};
+
+ DEFER([&] () {
+ close(pipefds[0]);
+ close(pipefds[1]);
+ close(notifyPipe[0]);
+ close(notifyPipe[1]);
+ });
+
+ if (pipe(pipefds) || pipe(notifyPipe))
+ return _error->Errno("pipe", "Failed to setup pipe");
+
+ Pager = ExecFork();
+ if (Pager < 0)
+ return _error->Errno("fork", "Failed to fork");
+ if (Pager == 0)
+ {
+ // Child process
+ if (dup2(pipefds[0], STDIN_FILENO) == -1)
+ goto err;
+ if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
+ goto err;
+
+ for (int sig = 1; sig < NSIG; sig++)
+ signal(sig, SIG_DFL);
+
+ for (auto &v: pagerEnv)
+ putenv(v.data());
+
+ {
+ // If our pager name contains a space we need to invoke it in a shell. Boooo!
+ char *cmd[] = {(char*)"/bin/sh", (char*)"-c", pager.data(), nullptr};
+ if (std::none_of(pager.begin(), pager.end(), isspace_ascii))
+ {
+ cmd[0] = pager.data();
+ cmd[1] = nullptr;
+ }
+ execvp(cmd[0], cmd);
+ }
+ err:
+ int err = errno;
+ FileFd::Write(notifyPipe[1], &err, sizeof(err));
+ _exit(128);
+ }
+
+ // Parent process.
+
+ // Figure out if we were able to execvp() the child successfully. To do so,
+ // read from the notifyPipe. If execvp() was successful, it was closed due
+ // to CLOEXEC. On failure, our child code writes an errno to it.
+ _error->PushToStack();
+ close(notifyPipe[1]); // Need to close our write end so our read doesn't block
+ notifyPipe[1] = -1;
+ if (int err = 0; FileFd::Read(notifyPipe[0], &err, sizeof(err))) {
+ errno = err;
+ _error->WarningE("PagerSetup", "Could not execute pager");
+ _error->MergeWithStack();
+ return true;
+ }
+ _error->RevertToStack();
+
+ // When running a pager, we must not be reading from stdin/tty, so
+ // let's be safe and open /dev/null in its place.
+ if (int const nullfd = open("/dev/null", O_RDONLY); nullfd != -1)
+ {
+ dup2(nullfd, STDIN_FILENO);
+ close(nullfd);
+ }
+
+ // Redirect the output(s) to the pager
+ if (dup2(pipefds[1], STDOUT_FILENO) == -1)
+ abort();
+ if (isatty(STDERR_FILENO) && dup2(pipefds[1], STDERR_FILENO) == -1)
+ abort();
+
+ // From now on, we can't show any progress messages as we are outputting
+ // to the pager.
+ _config->CndSet("quiet::NoUpdate", "1");
+
+ // Setup signal handlers and exit handlers for the pager, such that
+ // we wait for it.
+ for (auto signalToHandle : PagerSignalsToHandle)
+ signal(signalToHandle, SignalPager);
+ atexit(WaitPager);
+
+ return true;
+}
bool InitOutput(std::basic_streambuf<char> * const out) /*{{{*/
{
- if (!isatty(STDOUT_FILENO) && _config->FindI("quiet", -1) == -1)
+ if (not IsStdoutAtty() && _config->FindI("quiet", -1) == -1)
_config->Set("quiet","1");
c0out.rdbuf(out);
@@ -89,7 +253,7 @@ bool InitOutput(std::basic_streambuf<char> * const out) /*{{{*/
SigWinch(0);
}
- if (isatty(STDOUT_FILENO) == 0 || not _config->FindB("APT::Color", true) || getenv("NO_COLOR") != nullptr || getenv("APT_NO_COLOR") != nullptr)
+ if (not IsStdoutAtty() || not _config->FindB("APT::Color", true) || getenv("NO_COLOR") != nullptr || getenv("APT_NO_COLOR") != nullptr)
{
_config->Set("APT::Color", false);
_config->Set("APT::Color::Highlight", "");
diff --git a/apt-private/private-output.h b/apt-private/private-output.h
index 8e1d47225..aaa9a8ed9 100644
--- a/apt-private/private-output.h
+++ b/apt-private/private-output.h
@@ -26,6 +26,8 @@ APT_PUBLIC extern std::ofstream devnull;
APT_PUBLIC extern unsigned int ScreenWidth;
APT_PUBLIC bool InitOutput(std::basic_streambuf<char> * const out = std::cout.rdbuf());
+APT_PUBLIC bool InitOutputPager() APT_MUSTCHECK;
+APT_PUBLIC bool IsStdoutAtty();
void ListSingleVersion(pkgCacheFile &CacheFile, pkgRecords &records,
pkgCache::VerIterator const &V, std::ostream &out,
diff --git a/apt-private/private-search.cc b/apt-private/private-search.cc
index 19a3bf0fa..d6654eb5e 100644
--- a/apt-private/private-search.cc
+++ b/apt-private/private-search.cc
@@ -169,6 +169,9 @@ static bool FullTextSearch(CommandLine &CmdL) /*{{{*/
APT_FREE_PATTERNS();
progress.Done();
+ if (not InitOutputPager())
+ return false;
+
// FIXME: SORT! and make sorting flexible (alphabetic, by pkg status)
// output the sorted map
std::map<std::string, std::string>::const_iterator K;
diff --git a/apt-private/private-show.cc b/apt-private/private-show.cc
index ceef6707e..58c5d92c9 100644
--- a/apt-private/private-show.cc
+++ b/apt-private/private-show.cc
@@ -351,6 +351,10 @@ bool ShowPackage(CommandLine &CmdL) /*{{{*/
int const ShowVersion = _config->FindI("APT::Cache::Show::Version", 1);
pkgRecords Recs(CacheFile);
+
+ if (not InitOutputPager())
+ return false;
+
for (APT::VersionList::const_iterator Ver = verset.begin(); Ver != verset.end(); ++Ver)
{
pkgCache::VerFileIterator Vf;
@@ -441,6 +445,9 @@ bool ShowSrcPackage(CommandLine &CmdL) /*{{{*/
if (_error->PendingError() == true)
return false;
+ if (not InitOutputPager())
+ return false;
+
bool found = false;
// avoid showing identical records
std::set<std::string> seen;
@@ -496,6 +503,9 @@ bool Policy(CommandLine &CmdL)
if (unlikely(Plcy == nullptr))
return false;
+ if (not InitOutputPager())
+ return false;
+
// Print out all of the package files
if (CmdL.FileList[1] == 0)
{