Provide a variant of simple_prompt() that can be interrupted by ^C.

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
This commit is contained in:
Tom Lane 2021-11-17 19:09:54 -05:00
parent a148f8bc04
commit 5f1148224b
7 changed files with 100 additions and 19 deletions

View 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);

View 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))

View 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

View 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)

View File

@ -18,6 +18,8 @@
#include "postgres_fe.h"
#endif
#include <setjmp.h>
#include "common/string.h"
#include "lib/stringinfo.h"
@ -47,15 +49,20 @@
* 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 */

View File

@ -36,6 +36,22 @@
*/
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)

View File

@ -12,6 +12,14 @@
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 */