Add PSQL_WATCH_PAGER for psql's \watch command.

Allow a pager to be used by the \watch command.  This works but isn't
very useful with traditional pagers like "less", so use a different
environment variable.  The popular open source tool "pspg" (also by
Pavel) knows how to display the output if you set PSQL_WATCH_PAGER="pspg
--stream".

To make \watch react quickly when the user quits the pager or presses
^C, and also to increase the accuracy of its timing and decrease the
rate of useless context switches, change the main loop of the \watch
command to use sigwait() rather than a sleeping/polling loop, on Unix.

Supported on Unix only for now (like pspg).

Author: Pavel Stehule <pavel.stehule@gmail.com>
Author: Thomas Munro <thomas.munro@gmail.com>
Discussion: https://postgr.es/m/CAFj8pRBfzUUPz-3gN5oAzto9SDuRSq-TQPfXU_P6h0L7hO%2BEhg%40mail.gmail.com
This commit is contained in:
Thomas Munro 2021-07-13 11:13:48 +12:00
parent f014b1b9bb
commit 7c09d2797e
6 changed files with 184 additions and 15 deletions

View File

@ -3002,6 +3002,16 @@ lo_import 152801
(such as <filename>more</filename>) is used.
</para>
<para>
When using the <literal>\watch</literal> command to execute a query
repeatedly, the environment variable <envar>PSQL_WATCH_PAGER</envar>
is used to find the pager program instead, on Unix systems. This is
configured separately because it may confuse traditional pagers, but
can be used to send output to tools that understand
<application>psql</application>'s output format (such as
<filename>pspg --stream</filename>).
</para>
<para>
When the <literal>pager</literal> option is <literal>off</literal>, the pager
program is not used. When the <literal>pager</literal> option is
@ -4672,6 +4682,24 @@ PSQL_EDITOR_LINENUMBER_ARG='--line '
</listitem>
</varlistentry>
<varlistentry>
<term><envar>PSQL_WATCH_PAGER</envar></term>
<listitem>
<para>
When a query is executed repeatedly with the <command>\watch</command>
command, a pager is not used by default. This behavior can be changed
by setting <envar>PSQL_WATCH_PAGER</envar> to a pager command, on Unix
systems. The <literal>pspg</literal> pager (not part of
<productname>PostgreSQL</productname> but available in many open source
software distributions) can display the output of
<command>\watch</command> if started with the option
<literal>--stream</literal>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><envar>PSQLRC</envar></term>

View File

