diff mbox series

[v4,5/6] test-terminal: introduce --no-stdin-pty

Message ID d95180fc-8f8a-4e1d-987d-3aa0811be7de@gmail.com (mailing list archive)
State New, archived
Headers show
Series use the pager in 'add -p' | expand

Commit Message

Rubén Justo June 3, 2024, 8:38 p.m. UTC
In 18d8c26930 (test_terminal: redirect child process' stdin to a pty,
2015-08-04), t/test-terminal.perl learned to connect the child process'
stdin to a pty.  It works well for what was intended: satisfying an
`isatty(STDIN_FILENO)` check.

However, the fork introduced, that copies the stdin to the child
process, does not always manage to send all the information.

To illustrate this behaviour, we can use a function like this:

    f ()
    {
    	dd if=/dev/zero bs=1 count=10000 status=none |
    	t/test-terminal.perl cat - 2>/dev/null |
    	wc -c;
    }

We do not obtain the expected results when executing this function
100 times:

    $ for i in $(seq 100); do f; done | sort | uniq -c
         36 0
          4 1
         53 4095
          7 4159

If we do the same with a version that does not redirect stdin, a version
prior to 18d8c26930, the expected result is obtained:

    $ git checkout 18d8c26930~1
    $ for i in $(seq 100); do f; done | sort | uniq -c
        100 10000

In a subsequent commit, a new test is going to rely on test-terminate,
and it does not require stdin to be connected to a terminal, but all
piped data needs to be successfully transmitted to the child process.

To make this possible, add a new parameter "--no-stdin-pty" to allow
disabling the stdin redirection though a pty.

Signed-off-by: Rubén Justo <rjusto@gmail.com>
---
 t/test-terminal.perl | 32 ++++++++++++++++++--------------
 1 file changed, 18 insertions(+), 14 deletions(-)

Comments

Phillip Wood June 4, 2024, 10:05 a.m. UTC | #1
Hi Rubén

On 03/06/2024 21:38, Rubén Justo wrote:
> In 18d8c26930 (test_terminal: redirect child process' stdin to a pty,
> 2015-08-04), t/test-terminal.perl learned to connect the child process'
> stdin to a pty.  It works well for what was intended: satisfying an
> `isatty(STDIN_FILENO)` check.
> 
> However, the fork introduced, that copies the stdin to the child
> process, does not always manage to send all the information.

I think the problem maybe to do with the use of File::Copy, not with the 
fork. The man page for the copy function says

     Note  that  passing  in  files  as  handles  instead  of  names may
     lead to loss of information on some operating systems; it is
     recommended that you use file names whenever possible.

Rather than adding a new flag to work around a bug in our script it 
might be better to try and fix the bug by using a loop that reads blocks 
of data from the source and writes them to the destination instead of 
calling copy.

Best Wishes

Phillip

