| #include "git-compat-util.h" |
| #include "compat/terminal.h" |
| #include "gettext.h" |
| #include "sigchain.h" |
| #include "strbuf.h" |
| #include "run-command.h" |
| #include "string-list.h" |
| #include "hashmap.h" |
| |
| #if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE) |
| |
| static void restore_term_on_signal(int sig) |
| { |
| restore_term(); |
| /* restore_term calls sigchain_pop_common */ |
| raise(sig); |
| } |
| |
| #ifdef HAVE_DEV_TTY |
| |
| #define INPUT_PATH "/dev/tty" |
| #define OUTPUT_PATH "/dev/tty" |
| |
| static volatile sig_atomic_t term_fd_needs_closing; |
| static int term_fd = -1; |
| static struct termios old_term; |
| |
| static const char *background_resume_msg; |
| static const char *restore_error_msg; |
| static volatile sig_atomic_t ttou_received; |
| |
| /* async safe error function for use by signal handlers. */ |
| static void write_err(const char *msg) |
| { |
| write_in_full(2, "error: ", strlen("error: ")); |
| write_in_full(2, msg, strlen(msg)); |
| write_in_full(2, "\n", 1); |
| } |
| |
| static void print_background_resume_msg(int signo) |
| { |
| int saved_errno = errno; |
| sigset_t mask; |
| struct sigaction old_sa; |
| struct sigaction sa = { .sa_handler = SIG_DFL }; |
| |
| ttou_received = 1; |
| write_err(background_resume_msg); |
| sigaction(signo, &sa, &old_sa); |
| raise(signo); |
| sigemptyset(&mask); |
| sigaddset(&mask, signo); |
| sigprocmask(SIG_UNBLOCK, &mask, NULL); |
| /* Stopped here */ |
| sigprocmask(SIG_BLOCK, &mask, NULL); |
| sigaction(signo, &old_sa, NULL); |
| errno = saved_errno; |
| } |
| |
| static void restore_terminal_on_suspend(int signo) |
| { |
| int saved_errno = errno; |
| int res; |
| struct termios t; |
| sigset_t mask; |
| struct sigaction old_sa; |
| struct sigaction sa = { .sa_handler = SIG_DFL }; |
| int can_restore = 1; |
| |
| if (tcgetattr(term_fd, &t) < 0) |
| can_restore = 0; |
| |
| if (tcsetattr(term_fd, TCSAFLUSH, &old_term) < 0) |
| write_err(restore_error_msg); |
| |
| sigaction(signo, &sa, &old_sa); |
| raise(signo); |
| sigemptyset(&mask); |
| sigaddset(&mask, signo); |
| sigprocmask(SIG_UNBLOCK, &mask, NULL); |
| /* Stopped here */ |
| sigprocmask(SIG_BLOCK, &mask, NULL); |
| sigaction(signo, &old_sa, NULL); |
| if (!can_restore) { |
| write_err(restore_error_msg); |
| goto out; |
| } |
| /* |
| * If we resume in the background then we receive SIGTTOU when calling |
| * tcsetattr() below. Set up a handler to print an error message in that |
| * case. |
| */ |
| sigemptyset(&mask); |
| sigaddset(&mask, SIGTTOU); |
| sa.sa_mask = old_sa.sa_mask; |
| sa.sa_handler = print_background_resume_msg; |
| sa.sa_flags = SA_RESTART; |
| sigaction(SIGTTOU, &sa, &old_sa); |
| again: |
| ttou_received = 0; |
| sigprocmask(SIG_UNBLOCK, &mask, NULL); |
| res = tcsetattr(term_fd, TCSAFLUSH, &t); |
| sigprocmask(SIG_BLOCK, &mask, NULL); |
| if (ttou_received) |
| goto again; |
| else if (res < 0) |
| write_err(restore_error_msg); |
| sigaction(SIGTTOU, &old_sa, NULL); |
| out: |
| errno = saved_errno; |
| } |
| |
| static void reset_job_signals(void) |
| { |
| if (restore_error_msg) { |
| signal(SIGTTIN, SIG_DFL); |
| signal(SIGTTOU, SIG_DFL); |
| signal(SIGTSTP, SIG_DFL); |
| restore_error_msg = NULL; |
| background_resume_msg = NULL; |
| } |
| } |
| |
| static void close_term_fd(void) |
| { |
| if (term_fd_needs_closing) |
| close(term_fd); |
| term_fd_needs_closing = 0; |
| term_fd = -1; |
| } |
| |
| void restore_term(void) |
| { |
| if (term_fd < 0) |
| return; |
| |
| tcsetattr(term_fd, TCSAFLUSH, &old_term); |
| close_term_fd(); |
| sigchain_pop_common(); |
| reset_job_signals(); |
| } |
| |
| int save_term(enum save_term_flags flags) |
| { |
| struct sigaction sa; |
| |
| if (term_fd < 0) |
| term_fd = ((flags & SAVE_TERM_STDIN) |
| ? 0 |
| : open("/dev/tty", O_RDWR)); |
| if (term_fd < 0) |
| return -1; |
| term_fd_needs_closing = !(flags & SAVE_TERM_STDIN); |
| if (tcgetattr(term_fd, &old_term) < 0) { |
| close_term_fd(); |
| return -1; |
| } |
| sigchain_push_common(restore_term_on_signal); |
| /* |
| * If job control is disabled then the shell will have set the |
| * disposition of SIGTSTP to SIG_IGN. |
| */ |
| sigaction(SIGTSTP, NULL, &sa); |
| if (sa.sa_handler == SIG_IGN) |
| return 0; |
| |
| /* avoid calling gettext() from signal handler */ |
| background_resume_msg = _("cannot resume in the background, please use 'fg' to resume"); |
| restore_error_msg = _("cannot restore terminal settings"); |
| sa.sa_handler = restore_terminal_on_suspend; |
| sa.sa_flags = SA_RESTART; |
| sigemptyset(&sa.sa_mask); |
| sigaddset(&sa.sa_mask, SIGTSTP); |
| sigaddset(&sa.sa_mask, SIGTTIN); |
| sigaddset(&sa.sa_mask, SIGTTOU); |
| sigaction(SIGTSTP, &sa, NULL); |
| sigaction(SIGTTIN, &sa, NULL); |
| sigaction(SIGTTOU, &sa, NULL); |
| |
| return 0; |
| } |
| |
| static int disable_bits(enum save_term_flags flags, tcflag_t bits) |
| { |
| struct termios t; |
| |
| if (save_term(flags) < 0) |
| return -1; |
| |
| t = old_term; |
| |
| t.c_lflag &= ~bits; |
| if (bits & ICANON) { |
| t.c_cc[VMIN] = 1; |
| t.c_cc[VTIME] = 0; |
| } |
| if (!tcsetattr(term_fd, TCSAFLUSH, &t)) |
| return 0; |
| |
| sigchain_pop_common(); |
| reset_job_signals(); |
| close_term_fd(); |
| return -1; |
| } |
| |
| static int disable_echo(enum save_term_flags flags) |
| { |
| return disable_bits(flags, ECHO); |
| } |
| |
| static int enable_non_canonical(enum save_term_flags flags) |
| { |
| return disable_bits(flags, ICANON | ECHO); |
| } |
| |
| /* |
| * On macos it is not possible to use poll() with a terminal so use select |
| * instead. |
| */ |
| static int getchar_with_timeout(int timeout) |
| { |
| struct timeval tv, *tvp = NULL; |
| fd_set readfds; |
| int res; |
| |
| again: |
| if (timeout >= 0) { |
| tv.tv_sec = timeout / 1000; |
| tv.tv_usec = (timeout % 1000) * 1000; |
| tvp = &tv; |
| } |
| |
| FD_ZERO(&readfds); |
| FD_SET(0, &readfds); |
| res = select(1, &readfds, NULL, NULL, tvp); |
| if (!res) |
| return EOF; |
| if (res < 0) { |
| if (errno == EINTR) |
| goto again; |
| else |
| return EOF; |
| } |
| return getchar(); |
| } |
| |
| #elif defined(GIT_WINDOWS_NATIVE) |
| |
| #define INPUT_PATH "CONIN$" |
| #define OUTPUT_PATH "CONOUT$" |
| #define FORCE_TEXT "t" |
| |
| static int use_stty = 1; |
| static struct string_list stty_restore = STRING_LIST_INIT_DUP; |
| static HANDLE hconin = INVALID_HANDLE_VALUE; |
| static HANDLE hconout = INVALID_HANDLE_VALUE; |
| static DWORD cmode_in, cmode_out; |
| |
| void restore_term(void) |
| { |
| if (use_stty) { |
| int i; |
| struct child_process cp = CHILD_PROCESS_INIT; |
| |
| if (stty_restore.nr == 0) |
| return; |
| |
| strvec_push(&cp.args, "stty"); |
| for (i = 0; i < stty_restore.nr; i++) |
| strvec_push(&cp.args, stty_restore.items[i].string); |
| run_command(&cp); |
| string_list_clear(&stty_restore, 0); |
| return; |
| } |
| |
| sigchain_pop_common(); |
| |
| if (hconin == INVALID_HANDLE_VALUE) |
| return; |
| |
| SetConsoleMode(hconin, cmode_in); |
| CloseHandle(hconin); |
| if (cmode_out) { |
| assert(hconout != INVALID_HANDLE_VALUE); |
| SetConsoleMode(hconout, cmode_out); |
| CloseHandle(hconout); |
| } |
| |
| hconin = hconout = INVALID_HANDLE_VALUE; |
| } |
| |
| int save_term(enum save_term_flags flags) |
| { |
| hconin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE, |
| FILE_SHARE_READ, NULL, OPEN_EXISTING, |
| FILE_ATTRIBUTE_NORMAL, NULL); |
| if (hconin == INVALID_HANDLE_VALUE) |
| return -1; |
| |
| if (flags & SAVE_TERM_DUPLEX) { |
| hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, |
| FILE_SHARE_WRITE, NULL, OPEN_EXISTING, |
| FILE_ATTRIBUTE_NORMAL, NULL); |
| if (hconout == INVALID_HANDLE_VALUE) |
| goto error; |
| |
| GetConsoleMode(hconout, &cmode_out); |
| } |
| |
| GetConsoleMode(hconin, &cmode_in); |
| use_stty = 0; |
| sigchain_push_common(restore_term_on_signal); |
| return 0; |
| error: |
| CloseHandle(hconin); |
| hconin = INVALID_HANDLE_VALUE; |
| return -1; |
| } |
| |
| static int disable_bits(enum save_term_flags flags, DWORD bits) |
| { |
| if (use_stty) { |
| struct child_process cp = CHILD_PROCESS_INIT; |
| |
| strvec_push(&cp.args, "stty"); |
| |
| if (bits & ENABLE_LINE_INPUT) { |
| string_list_append(&stty_restore, "icanon"); |
| /* |
| * POSIX allows VMIN and VTIME to overlap with VEOF and |
| * VEOL - let's hope that is not the case on windows. |
| */ |
| strvec_pushl(&cp.args, "-icanon", "min", "1", "time", "0", NULL); |
| } |
| |
| if (bits & ENABLE_ECHO_INPUT) { |
| string_list_append(&stty_restore, "echo"); |
| strvec_push(&cp.args, "-echo"); |
| } |
| |
| if (bits & ENABLE_PROCESSED_INPUT) { |
| string_list_append(&stty_restore, "-ignbrk"); |
| string_list_append(&stty_restore, "intr"); |
| string_list_append(&stty_restore, "^c"); |
| strvec_push(&cp.args, "ignbrk"); |
| strvec_push(&cp.args, "intr"); |
| strvec_push(&cp.args, ""); |
| } |
| |
| if (run_command(&cp) == 0) |
| return 0; |
| |
| /* `stty` could not be executed; access the Console directly */ |
| use_stty = 0; |
| } |
| |
| if (save_term(flags) < 0) |
| return -1; |
| |
| if (!SetConsoleMode(hconin, cmode_in & ~bits)) { |
| CloseHandle(hconin); |
| hconin = INVALID_HANDLE_VALUE; |
| sigchain_pop_common(); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| static int disable_echo(enum save_term_flags flags) |
| { |
| return disable_bits(flags, ENABLE_ECHO_INPUT); |
| } |
| |
| static int enable_non_canonical(enum save_term_flags flags) |
| { |
| return disable_bits(flags, |
| ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); |
| } |
| |
| /* |
| * Override `getchar()`, as the default implementation does not use |
| * `ReadFile()`. |
| * |
| * This poses a problem when we want to see whether the standard |
| * input has more characters, as the default of Git for Windows is to start the |
| * Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case |
| * our `poll()` emulation calls `PeekNamedPipe()`, which seems to require |
| * `ReadFile()` to be called first to work properly (it only reports 0 |
| * available bytes, otherwise). |
| * |
| * So let's just override `getchar()` with a version backed by `ReadFile()` and |
| * go our merry ways from here. |
| */ |
| static int mingw_getchar(void) |
| { |
| DWORD read = 0; |
| unsigned char ch; |
| |
| if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL)) |
| return EOF; |
| |
| if (!read) { |
| error("Unexpected 0 read"); |
| return EOF; |
| } |
| |
| return ch; |
| } |
| #define getchar mingw_getchar |
| |
| static int getchar_with_timeout(int timeout) |
| { |
| struct pollfd pfd = { .fd = 0, .events = POLLIN }; |
| |
| if (poll(&pfd, 1, timeout) < 1) |
| return EOF; |
| |
| return getchar(); |
| } |
| |
| #endif |
| |
| #ifndef FORCE_TEXT |
| #define FORCE_TEXT |
| #endif |
| |
| char *git_terminal_prompt(const char *prompt, int echo) |
| { |
| static struct strbuf buf = STRBUF_INIT; |
| int r; |
| FILE *input_fh, *output_fh; |
| |
| input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT); |
| if (!input_fh) |
| return NULL; |
| |
| output_fh = fopen(OUTPUT_PATH, "w" FORCE_TEXT); |
| if (!output_fh) { |
| fclose(input_fh); |
| return NULL; |
| } |
| |
| if (!echo && disable_echo(0)) { |
| fclose(input_fh); |
| fclose(output_fh); |
| return NULL; |
| } |
| |
| fputs(prompt, output_fh); |
| fflush(output_fh); |
| |
| r = strbuf_getline_lf(&buf, input_fh); |
| if (!echo) { |
| putc('\n', output_fh); |
| fflush(output_fh); |
| } |
| |
| restore_term(); |
| fclose(input_fh); |
| fclose(output_fh); |
| |
| if (r == EOF) |
| return NULL; |
| return buf.buf; |
| } |
| |
| /* |
| * The `is_known_escape_sequence()` function returns 1 if the passed string |
| * corresponds to an Escape sequence that the terminal capabilities contains. |
| * |
| * To avoid depending on ncurses or other platform-specific libraries, we rely |
| * on the presence of the `infocmp` executable to do the job for us (failing |
| * silently if the program is not available or refused to run). |
| */ |
| struct escape_sequence_entry { |
| struct hashmap_entry entry; |
| char sequence[FLEX_ARRAY]; |
| }; |
| |
| static int sequence_entry_cmp(const void *hashmap_cmp_fn_data UNUSED, |
| const struct hashmap_entry *he1, |
| const struct hashmap_entry *he2, |
| const void *keydata) |
| { |
| const struct escape_sequence_entry |
| *e1 = container_of(he1, const struct escape_sequence_entry, entry), |
| *e2 = container_of(he2, const struct escape_sequence_entry, entry); |
| return strcmp(e1->sequence, keydata ? keydata : e2->sequence); |
| } |
| |
| static int is_known_escape_sequence(const char *sequence) |
| { |
| static struct hashmap sequences; |
| static int initialized; |
| |
| if (!initialized) { |
| struct child_process cp = CHILD_PROCESS_INIT; |
| struct strbuf buf = STRBUF_INIT; |
| char *p, *eol; |
| |
| hashmap_init(&sequences, sequence_entry_cmp, NULL, 0); |
| |
| strvec_pushl(&cp.args, "infocmp", "-L", "-1", NULL); |
| if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0)) |
| strbuf_setlen(&buf, 0); |
| |
| for (eol = p = buf.buf; *p; p = eol + 1) { |
| p = strchr(p, '='); |
| if (!p) |
| break; |
| p++; |
| eol = strchrnul(p, '\n'); |
| |
| if (starts_with(p, "\\E")) { |
| char *comma = memchr(p, ',', eol - p); |
| struct escape_sequence_entry *e; |
| |
| p[0] = '^'; |
| p[1] = '['; |
| FLEX_ALLOC_MEM(e, sequence, p, comma - p); |
| hashmap_entry_init(&e->entry, |
| strhash(e->sequence)); |
| hashmap_add(&sequences, &e->entry); |
| } |
| if (!*eol) |
| break; |
| } |
| initialized = 1; |
| } |
| |
| return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence); |
| } |
| |
| int read_key_without_echo(struct strbuf *buf) |
| { |
| static int warning_displayed; |
| int ch; |
| |
| if (warning_displayed || enable_non_canonical(SAVE_TERM_STDIN) < 0) { |
| if (!warning_displayed) { |
| warning("reading single keystrokes not supported on " |
| "this platform; reading line instead"); |
| warning_displayed = 1; |
| } |
| |
| return strbuf_getline(buf, stdin); |
| } |
| |
| strbuf_reset(buf); |
| ch = getchar(); |
| if (ch == EOF) { |
| restore_term(); |
| return EOF; |
| } |
| strbuf_addch(buf, ch); |
| |
| if (ch == '\033' /* ESC */) { |
| /* |
| * We are most likely looking at an Escape sequence. Let's try |
| * to read more bytes, waiting at most half a second, assuming |
| * that the sequence is complete if we did not receive any byte |
| * within that time. |
| * |
| * Start by replacing the Escape byte with ^[ */ |
| strbuf_splice(buf, buf->len - 1, 1, "^[", 2); |
| |
| /* |
| * Query the terminal capabilities once about all the Escape |
| * sequences it knows about, so that we can avoid waiting for |
| * half a second when we know that the sequence is complete. |
| */ |
| while (!is_known_escape_sequence(buf->buf)) { |
| ch = getchar_with_timeout(500); |
| if (ch == EOF) |
| break; |
| strbuf_addch(buf, ch); |
| } |
| } |
| |
| restore_term(); |
| return 0; |
| } |
| |
| #else |
| |
| int save_term(enum save_term_flags flags) |
| { |
| /* no duplex support available */ |
| return -!!(flags & SAVE_TERM_DUPLEX); |
| } |
| |
| void restore_term(void) |
| { |
| } |
| |
| char *git_terminal_prompt(const char *prompt, int echo) |
| { |
| return getpass(prompt); |
| } |
| |
| int read_key_without_echo(struct strbuf *buf) |
| { |
| static int warning_displayed; |
| const char *res; |
| |
| if (!warning_displayed) { |
| warning("reading single keystrokes not supported on this " |
| "platform; reading line instead"); |
| warning_displayed = 1; |
| } |
| |
| res = getpass(""); |
| strbuf_reset(buf); |
| if (!res) |
| return EOF; |
| strbuf_addstr(buf, res); |
| return 0; |
| } |
| |
| #endif |