@ -13,6 +13,7 @@
#include <utime.h>
#ifndef WIN32
#include <sys/stat.h> /* for stat() */
#include <sys/time.h> /* for setitimer() */
#include <fcntl.h> /* open() flags */
#include <unistd.h> /* for geteuid(), getpid(), stat() */
#else
@ -4894,8 +4895,17 @@ do_watch(PQExpBuffer query_buf, double sleep)
const char *strftime_fmt;
const char *user_title;
char *title;
const char *pagerprog = NULL;
FILE *pagerpipe = NULL;
int title_len;
int res = 0;
#ifndef WIN32
sigset_t sigalrm_sigchld_sigint;
sigset_t sigalrm_sigchld;
sigset_t sigint;
struct itimerval interval;
bool done = false;
#endif
if (!query_buf || query_buf->len <= 0)
{
@ -4903,6 +4913,58 @@ do_watch(PQExpBuffer query_buf, double sleep)
return false;
}
#ifndef WIN32
sigemptyset(&sigalrm_sigchld_sigint);
sigaddset(&sigalrm_sigchld_sigint, SIGCHLD);
sigaddset(&sigalrm_sigchld_sigint, SIGALRM);
sigaddset(&sigalrm_sigchld_sigint, SIGINT);
sigemptyset(&sigalrm_sigchld);
sigaddset(&sigalrm_sigchld, SIGCHLD);
sigaddset(&sigalrm_sigchld, SIGALRM);
sigemptyset(&sigint);
sigaddset(&sigint, SIGINT);
/*
* Block SIGALRM and SIGCHLD before we start the timer and the pager (if
* configured), to avoid races. sigwait() will receive them.
*/
sigprocmask(SIG_BLOCK, &sigalrm_sigchld, NULL);
/*
* Set a timer to interrupt sigwait() so we can run the query at the
* requested intervals.
*/
interval.it_value.tv_sec = sleep_ms / 1000;
interval.it_value.tv_usec = (sleep_ms % 1000) * 1000;
interval.it_interval = interval.it_value;
if (setitimer(ITIMER_REAL, &interval, NULL) < 0)
{
pg_log_error("could not set timer: %m");
done = true;
}
#endif
/*
* For \watch, we ignore the size of the result and always use the pager
* if PSQL_WATCH_PAGER is set. We also ignore the regular PSQL_PAGER or
* PAGER environment variables, because traditional pagers probably won't
* be very useful for showing a stream of results.
*/
#ifndef WIN32
pagerprog = getenv("PSQL_WATCH_PAGER");
#endif
if (pagerprog && myopt.topt.pager)
{
disable_sigpipe_trap();
pagerpipe = popen(pagerprog, "w");
if (!pagerpipe)
/* silently proceed without pager */
restore_sigpipe_trap();
}
/*
* Choose format for timestamps. We might eventually make this a \pset
* option. In the meantime, using a variable for the format suppresses
@ -4911,10 +4973,12 @@ do_watch(PQExpBuffer query_buf, double sleep)
strftime_fmt = "%c";
/*
* Set up rendering options, in particular, disable the pager, because
* nobody wants to be prompted while watching the output of 'watch'.
* Set up rendering options, in particular, disable the pager unless
* PSQL_WATCH_PAGER was successfully launched.
*/
myopt.topt.pager = 0;
if (!pagerpipe)
myopt.topt.pager = 0;
/*
* If there's a title in the user configuration, make sure we have room
@ -4929,7 +4993,6 @@ do_watch(PQExpBuffer query_buf, double sleep)
{
time_t timer;
char timebuf[128];
long i;
/*
* Prepare title for output. Note that we intentionally include a
@ -4948,7 +5011,7 @@ do_watch(PQExpBuffer query_buf, double sleep)
myopt.title = title;
/* Run the query and print out the results */
res = PSQLexecWatch(query_buf->data, &myopt);
res = PSQLexecWatch(query_buf->data, &myopt, pagerpipe);
/*
* PSQLexecWatch handles the case where we can no longer repeat the
@ -4957,6 +5020,11 @@ do_watch(PQExpBuffer query_buf, double sleep)
if (res <= 0)
break;
if (pagerpipe && ferror(pagerpipe))
break;
#ifdef WIN32
/*
* Set up cancellation of 'watch' via SIGINT. We redo this each time
* through the loop since it's conceivable something inside
@ -4967,12 +5035,10 @@ do_watch(PQExpBuffer query_buf, double sleep)
/*
* Enable 'watch' cancellations and wait a while before running the
* query again. Break the sleep into short intervals (at most 1s)
* since pg_usleep isn't interruptible on some platforms.
* query again. Break the sleep into short intervals (at most 1s).
*/
sigint_interrupt_enabled = true;
i = sleep_ms;
while (i > 0)
for (long i = sleep_ms; i > 0;)
{
long s = Min(i, 1000L);
@ -4982,8 +5048,57 @@ do_watch(PQExpBuffer query_buf, double sleep)
i -= s;
}
sigint_interrupt_enabled = false;
#else
/* sigwait() will handle SIGINT. */
sigprocmask(SIG_BLOCK, &sigint, NULL);
if (cancel_pressed)
done = true;
/* Wait for SIGINT, SIGCHLD or SIGALRM. */
while (!done)
{
int signal_received;
if (sigwait(&sigalrm_sigchld_sigint, &signal_received) < 0)
{
/* Some other signal arrived? */
if (errno == EINTR)
continue;
else
{
pg_log_error("could not wait for signals: %m");
done = true;
break;
}
}
/* On ^C or pager exit, it's time to stop running the query. */
if (signal_received == SIGINT || signal_received == SIGCHLD)
done = true;
/* Otherwise, we must have SIGALRM. Time to run the query again. */
break;
}
/* Unblock SIGINT so that slow queries can be interrupted. */
sigprocmask(SIG_UNBLOCK, &sigint, NULL);
if (done)
break;
#endif
}
if (pagerpipe)
{
pclose(pagerpipe);
restore_sigpipe_trap();
}
#ifndef WIN32
/* Disable the interval timer. */
memset(&interval, 0, sizeof(interval));
setitimer(ITIMER_REAL, &interval, NULL);
/* Unblock SIGINT, SIGCHLD and SIGALRM. */
sigprocmask(SIG_UNBLOCK, &sigalrm_sigchld_sigint, NULL);
#endif
pg_free(title);
return (res >= 0);
}