> To illustrate this behaviour, we can use a function like this:
> 
>      f ()
>      {
>      	dd if=/dev/zero bs=1 count=10000 status=none |
>      	t/test-terminal.perl cat - 2>/dev/null |
>      	wc -c;
>      }
> 
> We do not obtain the expected results when executing this function
> 100 times:
> 
>      $ for i in $(seq 100); do f; done | sort | uniq -c
>           36 0
>            4 1
>           53 4095
>            7 4159
> 
> If we do the same with a version that does not redirect stdin, a version
> prior to 18d8c26930, the expected result is obtained:
> 
>      $ git checkout 18d8c26930~1
>      $ for i in $(seq 100); do f; done | sort | uniq -c
>          100 10000
> 
> In a subsequent commit, a new test is going to rely on test-terminate,
> and it does not require stdin to be connected to a terminal, but all
> piped data needs to be successfully transmitted to the child process.
> 
> To make this possible, add a new parameter "--no-stdin-pty" to allow
> disabling the stdin redirection though a pty.
> 
> Signed-off-by: Rubén Justo <rjusto@gmail.com>
> ---
>   t/test-terminal.perl | 32 ++++++++++++++++++--------------
>   1 file changed, 18 insertions(+), 14 deletions(-)
> 
> diff --git a/t/test-terminal.perl b/t/test-terminal.perl
> index 3810e9bb43..85edc9e8b9 100755
> --- a/t/test-terminal.perl
> +++ b/t/test-terminal.perl
> @@ -12,10 +12,10 @@ sub start_child {
>   	if (not defined $pid) {
>   		die "fork failed: $!"
>   	} elsif ($pid == 0) {
> -		open STDIN, "<&", $in;
> +		open STDIN, "<&", $in if $in;
>   		open STDOUT, ">&", $out;
>   		open STDERR, ">&", $err;
> -		close $in;
> +		close $in if $in;
>   		close $out;
>   		exec(@$argv) or die "cannot exec '$argv->[0]': $!"
>   	}
> @@ -78,28 +78,32 @@ sub copy_stdio {
>   }
>   
>   if ($#ARGV < 1) {
> -	die "usage: test-terminal program args";
> +	die "usage: test-terminal [--no-stdin-pty] program args";
>   }
> +my $no_stdin_pty = $ARGV[0] eq '--no-stdin-pty';
> +shift @ARGV if $no_stdin_pty;
>   $ENV{TERM} = 'vt100';
> -my $parent_in = new IO::Pty;
> +my $parent_in = $no_stdin_pty ? undef : IO::Pty->new;
>   my $parent_out = new IO::Pty;
>   my $parent_err = new IO::Pty;
> -$parent_in->set_raw();
> +$parent_in->set_raw() if $parent_in;
>   $parent_out->set_raw();
>   $parent_err->set_raw();
> -$parent_in->slave->set_raw();
> +$parent_in->slave->set_raw() if $parent_in;
>   $parent_out->slave->set_raw();
>   $parent_err->slave->set_raw();
> -my $pid = start_child(\@ARGV, $parent_in->slave, $parent_out->slave, $parent_err->slave);
> -close $parent_in->slave;
> +my $pid = start_child(\@ARGV,$parent_in ? $parent_in->slave : undef, $parent_out->slave, $parent_err->slave);
> +close $parent_in->slave if $parent_in;
>   close $parent_out->slave;
>   close $parent_err->slave;
> -my $in_pid = copy_stdin($parent_in);
> +my $in_pid = $no_stdin_pty ? 0 : copy_stdin($parent_in);
>   copy_stdio($parent_out, $parent_err);
>   my $ret = finish_child($pid);
> -# If the child process terminates before our copy_stdin() process is able to
> -# write all of its data to $parent_in, the copy_stdin() process could stall.
> -# Send SIGTERM to it to ensure it terminates.
> -kill 'TERM', $in_pid;
> -finish_child($in_pid);
> +if ($in_pid) {
> +	# If the child process terminates before our copy_stdin() process is able to
> +	# write all of its data to $parent_in, the copy_stdin() process could stall.
> +	# Send SIGTERM to it to ensure it terminates.
> +	kill 'TERM', $in_pid;
> +	finish_child($in_pid);
> +}
>   exit($ret);
Jeff King June 4, 2024, 10:33 a.m. UTC | #2
On Tue, Jun 04, 2024 at 11:05:15AM +0100, Phillip Wood wrote:

> Hi Rubén
> 
> On 03/06/2024 21:38, Rubén Justo wrote:
> > In 18d8c26930 (test_terminal: redirect child process' stdin to a pty,
> > 2015-08-04), t/test-terminal.perl learned to connect the child process'
> > stdin to a pty.  It works well for what was intended: satisfying an
> > `isatty(STDIN_FILENO)` check.
> > 
> > However, the fork introduced, that copies the stdin to the child
> > process, does not always manage to send all the information.
> 
> I think the problem maybe to do with the use of File::Copy, not with the
> fork. The man page for the copy function says
> 
>     Note  that  passing  in  files  as  handles  instead  of  names may
>     lead to loss of information on some operating systems; it is
>     recommended that you use file names whenever possible.
> 
> Rather than adding a new flag to work around a bug in our script it might be
> better to try and fix the bug by using a loop that reads blocks of data from
> the source and writes them to the destination instead of calling copy.

I don't think I've seen missing data. But do note that the test_terminal
stdin handling is racy:

  https://lore.kernel.org/git/20190520125016.GA13474@sigill.intra.peff.net/

IMHO we should consider getting rid of it entirely. I think the only
thing that uses it is t4153 (AFAICT it is luckily not racy because it
does not actually read stdin, but only checks isatty).

-Peff
Junio C Hamano June 5, 2024, 3:39 p.m. UTC | #3
Jeff King <peff@peff.net> writes:

> IMHO we should consider getting rid of it entirely. I think the only
> thing that uses it is t4153 (AFAICT it is luckily not racy because it
> does not actually read stdin, but only checks isatty).

Sounds like a better approach than piling another workaround on the
test helper.  Reading the old discussion, we seem to have been in
agreement that we generally do not have to insist reading from a tty
and certainly the "add -p" codepath is not one of those "if your
other payload must come from the standard input, your instructions
to specify how to handle that data needs to come from elsewhere, and
that is /dev/tty" cases.

Thanks.
Rubén Justo June 5, 2024, 10:50 p.m. UTC | #4
On Tue, Jun 04, 2024 at 11:05:15AM +0100, Phillip Wood wrote:

> Rather than adding a new flag to work around a bug in our script it might be
> better to try and fix the bug by using a loop that reads blocks of data from
> the source and writes them to the destination instead of calling copy.

To be honest, I've tried.  I haven't found a way to fix it properly.

I also thought about removing it.  I agree with Peff, removing the stdin
redirection makes more sense.  IMHO simplifies.

But, perhaps we can happily live another almost-decade with this new
--no-stdin-pty, before finally removing the stdin redirection in this
helper. :-)
Jeff King June 6, 2024, 8:24 a.m. UTC | #5
On Wed, Jun 05, 2024 at 08:39:59AM -0700, Junio C Hamano wrote:

