From 24d7ce383a3d92feae6a641499448fde43206fd8 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 22 Feb 2022 18:53:32 +0000 Subject: [PATCH 1/4] terminal: always reset terminal when reading without echo Break out of the loop to ensure restore_term() is called before returning. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- compat/terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat/terminal.c b/compat/terminal.c index 5b903e7c7e3..fb8c70a6251 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -385,7 +385,7 @@ int read_key_without_echo(struct strbuf *buf) ch = getchar(); if (ch == EOF) - return 0; + break; strbuf_addch(buf, ch); } } From f7da756566e8abf18abdf9e30a8ed6d2088770de Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 22 Feb 2022 18:53:33 +0000 Subject: [PATCH 2/4] terminal: pop signal handler when terminal is restored When disable_bits() changes the terminal attributes it uses sigchain_push_common() to restore the terminal if a signal is received before restore_term() is called. However there is no corresponding call to sigchain_pop_common() when the settings are restored so the signal handler is left on the sigchain stack. This leaves the stack unbalanced so code such as sigchain_push_common(my_handler); ... read_key_without_echo(...); ... sigchain_pop_common(); pops the handler pushed by disable_bits() rather than the one it intended to. Additionally "git add -p" changes the terminal settings every time it reads a key press so the stack can grow significantly. In order to fix this save_term() now sets up the signal handler so restore_term() can unconditionally call sigchain_pop_common(). There are no callers of save_term() outside of terminal.c as the only external caller was removed by e3f7e01b50 ("Revert "editor: save and reset terminal after calling EDITOR"", 2021-11-22). Any future callers of save_term() should benefit from having the signal handler set up for them. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- compat/terminal.c | 17 +++++++++++++---- compat/terminal.h | 8 ++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/compat/terminal.c b/compat/terminal.c index fb8c70a6251..11288cfe5c9 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -11,7 +11,7 @@ static void restore_term_on_signal(int sig) { restore_term(); - sigchain_pop(sig); + /* restore_term calls sigchain_pop_common */ raise(sig); } @@ -31,14 +31,20 @@ void restore_term(void) tcsetattr(term_fd, TCSAFLUSH, &old_term); close(term_fd); term_fd = -1; + sigchain_pop_common(); } int save_term(int full_duplex) { if (term_fd < 0) term_fd = open("/dev/tty", O_RDWR); + if (term_fd < 0) + return -1; + if (tcgetattr(term_fd, &old_term) < 0) + return -1; + sigchain_push_common(restore_term_on_signal); - return (term_fd < 0) ? -1 : tcgetattr(term_fd, &old_term); + return 0; } static int disable_bits(tcflag_t bits) @@ -49,12 +55,12 @@ static int disable_bits(tcflag_t bits) goto error; t = old_term; - sigchain_push_common(restore_term_on_signal); t.c_lflag &= ~bits; if (!tcsetattr(term_fd, TCSAFLUSH, &t)) return 0; + sigchain_pop_common(); error: close(term_fd); term_fd = -1; @@ -100,6 +106,8 @@ void restore_term(void) return; } + sigchain_pop_common(); + if (hconin == INVALID_HANDLE_VALUE) return; @@ -134,6 +142,7 @@ int save_term(int full_duplex) GetConsoleMode(hconin, &cmode_in); use_stty = 0; + sigchain_push_common(restore_term_on_signal); return 0; error: CloseHandle(hconin); @@ -177,10 +186,10 @@ static int disable_bits(DWORD bits) if (save_term(0) < 0) return -1; - sigchain_push_common(restore_term_on_signal); if (!SetConsoleMode(hconin, cmode_in & ~bits)) { CloseHandle(hconin); hconin = INVALID_HANDLE_VALUE; + sigchain_pop_common(); return -1; } diff --git a/compat/terminal.h b/compat/terminal.h index e1770c575b2..0fb9fa147c4 100644 --- a/compat/terminal.h +++ b/compat/terminal.h @@ -1,7 +1,15 @@ #ifndef COMPAT_TERMINAL_H #define COMPAT_TERMINAL_H +/* + * Save the terminal attributes so they can be restored later by a + * call to restore_term(). Note that every successful call to + * save_term() must be matched by a call to restore_term() even if the + * attributes have not been changed. Returns 0 on success, -1 on + * failure. + */ int save_term(int full_duplex); +/* Restore the terminal attributes that were saved with save_term() */ void restore_term(void); char *git_terminal_prompt(const char *prompt, int echo); From 2c6860211f211dfa5625dd1a5a69a802b4384dbb Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 22 Feb 2022 18:53:34 +0000 Subject: [PATCH 3/4] terminal: set VMIN and VTIME in non-canonical mode If VMIN and VTIME are both set to zero then the terminal performs non-blocking reads which means that read_key_without_echo() returns EOF if there is no key press pending. This results in the user being unable to select anything when running "git add -p". Fix this by explicitly setting VMIN and VTIME when enabling non-canonical mode. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- compat/terminal.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/compat/terminal.c b/compat/terminal.c index 11288cfe5c9..3620184e790 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -57,6 +57,10 @@ static int disable_bits(tcflag_t bits) 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; @@ -159,7 +163,11 @@ static int disable_bits(DWORD bits) if (bits & ENABLE_LINE_INPUT) { string_list_append(&stty_restore, "icanon"); - strvec_push(&cp.args, "-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) { From ac618c418e5e16acd82dc3d8e4356a39150de5a2 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 22 Feb 2022 18:53:35 +0000 Subject: [PATCH 4/4] add -p: disable stdin buffering when interactive.singlekey is set The builtin "add -p" reads the key "F2" as three separate keys "^[", "O" and "Q". The "Q" causes it to quit which is probably not what the user was expecting. This is because it uses poll() to check for pending input when reading escape sequences but reads the input with getchar() which is buffered by default and so hoovers up all the pending input leading poll() think there isn't anything pending. Fix this by calling setbuf() to disable input buffering if interactive.singlekey is set. Looking at the comment above mingw_getchar() in terminal.c I wonder if that function is papering over this bug and could be removed. Unfortunately I don't have access to windows to test that. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- add-interactive.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index 6498ae196f1..ad78774ca26 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -70,6 +70,8 @@ void init_add_i_state(struct add_i_state *s, struct repository *r) &s->interactive_diff_algorithm); git_config_get_bool("interactive.singlekey", &s->use_single_key); + if (s->use_single_key) + setbuf(stdin, NULL); } void clear_add_i_state(struct add_i_state *s)