diff mbox series

[4/4] terminal: restore settings on SIGTSTP

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

Commit Message

Phillip Wood March 4, 2022, 1:11 p.m. UTC
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(-)

Comments

Ævar Arnfjörð Bjarmason March 5, 2022, 1:59 p.m. UTC | #1
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?
Phillip Wood March 7, 2022, 10:53 a.m. UTC | #2
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
Ævar Arnfjörð Bjarmason March 7, 2022, 11:49 a.m. UTC | #3
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 :)
Phillip Wood March 7, 2022, 1:49 p.m. UTC | #4
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
Ævar Arnfjörð Bjarmason March 7, 2022, 2:45 p.m. UTC | #5
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
Phillip Wood March 8, 2022, 10:54 a.m. UTC | #6
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
Johannes Schindelin March 9, 2022, 12:19 p.m. UTC | #7
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
Phillip Wood March 10, 2022, 4:06 p.m. UTC | #8
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 mbox series

Patch

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();
 }