View File

@ -592,12 +592,13 @@ PSQLexec(const char *query)
* e.g., because of the interrupt, -1 on error.
*/
int
PSQLexecWatch(const char *query, const printQueryOpt *opt)
PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
{
PGresult *res;
double elapsed_msec = 0;
instr_time before;
instr_time after;
FILE *fout;
if (!pset.db)
{
@ -638,14 +639,16 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt)
return 0;
}
fout = printQueryFout ? printQueryFout : pset.queryFout;
switch (PQresultStatus(res))
{
case PGRES_TUPLES_OK:
printQuery(res, opt, pset.queryFout, false, pset.logfile);
printQuery(res, opt, fout, false, pset.logfile);
break;
case PGRES_COMMAND_OK:
fprintf(pset.queryFout, "%s\n%s\n\n", opt->title, PQcmdStatus(res));
fprintf(fout, "%s\n%s\n\n", opt->title, PQcmdStatus(res));
break;
case PGRES_EMPTY_QUERY:
@ -668,7 +671,7 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt)
PQclear(res);
fflush(pset.queryFout);
fflush(fout);
/* Possible microtiming output */
if (pset.timing)

View File

@ -29,7 +29,7 @@ extern sigjmp_buf sigint_interrupt_jmp;
extern void psql_setup_cancel_handler(void);
extern PGresult *PSQLexec(const char *query);
extern int PSQLexecWatch(const char *query, const printQueryOpt *opt);
extern int PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout);
extern bool SendQuery(const char *query);

View File

@ -347,7 +347,7 @@ helpVariables(unsigned short int pager)
* Windows builds currently print one more line than non-Windows builds.
* Using the larger number is fine.
*/
output = PageOutput(158, pager ? &(pset.popt.topt) : NULL);
output = PageOutput(160, pager ? &(pset.popt.topt) : NULL);
fprintf(output, _("List of specially treated variables\n\n"));
@ -505,6 +505,10 @@ helpVariables(unsigned short int pager)
" alternative location for the command history file\n"));
fprintf(output, _(" PSQL_PAGER, PAGER\n"
" name of external pager program\n"));
#ifndef WIN32
fprintf(output, _(" PSQL_WATCH_PAGER\n"
" name of external pager program used for \\watch\n"));
#endif
fprintf(output, _(" PSQLRC\n"
" alternative location for the user's .psqlrc file\n"));
fprintf(output, _(" SHELL\n"

View File

@ -110,6 +110,13 @@ log_locus_callback(const char **filename, uint64 *lineno)
}
}
#ifndef WIN32
static void
empty_signal_handler(SIGNAL_ARGS)
{
}
#endif
/*
*
* main
@ -302,6 +309,18 @@ main(int argc, char *argv[])
psql_setup_cancel_handler();
#ifndef WIN32
/*
* do_watch() needs signal handlers installed (otherwise sigwait() will
* filter them out on some platforms), but doesn't need them to do
* anything, and they shouldn't ever run (unless perhaps a stray SIGALRM
* arrives due to a race when do_watch() cancels an itimer).
*/
pqsignal(SIGCHLD, empty_signal_handler);
pqsignal(SIGALRM, empty_signal_handler);
#endif
PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL);
SyncVariables();