> Jeff King <peff@peff.net> writes:
> 
> > IMHO we should consider getting rid of it entirely. I think the only
> > thing that uses it is t4153 (AFAICT it is luckily not racy because it
> > does not actually read stdin, but only checks isatty).
> 
> Sounds like a better approach than piling another workaround on the
> test helper.  Reading the old discussion, we seem to have been in
> agreement that we generally do not have to insist reading from a tty
> and certainly the "add -p" codepath is not one of those "if your
> other payload must come from the standard input, your instructions
> to specify how to handle that data needs to come from elsewhere, and
> that is /dev/tty" cases.

I think we got rid of all of the "read interactive input over tty"
cases. The one that I still see is am's heuristic to use isatty() to
decide whether the user might send patches over stdin.

I just sent a separate patch series which I think improves the
situation. And then either Rubén could build on top of that (if we think
it will graduate quickly) or he could do his optional patch, and we
could rip it back out when the two are merged.

-Peff
Jeff King June 6, 2024, 8:27 a.m. UTC | #6
On Thu, Jun 06, 2024 at 12:50:39AM +0200, Rubén Justo wrote:

> On Tue, Jun 04, 2024 at 11:05:15AM +0100, Phillip Wood wrote:
> 
> > Rather than adding a new flag to work around a bug in our script it might be
> > better to try and fix the bug by using a loop that reads blocks of data from
> > the source and writes them to the destination instead of calling copy.
> 
> To be honest, I've tried.  I haven't found a way to fix it properly.

