Message ID | 20220304131126.8293-5-phillip.wood123@gmail.com (mailing list archive) |
---|---|
State | Superseded |
Headers | show |
Series | builtin add -p: hopefully final readkey fixes | expand |
On Fri, Mar 04 2022, Phillip Wood wrote: > From: Phillip Wood <phillip.wood@dunelm.org.uk> > > 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> > --- > compat/terminal.c | 124 ++++++++++++++++++++++++++++++++++++++++++++-- > 1 file changed, 120 insertions(+), 4 deletions(-) > > diff --git a/compat/terminal.c b/compat/terminal.c > index 5d516ff546..79ab54c2f8 100644 > --- a/compat/terminal.c > +++ b/compat/terminal.c > @@ -1,4 +1,4 @@ > -#include "git-compat-util.h" > +#include "cache.h" > #include "compat/terminal.h" > #include "sigchain.h" > #include "strbuf.h" > @@ -23,6 +23,89 @@ static void restore_term_on_signal(int sig) > static int term_fd = -1; > static struct termios old_term; > > +static char *background_resume_msg; > +static char *restore_error_msg; > +static volatile sig_atomic_t ttou_received; > + > +static void write_msg(const char *msg) > +{ > + 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_msg(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_msg(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_msg(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_msg(restore_error_msg); > + sigaction(SIGTTOU, &old_sa, NULL); > + out: > + errno = saved_errno; > +} > + > void restore_term(void) > { > if (term_fd < 0) > @@ -32,10 +115,19 @@ void restore_term(void) > close(term_fd); > term_fd = -1; > sigchain_pop_common(); > + if (restore_error_msg) { > + signal(SIGTTIN, SIG_DFL); > + signal(SIGTTOU, SIG_DFL); > + signal(SIGTSTP, SIG_DFL); > + FREE_AND_NULL(restore_error_msg); > + FREE_AND_NULL(background_resume_msg); > + } > } > > int save_term(unsigned flags) > { > + struct sigaction sa; > + > if (term_fd < 0) > term_fd = (flags & SAVE_TERM_STDIN) ? 0 > : open("/dev/tty", O_RDWR); > @@ -44,6 +136,26 @@ int save_term(unsigned flags) > if (tcgetattr(term_fd, &old_term) < 0) > 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 = xstrdup(_("error: cannot resume in the background")); > + restore_error_msg = xstrdup(_("error: cannot restore terminal settings")); You don't need to xstrdup() the return values of gettext() (here _()), you'll get a pointer to static storage that's safe to hold on to for the duration of the program. In this case I think it would make sense to skip "error: " from the message itself. Eventually we'll get to making usage.c have that prefix translated, and can have some utility function exposed there (I have WIP patches for this already since a while ago). To translators it'll look like the same thing, and avoid churn when we make the "error: " prefix translatable. Aside: If you do keep the xstrdup() (perhaps an xstrfmt() with the above advice...) doesn't it make sense to add the "\n" here, so you'll have one write_in_full() above?
Hi Ævar On 05/03/2022 13:59, Ævar Arnfjörð Bjarmason wrote: > [...] >> int save_term(unsigned flags) >> { >> + struct sigaction sa; >> + >> if (term_fd < 0) >> term_fd = (flags & SAVE_TERM_STDIN) ? 0 >> : open("/dev/tty", O_RDWR); >> @@ -44,6 +136,26 @@ int save_term(unsigned flags) >> if (tcgetattr(term_fd, &old_term) < 0) >> 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 = xstrdup(_("error: cannot resume in the background")); >> + restore_error_msg = xstrdup(_("error: cannot restore terminal settings")); > > You don't need to xstrdup() the return values of gettext() (here _()), > you'll get a pointer to static storage that's safe to hold on to for the > duration of the program. I had a look at the documentation and could not see anything about the lifetime of the returned string, all it says is "don't alter it" > In this case I think it would make sense to skip "error: " from the > message itself. > > Eventually we'll get to making usage.c have that prefix translated, and > can have some utility function exposed there (I have WIP patches for > this already since a while ago). > > To translators it'll look like the same thing, and avoid churn when we > make the "error: " prefix translatable. Unless we add a function that returns a string rather than printing the message I don't see how it avoids churn in the future. Having the whole string with the "error: " prefix translated here does not add any extra burden to translators - it is still the same number of strings to translate. > Aside: If you do keep the xstrdup() (perhaps an xstrfmt() with the above > advice...) doesn't it make sense to add the "\n" here, so you'll have > one write_in_full() above? I decided to keep the translated string simpler by omitting the newline, calling write_in_full() twice isn't a bit deal (I don't think the output can be split by a write from another thread or signal handler in between). Best Wishes Phillip
On Mon, Mar 07 2022, Phillip Wood wrote: > Hi Ævar > > On 05/03/2022 13:59, Ævar Arnfjörð Bjarmason wrote: >> [...] >>> int save_term(unsigned flags) >>> { >>> + struct sigaction sa; >>> + >>> if (term_fd < 0) >>> term_fd = (flags & SAVE_TERM_STDIN) ? 0 >>> : open("/dev/tty", O_RDWR); >>> @@ -44,6 +136,26 @@ int save_term(unsigned flags) >>> if (tcgetattr(term_fd, &old_term) < 0) >>> 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 = xstrdup(_("error: cannot resume in the background")); >>> + restore_error_msg = xstrdup(_("error: cannot restore terminal settings")); >> You don't need to xstrdup() the return values of gettext() (here >> _()), >> you'll get a pointer to static storage that's safe to hold on to for the >> duration of the program. > > I had a look at the documentation and could not see anything about the > lifetime of the returned string, all it says is "don't alter it" I think this is overed in "11.2.7 Optimization of the *gettext functions", a pedantic reading might suggest not, but what's meant with the combination of that API documentation & the description of how MO files work is that you're just getting pointers into the already-loaded translation catalog, so it's safe to hold onto the pointer and re-use it later. In any case, if we're going to be paranoid about gettext() it would make sense to propose that as some general change to how we use it, we rely on this assumption holding in a lot of our use of the API: git grep '= _\(' Rather than sneak that partcular new assumption in here in this already tricky code... >> In this case I think it would make sense to skip "error: " from the >> message itself. >> Eventually we'll get to making usage.c have that prefix translated, >> and >> can have some utility function exposed there (I have WIP patches for >> this already since a while ago). >> To translators it'll look like the same thing, and avoid churn when >> we >> make the "error: " prefix translatable. > > Unless we add a function that returns a string rather than printing > the message I don't see how it avoids churn in the future. Having the > whole string with the "error: " prefix translated here does not add > any extra burden to translators - it is still the same number of > strings to translate. Because translators translate "we failed" for most errors, not "error: we failed". If and when we convert it from "error: we failed" to "we failed" they'll need to translate it again (although to be fair, the translation cache will help). And even then it'll be one of very few exceptions where the "error: " currently that *is* translated. >> Aside: If you do keep the xstrdup() (perhaps an xstrfmt() with the above >> advice...) doesn't it make sense to add the "\n" here, so you'll have >> one write_in_full() above? > > I decided to keep the translated string simpler by omitting the > newline, calling write_in_full() twice isn't a bit deal (I don't think > the output can be split by a write from another thread or signal > handler in between). Makes sense. FWIW I meant if you're going to xstrdup() or xstrfmt() it anyway you could do: xstrfmt("error: %s\n", _("the error")) And then do one call to write_in_full(). But I think just: msg = _("the error"); And then: const char *const = pfx = "error: "; const size_t len = strlen(pfx); write_in_full(2, pfx, len); write_in_full(2, msg, strlen(msg)); write_in_full(2, "\n", 1); Makes more sense :)
On 07/03/2022 11:49, Ævar Arnfjörð Bjarmason wrote: > > On Mon, Mar 07 2022, Phillip Wood wrote: > >> Hi Ævar >> >> On 05/03/2022 13:59, Ævar Arnfjörð Bjarmason wrote: >>> [...] >>>> int save_term(unsigned flags) >>>> { >>>> + struct sigaction sa; >>>> + >>>> if (term_fd < 0) >>>> term_fd = (flags & SAVE_TERM_STDIN) ? 0 >>>> : open("/dev/tty", O_RDWR); >>>> @@ -44,6 +136,26 @@ int save_term(unsigned flags) >>>> if (tcgetattr(term_fd, &old_term) < 0) >>>> 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 = xstrdup(_("error: cannot resume in the background")); >>>> + restore_error_msg = xstrdup(_("error: cannot restore terminal settings")); >>> You don't need to xstrdup() the return values of gettext() (here >>> _()), >>> you'll get a pointer to static storage that's safe to hold on to for the >>> duration of the program. >> >> I had a look at the documentation and could not see anything about the >> lifetime of the returned string, all it says is "don't alter it" > > I think this is overed in "11.2.7 Optimization of the *gettext > functions", a pedantic reading might suggest not, but what's meant with > the combination of that API documentation & the description of how MO > files work is that you're just getting pointers into the already-loaded > translation catalog, so it's safe to hold onto the pointer and re-use it > later. Strictly that section only shows it is safe if there are no other calls to gettext() before the returned string is used. I agree the implementation is likely to be just returning static strings but I can't find anywhere that says another implementation (e.g. on macos/*bsd) has to do that. > In any case, if we're going to be paranoid about gettext() it would make > sense to propose that as some general change to how we use it, we rely > on this assumption holding in a lot of our use of the API: > > git grep '= _\(' > > Rather than sneak that partcular new assumption in here in this already > tricky code... The ones I looked at are mostly not calling gettext() again before using the translated string (there is one exception in builtin/remote.c). In restore_term() I'm checking if the messages are NULL to see if job control is enabled, I could use a flag but I'm inclined to just keep coping the strings. > >>> In this case I think it would make sense to skip "error: " from the >>> message itself. >>> Eventually we'll get to making usage.c have that prefix translated, >>> and >>> can have some utility function exposed there (I have WIP patches for >>> this already since a while ago). >>> To translators it'll look like the same thing, and avoid churn when >>> we >>> make the "error: " prefix translatable. >> >> Unless we add a function that returns a string rather than printing >> the message I don't see how it avoids churn in the future. Having the >> whole string with the "error: " prefix translated here does not add >> any extra burden to translators - it is still the same number of >> strings to translate. > > Because translators translate "we failed" for most errors, not "error: > we failed". > > If and when we convert it from "error: we failed" to "we failed" they'll > need to translate it again (although to be fair, the translation cache > will help). > > And even then it'll be one of very few exceptions where the "error: " > currently that *is* translated. > >>> Aside: If you do keep the xstrdup() (perhaps an xstrfmt() with the above >>> advice...) doesn't it make sense to add the "\n" here, so you'll have >>> one write_in_full() above? >> >> I decided to keep the translated string simpler by omitting the >> newline, calling write_in_full() twice isn't a bit deal (I don't think >> the output can be split by a write from another thread or signal >> handler in between). > > Makes sense. > > FWIW I meant if you're going to xstrdup() or xstrfmt() it anyway you > could do: > > xstrfmt("error: %s\n", _("the error")) > > And then do one call to write_in_full(). > > But I think just: > > msg = _("the error"); > > And then: > > const char *const = pfx = "error: "; > const size_t len = strlen(pfx); > > write_in_full(2, pfx, len); > write_in_full(2, msg, strlen(msg)); > write_in_full(2, "\n", 1); > > Makes more sense :) Agreed, I'll change that. Best Wishes Phillip
On Mon, Mar 07 2022, Phillip Wood wrote: > On 07/03/2022 11:49, Ævar Arnfjörð Bjarmason wrote: >> On Mon, Mar 07 2022, Phillip Wood wrote: >> >>> Hi Ævar >>> >>> On 05/03/2022 13:59, Ævar Arnfjörð Bjarmason wrote: >>>> [...] >>>>> int save_term(unsigned flags) >>>>> { >>>>> + struct sigaction sa; >>>>> + >>>>> if (term_fd < 0) >>>>> term_fd = (flags & SAVE_TERM_STDIN) ? 0 >>>>> : open("/dev/tty", O_RDWR); >>>>> @@ -44,6 +136,26 @@ int save_term(unsigned flags) >>>>> if (tcgetattr(term_fd, &old_term) < 0) >>>>> 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 = xstrdup(_("error: cannot resume in the background")); >>>>> + restore_error_msg = xstrdup(_("error: cannot restore terminal settings")); >>>> You don't need to xstrdup() the return values of gettext() (here >>>> _()), >>>> you'll get a pointer to static storage that's safe to hold on to for the >>>> duration of the program. >>> >>> I had a look at the documentation and could not see anything about the >>> lifetime of the returned string, all it says is "don't alter it" >> I think this is overed in "11.2.7 Optimization of the *gettext >> functions", a pedantic reading might suggest not, but what's meant with >> the combination of that API documentation & the description of how MO >> files work is that you're just getting pointers into the already-loaded >> translation catalog, so it's safe to hold onto the pointer and re-use it >> later. > > Strictly that section only shows it is safe if there are no other > calls to gettext() before the returned string is used. I agree the > implementation is likely to be just returning static strings but I > can't find anywhere that says another implementation (e.g. on > macos/*bsd) has to do that. I agree. I'm 99.99% sure this is safe & portable use of the API, but I'm having some trouble finding documentation for that... >> In any case, if we're going to be paranoid about gettext() it would make >> sense to propose that as some general change to how we use it, we rely >> on this assumption holding in a lot of our use of the API: >> git grep '= _\(' >> Rather than sneak that partcular new assumption in here in this >> already >> tricky code... > > The ones I looked at are mostly not calling gettext() again before > using the translated string (there is one exception in > builtin/remote.c). Doesn't validate_encoding() in convert.c, process_entry() in merge-ort.c, setup_unpack_trees_porcelain() in unpack-trees.c cmd_mv() in builtin/mv.c etc. qualify? I.e. for a hypothetical gettext() that always returned the same pointer and just overwrote it with the latest message those would all emit bad output, wouldn't they? > In restore_term() I'm checking if the messages are NULL to see if job > control is enabled, I could use a flag but I'm inclined to just keep > coping the strings. Checking if they're NULL is orthagonal to whether we xstrdup() them. I.e. you'd just skip the xstrdup() and replace the FREE_AND_NULL with a "= NULL" assignment, no? Anyway, *if* I'm right that it's not new general paranoia with how gettext() is used I still think splitting up that part of the change would make sense, just for future readers etc. who'd wonder why it is that this already tricky signal handling etc. code needs that particular bit of special behavior. >>>> In this case I think it would make sense to skip "error: " from the >>>> message itself. >>>> Eventually we'll get to making usage.c have that prefix translated, >>>> and >>>> can have some utility function exposed there (I have WIP patches for >>>> this already since a while ago). >>>> To translators it'll look like the same thing, and avoid churn when >>>> we >>>> make the "error: " prefix translatable. >>> >>> Unless we add a function that returns a string rather than printing >>> the message I don't see how it avoids churn in the future. Having the >>> whole string with the "error: " prefix translated here does not add >>> any extra burden to translators - it is still the same number of >>> strings to translate. >> Because translators translate "we failed" for most errors, not >> "error: >> we failed". >> If and when we convert it from "error: we failed" to "we failed" >> they'll >> need to translate it again (although to be fair, the translation cache >> will help). >> And even then it'll be one of very few exceptions where the "error: >> " >> currently that *is* translated. >> >>>> Aside: If you do keep the xstrdup() (perhaps an xstrfmt() with the above >>>> advice...) doesn't it make sense to add the "\n" here, so you'll have >>>> one write_in_full() above? >>> >>> I decided to keep the translated string simpler by omitting the >>> newline, calling write_in_full() twice isn't a bit deal (I don't think >>> the output can be split by a write from another thread or signal >>> handler in between). >> Makes sense. >> FWIW I meant if you're going to xstrdup() or xstrfmt() it anyway you >> could do: >> xstrfmt("error: %s\n", _("the error")) >> And then do one call to write_in_full(). >> But I think just: >> msg = _("the error"); >> And then: >> const char *const = pfx = "error: "; >> const size_t len = strlen(pfx); >> write_in_full(2, pfx, len); >> write_in_full(2, msg, strlen(msg)); >> write_in_full(2, "\n", 1); >> Makes more sense :) > > Agreed, I'll change that. > > Best Wishes > > Phillip
On 07/03/2022 14:45, Ævar Arnfjörð Bjarmason wrote: >[...] > On Mon, Mar 07 2022, Phillip Wood wrote: > >> On 07/03/2022 11:49, Ævar Arnfjörð Bjarmason wrote: >>> On Mon, Mar 07 2022, Phillip Wood wrote: >>> >>>> Hi Ævar >>>> >>>> On 05/03/2022 13:59, Ævar Arnfjörð Bjarmason wrote: >>>>> [...] >>>>>> int save_term(unsigned flags) >>>>>> { >>>>>> + struct sigaction sa; >>>>>> + >>>>>> if (term_fd < 0) >>>>>> term_fd = (flags & SAVE_TERM_STDIN) ? 0 >>>>>> : open("/dev/tty", O_RDWR); >>>>>> @@ -44,6 +136,26 @@ int save_term(unsigned flags) >>>>>> if (tcgetattr(term_fd, &old_term) < 0) >>>>>> 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 = xstrdup(_("error: cannot resume in the background")); >>>>>> + restore_error_msg = xstrdup(_("error: cannot restore terminal settings")); >>>>> You don't need to xstrdup() the return values of gettext() (here >>>>> _()), >>>>> you'll get a pointer to static storage that's safe to hold on to for the >>>>> duration of the program. >>>> >>>> I had a look at the documentation and could not see anything about the >>>> lifetime of the returned string, all it says is "don't alter it" >>> I think this is overed in "11.2.7 Optimization of the *gettext >>> functions", a pedantic reading might suggest not, but what's meant with >>> the combination of that API documentation & the description of how MO >>> files work is that you're just getting pointers into the already-loaded >>> translation catalog, so it's safe to hold onto the pointer and re-use it >>> later. >> >> Strictly that section only shows it is safe if there are no other >> calls to gettext() before the returned string is used. I agree the >> implementation is likely to be just returning static strings but I >> can't find anywhere that says another implementation (e.g. on >> macos/*bsd) has to do that. > > I agree. I'm 99.99% sure this is safe & portable use of the API, but I'm > having some trouble finding documentation for that... > >>> In any case, if we're going to be paranoid about gettext() it would make >>> sense to propose that as some general change to how we use it, we rely >>> on this assumption holding in a lot of our use of the API: >>> git grep '= _\(' >>> Rather than sneak that partcular new assumption in here in this >>> already >>> tricky code... >> >> The ones I looked at are mostly not calling gettext() again before >> using the translated string (there is one exception in >> builtin/remote.c). > > Doesn't validate_encoding() in convert.c, process_entry() in > merge-ort.c, setup_unpack_trees_porcelain() in unpack-trees.c cmd_mv() > in builtin/mv.c etc. qualify? I only checked a few, cmd_mv() always assigns to the same variable so the previous value is overwritten anyway, some of the others such as unpack_trees are assuming the return value is valid after a subsequent call to gettext(). I found[1] which states The string returned must not be modified by the program and can be invalidated by a subsequent call to bind_textdomain_codeset() or setlocale(3C). so I think we can drop the copying. > I.e. for a hypothetical gettext() that always returned the same pointer > and just overwrote it with the latest message those would all emit bad > output, wouldn't they? > >> In restore_term() I'm checking if the messages are NULL to see if job >> control is enabled, I could use a flag but I'm inclined to just keep >> coping the strings. > > Checking if they're NULL is orthagonal to whether we xstrdup() > them. I.e. you'd just skip the xstrdup() and replace the FREE_AND_NULL > with a "= NULL" assignment, no? Yes, I'm not sure what I was thinking when I wrote that. Best Wishes Phillip [1] https://docs.oracle.com/cd/E88353_01/html/E37843/gettext-3c.html#REFMAN3Agettext-3c
Hi Phillip, On Fri, 4 Mar 2022, Phillip Wood wrote: > From: Phillip Wood <phillip.wood@dunelm.org.uk> > > 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. This description makes sense. From my understanding of signals, the code also does make sense, but it is unfortunate that it has to be so much code to implement something as straight-forward as suspend/resume. FWIW I tested the `add -p` command with these patches on Windows and it still works as well as when I had developed it. Thank you, Dscho
Hi Dscho On 09/03/2022 12:19, Johannes Schindelin wrote: > Hi Phillip, > > On Fri, 4 Mar 2022, Phillip Wood wrote: > >> From: Phillip Wood <phillip.wood@dunelm.org.uk> >> >> 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. > > This description makes sense. From my understanding of signals, the code > also does make sense, but it is unfortunate that it has to be so much code > to implement something as straight-forward as suspend/resume. Yes it is a lot of code. It would be a bit simpler if we omitted the warning about resuming in the background but I think that is worth having. There's also a lot of changing signal masks to avoid stopping twice if the user presses ^Z a second time while the signal handler is active. > FWIW I tested the `add -p` command with these patches on Windows and it > still works as well as when I had developed it. Thanks for testing this on Windows, I don't think we have any meaningful test coverage for interactive.singlekey and it is probably tricky to add because it relies on having a tty. Best Wishes Phillip > Thank you, > Dscho
diff --git a/compat/terminal.c b/compat/terminal.c index 5d516ff546..79ab54c2f8 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -1,4 +1,4 @@ -#include "git-compat-util.h" +#include "cache.h" #include "compat/terminal.h" #include "sigchain.h" #include "strbuf.h" @@ -23,6 +23,89 @@ static void restore_term_on_signal(int sig) static int term_fd = -1; static struct termios old_term; +static char *background_resume_msg; +static char *restore_error_msg; +static volatile sig_atomic_t ttou_received; + +static void write_msg(const char *msg) +{ + 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_msg(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_msg(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_msg(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_msg(restore_error_msg); + sigaction(SIGTTOU, &old_sa, NULL); + out: + errno = saved_errno; +} + void restore_term(void) { if (term_fd < 0) @@ -32,10 +115,19 @@ void restore_term(void) close(term_fd); term_fd = -1; sigchain_pop_common(); + if (restore_error_msg) { + signal(SIGTTIN, SIG_DFL); + signal(SIGTTOU, SIG_DFL); + signal(SIGTSTP, SIG_DFL); + FREE_AND_NULL(restore_error_msg); + FREE_AND_NULL(background_resume_msg); + } } int save_term(unsigned flags) { + struct sigaction sa; + if (term_fd < 0) term_fd = (flags & SAVE_TERM_STDIN) ? 0 : open("/dev/tty", O_RDWR); @@ -44,6 +136,26 @@ int save_term(unsigned flags) if (tcgetattr(term_fd, &old_term) < 0) 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 = xstrdup(_("error: cannot resume in the background")); + restore_error_msg = xstrdup(_("error: 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; } @@ -93,6 +205,7 @@ static int getchar_with_timeout(int timeout) fd_set readfds; int res; + again: if (timeout >= 0) { tv.tv_sec = timeout / 1000; tv.tv_usec = (timeout % 1000) * 1000; @@ -102,9 +215,12 @@ static int getchar_with_timeout(int timeout) FD_ZERO(&readfds); FD_SET(0, &readfds); res = select(1, &readfds, NULL, NULL, tvp); - if (res < 0) - return EOF; - + if (res < 0) { + if (errno == EINTR) + goto again; + else + return EOF; + } return getchar(); }