Add PSQL_WATCH_PAGER for psql's \watch command.
authorThomas Munro <[email protected]>
Mon, 12 Jul 2021 23:13:48 +0000 (11:13 +1200)
committerThomas Munro <[email protected]>
Mon, 12 Jul 2021 23:43:21 +0000 (11:43 +1200)
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 <[email protected]>
Author: Thomas Munro <[email protected]>
Discussion: https://postgr.es/m/CAFj8pRBfzUUPz-3gN5oAzto9SDuRSq-TQPfXU_P6h0L7hO%2BEhg%40mail.gmail.com

doc/src/sgml/ref/psql-ref.sgml
src/bin/psql/command.c
src/bin/psql/common.c
src/bin/psql/common.h
src/bin/psql/help.c
src/bin/psql/startup.c

index a8dfc41e406894b6d946ca489f500b9d859d3a70..da5330685727e2330c8634da1d6301e67cc297b8 100644 (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>
 
index 543401c6d6a79b2018ae287537f346898a719d45..d704c4220c738fc805c4d60cb6ab0b272d20b455 100644 (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);
 }
index 9a00499510929c97925fb053fbcc81c557808322..56407866782e2fe1775433b3b0f39b3f66884fbd 100644 (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)
index 041b2ac068a749c4eb267690dcf7f4271b4200c7..d8538a4e06cde288d14d67d2eb31c58a7ab572a6 100644 (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);
 
index 3c250d11cff9bfcd52327244cb8192f791ac6509..d3fda67edd38bff60a393ae7ec0f86fdd8613326 100644 (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"
index 110906a4e959b46d4540f6c78580bdde77d3e950..5f36f0d1c6dcbf894a01a9a2dbd9f045f5e9a497 100644 (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();