From 7c09d2797ecdf779e5dc3289497be85675f3d134 Mon Sep 17 00:00:00 2001 From: Thomas Munro Date: Tue, 13 Jul 2021 11:13:48 +1200 Subject: [PATCH] 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 Author: Thomas Munro Discussion: https://postgr.es/m/CAFj8pRBfzUUPz-3gN5oAzto9SDuRSq-TQPfXU_P6h0L7hO%2BEhg%40mail.gmail.com --- doc/src/sgml/ref/psql-ref.sgml | 28 +++++++ src/bin/psql/command.c | 133 ++++++++++++++++++++++++++++++--- src/bin/psql/common.c | 11 ++- src/bin/psql/common.h | 2 +- src/bin/psql/help.c | 6 +- src/bin/psql/startup.c | 19 +++++ 6 files changed, 184 insertions(+), 15 deletions(-) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index a8dfc41e406..da533068572 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -3002,6 +3002,16 @@ lo_import 152801 (such as more) is used. + + When using the \watch command to execute a query + repeatedly, the environment variable PSQL_WATCH_PAGER + 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 + psql's output format (such as + pspg --stream). + + When the pager option is off, the pager program is not used. When the pager option is @@ -4672,6 +4682,24 @@ PSQL_EDITOR_LINENUMBER_ARG='--line ' + + PSQL_WATCH_PAGER + + + + When a query is executed repeatedly with the \watch + command, a pager is not used by default. This behavior can be changed + by setting PSQL_WATCH_PAGER to a pager command, on Unix + systems. The pspg pager (not part of + PostgreSQL but available in many open source + software distributions) can display the output of + \watch if started with the option + --stream. + + + + + PSQLRC diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 543401c6d6a..d704c4220c7 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -13,6 +13,7 @@ #include #ifndef WIN32 #include /* for stat() */ +#include /* for setitimer() */ #include /* open() flags */ #include /* 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); } diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 9a004995109..56407866782 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -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) diff --git a/src/bin/psql/common.h b/src/bin/psql/common.h index 041b2ac068a..d8538a4e06c 100644 --- a/src/bin/psql/common.h +++ b/src/bin/psql/common.h @@ -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); diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 3c250d11cff..d3fda67edd3 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -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" diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index 110906a4e95..5f36f0d1c6d 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -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();