I think File::Copy() is not the culprit. The problem is more at the
syscall level. Once test_terminal finishes writing all of the output to
the tty, it closes its end of the descriptor. And now the tty is "gone",
isatty() returns false, and further reads get EIO (even if we didn't
read all of the bytes! They're lost).

So I think the only thing we could do is _not_ actually close the
descriptor. But now we don't have a way to signal EOF to the other side.
Unless perhaps there is some clever way to do so. I'd still favor
ripping it out.

> I also thought about removing it.  I agree with Peff, removing the stdin
> redirection makes more sense.  IMHO simplifies.
> 
> But, perhaps we can happily live another almost-decade with this new
> --no-stdin-pty, before finally removing the stdin redirection in this
> helper. :-)

Hopefully not a decade. ;) I just sent a series, and I think you could
either build on top, or the merge resolution could just drop your new
option.

-Peff
Rubén Justo June 9, 2024, 7:26 a.m. UTC | #7
On Thu, Jun 06, 2024 at 04:27:48AM -0400, Jeff King wrote:

> I just sent a series, and I think you could either build on top, or
> the merge resolution could just drop your new option.

I've already responded to your series, but just to confirm here.  I'm
glad to drop this step and building on top of jk/am-retry.

Thanks!
diff mbox series

Patch

diff --git a/t/test-terminal.perl b/t/test-terminal.perl
index 3810e9bb43..85edc9e8b9 100755
--- a/t/test-terminal.perl
+++ b/t/test-terminal.perl
@@ -12,10 +12,10 @@  sub start_child {
 	if (not defined $pid) {
 		die "fork failed: $!"
 	} elsif ($pid == 0) {
-		open STDIN, "<&", $in;
+		open STDIN, "<&", $in if $in;
 		open STDOUT, ">&", $out;
 		open STDERR, ">&", $err;
-		close $in;
+		close $in if $in;
 		close $out;
 		exec(@$argv) or die "cannot exec '$argv->[0]': $!"
 	}
@@ -78,28 +78,32 @@  sub copy_stdio {
 }
 
 if ($#ARGV < 1) {
-	die "usage: test-terminal program args";
+	die "usage: test-terminal [--no-stdin-pty] program args";
 }
+my $no_stdin_pty = $ARGV[0] eq '--no-stdin-pty';
+shift @ARGV if $no_stdin_pty;
 $ENV{TERM} = 'vt100';
-my $parent_in = new IO::Pty;
+my $parent_in = $no_stdin_pty ? undef : IO::Pty->new;
 my $parent_out = new IO::Pty;
 my $parent_err = new IO::Pty;
-$parent_in->set_raw();
+$parent_in->set_raw() if $parent_in;
 $parent_out->set_raw();
 $parent_err->set_raw();
-$parent_in->slave->set_raw();
+$parent_in->slave->set_raw() if $parent_in;
 $parent_out->slave->set_raw();
 $parent_err->slave->set_raw();
-my $pid = start_child(\@ARGV, $parent_in->slave, $parent_out->slave, $parent_err->slave);
-close $parent_in->slave;
+my $pid = start_child(\@ARGV,$parent_in ? $parent_in->slave : undef, $parent_out->slave, $parent_err->slave);
+close $parent_in->slave if $parent_in;
 close $parent_out->slave;
 close $parent_err->slave;
-my $in_pid = copy_stdin($parent_in);
+my $in_pid = $no_stdin_pty ? 0 : copy_stdin($parent_in);
 copy_stdio($parent_out, $parent_err);
 my $ret = finish_child($pid);
-# If the child process terminates before our copy_stdin() process is able to
-# write all of its data to $parent_in, the copy_stdin() process could stall.
-# Send SIGTERM to it to ensure it terminates.
-kill 'TERM', $in_pid;
-finish_child($in_pid);
+if ($in_pid) {
+	# If the child process terminates before our copy_stdin() process is able to
+	# write all of its data to $parent_in, the copy_stdin() process could stall.
+	# Send SIGTERM to it to ensure it terminates.
+	kill 'TERM', $in_pid;
+	finish_child($in_pid);
+}
 exit($ret);