terminal: restore settings on SIGTSTP

If the user suspends git while it is waiting for a keypress reset the terminal before stopping and restore the settings when git resumes. If the user tries to resume in the background print an error message (taking care to use async safe functions) before stopping again. Ideally we would reprint the prompt for the user when git resumes but this patch just restarts the read(). The signal handler is established with sigaction() rather than using sigchain_push() as this allows us to control the signal mask when the handler is invoked and ensure SA_RESTART is used to restart the read() when resuming. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Phillip Wood committed Mar 16, 2022 at 18:54 UTC 0f584debc74afefdd24449136ffb2c77859e38c9
1 file changed +129 -3
compat/terminal.c
+129 -3
@@ -1,4 +1,4 @@
1 -#include "git-compat-util.h"
1 +#include "cache.h"
2 #include "compat/terminal.h"
3 #include "sigchain.h"
4 #include "strbuf.h"
@@ -24,6 +24,102 @@ static volatile sig_atomic_t term_fd_needs_closing;
24 static int term_fd = -1;
25 static struct termios old_term;
26
27 +static const char *background_resume_msg;
28 +static const char *restore_error_msg;
29 +static volatile sig_atomic_t ttou_received;
30 +
31 +/* async safe error function for use by signal handlers. */
32 +static void write_err(const char *msg)
33 +{
34 + write_in_full(2, "error: ", strlen("error: "));
35 + write_in_full(2, msg, strlen(msg));
36 + write_in_full(2, "\n", 1);
37 +}
38 +
39 +static void print_background_resume_msg(int signo)
40 +{
41 + int saved_errno = errno;
42 + sigset_t mask;
43 + struct sigaction old_sa;
44 + struct sigaction sa = { .sa_handler = SIG_DFL };
45 +
46 + ttou_received = 1;
47 + write_err(background_resume_msg);
48 + sigaction(signo, &sa, &old_sa);
49 + raise(signo);
50 + sigemptyset(&mask);
51 + sigaddset(&mask, signo);
52 + sigprocmask(SIG_UNBLOCK, &mask, NULL);
53 + /* Stopped here */
54 + sigprocmask(SIG_BLOCK, &mask, NULL);
55 + sigaction(signo, &old_sa, NULL);
56 + errno = saved_errno;
57 +}
58 +
59 +static void restore_terminal_on_suspend(int signo)
60 +{
61 + int saved_errno = errno;
62 + int res;
63 + struct termios t;
64 + sigset_t mask;
65 + struct sigaction old_sa;
66 + struct sigaction sa = { .sa_handler = SIG_DFL };
67 + int can_restore = 1;
68 +
69 + if (tcgetattr(term_fd, &t) < 0)
70 + can_restore = 0;
71 +
72 + if (tcsetattr(term_fd, TCSAFLUSH, &old_term) < 0)
73 + write_err(restore_error_msg);
74 +
75 + sigaction(signo, &sa, &old_sa);
76 + raise(signo);
77 + sigemptyset(&mask);
78 + sigaddset(&mask, signo);
79 + sigprocmask(SIG_UNBLOCK, &mask, NULL);
80 + /* Stopped here */
81 + sigprocmask(SIG_BLOCK, &mask, NULL);
82 + sigaction(signo, &old_sa, NULL);
83 + if (!can_restore) {
84 + write_err(restore_error_msg);
85 + goto out;
86 + }
87 + /*
88 + * If we resume in the background then we receive SIGTTOU when calling
89 + * tcsetattr() below. Set up a handler to print an error message in that
90 + * case.
91 + */
92 + sigemptyset(&mask);
93 + sigaddset(&mask, SIGTTOU);
94 + sa.sa_mask = old_sa.sa_mask;
95 + sa.sa_handler = print_background_resume_msg;
96 + sa.sa_flags = SA_RESTART;
97 + sigaction(SIGTTOU, &sa, &old_sa);
98 + again:
99 + ttou_received = 0;
100 + sigprocmask(SIG_UNBLOCK, &mask, NULL);
101 + res = tcsetattr(term_fd, TCSAFLUSH, &t);
102 + sigprocmask(SIG_BLOCK, &mask, NULL);
103 + if (ttou_received)
104 + goto again;
105 + else if (res < 0)
106 + write_err(restore_error_msg);
107 + sigaction(SIGTTOU, &old_sa, NULL);
108 + out:
109 + errno = saved_errno;
110 +}
111 +
112 +static void reset_job_signals(void)
113 +{
114 + if (restore_error_msg) {
115 + signal(SIGTTIN, SIG_DFL);
116 + signal(SIGTTOU, SIG_DFL);
117 + signal(SIGTSTP, SIG_DFL);
118 + restore_error_msg = NULL;
119 + background_resume_msg = NULL;
120 + }
121 +}
122 +
123 static void close_term_fd(void)
124 {
125 if (term_fd_needs_closing)
@@ -40,10 +136,13 @@ void restore_term(void)
136 tcsetattr(term_fd, TCSAFLUSH, &old_term);
137 close_term_fd();
138 sigchain_pop_common();
139 + reset_job_signals();
140 }
141
142 int save_term(enum save_term_flags flags)
143 {
144 + struct sigaction sa;
145 +
146 if (term_fd < 0)
147 term_fd = ((flags & SAVE_TERM_STDIN)
148 ? 0
@@ -56,6 +155,26 @@ int save_term(enum save_term_flags flags)
155 return -1;
156 }
157 sigchain_push_common(restore_term_on_signal);
158 + /*
159 + * If job control is disabled then the shell will have set the
160 + * disposition of SIGTSTP to SIG_IGN.
161 + */
162 + sigaction(SIGTSTP, NULL, &sa);
163 + if (sa.sa_handler == SIG_IGN)
164 + return 0;
165 +
166 + /* avoid calling gettext() from signal handler */
167 + background_resume_msg = _("cannot resume in the background, please use 'fg' to resume");
168 + restore_error_msg = _("cannot restore terminal settings");
169 + sa.sa_handler = restore_terminal_on_suspend;
170 + sa.sa_flags = SA_RESTART;
171 + sigemptyset(&sa.sa_mask);
172 + sigaddset(&sa.sa_mask, SIGTSTP);
173 + sigaddset(&sa.sa_mask, SIGTTIN);
174 + sigaddset(&sa.sa_mask, SIGTTOU);
175 + sigaction(SIGTSTP, &sa, NULL);
176 + sigaction(SIGTTIN, &sa, NULL);
177 + sigaction(SIGTTOU, &sa, NULL);
178
179 return 0;
180 }
@@ -78,6 +197,7 @@ static int disable_bits(enum save_term_flags flags, tcflag_t bits)
197 return 0;
198
199 sigchain_pop_common();
200 + reset_job_signals();
201 close_term_fd();
202 return -1;
203 }
@@ -102,6 +222,7 @@ static int getchar_with_timeout(int timeout)
222 fd_set readfds;
223 int res;
224
225 + again:
226 if (timeout >= 0) {
227 tv.tv_sec = timeout / 1000;
228 tv.tv_usec = (timeout % 1000) * 1000;
@@ -111,9 +232,14 @@ static int getchar_with_timeout(int timeout)
232 FD_ZERO(&readfds);
233 FD_SET(0, &readfds);
234 res = select(1, &readfds, NULL, NULL, tvp);
114 - if (res <= 0)
235 + if (!res)
236 return EOF;
116 -
237 + if (res < 0) {
238 + if (errno == EINTR)
239 + goto again;
240 + else
241 + return EOF;
242 + }
243 return getchar();
244 }
245