diff options
Diffstat (limited to 'subversion/libsvn_subr/prompt.c')
-rw-r--r-- | subversion/libsvn_subr/prompt.c | 610 |
1 files changed, 517 insertions, 93 deletions
diff --git a/subversion/libsvn_subr/prompt.c b/subversion/libsvn_subr/prompt.c index a4363a6..d0c29d0 100644 --- a/subversion/libsvn_subr/prompt.c +++ b/subversion/libsvn_subr/prompt.c @@ -29,8 +29,10 @@ #include <apr_lib.h> #include <apr_poll.h> +#include <apr_portable.h> #include "svn_cmdline.h" +#include "svn_ctype.h" #include "svn_string.h" #include "svn_auth.h" #include "svn_error.h" @@ -39,41 +41,441 @@ #include "private/svn_cmdline_private.h" #include "svn_private_config.h" +#ifdef WIN32 +#include <conio.h> +#elif defined(HAVE_TERMIOS_H) +#include <signal.h> +#include <termios.h> +#endif + -/* Wait for input on @a *f. Doing all allocations - * in @a pool. This functions is based on apr_wait_for_io_or_timeout(). - * Note that this will return an EINTR on a signal. - * - * ### FIX: When APR gives us a better way of doing this use it. */ -static apr_status_t wait_for_input(apr_file_t *f, - apr_pool_t *pool) +/* Descriptor of an open terminal */ +typedef struct terminal_handle_t terminal_handle_t; +struct terminal_handle_t { -#ifndef WIN32 - apr_pollfd_t pollset; - int srv, n; - - pollset.desc_type = APR_POLL_FILE; - pollset.desc.f = f; - pollset.p = pool; - pollset.reqevents = APR_POLLIN; - - srv = apr_poll(&pollset, 1, &n, -1); - - if (n == 1 && pollset.rtnevents & APR_POLLIN) - return APR_SUCCESS; - - return srv; -#else - /* APR specs say things that are unimplemented are supposed to return - * APR_ENOTIMPL. But when trying to use APR_POLL_FILE with apr_poll - * on Windows it returns APR_EBADF instead. So just return APR_ENOTIMPL - * ourselves here. - */ - return APR_ENOTIMPL; + apr_file_t *infd; /* input file handle */ + apr_file_t *outfd; /* output file handle */ + svn_boolean_t noecho; /* terminal echo was turned off */ + svn_boolean_t close_handles; /* close handles when closing the terminal */ + apr_pool_t *pool; /* pool associated with the file handles */ + +#ifdef HAVE_TERMIOS_H + svn_boolean_t restore_state; /* terminal state was changed */ + apr_os_file_t osinfd; /* OS-specific handle for infd */ + struct termios attr; /* saved terminal attributes */ #endif +}; + +/* Initialize safe state of terminal_handle_t. */ +static void +terminal_handle_init(terminal_handle_t *terminal, + apr_file_t *infd, apr_file_t *outfd, + svn_boolean_t noecho, svn_boolean_t close_handles, + apr_pool_t *pool) +{ + memset(terminal, 0, sizeof(*terminal)); + terminal->infd = infd; + terminal->outfd = outfd; + terminal->noecho = noecho; + terminal->close_handles = close_handles; + terminal->pool = pool; +} + +/* + * Common pool cleanup handler for terminal_handle_t. Closes TERMINAL. + * If CLOSE_HANDLES is TRUE, close the terminal file handles. + * If RESTORE_STATE is TRUE, restores the TERMIOS flags of the terminal. + */ +static apr_status_t +terminal_cleanup_handler(terminal_handle_t *terminal, + svn_boolean_t close_handles, + svn_boolean_t restore_state) +{ + apr_status_t status = APR_SUCCESS; + +#ifdef HAVE_TERMIOS_H + /* Restore terminal state flags. */ + if (restore_state && terminal->restore_state) + tcsetattr(terminal->osinfd, TCSANOW, &terminal->attr); +#endif + + /* Close terminal handles. */ + if (close_handles && terminal->close_handles) + { + apr_file_t *const infd = terminal->infd; + apr_file_t *const outfd = terminal->outfd; + + if (infd) + { + terminal->infd = NULL; + status = apr_file_close(infd); + } + + if (!status && outfd && outfd != infd) + { + terminal->outfd = NULL; + status = apr_file_close(terminal->outfd); + } + } + return status; } +/* Normal pool cleanup for a terminal. */ +static apr_status_t terminal_plain_cleanup(void *baton) +{ + return terminal_cleanup_handler(baton, FALSE, TRUE); +} + +/* Child pool cleanup for a terminal -- does not restore echo state. */ +static apr_status_t terminal_child_cleanup(void *baton) +{ + return terminal_cleanup_handler(baton, FALSE, FALSE); +} + +/* Explicitly close the terminal, removing its cleanup handlers. */ +static svn_error_t * +terminal_close(terminal_handle_t *terminal) +{ + apr_status_t status; + + /* apr_pool_cleanup_kill() removes both normal and child cleanup */ + apr_pool_cleanup_kill(terminal->pool, terminal, terminal_plain_cleanup); + + status = terminal_cleanup_handler(terminal, TRUE, TRUE); + if (status) + return svn_error_create(status, NULL, _("Can't close terminal")); + return SVN_NO_ERROR; +} + +/* Allocate and open *TERMINAL. If NOECHO is TRUE, try to turn off + terminal echo. Use POOL for all allocations.*/ +static svn_error_t * +terminal_open(terminal_handle_t **terminal, svn_boolean_t noecho, + apr_pool_t *pool) +{ + apr_status_t status; + +#ifdef WIN32 + /* On Windows, we'll use the console API directly if the process has + a console attached; otherwise we'll just use stdin and stderr. */ + const HANDLE conin = CreateFileW(L"CONIN$", GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + *terminal = apr_palloc(pool, sizeof(terminal_handle_t)); + if (conin != INVALID_HANDLE_VALUE) + { + /* The process has a console. */ + CloseHandle(conin); + terminal_handle_init(*terminal, NULL, NULL, noecho, FALSE, NULL); + return SVN_NO_ERROR; + } +#else /* !WIN32 */ + /* Without evidence to the contrary, we'll assume this is *nix and + try to open /dev/tty. If that fails, we'll use stdin for input + and stderr for prompting. */ + apr_file_t *tmpfd; + status = apr_file_open(&tmpfd, "/dev/tty", + APR_READ | APR_WRITE, + APR_OS_DEFAULT, pool); + *terminal = apr_palloc(pool, sizeof(terminal_handle_t)); + if (!status) + { + /* We have a terminal handle that we can use for input and output. */ + terminal_handle_init(*terminal, tmpfd, tmpfd, FALSE, TRUE, pool); + } +#endif /* !WIN32 */ + else + { + /* There is no terminal. Sigh. */ + apr_file_t *infd; + apr_file_t *outfd; + + status = apr_file_open_stdin(&infd, pool); + if (status) + return svn_error_wrap_apr(status, _("Can't open stdin")); + status = apr_file_open_stderr(&outfd, pool); + if (status) + return svn_error_wrap_apr(status, _("Can't open stderr")); + terminal_handle_init(*terminal, infd, outfd, FALSE, FALSE, pool); + } + +#ifdef HAVE_TERMIOS_H + /* Set terminal state */ + if (0 == apr_os_file_get(&(*terminal)->osinfd, (*terminal)->infd)) + { + if (0 == tcgetattr((*terminal)->osinfd, &(*terminal)->attr)) + { + struct termios attr = (*terminal)->attr; + /* Turn off signal handling and canonical input mode */ + attr.c_lflag &= ~(ISIG | ICANON); + attr.c_cc[VMIN] = 1; /* Read one byte at a time */ + attr.c_cc[VTIME] = 0; /* No timeout, wait indefinitely */ + attr.c_lflag &= ~(ECHO); /* Turn off echo */ + if (0 == tcsetattr((*terminal)->osinfd, TCSAFLUSH, &attr)) + { + (*terminal)->noecho = noecho; + (*terminal)->restore_state = TRUE; + } + } + } +#endif /* HAVE_TERMIOS_H */ + + /* Register pool cleanup to close handles and restore echo state. */ + apr_pool_cleanup_register((*terminal)->pool, *terminal, + terminal_plain_cleanup, + terminal_child_cleanup); + return SVN_NO_ERROR; +} + +/* Write a null-terminated STRING to TERMINAL. + Use POOL for allocations related to converting STRING from UTF-8. */ +static svn_error_t * +terminal_puts(const char *string, terminal_handle_t *terminal, + apr_pool_t *pool) +{ + svn_error_t *err; + apr_status_t status; + const char *converted; + + err = svn_cmdline_cstring_from_utf8(&converted, string, pool); + if (err) + { + svn_error_clear(err); + converted = svn_cmdline_cstring_from_utf8_fuzzy(string, pool); + } + +#ifdef WIN32 + if (!terminal->outfd) + { + /* See terminal_open; we're using Console I/O. */ + _cputs(converted); + return SVN_NO_ERROR; + } +#endif + + status = apr_file_write_full(terminal->outfd, converted, + strlen(converted), NULL); + if (!status) + status = apr_file_flush(terminal->outfd); + if (status) + return svn_error_wrap_apr(status, _("Can't write to terminal")); + return SVN_NO_ERROR; +} + +/* These codes can be returned from terminal_getc instead of a character. */ +#define TERMINAL_NONE 0x80000 /* no character read, retry */ +#define TERMINAL_DEL (TERMINAL_NONE + 1) /* the input was a deleteion */ +#define TERMINAL_EOL (TERMINAL_NONE + 2) /* end of input/end of line */ +#define TERMINAL_EOF (TERMINAL_NONE + 3) /* end of file during input */ + +/* Helper for terminal_getc: writes CH to OUTFD as a control char. */ +#ifndef WIN32 +static void +echo_control_char(char ch, apr_file_t *outfd) +{ + if (svn_ctype_iscntrl(ch)) + { + const char substitute = (ch < 32? '@' + ch : '?'); + apr_file_putc('^', outfd); + apr_file_putc(substitute, outfd); + } + else if (svn_ctype_isprint(ch)) + { + /* Pass printable characters unchanged. */ + apr_file_putc(ch, outfd); + } + else + { + /* Everything else is strange. */ + apr_file_putc('^', outfd); + apr_file_putc('!', outfd); + } +} +#endif /* WIN32 */ + +/* Read one character or control code from TERMINAL, returning it in CODE. + if CAN_ERASE and the input was a deletion, emit codes to erase the + last character displayed on the terminal. + Use POOL for all allocations. */ +static svn_error_t * +terminal_getc(int *code, terminal_handle_t *terminal, + svn_boolean_t can_erase, apr_pool_t *pool) +{ + const svn_boolean_t echo = !terminal->noecho; + apr_status_t status = APR_SUCCESS; + char ch; + +#ifdef WIN32 + if (!terminal->infd) + { + /* See terminal_open; we're using Console I/O. */ + + /* The following was hoisted from APR's getpass for Windows. */ + int concode = _getch(); + switch (concode) + { + case '\r': /* end-of-line */ + *code = TERMINAL_EOL; + if (echo) + _cputs("\r\n"); + break; + + case EOF: /* end-of-file */ + case 26: /* Ctrl+Z */ + *code = TERMINAL_EOF; + if (echo) + _cputs((concode == EOF ? "[EOF]\r\n" : "^Z\r\n")); + break; + + case 3: /* Ctrl+C, Ctrl+Break */ + /* _getch() bypasses Ctrl+C but not Ctrl+Break detection! */ + if (echo) + _cputs("^C\r\n"); + return svn_error_create(SVN_ERR_CANCELLED, NULL, NULL); + + case 0: /* Function code prefix */ + case 0xE0: + concode = (concode << 4) | _getch(); + /* Catch {DELETE}, {<--}, Num{DEL} and Num{<--} */ + if (concode == 0xE53 || concode == 0xE4B + || concode == 0x053 || concode == 0x04B) + { + *code = TERMINAL_DEL; + if (can_erase) + _cputs("\b \b"); + } + else + { + *code = TERMINAL_NONE; + _putch('\a'); + } + break; + + case '\b': /* BS */ + case 127: /* DEL */ + *code = TERMINAL_DEL; + if (can_erase) + _cputs("\b \b"); + break; + + default: + if (!apr_iscntrl(concode)) + { + *code = (int)(unsigned char)concode; + _putch(echo ? concode : '*'); + } + else + { + *code = TERMINAL_NONE; + _putch('\a'); + } + } + return SVN_NO_ERROR; + } +#elif defined(HAVE_TERMIOS_H) + if (terminal->restore_state) + { + /* We're using a bytewise-immediate termios input */ + const struct termios *const attr = &terminal->attr; + + status = apr_file_getc(&ch, terminal->infd); + if (status) + return svn_error_wrap_apr(status, _("Can't read from terminal")); + + if (ch == attr->c_cc[VINTR] || ch == attr->c_cc[VQUIT]) + { + /* Break */ + echo_control_char(ch, terminal->outfd); + return svn_error_create(SVN_ERR_CANCELLED, NULL, NULL); + } + else if (ch == '\r' || ch == '\n' || ch == attr->c_cc[VEOL]) + { + /* Newline */ + *code = TERMINAL_EOL; + apr_file_putc('\n', terminal->outfd); + } + else if (ch == '\b' || ch == attr->c_cc[VERASE]) + { + /* Delete */ + *code = TERMINAL_DEL; + if (can_erase) + { + apr_file_putc('\b', terminal->outfd); + apr_file_putc(' ', terminal->outfd); + apr_file_putc('\b', terminal->outfd); + } + } + else if (ch == attr->c_cc[VEOF]) + { + /* End of input */ + *code = TERMINAL_EOF; + echo_control_char(ch, terminal->outfd); + } + else if (ch == attr->c_cc[VSUSP]) + { + /* Suspend */ + *code = TERMINAL_NONE; + kill(0, SIGTSTP); + } + else if (!apr_iscntrl(ch)) + { + /* Normal character */ + *code = (int)(unsigned char)ch; + apr_file_putc((echo ? ch : '*'), terminal->outfd); + } + else + { + /* Ignored character */ + *code = TERMINAL_NONE; + apr_file_putc('\a', terminal->outfd); + } + return SVN_NO_ERROR; + } +#endif /* HAVE_TERMIOS_H */ + + /* Fall back to plain stream-based I/O. */ +#ifndef WIN32 + /* Wait for input on termin. This code is based on + apr_wait_for_io_or_timeout(). + Note that this will return an EINTR on a signal. */ + { + apr_pollfd_t pollset; + int n; + + pollset.desc_type = APR_POLL_FILE; + pollset.desc.f = terminal->infd; + pollset.p = pool; + pollset.reqevents = APR_POLLIN; + + status = apr_poll(&pollset, 1, &n, -1); + + if (n == 1 && pollset.rtnevents & APR_POLLIN) + status = APR_SUCCESS; + } +#endif /* !WIN32 */ + + if (!status) + status = apr_file_getc(&ch, terminal->infd); + if (APR_STATUS_IS_EINTR(status)) + { + *code = TERMINAL_NONE; + return SVN_NO_ERROR; + } + else if (APR_STATUS_IS_EOF(status)) + { + *code = TERMINAL_EOF; + return SVN_NO_ERROR; + } + else if (status) + return svn_error_wrap_apr(status, _("Can't read from terminal")); + + *code = (int)(unsigned char)ch; + return SVN_NO_ERROR; +} + + /* Set @a *result to the result of prompting the user with @a * prompt_msg. Use @ *pb to get the cancel_func and cancel_baton. * Do not call the cancel_func if @a *pb is NULL. @@ -91,77 +493,91 @@ prompt(const char **result, /* XXX: If this functions ever starts using members of *pb * which were not included in svn_cmdline_prompt_baton_t, * we need to update svn_cmdline_prompt_user2 and its callers. */ - apr_status_t status; - apr_file_t *fp; + + svn_boolean_t saw_first_half_of_eol = FALSE; + svn_stringbuf_t *strbuf = svn_stringbuf_create_empty(pool); + terminal_handle_t *terminal; + int code; char c; - svn_stringbuf_t *strbuf = svn_stringbuf_create("", pool); + SVN_ERR(terminal_open(&terminal, hide, pool)); + SVN_ERR(terminal_puts(prompt_msg, terminal, pool)); - status = apr_file_open_stdin(&fp, pool); - if (status) - return svn_error_wrap_apr(status, _("Can't open stdin")); - - if (! hide) + while (1) { - svn_boolean_t saw_first_half_of_eol = FALSE; - SVN_ERR(svn_cmdline_fputs(prompt_msg, stderr, pool)); - fflush(stderr); + SVN_ERR(terminal_getc(&code, terminal, (strbuf->len > 0), pool)); + + /* Check for cancellation after a character has been read, some + input processing modes may eat ^C and we'll only notice a + cancellation signal after characters have been read -- + sometimes even after a newline. */ + if (pb) + SVN_ERR(pb->cancel_func(pb->cancel_baton)); - while (1) + switch (code) { - /* Hack to allow us to not block for io on the prompt, so - * we can cancel. */ - if (pb) - SVN_ERR(pb->cancel_func(pb->cancel_baton)); - status = wait_for_input(fp, pool); - if (APR_STATUS_IS_EINTR(status)) - continue; - else if (status && status != APR_ENOTIMPL) - return svn_error_wrap_apr(status, _("Can't read stdin")); - - status = apr_file_getc(&c, fp); - if (status) - return svn_error_wrap_apr(status, _("Can't read stdin")); - - if (saw_first_half_of_eol) - { - if (c == APR_EOL_STR[1]) - break; - else - saw_first_half_of_eol = FALSE; - } - else if (c == APR_EOL_STR[0]) + case TERMINAL_NONE: + /* Nothing useful happened; retry. */ + continue; + + case TERMINAL_DEL: + /* Delete the last input character. terminal_getc takes care + of erasing the feedback from the terminal, if applicable. */ + svn_stringbuf_chop(strbuf, 1); + continue; + + case TERMINAL_EOL: + /* End-of-line means end of input. Trick the EOL-detection code + below to stop reading. */ + saw_first_half_of_eol = TRUE; + c = APR_EOL_STR[1]; /* Could be \0 but still stops reading. */ + break; + + case TERMINAL_EOF: + return svn_error_create( + APR_EOF, + terminal_close(terminal), + _("End of file while reading from terminal")); + + default: + /* Convert the returned code back to the character. */ + c = (char)code; + } + + if (saw_first_half_of_eol) + { + if (c == APR_EOL_STR[1]) + break; + else + saw_first_half_of_eol = FALSE; + } + else if (c == APR_EOL_STR[0]) + { + /* GCC might complain here: "warning: will never be executed" + * That's fine. This is a compile-time check for "\r\n\0" */ + if (sizeof(APR_EOL_STR) == 3) { - /* GCC might complain here: "warning: will never be executed" - * That's fine. This is a compile-time check for "\r\n\0" */ - if (sizeof(APR_EOL_STR) == 3) - { - saw_first_half_of_eol = TRUE; - continue; - } - else if (sizeof(APR_EOL_STR) == 2) - break; - else - /* ### APR_EOL_STR holds more than two chars? Who - ever heard of such a thing? */ - SVN_ERR_MALFUNCTION(); + saw_first_half_of_eol = TRUE; + continue; } - - svn_stringbuf_appendbyte(strbuf, c); + else if (sizeof(APR_EOL_STR) == 2) + break; + else + /* ### APR_EOL_STR holds more than two chars? Who + ever heard of such a thing? */ + SVN_ERR_MALFUNCTION(); } + + svn_stringbuf_appendbyte(strbuf, c); } - else - { - const char *prompt_stdout; - size_t bufsize = 300; - SVN_ERR(svn_cmdline_cstring_from_utf8(&prompt_stdout, prompt_msg, - pool)); - svn_stringbuf_ensure(strbuf, bufsize); - status = apr_password_get(prompt_stdout, strbuf->data, &bufsize); - if (status) - return svn_error_wrap_apr(status, _("Can't get password")); + if (terminal->noecho) + { + /* If terminal echo was turned off, make sure future output + to the terminal starts on a new line, as expected. */ + SVN_ERR(terminal_puts(APR_EOL_STR, terminal, pool)); } + SVN_ERR(terminal_close(terminal)); return svn_cmdline_cstring_to_utf8(result, strbuf->data, pool); } @@ -179,9 +595,13 @@ maybe_print_realm(const char *realm, apr_pool_t *pool) { if (realm) { - SVN_ERR(svn_cmdline_fprintf(stderr, pool, - _("Authentication realm: %s\n"), realm)); - fflush(stderr); + terminal_handle_t *terminal; + SVN_ERR(terminal_open(&terminal, FALSE, pool)); + SVN_ERR(terminal_puts( + apr_psprintf(pool, + _("Authentication realm: %s\n"), realm), + terminal, pool)); + SVN_ERR(terminal_close(terminal)); } return SVN_NO_ERROR; @@ -396,13 +816,17 @@ plaintext_prompt_helper(svn_boolean_t *may_save_plaintext, svn_boolean_t answered = FALSE; svn_cmdline_prompt_baton2_t *pb = baton; const char *config_path = NULL; + terminal_handle_t *terminal; if (pb) SVN_ERR(svn_config_get_user_config_path(&config_path, pb->config_dir, SVN_CONFIG_CATEGORY_SERVERS, pool)); - SVN_ERR(svn_cmdline_fprintf(stderr, pool, prompt_text, realmstring, - config_path)); + SVN_ERR(terminal_open(&terminal, FALSE, pool)); + SVN_ERR(terminal_puts(apr_psprintf(pool, prompt_text, + realmstring, config_path), + terminal, pool)); + SVN_ERR(terminal_close(terminal)); do { |