#2868 PR merged: In the UserInput function drain stdin if stdin is a terminal

Labels: enhancement, fixed / solved / done, minor bug

jsmeix opened issue at 2022-09-22 14:29:

Works sufficiently well for me
during "rear recover" with MIGRATION_MODE="true"
(where several user dialogs appear).

  • Brief description of the changes in this pull request:

In the UserInput function
in lib/_input-output-functions.sh
drain stdin if stdin is a terminal (i.e. in interactive mode).

jsmeix commented at 2022-09-23 09:45:

FYI
what "help read" shows on SLES11 SP4 with bash version 3.2.57

# help read
read: read [-ers] [-u fd] [-t timeout] [-p prompt] [-a array] [-n nchars] [-d delim] [name ...]
    One line is read from the standard input, or from file descriptor FD if the
    -u option is supplied, and the first word is assigned to the first NAME,
    the second word to the second NAME, and so on, with leftover words assigned
    to the last NAME.  Only the characters found in $IFS are recognized as word
    delimiters.  If no NAMEs are supplied, the line read is stored in the REPLY
    variable.  If the -r option is given, this signifies `raw' input, and
    backslash escaping is disabled.  The -d option causes read to continue
    until the first character of DELIM is read, rather than newline.  If the -p
    option is supplied, the string PROMPT is output without a trailing newline
    before attempting to read.  If -a is supplied, the words read are assigned
    to sequential indices of ARRAY, starting at zero.  If -e is supplied and
    the shell is interactive, readline is used to obtain the line.  If -n is
    supplied with a non-zero NCHARS argument, read returns after NCHARS
    characters have been read.  The -s option causes input coming from a
    terminal to not be echoed.

    The -t option causes read to time out and return failure if a complete line
    of input is not read within TIMEOUT seconds.  If the TMOUT variable is set,
    its value is the default timeout.  The return code is zero, unless end-of-file
    is encountered, read times out, or an invalid file descriptor is supplied as
    the argument to -u.

jsmeix commented at 2022-09-23 11:53:

Via my last
https://github.com/rear/rear/pull/2868/commits/6ab2c2a2d4a9fede920fd18a726ac41801cd0930
I simplified it to a single command

test -t 0 && while read -t 0 ; do read -s ; done

pcahyna commented at 2022-09-23 12:12:

This does not drain characters not terminated by ENTER, try

sleep 5; while read -t 0 ; do read -s ; done

and press some keys (without ENTER) during the sleep. You will see that the characters remain.

jsmeix commented at 2022-09-23 12:41:

Yes this is a known drawback of how read -t 0 is implemented.
It only looks if there is something in stdin.
It does not look if there is something
in the terminal buffer that is not yet in stdin.
See my comment in my last commit.

I tested it right now and it behaves "good enough" for me.

During my test I typed plain '9' without ENTER
(note the 9Marking component... line)
before another UserInput dialog appeared and got
(with MIGRATION_MODE="true" to get those dialogs):

RESCUE localhost:~ # rear -D recover
...
User confirmed disk layout file
9Marking component '/dev/sda' as done in /var/lib/rear/layout/disktodo.conf
Marking component '/dev/sda1' as done in /var/lib/rear/layout/disktodo.conf
...
Running 'layout/recreate' stage ======================
UserInput -I LAYOUT_CODE_CONFIRMATION needed in /usr/share/rear/layout/recreate/default/100_confirm_layout_code.sh line 26
Confirm or edit the disk recreation script
1) Confirm disk recreation script and continue 'rear recover'
2) Edit disk recreation script (/var/lib/rear/layout/diskrestore.sh)
3) View disk recreation script (/var/lib/rear/layout/diskrestore.sh)
4) View original disk space usage (/var/lib/rear/layout/config/df.txt)
5) Use Relax-and-Recover shell and return back to here
6) Abort 'rear recover'
(default '1' timeout 300 seconds)

UserInput: Result is '9'
UserInput -I LAYOUT_CODE_CONFIRMATION needed in /usr/share/rear/layout/recreate/default/100_confirm_layout_code.sh line 26
Confirm or edit the disk recreation script
1) Confirm disk recreation script and continue 'rear recover'
2) Edit disk recreation script (/var/lib/rear/layout/diskrestore.sh)
3) View disk recreation script (/var/lib/rear/layout/diskrestore.sh)
4) View original disk space usage (/var/lib/rear/layout/config/df.txt)
5) Use Relax-and-Recover shell and return back to here
6) Abort 'rear recover'
(default '1' timeout 300 seconds)

when I hit only ENTER in that dialog
because that dialog got '9' and ENTER at its stdin
when ENTER was hit (which triggers to move the '9'
from the terminal buffer into rear's stdin).

By the way:
I hit ENTER many times between dialogs
and typed lots of stuff during backup restore.
All behaved sufficiently well for me.

pcahyna commented at 2022-09-23 12:43:

I also wonder why -t in the second read is not needed. I would be afraid that read -s without -t would block forever if there is no more input at this point, but apparently this does not happen.

pcahyna commented at 2022-09-23 12:44:

Yes this is a known drawback of how read -t 0 is implemented.

My question is, why not add -n 1000 back? It seemed to get rid of this drawback.

jsmeix commented at 2022-09-23 12:50:

I also wondered why -t 1 in the second read is not needed.
I also expected it to wait endlessly for input.
I got that from a colleague and tried it and it works for me.
See also the function clear_stdin() in
https://superuser.com/questions/276531/clear-stdin-before-reading
where while read none; do :; done seems to work.
When I call this on command line it waits for input.

jsmeix commented at 2022-09-23 12:53:

Next week I try out how it behaves with re-added -n 1000.
And perhaps I should add -t 1 just as a safety net.

jsmeix commented at 2022-09-26 12:46:

Hmpf!
I hate this fragile terminal IO stuff.

I can produce an endless 'read' loop with

while read -t 0 ; do read ... ; done

on comandline with bash 4.4.23
depending on how the process that does the 'read'
is connected with the process that provides read's input
and additionally depending on some (weird) 'sleep':

# ( echo start ; sleep 2 ; echo -en 'foo\nbar' ; sleep 2 ; echo end ) | ( while read -t 0 ; do echo reading ; read -t 1 -n 1000 -d '' ; echo "'$REPLY'" ; sleep 1 ; done )
reading
'start
'
reading
'foo
bar'
reading
'end
'
reading
''
reading
''
...
[continues endlessly]

versus

# ( echo start ; sleep 2 ; echo -en 'foo\nbar' ; sleep 2 ; echo end ) | ( while read -t 0 ; do echo reading ; read -t 1 -n 1000 -d '' ; echo "'$REPLY'" ; done )
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe

or

# { echo start ; sleep 2 ; echo -en 'foo\nbar' ; sleep 2 ; echo end ; } | { while read -t 0 ; do echo reading ; read -t 1 -n 1000 -d '' ; echo "'$REPLY'" ; sleep 1 ; done ; }
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe

Because I cannot know how the user will connect
the process that does the 'read' (usr/sbin/rear)
with the process that provides read's input
(the users's normal terminal or something else
where test -t 0 still results zero exit code),
I will not risk any possibility to hang up ReaR
only because to avoid possible bad outcomes when
a user carelessly types things on his keyboard
regardless that he should not type randomly
while a program is running that uses stdin.

So I will not use any kind of unlimited loop.

Meanwhile I think a single

read -s -t 1 -n 1000 -d ''

is sufficient for what is actually needed here in practice
which is primarily to drain some accidental additional ENTER
which can accidentally happen also to experienced users
(in contrast to random typing things on the keyboard).

Of course it happened to me during the sequence of user input
dialogs during "rear recover" with MIGRATION_MODE="true"
where most dialogs are OK so one gets used to hit ENTER
until the one dialog appears where one needs to select
something specific (instead of the default via ENTER).

How read -s -t 1 -n 1000 -d '' behaves for me:

On SLES10 with bash 3.1.17:

# for i in $( seq 10 ) ; do echo $i ; done | ( read -s -t 1 -n 1000 -d '' ; echo "'$REPLY'" | od -a )
0000000   '   1  nl   2  nl   3  nl   4  nl   5  nl   6  nl   7  nl   8
0000020  nl   9  nl   1   0  nl   '  nl

# for i in $( seq 10 ) ; do echo $i ; done | ( read -s -t 1 -n 10 -d '' ; echo "'$REPLY'" | od -a )
0000000   '   1  nl   2  nl   3  nl   4  nl   5  nl   '  nl

# time read -s -t 1 -n 1000 -d ''
real    0m1.002s
user    0m0.000s
sys     0m0.000s

On openSUSE Leap 15.3 with bash 4.4.23:

# for i in $( seq 10 ) ; do echo $i ; done | ( read -s -t 1 -n 1000 -d '' ; echo "'$REPLY'" | od -a )
0000000   '   1  nl   2  nl   3  nl   4  nl   5  nl   6  nl   7  nl   8
0000020  nl   9  nl   1   0  nl   '  nl

# for i in $( seq 10 ) ; do echo $i ; done | ( read -s -t 1 -n 10 -d '' ; echo "'$REPLY'" | od -a )
0000000   '   1  nl   2  nl   3  nl   4  nl   5  nl   '  nl

# time read -s -t 1 -n 1000 -d ''
real    0m1.000s
user    0m0.000s
sys     0m0.000s

This causes again a one second delay when there is no input
but that should not matter in practice in interactive mode.
This one second delay might even be useful in practice
because this way the user may not too easily get used
to carelessly hit ENTER ENTER ENTER... when there is
a sequence of dialogs.

What matters first and foremost is: "It Has To Work."
RFC 1925 item 1 https://datatracker.ietf.org/doc/html/rfc1925

See also the "Maintain backward compatibility" section
in https://github.com/rear/rear/wiki/Coding-Style

Preferably use simple generic functionality
that works on any Linux system.
Better very simple code than oversophisticated
(possibly fragile) constructs.

jsmeix commented at 2022-09-26 13:48:

I tested my latest commit
https://github.com/rear/rear/pull/2868/commits/5f28472c018c312f8c6ba2eeee0fa340ceffecaa
during "rear recover" with MIGRATION_MODE="true"
and it behaves perfectly well for me in practice, cf.
https://github.com/rear/rear/pull/2868#issuecomment-1257988466
what it is primarily meant to do in practice.

jsmeix commented at 2022-09-26 13:49:

@pcahyna @rear/contributors
if there are no objections I would like to merge it
tomorrow afternoon in its current state.
If later issues appear I will of course fix them.

pcahyna commented at 2022-09-26 16:51:

@jsmeix I agree with the reasoning that simple code covering the most problematic case is preferred. One question, why have you added -d ''? That was not in any of the previous attempts and experiments as far as I could see. According to the manual, "The first character of DELIM is used to terminate the input line, rather than newline. If DELIM is the empty string, 'read' will terminate a line when it reads a NUL character." So, are you counting on the fact that the NUL character should never appear in the input and therefore this allows the command to skip multiple ENTERs ?

jsmeix commented at 2022-09-27 07:09:

Yes, that's the intent behind -d ''.
I have that from
https://superuser.com/questions/276531/clear-stdin-before-reading
which reads (excerpt):

read -d '' -t 0.1 -n 10000

This reads multiple lines of inputs,
if the user inadvertently pressed enter multiple times

For me -d '' seems to also work with bash 3.x
but neither "man bash" nor "help read" tell about
what 'read' does when the delimiter is the empty string.
This is only described in
https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html
which is currtently "for Bash, Version 5.2.", see
https://www.gnu.org/software/bash/manual/html_node/index.html

Previously I had tested -d '~' see
https://github.com/rear/rear/pull/2868#discussion_r978452551

What works for me even with bash 3.x is
using an artificial delimiter character
that is sufficiently unlikely
to be accidentally entered by the user:

# echo -e 'One\nTwo\nThree' | ( read -t 1 -d '~' -n 10 D ; read R1 ; read R2 ; echo D "$D" ; echo R1 "$R1" ; echo R2 "$R2" )
D One
Two
Th
R1 ree
R2

jsmeix commented at 2022-09-27 07:18:

I think I can avoid
the one second delay when there is no input
in a simple and fail safe way via

test -t 0 && read -t 0 && read -s -t 1 -n 1000 -d ''

When read -t 0 falsely results non zero exit code
but there is someting in stdin, then
draining stdin would be falsely skipped which is no big issue
because then UserInput() falls back to working as before and
normally there should be nothing accidentally pending in stdin.

When read -t 0 falsely results zero exit code
but there is nothing in stdin, then
the subsequent rear -t 1 times out after one second.

jsmeix commented at 2022-09-27 11:11:

read -t 0 may not work sufficiently reliably
when automated input is provided (here with bash 4.4.23):

# for i in $( seq 5 ) ; do echo -n $i ; done | ( read -t 0 && read -s -t 1 -n 100 -d '' ; echo -n "'$REPLY'" | od -a )
0000000   '   '
0000002

# for i in $( seq 5 ) ; do echo -n $i ; done | ( read -t 0 && read -s -t 1 -n 100 -d '' ; echo -n "'$REPLY'" | od -a )
0000000   '   '
0000002
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe

I think this happens
when read -t 0 is called in the pipe consumer
before the pipe producer made actual output
and the "Broken pipe" happens when the pipe consumer
already finished so the 'echo' in the pipe producer
writes into a "Broken pipe".

With a sleep in the pipe consumer it works:

# for i in $( seq 5 ) ; do echo -n $i ; done | ( sleep 1 ; read -t 0 && read -s -t 1 -n 100 -d '' ; echo -n "'$REPLY'" | od -a )
0000000   '   1   2   3   4   5   '
0000007

I think in ReaR such a race condition cannot happen
because the stdin producer process
(usually the user's terminal emulation program)
is already running when the usr/sbin/rear process
gets started by the user on his termial.

When usr/sbin/rear is run where stdin is not a terminal
the test -t 0 won't let the 'read' calls happen.

When usr/sbin/rear is run by the user in whatever special
way where stdin is a terminal and somehow automated input
is fed with a little delay into usr/sbin/rear, then
the 'read' calls should happen late enough to work OK.

jsmeix commented at 2022-09-27 12:33:

I tested my latest commit
https://github.com/rear/rear/pull/2868/commits/c482b2bdc6cdad5be077026b01ce4e42fd5e2d53
during "rear recover" with MIGRATION_MODE="true"
and it behaves perfectly well for me.

jsmeix commented at 2022-09-27 12:35:

@pcahyna @rear/contributors
if there are no objections I would like to merge it
tomorrow afternoon in its last state which is

test -t 0 && read -t 0 && read -s -t 1 -n 1000 -d ''

If issues appear later I will of course fix them.


[Export of Github issue for rear/rear.]