Provide a variant of simple_prompt() that can be interrupted by ^C.
authorTom Lane <[email protected]>
Thu, 18 Nov 2021 00:09:54 +0000 (19:09 -0500)
committerTom Lane <[email protected]>
Thu, 18 Nov 2021 00:09:54 +0000 (19:09 -0500)
Up to now, you couldn't escape out of psql's \password command
by typing control-C (or other local spelling of SIGINT).  This
is pretty user-unfriendly, so improve it.  To do so, we have to
modify the functions provided by pg_get_line.c; but we don't
want to mess with psql's SIGINT handler setup, so provide an
API that lets that handler cause the cancel to occur.

This relies on the assumption that we won't do any major harm by
longjmp'ing out of fgets().  While that's obviously a little shaky,
we've long had the same assumption in the main input loop, and few
issues have been reported.

psql has some other simple_prompt() calls that could usefully
be improved the same way; for now, just deal with \password.

Nathan Bossart, minor tweaks by me

Discussion: https://postgr.es/m/747443.1635536754@sss.pgh.pa.us

src/backend/libpq/hba.c
src/bin/initdb/initdb.c
src/bin/psql/command.c
src/bin/psql/nls.mk
src/common/pg_get_line.c
src/common/sprompt.c
src/include/common/string.h

index 3be8778d21668ae2321cac94390cdbeb1bb18641..4328eb74fe69e75e9099b32b173301b303fa8f2e 100644 (file)
@@ -500,7 +500,7 @@ tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
                /* Collect the next input line, handling backslash continuations */
                resetStringInfo(&buf);
 
-               while (pg_get_line_append(file, &buf))
+               while (pg_get_line_append(file, &buf, NULL))
                {
                        /* Strip trailing newline, including \r in case we're on Windows */
                        buf.len = pg_strip_crlf(buf.data);
index 31839c1a198d717e6edf8f819bc7aca84ba03e33..3c61c789e46e0e6992e4adebea5f5500127e12cb 100644 (file)
@@ -1497,7 +1497,7 @@ get_su_pwd(void)
                                                 pwfilename);
                        exit(1);
                }
-               pwd1 = pg_get_line(pwf);
+               pwd1 = pg_get_line(pwf, NULL);
                if (!pwd1)
                {
                        if (ferror(pwf))
index 3de9d096fd3fed961de95fc6bb122492afdde634..102bc5956b7b48db9c5b6cae68c981bd87c4e332 100644 (file)
@@ -2025,9 +2025,10 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
        {
                char       *user = psql_scan_slash_option(scan_state,
                                                                                                  OT_SQLID, NULL, true);
-               char       *pw1;
-               char       *pw2;
+               char       *pw1 = NULL;
+               char       *pw2 = NULL;
                PQExpBufferData buf;
+               PromptInterruptContext prompt_ctx;
 
                if (user == NULL)
                {
@@ -2042,13 +2043,24 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
                        PQclear(res);
                }
 
+               /* Set up to let SIGINT cancel simple_prompt_extended() */
+               prompt_ctx.jmpbuf = sigint_interrupt_jmp;
+               prompt_ctx.enabled = &sigint_interrupt_enabled;
+               prompt_ctx.canceled = false;
+
                initPQExpBuffer(&buf);
                printfPQExpBuffer(&buf, _("Enter new password for user \"%s\": "), user);
 
-               pw1 = simple_prompt(buf.data, false);
-               pw2 = simple_prompt("Enter it again: ", false);
+               pw1 = simple_prompt_extended(buf.data, false, &prompt_ctx);
+               if (!prompt_ctx.canceled)
+                       pw2 = simple_prompt_extended("Enter it again: ", false, &prompt_ctx);
 
-               if (strcmp(pw1, pw2) != 0)
+               if (prompt_ctx.canceled)
+               {
+                       /* fail silently */
+                       success = false;
+               }
+               else if (strcmp(pw1, pw2) != 0)
                {
                        pg_log_error("Passwords didn't match.");
                        success = false;
@@ -2081,8 +2093,10 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
                }
 
                free(user);
-               free(pw1);
-               free(pw2);
+               if (pw1)
+                       free(pw1);
+               if (pw2)
+                       free(pw2);
                termPQExpBuffer(&buf);
        }
        else
index 5da216f8f6d56f521cda18ed6a676cc69985c9e5..a1b73289430c083821bf340b9e6ff8966186b275 100644 (file)
@@ -10,5 +10,5 @@ GETTEXT_FILES    = $(FRONTEND_COMMON_GETTEXT_FILES) \
                    ../../common/exec.c ../../common/fe_memutils.c ../../common/username.c \
                    ../../common/wait_error.c
 GETTEXT_TRIGGERS = $(FRONTEND_COMMON_GETTEXT_TRIGGERS) \
-                   N_ simple_prompt
+                   N_ simple_prompt simple_prompt_extended
 GETTEXT_FLAGS    = $(FRONTEND_COMMON_GETTEXT_FLAGS)
index a80d196156d6fc30dab234c682af4be5db15059c..889472b28d15364f18aab78a7e9857092a15948f 100644 (file)
@@ -18,6 +18,8 @@
 #include "postgres_fe.h"
 #endif
 
+#include <setjmp.h>
+
 #include "common/string.h"
 #include "lib/stringinfo.h"
 
  * to collect lots of long-lived data.  A less memory-hungry option
  * is to use pg_get_line_buf() or pg_get_line_append() in a loop,
  * then pstrdup() each line.
+ *
+ * prompt_ctx can optionally be provided to allow this function to be
+ * canceled via an existing SIGINT signal handler that will longjmp to the
+ * specified place only when *(prompt_ctx->enabled) is true.  If canceled,
+ * this function returns NULL, and prompt_ctx->canceled is set to true.
  */
 char *
-pg_get_line(FILE *stream)
+pg_get_line(FILE *stream, PromptInterruptContext *prompt_ctx)
 {
        StringInfoData buf;
 
        initStringInfo(&buf);
 
-       if (!pg_get_line_append(stream, &buf))
+       if (!pg_get_line_append(stream, &buf, prompt_ctx))
        {
                /* ensure that free() doesn't mess up errno */
                int                     save_errno = errno;
@@ -89,7 +96,7 @@ pg_get_line_buf(FILE *stream, StringInfo buf)
 {
        /* We just need to drop any data from the previous call */
        resetStringInfo(buf);
-       return pg_get_line_append(stream, buf);
+       return pg_get_line_append(stream, buf, NULL);
 }
 
 /*
@@ -107,15 +114,48 @@ pg_get_line_buf(FILE *stream, StringInfo buf)
  *
  * In the false-result case, the contents of *buf are logically unmodified,
  * though it's possible that the buffer has been resized.
+ *
+ * prompt_ctx can optionally be provided to allow this function to be
+ * canceled via an existing SIGINT signal handler that will longjmp to the
+ * specified place only when *(prompt_ctx->enabled) is true.  If canceled,
+ * this function returns false, and prompt_ctx->canceled is set to true.
  */
 bool
-pg_get_line_append(FILE *stream, StringInfo buf)
+pg_get_line_append(FILE *stream, StringInfo buf,
+                                  PromptInterruptContext *prompt_ctx)
 {
        int                     orig_len = buf->len;
 
-       /* Read some data, appending it to whatever we already have */
-       while (fgets(buf->data + buf->len, buf->maxlen - buf->len, stream) != NULL)
+       if (prompt_ctx && sigsetjmp(*((sigjmp_buf *) prompt_ctx->jmpbuf), 1) != 0)
        {
+               /* Got here with longjmp */
+               prompt_ctx->canceled = true;
+               /* Discard any data we collected before detecting error */
+               buf->len = orig_len;
+               buf->data[orig_len] = '\0';
+               return false;
+       }
+
+       /* Loop until newline or EOF/error */
+       for (;;)
+       {
+               char       *res;
+
+               /* Enable longjmp while waiting for input */
+               if (prompt_ctx)
+                       *(prompt_ctx->enabled) = true;
+
+               /* Read some data, appending it to whatever we already have */
+               res = fgets(buf->data + buf->len, buf->maxlen - buf->len, stream);
+
+               /* Disable longjmp again, then break if fgets failed */
+               if (prompt_ctx)
+                       *(prompt_ctx->enabled) = false;
+
+               if (res == NULL)
+                       break;
+
+               /* Got data, so update buf->len */
                buf->len += strlen(buf->data + buf->len);
 
                /* Done if we have collected a newline */
index f3a891a260ba3d644f03803012856ddade296c9b..917676b58ceee8476fe170db48258a7f3458e4e8 100644 (file)
  */
 char *
 simple_prompt(const char *prompt, bool echo)
+{
+       return simple_prompt_extended(prompt, echo, NULL);
+}
+
+/*
+ * simple_prompt_extended
+ *
+ * This is the same as simple_prompt(), except that prompt_ctx can
+ * optionally be provided to allow this function to be canceled via an
+ * existing SIGINT signal handler that will longjmp to the specified place
+ * only when *(prompt_ctx->enabled) is true.  If canceled, this function
+ * returns an empty string, and prompt_ctx->canceled is set to true.
+ */
+char *
+simple_prompt_extended(const char *prompt, bool echo,
+                                          PromptInterruptContext *prompt_ctx)
 {
        char       *result;
        FILE       *termin,
@@ -126,7 +142,7 @@ simple_prompt(const char *prompt, bool echo)
                fflush(termout);
        }
 
-       result = pg_get_line(termin);
+       result = pg_get_line(termin, prompt_ctx);
 
        /* If we failed to read anything, just return an empty string */
        if (result == NULL)
index 686c158efe732a03138792b480a92e6af21ec02d..8eb5271ec866cab6b927e021fde8c7ff651c9090 100644 (file)
 
 struct StringInfoData;                 /* avoid including stringinfo.h here */
 
+typedef struct PromptInterruptContext
+{
+       /* To avoid including <setjmp.h> here, jmpbuf is declared "void *" */
+       void       *jmpbuf;                     /* existing longjmp buffer */
+       volatile bool *enabled;         /* flag that enables longjmp-on-interrupt */
+       bool            canceled;               /* indicates whether cancellation occurred */
+} PromptInterruptContext;
+
 /* functions in src/common/string.c */
 extern bool pg_str_endswith(const char *str, const char *end);
 extern int     strtoint(const char *pg_restrict str, char **pg_restrict endptr,
@@ -21,11 +29,14 @@ extern int  pg_strip_crlf(char *str);
 extern bool pg_is_ascii(const char *str);
 
 /* functions in src/common/pg_get_line.c */
-extern char *pg_get_line(FILE *stream);
+extern char *pg_get_line(FILE *stream, PromptInterruptContext *prompt_ctx);
 extern bool pg_get_line_buf(FILE *stream, struct StringInfoData *buf);
-extern bool pg_get_line_append(FILE *stream, struct StringInfoData *buf);
+extern bool pg_get_line_append(FILE *stream, struct StringInfoData *buf,
+                                                          PromptInterruptContext *prompt_ctx);
 
 /* functions in src/common/sprompt.c */
 extern char *simple_prompt(const char *prompt, bool echo);
+extern char *simple_prompt_extended(const char *prompt, bool echo,
+                                                                       PromptInterruptContext *prompt_ctx);
 
 #endif                                                 /* COMMON_STRING_H */