public inbox for libc-alpha@sourceware.org
 help / color / mirror / Atom feed
* RFE: enable buffering on null-terminated data
       [not found] ` <9831afe6-958a-fbd3-9434-05dd0c9b602a@draigBrady.com>
@ 2024-03-10 15:29   ` Zachary Santer
  2024-03-10 20:36     ` Carl Edquist
  0 siblings, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-03-10 15:29 UTC (permalink / raw)
  To: libc-alpha; +Cc: coreutils, p

Was "stdbuf feature request - line buffering but for null-terminated data"

See below.

On Sun, Mar 10, 2024 at 5:38 AM Pádraig Brady <P@draigbrady.com> wrote:
>
> On 09/03/2024 16:30, Zachary Santer wrote:
> > 'stdbuf --output=L' will line-buffer the command's output stream.
> > Pretty useful, but that's looking for newlines. Filenames should be
> > passed between utilities in a null-terminated fashion, because the
> > null byte is the only byte that can't appear within one.
> >
> > If I want to buffer output data on null bytes, the closest I can get
> > is 'stdbuf --output=0', which doesn't buffer at all. This is pretty
> > inefficient.
> >
> > 0 means unbuffered, and Z is already taken for, I guess, zebibytes.
> > --output=N, then?
> >
> > Would this require a change to libc implementations, or is it possible now?
>
> This does seem like useful functionality,
> but it would require support for libc implementations first.
>
> cheers,
> Pádraig

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-10 15:29   ` RFE: enable buffering on null-terminated data Zachary Santer
@ 2024-03-10 20:36     ` Carl Edquist
  2024-03-11  3:48       ` Zachary Santer
  0 siblings, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-03-10 20:36 UTC (permalink / raw)
  To: Zachary Santer; +Cc: libc-alpha, coreutils, p

[-- Attachment #1: Type: text/plain, Size: 2952 bytes --]

Hi Zack,

This sounds like a potentially useful feature (it'd probably belong with a 
corresponding new buffer mode in setbuf(3)) ...

> Filenames should be passed between utilities in a null-terminated 
> fashion, because the null byte is the only byte that can't appear within 
> one.

Out of curiosity, do you have an example command line for your use case?

> If I want to buffer output data on null bytes, the closest I can get is 
> 'stdbuf --output=0', which doesn't buffer at all. This is pretty 
> inefficient.

I'm just thinking that find(1), for instance, will end up calling write(2) 
exactly once per filename (-print or -print0) if run under stdbuf 
unbuffered, which is the same as you'd get with a corresponding stdbuf 
line-buffered mode (newline or null-terminated).

It seems that where line buffering improves performance over unbuffered is 
when there are several calls to (for example) printf(3) in constructing a 
single line.  find(1), and some filters like grep(1), will write a line at 
a time in unbuffered mode, and thus don't seem to benefit at all from line 
buffering.  On the other hand, cut(1) appears to putchar(3) a byte at a 
time, which in unbuffered mode will (like you say) be pretty inefficient.

So, depending on your use case, a new null-terminated line buffered option 
may or may not actually improve efficiency over unbuffered mode.


You can run your commands under strace like

     stdbuf --output=X  strace -c -ewrite  command ... | ...

to count the number of actual writes for each buffering mode.


Carl


PS, "find -printf" recognizes a '\c' escape to flush the output, in case 
that helps.  So "find -printf '%p\0\c'" would, for instance, already 
behave the same as "stdbuf --output=N  find -print0" with the new stdbuf 
output mode you're suggesting.

(Though again, this doesn't actually seem to be any more efficient than 
running "stdbuf --output=0  find -print0")

On Sun, 10 Mar 2024, Zachary Santer wrote:

> Was "stdbuf feature request - line buffering but for null-terminated data"
>
> See below.
>
> On Sun, Mar 10, 2024 at 5:38 AM Pádraig Brady <P@draigbrady.com> wrote:
>>
>> On 09/03/2024 16:30, Zachary Santer wrote:
>>> 'stdbuf --output=L' will line-buffer the command's output stream.
>>> Pretty useful, but that's looking for newlines. Filenames should be
>>> passed between utilities in a null-terminated fashion, because the
>>> null byte is the only byte that can't appear within one.
>>>
>>> If I want to buffer output data on null bytes, the closest I can get
>>> is 'stdbuf --output=0', which doesn't buffer at all. This is pretty
>>> inefficient.
>>>
>>> 0 means unbuffered, and Z is already taken for, I guess, zebibytes.
>>> --output=N, then?
>>>
>>> Would this require a change to libc implementations, or is it possible now?
>>
>> This does seem like useful functionality,
>> but it would require support for libc implementations first.
>>
>> cheers,
>> Pádraig
>
>

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-10 20:36     ` Carl Edquist
@ 2024-03-11  3:48       ` Zachary Santer
  2024-03-11 11:54         ` Carl Edquist
  0 siblings, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-03-11  3:48 UTC (permalink / raw)
  To: Carl Edquist; +Cc: libc-alpha, coreutils, p

[-- Attachment #1: Type: text/plain, Size: 5283 bytes --]

On Sun, Mar 10, 2024 at 4:36 PM Carl Edquist <edquist@cs.wisc.edu> wrote:
>
> Hi Zack,
>
> This sounds like a potentially useful feature (it'd probably belong with a
> corresponding new buffer mode in setbuf(3)) ...
>
> > Filenames should be passed between utilities in a null-terminated
> > fashion, because the null byte is the only byte that can't appear within
> > one.
>
> Out of curiosity, do you have an example command line for your use case?

My use for 'stdbuf --output=L' is to be able to run a command within a
bash coprocess. (Really, a background process communicating with the
parent process through FIFOs, since Bash prints a warning message if
you try to run more than one coprocess at a time. Shouldn't make a
difference here.) See coproc-buffering, attached. Without making the
command's output either line-buffered or unbuffered, what I'm doing
there would deadlock. I feed one line in and then expect to be able to
read a transformed line immediately. If that transformed line is stuck
in a buffer that's still waiting to be filled, then nothing happens.

I swear doing this actually makes sense in my application.

$ ./coproc-buffering 100000
Line-buffered:
real    0m17.795s
user    0m6.234s
sys     0m11.469s
Unbuffered:
real    0m21.656s
user    0m6.609s
sys     0m14.906s

When I initially implemented this thing, I felt lucky that the data I
was passing in were lines ending in newlines, and not null-terminated,
since my script gets to benefit from 'stdbuf --output=L'. Truth be
told, I don't currently have a need for --output=N. Of course, sed and
all sorts of other Linux command-line tools can produce or handle
null-terminated data.

> > If I want to buffer output data on null bytes, the closest I can get is
> > 'stdbuf --output=0', which doesn't buffer at all. This is pretty
> > inefficient.
>
> I'm just thinking that find(1), for instance, will end up calling write(2)
> exactly once per filename (-print or -print0) if run under stdbuf
> unbuffered, which is the same as you'd get with a corresponding stdbuf
> line-buffered mode (newline or null-terminated).
>
> It seems that where line buffering improves performance over unbuffered is
> when there are several calls to (for example) printf(3) in constructing a
> single line.  find(1), and some filters like grep(1), will write a line at
> a time in unbuffered mode, and thus don't seem to benefit at all from line
> buffering.  On the other hand, cut(1) appears to putchar(3) a byte at a
> time, which in unbuffered mode will (like you say) be pretty inefficient.
>
> So, depending on your use case, a new null-terminated line buffered option
> may or may not actually improve efficiency over unbuffered mode.

I hadn't considered that.

> You can run your commands under strace like
>
>      stdbuf --output=X  strace -c -ewrite  command ... | ...
>
> to count the number of actual writes for each buffering mode.

I'm running bash in MSYS2 on a Windows machine, so hopefully that
doesn't invalidate any assumptions. Now setting up strace around the
things within the coprocess, and only passing in one line, I now have
coproc-buffering-strace, attached. Giving the argument 'L', both sed
and expand call write() once. Giving the argument 0, sed calls write()
twice and expand calls it a bunch of times, seemingly once for each
character it outputs. So I guess that's it.

$ ./coproc-buffering-strace L
|        Line with tabs   why?|

$ grep -c -F 'write:' sed-trace.txt expand-trace.txt
sed-trace.txt:1
expand-trace.txt:1

$ ./coproc-buffering-strace 0
|        Line with tabs   why?|

$ grep -c -F 'write:' sed-trace.txt expand-trace.txt
sed-trace.txt:2
expand-trace.txt:30

> Carl
>
>
> PS, "find -printf" recognizes a '\c' escape to flush the output, in case
> that helps.  So "find -printf '%p\0\c'" would, for instance, already
> behave the same as "stdbuf --output=N  find -print0" with the new stdbuf
> output mode you're suggesting.
>
> (Though again, this doesn't actually seem to be any more efficient than
> running "stdbuf --output=0  find -print0")
>
> On Sun, 10 Mar 2024, Zachary Santer wrote:
>
> > Was "stdbuf feature request - line buffering but for null-terminated data"
> >
> > See below.
> >
> > On Sun, Mar 10, 2024 at 5:38 AM Pádraig Brady <P@draigbrady.com> wrote:
> >>
> >> On 09/03/2024 16:30, Zachary Santer wrote:
> >>> 'stdbuf --output=L' will line-buffer the command's output stream.
> >>> Pretty useful, but that's looking for newlines. Filenames should be
> >>> passed between utilities in a null-terminated fashion, because the
> >>> null byte is the only byte that can't appear within one.
> >>>
> >>> If I want to buffer output data on null bytes, the closest I can get
> >>> is 'stdbuf --output=0', which doesn't buffer at all. This is pretty
> >>> inefficient.
> >>>
> >>> 0 means unbuffered, and Z is already taken for, I guess, zebibytes.
> >>> --output=N, then?
> >>>
> >>> Would this require a change to libc implementations, or is it possible now?
> >>
> >> This does seem like useful functionality,
> >> but it would require support for libc implementations first.
> >>
> >> cheers,
> >> Pádraig
> >
> >

[-- Attachment #2: coproc-buffering --]
[-- Type: application/octet-stream, Size: 1154 bytes --]

#!/usr/bin/env bash

set -o nounset -o noglob +o braceexpand
shopt -s lastpipe
export LC_ALL='C.UTF-8'

tab_spaces=8

sed_expr='s/[[:blank:]]+$//'

test=$'  \tLine with tabs\t why?\t  '

repeat="${1}"

coproc line_buffered {
  stdbuf --output=L -- \
      sed --binary --regexp-extended --expression="${sed_expr}" |
    stdbuf --output=L -- \
        expand --tabs="${tab_spaces}"
}

printf '%s' "Line-buffered:"
time {
  for (( i = 0; i < repeat; i++ )); do
    printf '%s\n' "${test}" >&"${line_buffered[1]}"
    IFS='' read -r line <&"${line_buffered[0]}"
    printf '|%s|\n' "${line}" > /dev/null
  done
}

exec {line_buffered[0]}<&- {line_buffered[1]}>&-
wait "${line_buffered_PID}"

coproc unbuffered {
  stdbuf --output=0 -- \
      sed --binary --regexp-extended --expression="${sed_expr}" |
    stdbuf --output=0 -- \
        expand --tabs="${tab_spaces}"
}

printf '%s' "Unbuffered:"
time {
  for (( i = 0; i < repeat; i++ )); do
    printf '%s\n' "${test}" >&"${unbuffered[1]}"
    IFS='' read -r line <&"${unbuffered[0]}"
    printf '|%s|\n' "${line}" > /dev/null
  done
}

exec {unbuffered[0]}<&- {unbuffered[1]}>&-
wait "${unbuffered_PID}"

[-- Attachment #3: coproc-buffering-strace --]
[-- Type: application/octet-stream, Size: 695 bytes --]

#!/usr/bin/env bash

set -o nounset -o noglob +o braceexpand
shopt -s lastpipe
export LC_ALL='C.UTF-8'

tab_spaces=8

sed_expr='s/[[:blank:]]+$//'

test=$'  \tLine with tabs\t why?\t  '

buffer_setting="${1}"

coproc buffer_test {
  stdbuf --output="${buffer_setting}" -- \
      strace -e -o sed-trace.txt \
      sed --binary --regexp-extended --expression="${sed_expr}" |
   stdbuf --output="${buffer_setting}" -- \
       strace -e -o expand-trace.txt \
       expand --tabs="${tab_spaces}"
}

printf '%s\n' "${test}" >&"${buffer_test[1]}"
IFS='' read -r line <&"${buffer_test[0]}"
printf '|%s|\n' "${line//$'\t'/TAB}"

exec {buffer_test[0]}<&- {buffer_test[1]}>&-
wait "${buffer_test_PID}"

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-11  3:48       ` Zachary Santer
@ 2024-03-11 11:54         ` Carl Edquist
  2024-03-11 15:12           ` Examples of concurrent coproc usage? Zachary Santer
  2024-03-12  3:34           ` RFE: enable buffering on null-terminated data Zachary Santer
  0 siblings, 2 replies; 54+ messages in thread
From: Carl Edquist @ 2024-03-11 11:54 UTC (permalink / raw)
  To: Zachary Santer; +Cc: libc-alpha, coreutils, p

[-- Attachment #1: Type: text/plain, Size: 7977 bytes --]

On Sun, 10 Mar 2024, Zachary Santer wrote:

> On Sun, Mar 10, 2024 at 4:36 PM Carl Edquist <edquist@cs.wisc.edu> wrote:
>>
>> Out of curiosity, do you have an example command line for your use case?
>
> My use for 'stdbuf --output=L' is to be able to run a command within a
> bash coprocess.

Oh, cool, now you're talking!  ;)


> (Really, a background process communicating with the parent process 
> through FIFOs, since Bash prints a warning message if you try to run 
> more than one coprocess at a time. Shouldn't make a difference here.)

(Kind of a side-note ... bash's limited coprocess handling was a long 
standing annoyance for me in the past, to the point that I wrote a bash 
coprocess management library to handle multiple active coprocess and give 
convenient methods for interaction.  Perhaps the trickiest bit about 
multiple coprocesses open at once (which I suspect is the reason support 
was never added to bash) is that you don't want the second and subsequent 
coprocesses to inherit the pipe fds of prior open coprocesses.  This can 
result in deadlock if, for instance, you close your write end to coproc1, 
but coproc1 continues to wait for input because coproc2 also has a copy of 
a write end of the pipe to coproc1's input.  So you need to be smart about 
subsequent coprocesses first closing all fds associated with other 
coprocesses.

Word to the wise: you might encounter this issue (coproc2 prevents coproc1 
from seeing its end-of-input) even though you are rigging this up yourself 
with FIFOs rather than bash's coproc builtin.)


> See coproc-buffering, attached.

Thanks!

> Without making the command's output either line-buffered or unbuffered, 
> what I'm doing there would deadlock. I feed one line in and then expect 
> to be able to read a transformed line immediately. If that transformed 
> line is stuck in a buffer that's still waiting to be filled, then 
> nothing happens.
>
> I swear doing this actually makes sense in my application.

Yeah makes sense!  I am familiar with the problem you're describing.

(In my coprocess management library, I effectively run every coproc with 
--output=L by default, by eval'ing the output of 'env -i stdbuf -oL env', 
because most of the time for a coprocess, that's whats wanted/necessary.)


... Although, for your example coprocess use, where the shell both 
produces the input for the coproc and consumes its output, you might be 
able to simplify things by making the producer and consumer separate 
processes.  Then you could do a simpler 'producer | filter | consumer' 
without having to worry about buffering at all.  But if the producer and 
consumer need to be in the same process (eg they share state and are 
logically interdependent), then yeah that's where you need a coprocess for 
the filter.

... On the other hand, if the issue is that someone is producing one line 
at a time _interactively_ (that is, inputting text or commands from a 
terminal), then you might argue that the performance hit for unbuffered 
output will be insignificant compared to time spent waiting for terminal 
input.


> $ ./coproc-buffering 100000
> Line-buffered:
> real    0m17.795s
> user    0m6.234s
> sys     0m11.469s
> Unbuffered:
> real    0m21.656s
> user    0m6.609s
> sys     0m14.906s

Yeah, this makes sense in your particular example.

It looks like expand(1) uses putchar(3), so in unbuffered mode this 
translates to one write(2) call for every byte.  sed(1) is not quite as 
bad - in unbuffered it appears to output the line and the newline 
terminator separately, so two write(2) calls for every line.

So in both cases (but especially for expand), line buffering reduces the 
number of write(2) calls.

(Although given your time output, you might say the performance hit for 
unbuffered is not that huge.)


> When I initially implemented this thing, I felt lucky that the data I 
> was passing in were lines ending in newlines, and not null-terminated, 
> since my script gets to benefit from 'stdbuf --output=L'.

:thumbsup:


> Truth be told, I don't currently have a need for --output=N.

Mmm-hmm  :)


> Of course, sed and all sorts of other Linux command-line tools can 
> produce or handle null-terminated data.

Definitely.  So in the general case, theoretically it seems as useful to 
buffer output on nul bytes.

Note that for gnu sed in particular, there is a -u/--unbuffered option, 
which will effectively give you line buffered output, including buffering 
on nul bytes with -z/--null-data .

... I'll be honest though, I am having trouble imagining a realistic 
pipeline that filters filenames with embedded newlines using expand(1) 
;)

...

But, I want to be a good sport here and contrive an actual use case.

So for fun, say I want to use cut(1) (which performs poorly when 
unbuffered) in a coprocess that takes null-terminated file paths on input 
and outputs the first directory component (which possibly contains 
embedded newlines).

The basic command in the coprocess would be:

 	cut -d/ -f1 -z

but with the default block buffering for pipe output, that will hang (the 
problem you describe) if you expect to read a record back from it after 
each record sent.


The unbuffered approach works, but (as discussed) is pretty inefficient:

 	stdbuf --output=0  cut -d/ -f1 -z


But, if we swap nul bytes and newlines before and after cut, then we can 
run cut with regular newline line buffering, and get the desired effect:

 	stdbuf --output=0 tr '\0\n' '\n\0' |
 	stdbuf --output=L cut -d/ -f1      |
 	stdbuf --output=0 tr '\0\n' '\n\0'


The embedded newlines in filenames will be passed by tr(1) to cut(1) as 
embedded nul bytes, cut will line-buffer its output, and the second tr 
will restore the original embedded newlines & null-terminated records.

Note that unbuffered tr(1) will still output its translated input in 
blocks (with fwrite(3)) rather than a byte at a time, so tr will 
effectively give buffered output with the same size as the input records.

(That is, newline or null-terminated input records will effectively 
produce newline or null-terminated output buffering, respectively.)


I'd venture to guess that most of the standard filters could be made to 
pass along null-terminated records as line-buffered records the same way. 
Might even package it into a convenience function to set them up:


 	swap_znl () { stdbuf -o0 tr '\0\n' '\n\0'; }

 	nulterm_via_linebuf () { swap_znl | stdbuf -oL "$@" | swap_znl; }


Then, for example, stand it up with bash's coproc:

 	$ coproc DC1 { nulterm_via_linebuf cut -d/ -f1; }

 	$ printf 'a\nb/c\nd/efg\0' >&${DC1[1]}
 	$ IFS='' read -rd '' -u ${DC1[0]} DIR
 	$ echo "[$DIR]"
 	[a
 	b]

(or however else you manage your coprocesses.)

It's a workaround, and it keeps the kind of buffering you'd get with a 
'stdbuf --output=N', but to be fair the extra data shoveling is not 
exactly free.

...

So ... again in theory I also feel like a null-terminated buffering mode 
for stdbuf(1) (and setbuf(3)) is kind of a missing feature.  It may just 
be that nobody has actually had a real need for it.  (Yet?)



> I'm running bash in MSYS2 on a Windows machine, so hopefully that 
> doesn't invalidate any assumptions.

Ooh.  No idea.  Your strace and sed might have different options than 
mine.  Also, I am not sure if there are different pipe and fd duplication 
semantics, compared to linux.  But, based on the examples & output you're 
giving, I think we're on the same page for the discussion.


> Now setting up strace around the things within the coprocess, and only 
> passing in one line, I now have coproc-buffering-strace, attached. 
> Giving the argument 'L', both sed and expand call write() once. Giving 
> the argument 0, sed calls write() twice and expand calls it a bunch of 
> times, seemingly once for each character it outputs. So I guess that's 
> it.

:thumbsup:  Yeah that matches what I was seeing also.


Thanks for humoring the peanut gallery here :D

Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Examples of concurrent coproc usage?
  2024-03-11 11:54         ` Carl Edquist
@ 2024-03-11 15:12           ` Zachary Santer
  2024-03-14  9:58             ` Carl Edquist
  2024-03-12  3:34           ` RFE: enable buffering on null-terminated data Zachary Santer
  1 sibling, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-03-11 15:12 UTC (permalink / raw)
  To: Carl Edquist, bug-bash; +Cc: libc-alpha

Was "RFE: enable buffering on null-terminated data"

On Mon, Mar 11, 2024 at 7:54 AM Carl Edquist <edquist@cs.wisc.edu> wrote:
>
> On Sun, 10 Mar 2024, Zachary Santer wrote:
>
> > On Sun, Mar 10, 2024 at 4:36 PM Carl Edquist <edquist@cs.wisc.edu> wrote:
> >>
> >> Out of curiosity, do you have an example command line for your use case?
> >
> > My use for 'stdbuf --output=L' is to be able to run a command within a
> > bash coprocess.
>
> Oh, cool, now you're talking!  ;)
>
>
> > (Really, a background process communicating with the parent process
> > through FIFOs, since Bash prints a warning message if you try to run
> > more than one coprocess at a time. Shouldn't make a difference here.)
>
> (Kind of a side-note ... bash's limited coprocess handling was a long
> standing annoyance for me in the past, to the point that I wrote a bash
> coprocess management library to handle multiple active coprocess and give
> convenient methods for interaction.  Perhaps the trickiest bit about
> multiple coprocesses open at once (which I suspect is the reason support
> was never added to bash) is that you don't want the second and subsequent
> coprocesses to inherit the pipe fds of prior open coprocesses.  This can
> result in deadlock if, for instance, you close your write end to coproc1,
> but coproc1 continues to wait for input because coproc2 also has a copy of
> a write end of the pipe to coproc1's input.  So you need to be smart about
> subsequent coprocesses first closing all fds associated with other
> coprocesses.

https://lists.gnu.org/archive/html/help-bash/2021-03/msg00296.html
https://lists.gnu.org/archive/html/help-bash/2021-04/msg00136.html

You're on the money, though there is a preprocessor directive you can
build bash with that will allow it to handle multiple concurrent
coprocesses without complaining: MULTIPLE_COPROCS=1. Chet Ramey's
sticking point was that he hadn't seen coprocesses used enough in the
wild to satisfactorily test that his implementation did in fact keep
the coproc file descriptors out of subshells. If you've got examples
you can direct him to, I'd really appreciate it.

> Word to the wise: you might encounter this issue (coproc2 prevents coproc1
> from seeing its end-of-input) even though you are rigging this up yourself
> with FIFOs rather than bash's coproc builtin.)

In my case, it's mostly a non-issue, because I fork the - now three -
background processes before exec'ing automatic fds redirecting to/from
their FIFO's in the parent process. All the automatic fds get put in
an array, and I do close them all at the beginning of a subsequent
process substitution.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-11 11:54         ` Carl Edquist
  2024-03-11 15:12           ` Examples of concurrent coproc usage? Zachary Santer
@ 2024-03-12  3:34           ` Zachary Santer
  2024-03-14 14:15             ` Carl Edquist
  1 sibling, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-03-12  3:34 UTC (permalink / raw)
  To: Carl Edquist; +Cc: libc-alpha, coreutils, p

[-- Attachment #1: Type: text/plain, Size: 3163 bytes --]

On Mon, Mar 11, 2024 at 7:54 AM Carl Edquist <edquist@cs.wisc.edu> wrote:
>
> (In my coprocess management library, I effectively run every coproc with
> --output=L by default, by eval'ing the output of 'env -i stdbuf -oL env',
> because most of the time for a coprocess, that's whats wanted/necessary.)

Surrounded by 'set -a' and 'set +a', I guess? Now that's interesting.
I just added that to a script I have that prints lines output by
another command that it runs, generally a build script, to the command
line, but updating the same line over and over again. I want to see if
it updates more continuously like that.

> ... Although, for your example coprocess use, where the shell both
> produces the input for the coproc and consumes its output, you might be
> able to simplify things by making the producer and consumer separate
> processes.  Then you could do a simpler 'producer | filter | consumer'
> without having to worry about buffering at all.  But if the producer and
> consumer need to be in the same process (eg they share state and are
> logically interdependent), then yeah that's where you need a coprocess for
> the filter.

Yeah, there's really no way to break what I'm doing into a standard pipeline.

> (Although given your time output, you might say the performance hit for
> unbuffered is not that huge.)

We see a somewhat bigger difference, at least proportionally, if we
get bash more or less out of the way. See command-buffering, attached.

Standard:
real    0m0.202s
user    0m0.280s
sys     0m0.076s
Line-buffered:
real    0m0.497s
user    0m0.374s
sys     0m0.545s
Unbuffered:
real    0m0.648s
user    0m0.544s
sys     0m0.702s

In coproc-buffering, unbuffered output was 21.7% slower than
line-buffered output, whereas here it's 30.4% slower.

Of course, using line-buffered or unbuffered output in this situation
makes no sense. Where it might be useful in a pipeline is when an
earlier command in a pipeline might only print things occasionally,
and you want those things transformed and printed to the command line
immediately.

> So ... again in theory I also feel like a null-terminated buffering mode
> for stdbuf(1) (and setbuf(3)) is kind of a missing feature.

My assumption is that line-buffering through setbuf(3) was implemented
for printing to the command line, so its availability to stdbuf(1) is
just a useful side effect.

In the BUGS section in the man page for stdbuf(1), we see:
On GLIBC platforms, specifying a buffer size, i.e., using fully
buffered mode will result in undefined operation.

If I'm not mistaken, then buffer modes other than 0 and L don't
actually work. Maybe I should count my blessings here. I don't know
what's going on in the background that would explain glibc not
supporting any of that, or stdbuf(1) implementing features that aren't
supported on the vast majority of systems where it will be installed.

> It may just
> be that nobody has actually had a real need for it.  (Yet?)

I imagine if anybody has, they just set --output=0 and moved on. Bash
scripts aren't the fastest thing in the world, anyway.

[-- Attachment #2: command-buffering --]
[-- Type: application/octet-stream, Size: 887 bytes --]

#!/usr/bin/env bash

set -o nounset -o noglob +o braceexpand
shopt -s lastpipe
export LC_ALL='C.UTF-8'

tab_spaces=8

sed_expr='s/[[:blank:]]+$//'

test=$'  \tLine with tabs\t why?\t  '

repeat="${1}"

for (( i = 0; i < repeat; i++ )); do
  printf '%s\n' "${test}"
done > tab-input.txt

printf '%s' "Standard:"
time {
  sed --binary --regexp-extended --expression="${sed_expr}" < tab-input.txt |
    expand --tabs="${tab_spaces}" > /dev/null
}

printf '%s' "Line-buffered:"
time {
  stdbuf --output=L -- \
      sed --binary --regexp-extended --expression="${sed_expr}" < tab-input.txt |
    stdbuf --output=L -- \
        expand --tabs="${tab_spaces}" > /dev/null
}

printf '%s' "Unbuffered:"
time {
  stdbuf --output=0 -- \
      sed --binary --regexp-extended --expression="${sed_expr}" < tab-input.txt |
    stdbuf --output=0 -- \
        expand --tabs="${tab_spaces}" > /dev/null
}

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-03-11 15:12           ` Examples of concurrent coproc usage? Zachary Santer
@ 2024-03-14  9:58             ` Carl Edquist
  2024-03-17 19:40               ` Zachary Santer
                                 ` (3 more replies)
  0 siblings, 4 replies; 54+ messages in thread
From: Carl Edquist @ 2024-03-14  9:58 UTC (permalink / raw)
  To: Zachary Santer; +Cc: bug-bash, libc-alpha

[-- Attachment #1: Type: text/plain, Size: 11125 bytes --]

[My apologies up front for the length of this email.  The short story is I 
played around with the multi-coproc support: the fd closing seems to work 
fine to prevent deadlock, but I found one bug apparently introduced with 
multi-coproc support, and one other coproc bug that is not new.]

On Mon, 11 Mar 2024, Zachary Santer wrote:

> Was "RFE: enable buffering on null-terminated data"
>
> On Mon, Mar 11, 2024 at 7:54 AM Carl Edquist <edquist@cs.wisc.edu> wrote:
>>
>> (Kind of a side-note ... bash's limited coprocess handling was a long 
>> standing annoyance for me in the past, to the point that I wrote a bash 
>> coprocess management library to handle multiple active coprocess and 
>> give convenient methods for interaction.  Perhaps the trickiest bit 
>> about multiple coprocesses open at once (which I suspect is the reason 
>> support was never added to bash) is that you don't want the second and 
>> subsequent coprocesses to inherit the pipe fds of prior open 
>> coprocesses.  This can result in deadlock if, for instance, you close 
>> your write end to coproc1, but coproc1 continues to wait for input 
>> because coproc2 also has a copy of a write end of the pipe to coproc1's 
>> input.  So you need to be smart about subsequent coprocesses first 
>> closing all fds associated with other coprocesses.
>
> https://lists.gnu.org/archive/html/help-bash/2021-03/msg00296.html
> https://lists.gnu.org/archive/html/help-bash/2021-04/msg00136.html

Oh hey!  Look at that.  Thanks for the links to this thread - I gave them 
a read (along with the old thread from 2011-04).  I feel a little bad I 
missed the 2021 discussion.


> You're on the money, though there is a preprocessor directive you can 
> build bash with that will allow it to handle multiple concurrent 
> coprocesses without complaining: MULTIPLE_COPROCS=1.

Who knew!  Thanks for mentioning it.  When I saw that "only one active 
coprocess at a time" was _still_ listed in the bugs section in bash 5, I 
figured multiple coprocess support had just been abandoned.  Chet, that's 
cool that you implemented it.

I kind of went all-out on my bash coprocess management library though 
(mostly back in 2014-2016) ... It's pretty feature-rich and pleasant to 
use -- to the point that I don't think there is any going-back to bash's 
internal coproc for me, even with multiple coprocess are support.  I 
implemented it with shell functions, so it doesn't rely on compiling 
anything or the latest version of bash being present.  (I even added bash3 
support for older systems.)

> Chet Ramey's sticking point was that he hadn't seen coprocesses used 
> enough in the wild to satisfactorily test that his implementation did in 
> fact keep the coproc file descriptors out of subshells.

To be fair coproc is kind of a niche feature.  But I think more people 
would play with it if it were less awkward to use and if they felt free to 
experiment with multiple coprocs.

By the way, I agree with the Chet's exact description of the problems 
here:

     https://lists.gnu.org/archive/html/help-bash/2021-03/msg00282.html

The issue is separate from the stdio buffering discussion; the issue here 
is with child processes (and I think not foreground subshells, but 
specifically background processes, including coprocesses) inheriting the 
shell's fds that are open to pipes connected to an active coprocess.

Not getting a sigpipe/write failure results in a coprocess sitting around 
longer than it ought to, but it's not obvious (to me) how this leads to 
deadlock, since the shell at least has closed its read end of the pipe to 
that coprocess, so at least you aren't going to hang trying to read from 
it.

On the other hand, a coprocess not seeing EOF will cause deadlock pretty 
readily, especially if it processes all its input before producing output 
(as with wc, sort, sha1sum).  Trying to read from the coprocess will hang 
indefinitely if the coprocess is still waiting for input, which is the 
case if there is another copy of the write end of its read pipe open 
somewhere.


> If you've got examples you can direct him to, I'd really appreciate it.

[My original use cases for multiple coprocesses were (1) for 
programmatically interacting with multiple command-line database clients 
together, and (2) for talking to multiple interactive command-line game 
engines (othello) to play each other.

Perl's IPC::Open2 works, too, but it's easier to experiment on the fly in 
bash.

And in general having the freedom to play with multiple coprocesses helps 
mock up more complicated pipelines, or even webs of interconnected 
processes.]

But you can create a deadlock without doing anything fancy.


Well, *without multi-coproc support*, here's a simple wc example; first 
with a single coproc:

 	$ coproc WC { wc; }
 	$ exec {WC[1]}>&-
 	$ read -u ${WC[0]} X
 	$ echo $X
 	0 0 0

This works as expected.

But if you try it with a second coproc (again, without multi-coproc 
support), the second coproc will inherit copies of the shell's read and 
write pipe fds to the first coproc, and the read will hang (as described 
above), as the first coproc doesn't see EOF:

 	$ coproc WC { wc; }
 	$ coproc CAT { cat; }
 	$ exec {WC[1]}>&-
 	$ read -u ${WC[0]} X

 	# HANGS


But, this can be observed even before attempting the read that hangs.

You can 'ps' to see the user shell (bash), the coprocs' shells (bash), and 
the coprocs' commands (wc & cat).  Then 'ls -l /proc/PID/fd/' to see what 
they have open:

- The user shell has its copies of the read & write fds open for both 
coprocs (as it should)

- The coproc commands (wc & cat) each have only a single read & write pipe 
open, on fd 0 & 1 (as they should)

- The first coproc's shell (WC) has only a single read & write pipe open, 
on fd 0 & 1 (as it should)

- The second coproc's shell (CAT) has its own read & write pipes open, on 
fd 0 & 1 (good), but it also has a copy of the user shell's read & write 
pipe fds to the first coproc (WC) open (on fd 60 & 63 in this case, which 
it inherited when forking from the user shell)

(And in general, latter coproc shells will have stray copies of the user 
shell's r/w ends from all previous coprocs.)

So, you can examine the situation after setting up coprocs, to see if all 
the coproc-related processes have just two pipes open (on fd 0 & 1).  If 
this is the case, I think that suffices to convince me anyway that no 
deadlocks related to stray open fds can happen.  But if any of them has 
other pipes open (inherited from the user shell), that indicates the 
problem.


I tried compiling the latest bash with MULTIPLE_COPROCS=1 (version 
5.2.21(1)) to test out the multi-coproc support.

I tried standing up the above WC and CAT coprocs, together with some 
others to check that the behavior looked ok for pipelines also (which I 
think was one of Chet's concerns)

 	$ coproc WC { wc; }
 	$ coproc CAT { cat; }
 	$ coproc CAT3 { cat | cat | cat; }
 	$ coproc CAT4 { cat | cat | cat | cat; }
 	$ coproc CATX { cat ; }

And as far as the fd situation, everything checks out: the user shell has 
fds open to all the coprocs, and the coproc shells & coproc commands 
(including all the cat's in the pipelines) have only a single read & write 
pipe open on fd 0 & 1.  So, the multi-coproc code seems to be closing the 
shell's copies correctly.

[The examples are boring, but their point is just to investigate the 
stray-fd question.]


HOWEVER!!!

Unexpectedly, the new multi-coproc code seems to close the user shell's 
end of a coprocess's pipes, once the coprocess has terminated.  When 
compiled with MULTIPLE_COPROCS=1, this is true even if there is only a 
single coproc:

 	$ coproc WC { wc; }
 	$ exec {WC[1]}>&-
 	[1]+  Done                    coproc WC { wc; }

 	# WC var gets cleared!!
 	# shell's ${WC[0]} is also closed!

 	# now, can't do:

 	$ read -u ${WC[0]} X
 	$ echo $X

I'm attaching a "bad-coproc-log.txt" with more detailed ps & ls output 
examining the open fds at each step, to make it clear what's happening.

This is a bug.  The shell should not automatically close its read pipe to 
a coprocess that has terminated -- it should stay open to read the final 
output, and the user should be responsible for closing the read end 
explicitly.

This is more obvious for commands that wait until they see EOF before 
generating any output (wc, sort, sha1sum).  But it's also true for any 
command that produces output (filters (sed) or generators (ls)).  If the 
shell's read end is closed automatically, any final output waiting in the 
pipe will be discarded.

It also invites trouble if the shell variable that holds the fds gets 
removed unexpectedly when the coprocess terminates.  (Suddenly the 
variable expands to an empty string.)  It seems to me that the proper time 
to clear the coproc variable (if at all) is after the user has explicitly 
closed both of the fds.  *Or* else add an option to the coproc keyword to 
explicitly close the coproc - which will close both fds and clear the 
variable.

...

Separately, I consider the following coproc behavior to be weird, fragile, 
and broken.

If you fg a coproc, then stop and bg it, it dies.  Why?  Apparently the 
shell abandons the coproc when it is stopped, closes the pipe fds for it, 
and clears the fd variable.

 	$ coproc CAT { cat; }
 	[1] 10391

 	$ fg
 	coproc CAT { cat; }

 	# oops!

 	^Z
 	[1]+  Stopped                 coproc CAT { cat; }

 	$ echo ${CAT[@]}  # what happened to the fds?

 	$ ls -lgo /proc/$$/fd/
 	total 0
 	lrwx------ 1 64 Mar 14 02:26 0 -> /dev/pts/3
 	lrwx------ 1 64 Mar 14 02:26 1 -> /dev/pts/3
 	lrwx------ 1 64 Mar 14 02:25 2 -> /dev/pts/3
 	lrwx------ 1 64 Mar 14 02:26 255 -> /dev/pts/3

 	$ bg
 	[1]+ coproc CAT { cat; } &

 	$
 	[1]+  Done                    coproc CAT { cat; }

 	$ # sad user :(


This behavior is not new to the multi-coproc support.  But just the same 
it seems broken for the shell to automatically close the fds to 
coprocesses.  That should be done explicitly by the user.


>> Word to the wise: you might encounter this issue (coproc2 prevents 
>> coproc1 from seeing its end-of-input) even though you are rigging this 
>> up yourself with FIFOs rather than bash's coproc builtin.)
>
> In my case, it's mostly a non-issue, because I fork the - now three - 
> background processes before exec'ing automatic fds redirecting to/from 
> their FIFO's in the parent process. All the automatic fds get put in an 
> array, and I do close them all at the beginning of a subsequent process 
> substitution.

That's a nice trick with the shell backgrounding all the coprocesses 
before connecting the fifos.  But yeah, to make subsequent coprocesses you 
do still have to close the copy of the user shell's fds that the coprocess 
shell inherits.  It sounds like you are doing that (nice!), but in any 
case it requires some care, and as these stack up it is really handy to 
have something manage it all for you.

(Perhaps this is where I ask if you are happy with your solution or if you 
would like to try out something wildly more flexible...)


Happy coprocessing! :)

Carl

[-- Attachment #2: Type: text/plain, Size: 1358 bytes --]

$ coproc WC { wc; }
[1] 10038

$ ps
  PID TTY          TIME CMD
 9926 pts/3    00:00:00 bash
10038 pts/3    00:00:00 bash
10039 pts/3    00:00:00 wc
10040 pts/3    00:00:00 ps

$ ls -lgo /proc/{$$,10038,10039}/fd/
/proc/10038/fd/:
total 0
lr-x------ 1 64 Mar 14 02:29 0 -> pipe:[81214]
l-wx------ 1 64 Mar 14 02:29 1 -> pipe:[81213]
lrwx------ 1 64 Mar 14 02:28 2 -> /dev/pts/3
lrwx------ 1 64 Mar 14 02:29 255 -> /dev/pts/3

/proc/10039/fd/:
total 0
lr-x------ 1 64 Mar 14 02:29 0 -> pipe:[81214]
l-wx------ 1 64 Mar 14 02:29 1 -> pipe:[81213]
lrwx------ 1 64 Mar 14 02:28 2 -> /dev/pts/3

/proc/9926/fd/:
total 0
lrwx------ 1 64 Mar 14 02:26 0 -> /dev/pts/3
lrwx------ 1 64 Mar 14 02:26 1 -> /dev/pts/3
lrwx------ 1 64 Mar 14 02:25 2 -> /dev/pts/3
lrwx------ 1 64 Mar 14 02:26 255 -> /dev/pts/3
l-wx------ 1 64 Mar 14 02:26 60 -> pipe:[81214]
lr-x------ 1 64 Mar 14 02:26 63 -> pipe:[81213]

$ echo ${WC[@]}
63 60

$ exec {WC[1]}>&-
[1]+  Done                    coproc WC { wc; }

$ ps
  PID TTY          TIME CMD
 9926 pts/3    00:00:00 bash
10042 pts/3    00:00:00 ps

$ echo ${WC[@]}

$ ls -lgo /proc/$$/fd/
total 0
lrwx------ 1 64 Mar 14 02:26 0 -> /dev/pts/3
lrwx------ 1 64 Mar 14 02:26 1 -> /dev/pts/3
lrwx------ 1 64 Mar 14 02:25 2 -> /dev/pts/3
lrwx------ 1 64 Mar 14 02:26 255 -> /dev/pts/3


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-12  3:34           ` RFE: enable buffering on null-terminated data Zachary Santer
@ 2024-03-14 14:15             ` Carl Edquist
  2024-03-18  0:12               ` Zachary Santer
  0 siblings, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-03-14 14:15 UTC (permalink / raw)
  To: Zachary Santer; +Cc: libc-alpha, coreutils, p

[-- Attachment #1: Type: text/plain, Size: 7508 bytes --]


On Mon, 11 Mar 2024, Zachary Santer wrote:

> On Mon, Mar 11, 2024 at 7:54 AM Carl Edquist <edquist@cs.wisc.edu> 
> wrote:
>>
>> (In my coprocess management library, I effectively run every coproc 
>> with --output=L by default, by eval'ing the output of 'env -i stdbuf 
>> -oL env', because most of the time for a coprocess, that's whats 
>> wanted/necessary.)
>
> Surrounded by 'set -a' and 'set +a', I guess? Now that's interesting.

Ah, no - I use the 'VAR=VAL command line' syntax so that it's specific to 
the command (it's not left exported to the shell).

Effectively the coprocess commands are run with

 	LD_PRELOAD=... _STDBUF_O=L command line

This allow running shell functions for the command line, which will all 
get the desired stdbuf behavior.  Because you can't pass a shell function 
(within the context of the current shell) as the command to stdbuf.

As far as I can tell, the stdbuf tool sets LD_PRELOAD (to point to 
libstdbuf.so) and your custom buffering options in _STDBUF_{I,O,E}, in the 
environment for the program it runs.  The double-env thing there is just a 
way to cleanly get exactly the env vars that stdbuf sets.  The values 
don't change, but since they are an implementation detail of stdbuf, it's 
a bit more portable to grab the values this way rather than hard code 
them.  This is done only once per shell session to extract the values, and 
save them to a private variable, and then they are used for the command 
line as show above.

Of course, if "command line" starts with "stdbuf --output=0" or whatever, 
that will override the new line-buffered default.


You can definitely export it to your shell though, either with 'set -a' 
like you said, or with the export command.  After that everything you run 
should get line-buffered stdio by default.


> I just added that to a script I have that prints lines output by another 
> command that it runs, generally a build script, to the command line, but 
> updating the same line over and over again. I want to see if it updates 
> more continuously like that.

So, a lot of times build scripts run a bunch of individual commands. 
Each of those commands has an implied flush when it terminates, so you 
will get the output from each of them promptly (as each command 
completes), even without using stdbuf.

Where things get sloppy is if you add some stuff in a pipeline after your 
build script, which results in things getting block-buffered along the 
way:

 	$ ./build.sh | sed s/what/ever/ | tee build.log

And there you will definitely see a difference.


 	sloppy () {
 		for x in {1..10}; do sleep .2; echo $x; done |
 		sed s/^/:::/ | cat
 	}

 	{
 		echo before:
 		sloppy
 		echo

 		export $(env -i stdbuf -oL env)

 		echo after:
 		sloppy
 	}

> Yeah, there's really no way to break what I'm doing into a standard 
> pipeline.

I admit I'm curious what you're up to  :)


> Of course, using line-buffered or unbuffered output in this situation 
> makes no sense. Where it might be useful in a pipeline is when an 
> earlier command in a pipeline might only print things occasionally, and 
> you want those things transformed and printed to the command line 
> immediately.

Right ... And in that case, losing the performance benefit of a larger 
block buffer is a smaller price to pay.

> My assumption is that line-buffering through setbuf(3) was implemented 
> for printing to the command line, so its availability to stdbuf(1) is 
> just a useful side effect.

Right, stdbuf(1) leverages setbuf(3).

setbuf(3) tweaks the buffering behavior of stdio streams (stdin, stdout, 
stderr, and anything else you open with, eg, fopen(3)).  It's not really 
limited to terminal applications, but yeah it makes it easier to ensure 
that your calls to printf(3) actually get output after each line (whether 
that's to a file or a pipe or a tty), without having to call an explicit 
fflush(3) of stdout every time.

stdbuf(1) sets LD_PRELOAD to libstdbuf.so for your program, causing it to 
call setbuf(3) at program startup based on the values of _STDBUF_* in the 
environment (which stdbuf(1) also sets).

(That's my read of it anyway.)

> In the BUGS section in the man page for stdbuf(1), we see: On GLIBC 
> platforms, specifying a buffer size, i.e., using fully buffered mode 
> will result in undefined operation.

Eheh xD

Oh, I imagine "undefined operation" means something more like 
"unspecified" here.  stdbuf(1) uses setbuf(3), so the behavior you'll get 
should be whatever the setbuf(3) from the libc on your system does.

I think all this means is that the C/POSIX standards are a bit loose about 
what is required of setbuf(3) when a buffer size is specified, and there 
is room in the standard for it to be interpreted as only a hint.

> If I'm not mistaken, then buffer modes other than 0 and L don't actually 
> work. Maybe I should count my blessings here. I don't know what's going 
> on in the background that would explain glibc not supporting any of 
> that, or stdbuf(1) implementing features that aren't supported on the 
> vast majority of systems where it will be installed.

Hey try it right?

Works for me (on glibc-2.23)

 	$ for s in 8k 16k 32k 1M; do
 	    echo ::: $s :::
 	    { stdbuf -o$s strace -ewrite tr 1 2
 	    } < /dev/zero 2>&1 > /dev/null | head -3
 	    echo
 	  done

 	::: 8k :::
 	write(1, "\0\0\0\0\0\0\0\0"..., 8192) = 8192
 	write(1, "\0\0\0\0\0\0\0\0"..., 8192) = 8192
 	write(1, "\0\0\0\0\0\0\0\0"..., 8192) = 8192

 	::: 16k :::
 	write(1, "\0\0\0\0\0\0\0\0"..., 16384) = 16384
 	write(1, "\0\0\0\0\0\0\0\0"..., 16384) = 16384
 	write(1, "\0\0\0\0\0\0\0\0"..., 16384) = 16384

 	::: 32k :::
 	write(1, "\0\0\0\0\0\0\0\0"..., 32768) = 32768
 	write(1, "\0\0\0\0\0\0\0\0"..., 32768) = 32768
 	write(1, "\0\0\0\0\0\0\0\0"..., 32768) = 32768

 	::: 1M :::
 	write(1, "\0\0\0\0\0\0\0\0"..., 1048576) = 1048576
 	write(1, "\0\0\0\0\0\0\0\0"..., 1048576) = 1048576
 	write(1, "\0\0\0\0\0\0\0\0"..., 1048576) = 1048576



>> It may just be that nobody has actually had a real need for it. 
>> (Yet?)
>
> I imagine if anybody has, they just set --output=0 and moved on. Bash 
> scripts aren't the fastest thing in the world, anyway.

Ouch.  Ouch.  Ouuuuch.  :)

While that's true if you're talking about bash itself doing the actual 
computation and data processing, the main work of the shell is making it 
easy to set up pipelines for other (very fast) programs to pass their data 
around.

The stdbuf tool is not meant for the shell!  It's meant for those very 
fast programs that the shell stands up.

Using stdbuf to tweak a very fast program, causing it to output more often 
at newlines over pipes rather than at block boundaries, does slow down 
those programs somewhat.  But as we've discussed, this is necessary for 
certain pipelines that have two-way communication (including coprocesses), 
or in general any time you want the output immediately.

What may not be obvious is that the shell does not need to get involved 
with writing input for a coprocess or reading its output - the shell can 
start other (very fast) programs with input/output redirected to/from the 
coprocess pipes to do that processing.

My point though earlier was that a null-terminated record buffering mode, 
as useful as it sounds on the surface (for null-terminated paths), may 
actually be something _nobody_ has ever actually needed for an actual (not 
contrived) workflow.

But then again I say "Yet?" - because, never say never.


Happy line-buffering  :)

Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-03-14  9:58             ` Carl Edquist
@ 2024-03-17 19:40               ` Zachary Santer
  2024-04-01 19:24               ` Chet Ramey
                                 ` (2 subsequent siblings)
  3 siblings, 0 replies; 54+ messages in thread
From: Zachary Santer @ 2024-03-17 19:40 UTC (permalink / raw)
  To: Carl Edquist; +Cc: bug-bash, libc-alpha

On Thu, Mar 14, 2024 at 6:57 AM Carl Edquist <edquist@cs.wisc.edu> wrote:

> (And in general, latter coproc shells will have stray copies of the user
> shell's r/w ends from all previous coprocs.)

I didn't know that without MULTIPLE_COPROCS=1, bash wouldn't even
attempt to keep the fds from earlier coprocs out of later coprocs.

> Unexpectedly, the new multi-coproc code seems to close the user shell's
> end of a coprocess's pipes, once the coprocess has terminated.  When
> compiled with MULTIPLE_COPROCS=1, this is true even if there is only a
> single coproc:

> This is a bug.  The shell should not automatically close its read pipe to
> a coprocess that has terminated -- it should stay open to read the final
> output, and the user should be responsible for closing the read end
> explicitly.

> It also invites trouble if the shell variable that holds the fds gets
> removed unexpectedly when the coprocess terminates.  (Suddenly the
> variable expands to an empty string.)  It seems to me that the proper time
> to clear the coproc variable (if at all) is after the user has explicitly
> closed both of the fds.  *Or* else add an option to the coproc keyword to
> explicitly close the coproc - which will close both fds and clear the
> variable.

I agree. This was the discussion in [1], where it sounds like this was
the intended behavior. The array that bash originally created to store
the coproc fds is removed immediately, but the fds are evidently
closed at some later, indeterminate point. So, if you store the coproc
fds in a different array than the one bash gave you, you might still
be able to read from the read fd for a little while. That sounds
suspiciously like a race condition, though. The behavior without
MULTIPLE_COPROCS=1 might have changed since that discussion.

> That's a nice trick with the shell backgrounding all the coprocesses
> before connecting the fifos.  But yeah, to make subsequent coprocesses you
> do still have to close the copy of the user shell's fds that the coprocess
> shell inherits.  It sounds like you are doing that (nice!), but in any
> case it requires some care, and as these stack up it is really handy to
> have something manage it all for you.

I absolutely learned more about what I was doing from that
conversation with Chet three years ago.

> (Perhaps this is where I ask if you are happy with your solution or if you
> would like to try out something wildly more flexible...)

Admittedly, I am very curious to see your bash coprocess management
library. I don't know how you could implement coprocesses outside of
bash's coproc keyword without using FIFOs somehow.

> Happy coprocessing! :)

Thanks for your detailed description of all this.

[1] https://lists.gnu.org/archive/html/help-bash/2021-04/msg00136.html

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-14 14:15             ` Carl Edquist
@ 2024-03-18  0:12               ` Zachary Santer
  2024-03-19  5:24                 ` Kaz Kylheku
  0 siblings, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-03-18  0:12 UTC (permalink / raw)
  To: Carl Edquist; +Cc: libc-alpha, coreutils, p

On Thu, Mar 14, 2024 at 11:14 AM Carl Edquist <edquist@cs.wisc.edu> wrote:

> Where things get sloppy is if you add some stuff in a pipeline after your
> build script, which results in things getting block-buffered along the
> way:
>
>         $ ./build.sh | sed s/what/ever/ | tee build.log
>
> And there you will definitely see a difference.

Sadly, the man page for stdbuf specifically calls out tee as being
unaffected by stdbuf, because it adjusts the buffering of its standard
streams itself. The script I mentioned pipes everything through tee,
and I don't think I'm willing to refactor it not to. Ah well.

> Oh, I imagine "undefined operation" means something more like
> "unspecified" here.  stdbuf(1) uses setbuf(3), so the behavior you'll get
> should be whatever the setbuf(3) from the libc on your system does.
>
> I think all this means is that the C/POSIX standards are a bit loose about
> what is required of setbuf(3) when a buffer size is specified, and there
> is room in the standard for it to be interpreted as only a hint.

> Works for me (on glibc-2.23)

Thanks for setting me straight here.

> What may not be obvious is that the shell does not need to get involved
> with writing input for a coprocess or reading its output - the shell can
> start other (very fast) programs with input/output redirected to/from the
> coprocess pipes to do that processing.

Gosh, I'd like to see an example of that, too.

> My point though earlier was that a null-terminated record buffering mode,
> as useful as it sounds on the surface (for null-terminated paths), may
> actually be something _nobody_ has ever actually needed for an actual (not
> contrived) workflow.

I considered how it seemed like something people could need years ago
and only thought to email into email lists about it last weekend.
Maybe there are all sorts of people out there who have been using
'stdbuf --output=0' on null-terminated data for years and never
thought to raise the issue. I know that's not a very strong argument,
though.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-18  0:12               ` Zachary Santer
@ 2024-03-19  5:24                 ` Kaz Kylheku
  2024-03-19 12:50                   ` Zachary Santer
  0 siblings, 1 reply; 54+ messages in thread
From: Kaz Kylheku @ 2024-03-19  5:24 UTC (permalink / raw)
  To: Zachary Santer; +Cc: Carl Edquist, libc-alpha, coreutils, p

On 2024-03-17 17:12, Zachary Santer wrote:
> On Thu, Mar 14, 2024 at 11:14 AM Carl Edquist <edquist@cs.wisc.edu> wrote:
> 
>> Where things get sloppy is if you add some stuff in a pipeline after your
>> build script, which results in things getting block-buffered along the
>> way:
>>
>>         $ ./build.sh | sed s/what/ever/ | tee build.log
>>
>> And there you will definitely see a difference.
> 
> Sadly, the man page for stdbuf specifically calls out tee as being
> unaffected by stdbuf, because it adjusts the buffering of its standard
> streams itself. The script I mentioned pipes everything through tee,
> and I don't think I'm willing to refactor it not to. Ah well.

But what tee does is set up _IONBF on its output streams,
including stdout.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-19  5:24                 ` Kaz Kylheku
@ 2024-03-19 12:50                   ` Zachary Santer
  2024-03-20  8:55                     ` Carl Edquist
  0 siblings, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-03-19 12:50 UTC (permalink / raw)
  To: Kaz Kylheku; +Cc: Carl Edquist, libc-alpha, coreutils, p

On Tue, Mar 19, 2024 at 1:24 AM Kaz Kylheku <kaz@kylheku.com> wrote:
>
> But what tee does is set up _IONBF on its output streams,
> including stdout.

So it doesn't buffer at all. Awesome. Nevermind.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: RFE: enable buffering on null-terminated data
  2024-03-19 12:50                   ` Zachary Santer
@ 2024-03-20  8:55                     ` Carl Edquist
  2024-04-19  0:16                       ` Modify buffering of standard streams via environment variables (not LD_PRELOAD)? Zachary Santer
  0 siblings, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-03-20  8:55 UTC (permalink / raw)
  To: Zachary Santer; +Cc: Kaz Kylheku, libc-alpha, coreutils, p

[-- Attachment #1: Type: text/plain, Size: 1499 bytes --]


On Tue, 19 Mar 2024, Zachary Santer wrote:

> On Tue, Mar 19, 2024 at 1:24 AM Kaz Kylheku <kaz@kylheku.com> wrote:
>>
>> But what tee does is set up _IONBF on its output streams,
>> including stdout.
>
> So it doesn't buffer at all. Awesome. Nevermind.

Yay!  :D

And since tee uses fwrite to copy whatever input is available, that will 
mean 'records' are output on the same boundaries as the input (whether 
that be newlines, nuls, or just block boundaries).  So putting tee in the 
middle of a pipeline shouldn't itself interfere with whatever else you're 
up to.  (AND it's still relatively efficient, compared to some tools like 
cut that putchar a byte at a time.)

My note about pipelines like this though:

 	$ ./build.sh | sed s/what/ever/ | tee build.log

is that with the default stdio buffering, while all the commands in 
build.sh will be implicitly self-flushing, the sed in the middle will end 
up batching its output into blocks, so tee will also repeat them in 
blocks.

However, if stdbuf's magic env vars are exported in your shell (either by 
doing a trick like 'export $(env -i stdbuf -oL env)', or else more simply 
by first starting a new shell with 'stdbuf -oL bash'), then every command 
in your pipelines will start with the new default line-buffered stdout. 
That way your line-items from build.sh should get passed all the way 
through the pipeline as they are produced.


(But, proof's in the pudding, so whatever works for you :D )


Happy putting all the way!

Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-03-14  9:58             ` Carl Edquist
  2024-03-17 19:40               ` Zachary Santer
@ 2024-04-01 19:24               ` Chet Ramey
  2024-04-01 19:31                 ` Chet Ramey
  2024-04-03 14:32               ` Chet Ramey
  2024-04-17 14:37               ` Chet Ramey
  3 siblings, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-01 19:24 UTC (permalink / raw)
  To: Carl Edquist, Zachary Santer; +Cc: chet.ramey, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 1431 bytes --]

On 3/14/24 5:58 AM, Carl Edquist wrote:


> But you can create a deadlock without doing anything fancy.
> 
> 
> Well, *without multi-coproc support*, here's a simple wc example; first 
> with a single coproc:
> 
>      $ coproc WC { wc; }
>      $ exec {WC[1]}>&-
>      $ read -u ${WC[0]} X
>      $ echo $X
>      0 0 0
> 
> This works as expected.
> 
> But if you try it with a second coproc (again, without multi-coproc 
> support), the second coproc will inherit copies of the shell's read and 
> write pipe fds to the first coproc, and the read will hang (as described 
> above), as the first coproc doesn't see EOF:
> 
>      $ coproc WC { wc; }
>      $ coproc CAT { cat; }
>      $ exec {WC[1]}>&-
>      $ read -u ${WC[0]} X
> 
>      # HANGS
> 
> 
> But, this can be observed even before attempting the read that hangs.

Let's see if we can tackle these one at a time. This seems like it would be
pretty easy to fix if a coproc closed the fds corresponding to an existing
coproc in the child after the fork. That wouldn't really change anything
regarding how scripts have to manually manage multiple coprocs, but it
will prevent the shell from hanging.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-01 19:24               ` Chet Ramey
@ 2024-04-01 19:31                 ` Chet Ramey
  2024-04-02 16:22                   ` Carl Edquist
  0 siblings, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-01 19:31 UTC (permalink / raw)
  To: Carl Edquist, Zachary Santer; +Cc: chet.ramey, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 1718 bytes --]

On 4/1/24 3:24 PM, Chet Ramey wrote:
> On 3/14/24 5:58 AM, Carl Edquist wrote:
> 
> 
>> But you can create a deadlock without doing anything fancy.
>>
>>
>> Well, *without multi-coproc support*, here's a simple wc example; first 
>> with a single coproc:
>>
>>      $ coproc WC { wc; }
>>      $ exec {WC[1]}>&-
>>      $ read -u ${WC[0]} X
>>      $ echo $X
>>      0 0 0
>>
>> This works as expected.
>>
>> But if you try it with a second coproc (again, without multi-coproc 
>> support), the second coproc will inherit copies of the shell's read and 
>> write pipe fds to the first coproc, and the read will hang (as described 
>> above), as the first coproc doesn't see EOF:
>>
>>      $ coproc WC { wc; }
>>      $ coproc CAT { cat; }
>>      $ exec {WC[1]}>&-
>>      $ read -u ${WC[0]} X
>>
>>      # HANGS
>>
>>
>> But, this can be observed even before attempting the read that hangs.
> 
> Let's see if we can tackle these one at a time. This seems like it would be
> pretty easy to fix if a coproc closed the fds corresponding to an existing
> coproc in the child after the fork. That wouldn't really change anything
> regarding how scripts have to manually manage multiple coprocs, but it
> will prevent the shell from hanging.
> 

I sent this before I was ready. This would be equivalent to changing the
commands to use

coproc CAT { exec {WC[0]}<&- {WC[1]}>&- ; cat; }

but the script writer wouldn't have to manage it.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-01 19:31                 ` Chet Ramey
@ 2024-04-02 16:22                   ` Carl Edquist
  2024-04-03 13:54                     ` Chet Ramey
  0 siblings, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-04-02 16:22 UTC (permalink / raw)
  To: Chet Ramey; +Cc: Zachary Santer, bug-bash, libc-alpha

[-- Attachment #1: Type: text/plain, Size: 1949 bytes --]

On Mon, 1 Apr 2024, Chet Ramey wrote:

> On 4/1/24 3:24 PM, Chet Ramey wrote:
>>  On 3/14/24 5:58 AM, Carl Edquist wrote:
>>
>>>  Well, *without multi-coproc support*, here's a simple wc example; first
>>>  with a single coproc:
>>>
>>>       $ coproc WC { wc; }
>>>       $ exec {WC[1]}>&-
>>>       $ read -u ${WC[0]} X
>>>       $ echo $X
>>>       0 0 0
>>>
>>>  This works as expected.
>>>
>>>  But if you try it with a second coproc (again, without multi-coproc
>>>  support), the second coproc will inherit copies of the shell's read and
>>>  write pipe fds to the first coproc, and the read will hang (as described
>>>  above), as the first coproc doesn't see EOF:
>>>
>>>       $ coproc WC { wc; }
>>>       $ coproc CAT { cat; }
>>>       $ exec {WC[1]}>&-
>>>       $ read -u ${WC[0]} X
>>>
>>>       # HANGS
>>> 
>>>
>>>  But, this can be observed even before attempting the read that hangs.
>>
>>  Let's see if we can tackle these one at a time. This seems like it
>>  would be pretty easy to fix if a coproc closed the fds corresponding
>>  to an existing coproc in the child after the fork. That wouldn't
>>  really change anything regarding how scripts have to manually manage
>>  multiple coprocs, but it will prevent the shell from hanging.
>> 
>
> I sent this before I was ready. This would be equivalent to changing the 
> commands to use
>
> coproc CAT { exec {WC[0]}<&- {WC[1]}>&- ; cat; }
>
> but the script writer wouldn't have to manage it.


Agreed.

And just to note two things (in case it wasn't clear) - (1) the above 
example that hangs is with the default bash, compiled _without_ 
multi-coproc support; and (2):

> This seems like it would be pretty easy to fix if a coproc closed the 
> fds corresponding to an existing coproc in the child after the fork

the forked coproc has to close its fds to/from _all_ other existing 
coprocs (as there can be several).


Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-02 16:22                   ` Carl Edquist
@ 2024-04-03 13:54                     ` Chet Ramey
  0 siblings, 0 replies; 54+ messages in thread
From: Chet Ramey @ 2024-04-03 13:54 UTC (permalink / raw)
  To: Carl Edquist; +Cc: chet.ramey, Zachary Santer, bug-bash, libc-alpha

On 4/2/24 12:22 PM, Carl Edquist wrote:

>> This seems like it would be pretty easy to fix if a coproc closed the fds 
>> corresponding to an existing coproc in the child after the fork
> 
> the forked coproc has to close its fds to/from _all_ other existing coprocs 
> (as there can be several).

And there is the issue. Without multi-coproc support, the shell only keeps
track of one coproc at a time, so there's only one set of pipe file
descriptors to close.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-03-14  9:58             ` Carl Edquist
  2024-03-17 19:40               ` Zachary Santer
  2024-04-01 19:24               ` Chet Ramey
@ 2024-04-03 14:32               ` Chet Ramey
  2024-04-03 17:19                 ` Zachary Santer
  2024-04-04 12:52                 ` Carl Edquist
  2024-04-17 14:37               ` Chet Ramey
  3 siblings, 2 replies; 54+ messages in thread
From: Chet Ramey @ 2024-04-03 14:32 UTC (permalink / raw)
  To: Carl Edquist, Zachary Santer; +Cc: chet.ramey, bug-bash, libc-alpha

On 3/14/24 5:58 AM, Carl Edquist wrote:

> HOWEVER!!!
> 
> Unexpectedly, the new multi-coproc code seems to close the user shell's end 
> of a coprocess's pipes, once the coprocess has terminated.  When compiled 
> with MULTIPLE_COPROCS=1, this is true even if there is only a single coproc:
> 
>      $ coproc WC { wc; }
>      $ exec {WC[1]}>&-
>      [1]+  Done                    coproc WC { wc; }
> 
>      # WC var gets cleared!!
>      # shell's ${WC[0]} is also closed!
> 
>      # now, can't do:
> 
>      $ read -u ${WC[0]} X
>      $ echo $X
> 
> I'm attaching a "bad-coproc-log.txt" with more detailed ps & ls output 
> examining the open fds at each step, to make it clear what's happening.

It's straightforward: the coproc process terminates, the shell reaps it,
marks it as dead, notifies the user that the process exited, and reaps it
before printing the next prompt. I don't observe any different behavior
between the default and when compiled for multiple coprocs.

It depends on when the process terminates as to whether you get a prompt
back and need to run an additional command before reaping the coproc
(macOS, RHEL), which gives you the opportunity to run the `read' command:

$ coproc WC { wc; }
[1] 48057
$ exec {WC[1]}>&-
$ read -u ${WC[0]} X
[1]+  Done                    coproc WC { wc; }
bash: DEBUG warning: cpl_reap: deleting 48057
$ echo $X
0 0 0

(I put in a trace statement to show exactly when the coproc gets reaped and
deallocated.)

I can't reproduce your results with non-interactive shells, either, with
job control enabled or disabled.


> This is a bug.  The shell should not automatically close its read pipe to a 
> coprocess that has terminated -- it should stay open to read the final 
> output, and the user should be responsible for closing the read end 
> explicitly.

How long should the shell defer deallocating the coproc after the process
terminates? What should it do to make sure that the variables don't hang
around with invalid file descriptors? Or should the user be responsible for
unsetting the array variable too? (That's never been a requirement,
obviously.)


> It also invites trouble if the shell variable that holds the fds gets 
> removed unexpectedly when the coprocess terminates.  (Suddenly the variable 
> expands to an empty string.)  It seems to me that the proper time to clear 
> the coproc variable (if at all) is after the user has explicitly closed 
> both of the fds. 

That requires adding more plumbing than I want to, especially since the
user can always save the file descriptors of interest into another
variable if they want to use them after the coproc terminates.

> *Or* else add an option to the coproc keyword to 
> explicitly close the coproc - which will close both fds and clear the 
> variable.

Not going to add any more options to reserved words; that does more
violence to the grammar than I want.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-03 14:32               ` Chet Ramey
@ 2024-04-03 17:19                 ` Zachary Santer
  2024-04-08 15:07                   ` Chet Ramey
  2024-04-04 12:52                 ` Carl Edquist
  1 sibling, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-04-03 17:19 UTC (permalink / raw)
  To: chet.ramey; +Cc: Carl Edquist, bug-bash, libc-alpha

On Wed, Apr 3, 2024 at 10:32 AM Chet Ramey <chet.ramey@case.edu> wrote:
>
> How long should the shell defer deallocating the coproc after the process
> terminates? What should it do to make sure that the variables don't hang
> around with invalid file descriptors? Or should the user be responsible for
> unsetting the array variable too? (That's never been a requirement,
> obviously.)

For sake of comparison, and because I don't know the answer, what does
bash do behind the scenes in this situation?

exec {fd}< <( some command )
while IFS='' read -r line <&"${fd}"; do
  # do stuff
done
{fd}<&-

Because the command in the process substitution isn't waiting for
input, (I think) it could've exited at any point before all of its
output has been consumed. Even so, bash appears to handle this
seamlessly.

As the programmer, I know ${fd} contains an fd that's no longer valid
after this point, despite it not being unset.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-03 14:32               ` Chet Ramey
  2024-04-03 17:19                 ` Zachary Santer
@ 2024-04-04 12:52                 ` Carl Edquist
  2024-04-04 23:23                   ` Martin D Kealey
  2024-04-08 16:21                   ` Chet Ramey
  1 sibling, 2 replies; 54+ messages in thread
From: Carl Edquist @ 2024-04-04 12:52 UTC (permalink / raw)
  To: Chet Ramey; +Cc: Zachary Santer, bug-bash, libc-alpha

Hi Chet, thanks for taking the time to review this  :D

[My apologies again upfront for another lengthy (comprehensive?) email.]


On Wed, 3 Apr 2024, Chet Ramey wrote:

> On 4/2/24 12:22 PM, Carl Edquist wrote:
>
>>  the forked coproc has to close its fds to/from _all_ other existing
>>  coprocs (as there can be several).
>
> And there is the issue. Without multi-coproc support, the shell only 
> keeps track of one coproc at a time, so there's only one set of pipe 
> file descriptors to close.

Right, exactly.  The example with the default build (showing the essential 
case that causes deadlock) was to highlight that your multi-coproc support 
code apparently does indeed correctly track and close all these fds, and 
thus prevents the deadlock issue.


On Wed, 3 Apr 2024, Chet Ramey wrote:

> It's straightforward: the coproc process terminates, the shell reaps it, 
> marks it as dead, notifies the user that the process exited, and reaps 
> it before printing the next prompt. I don't observe any different 
> behavior between the default and when compiled for multiple coprocs.
>
> It depends on when the process terminates as to whether you get a prompt 
> back and need to run an additional command before reaping the coproc 
> (macOS, RHEL), which gives you the opportunity to run the `read' 
> command:

Ah, my mistake then - thanks for explaining.  I must have been thrown off 
by the timing, running it with and without an intervening interactive 
prompt before the read command.  When run interactively, an extra 'Enter' 
(or not) before the read command changes the behavior.

So in that case, this issue (that the shell closes its read-end of the 
pipe from a reaped coproc, potentially before being able to read the final 
output) was already there and is not specific to the multi-coproc code.

But in any case, it seems like this is a race then?  That is, whether the 
child process terminates before or after the prompt in question.

> $ coproc WC { wc; }
> [1] 48057
> $ exec {WC[1]}>&-
> $ read -u ${WC[0]} X
> [1]+  Done                    coproc WC { wc; }
> bash: DEBUG warning: cpl_reap: deleting 48057
> $ echo $X
> 0 0 0
>
> (I put in a trace statement to show exactly when the coproc gets reaped and
> deallocated.)

Thanks! (for taking the time to play with this)

Though apparently it's still a race here.  If you diagram the shell and 
coproc (child) processes, I think you'll see that your DEBUG statement can 
also happen _before_ the read command, which would then fail.  You can 
contrive this by adding a small sleep (eg, 0.1s) at the end of 
execute_builtin_or_function (in execute_cmd.c), just before it returns.

Eg:

 	diff --git a/execute_cmd.c b/execute_cmd.c
 	index ed1063e..c72f322 100644
 	--- a/execute_cmd.c
 	+++ b/execute_cmd.c
 	@@ -5535,6 +5535,7 @@ execute_builtin_or_function (words, builtin, var, redirects,
 	   discard_unwind_frame ("saved_fifos");
 	 #endif

 	+  usleep(100000);
 	   return (result);
 	 }


If I do this, I consistently see "read: X: invalid file descriptor 
specification" running the above 4-line "coproc WC" example in a script, 
demonstrating that there is no guarantee that the read command will start 
before the WC coproc is reaped and {WC[0]} is closed, even though it's the 
next statement after 'exec {WC[1]}>&-'.

But (as I'll try to show) you can trip up on this race even without 
slowing down bash itself artificially.


> I can't reproduce your results with non-interactive shells, either, with 
> job control enabled or disabled.

That's fair; let's try it with a script:

 	$ cat cope.sh
 	#!/bin/bash

 	coproc WC { wc; }
 	jobs
 	exec {WC[1]}>&-
 	[[ $1 ]] && sleep "$1"
 	jobs
 	read -u ${WC[0]} X
 	echo $X


Run without sleep, the wc output is seen:

 	$ ./cope.sh
 	[1]+  Running                 coproc WC { wc; } &
 	[1]+  Running                 coproc WC { wc; } &
 	0 0 0


Run with a brief sleep after closing the write end, and it breaks:

 	$ ./cope.sh .1
 	[1]+  Running                 coproc WC { wc; } &
 	[1]+  Done                    coproc WC { wc; }
 	./cope.sh: line 8: read: X: invalid file descriptor specification


And, if I run with "0" for a sleep time, it intermittently behaves like 
either of the above.  Racy!


>> This is a bug.  The shell should not automatically close its read pipe 
>> to a coprocess that has terminated -- it should stay open to read the 
>> final output, and the user should be responsible for closing the read 
>> end explicitly.
>
> How long should the shell defer deallocating the coproc after the 
> process terminates?

I only offer my opinion here, but it strikes me that it definitely should 
_not_ be based on an amount of _time_.  That's inherently racy.

In the above example, there is only a single line to read; but the general 
case there may be many 'records' sitting in the pipe waiting to be 
processed, and processing each record may take an arbitrary amount of 
time.  (Consider a coproc containing a sort command, for example, that 
produces all its output lines at once after it sees EOF, and then 
terminates.)


Zack illustrated basically the same point with his example:

>	exec {fd}< <( some command )
>	while IFS='' read -r line <&"${fd}"; do
>	  # do stuff
>	done
>	{fd}<&-

A process-substitution open to the shell like this is effectively a 
one-ended coproc (though not in the jobs list), and it behaves reliably 
here because the user can count on {fd} to remain open even after the 
child process terminates.


So, the user can determine when the coproc fds are no longer needed, 
whether that's when EOF is hit trying to read from the coproc, or whatever 
other condition.


Personally I like the idea of 'closing' a coproc explicitly, but if it's a 
bother to add options to the coproc keyword, then I would say just let the 
user be responsible for closing the fds.  Once the coproc has terminated 
_and_ the coproc's fds are closed, then the coproc can be deallocated.

Apparently there is already some detection in there for when the coproc 
fds get closed, as the {NAME[@]} fd array members get set to -1 
automatically when when you do, eg, 'exec {NAME[0]}<&-'.  So perhaps this 
won't be a radical change.

Alternatively (or, additionally), you could interpret 'unset NAME' for a 
coproc to mean "deallocate the coproc."  That is, close the {NAME[@]} fds, 
unset the NAME variable, and remove any coproc bookkeeping for NAME.

(Though if the coproc child process hasn't terminated on its own yet, 
still it shouldn't be killed, and perhaps it should remain in the jobs 
list as a background process until it's done.)

...

[And if you _really_ don't want to defer deallocating a coproc after it 
terminates, I suppose you can go ahead and deallocate it in terms of 
removing it from the jobs list and dropping any bookkeeping for it - as 
long as you leave the fds and fd variables intact for the user.

It's a little dicey, but in theory it should not lead to deadlock, even if 
copies of these (now untracked) reaped-coproc pipe fds end up in other 
coprocs.  Why not?  Because (1) this coproc is dead and thus won't be 
trying to read anymore from its (now closed) end, and (2) reads from the 
shell's end will not block since there are no copies of the coproc's write 
end open anymore.

Still, it'd be cleaner to defer deallocation, to avoid these stray (albeit 
harmless) copies of fds making their way into new coprocs.]


> What should it do to make sure that the variables don't hang around with 
> invalid file descriptors?

First, just to be clear, the fds to/from the coproc pipes are not invalid 
when the coproc terminates (you can still read from them); they are only 
invalid after they are closed.

The surprising bit is when they become invalid unexpectedly (from the 
point of view of the user) because the shell closes them automatically, 
at the somewhat arbitrary timing when the coproc is reaped.


Second, why is it a problem if the variables keep their (invalid) fds 
after closing them, if the user is the one that closed them anyway?

Isn't this how it works with the auto-assigned fd redirections?

Eg:

 	$ exec {d}<.
 	$ echo $d
 	10
 	$ exec {d}<&-
 	$ echo $d
 	10


But, as noted, bash apparently already ensures that the variables don't 
hang around with invalid file descriptors, as once you close them the 
corresponding variable gets updated to "-1".


> Or should the user be responsible for unsetting the array variable too? 
> (That's never been a requirement, obviously.)

On the one hand, bash is already messing with the coproc array variables 
(setting the values to -1 when the user closes the fds), so it's not 
really a stretch in my mind for bash to unset the whole variable when the 
coproc is deallocated.  On the other hand, as mentioned above, bash leaves 
automatically allocated fd variables intact after the user explicitly 
closes them.

So I guess either way seems reasonable.

If the user has explicitly closed both fd ends for a coproc, it should not 
be a surprise to the user either way - whether the variable gets unset 
automatically, or whether it remains with (-1 -1).

Since you are already unsetting the variable when the coproc is 
deallocated though, I'd say it's fine to keep doing that -- just don't 
deallocate the coproc before the user has closed both fds.


>> It also invites trouble if the shell variable that holds the fds gets 
>> removed unexpectedly when the coprocess terminates.  (Suddenly the 
>> variable expands to an empty string.)  It seems to me that the proper 
>> time to clear the coproc variable (if at all) is after the user has 
>> explicitly closed both of the fds.
>
> That requires adding more plumbing than I want to,

Your project your call :D

> especially since the user can always save the file descriptors of 
> interest into another variable if they want to use them after the coproc 
> terminates.

*Except* that it's inherently a race condition whether the original 
variables will still be intact to save them.

Even if you attempt to save them immediately:

 	coproc X { exit; }
 	X_BACKUP=( ${X[@]} )

it's not guaranteed that X_BACKUP=(...) will run before coproc X has been 
deallocated, and the X variable cleared.

No doubt this hasn't escaped you, but in any case you can see it for 
yourself if you introduce a small delay in execute_coproc, in the parent, 
just after the call to make_child:


 	diff --git a/execute_cmd.c b/execute_cmd.c
 	index ed1063e..5949e3e 100644
 	--- a/execute_cmd.c
 	+++ b/execute_cmd.c
 	@@ -2440,6 +2440,8 @@ execute_coproc (command, pipe_in, pipe_out,
 	fds_to_close)

 	       exit (estat);
 	     }
 	+  else
 	+    usleep(100000);

 	   close (rpipe[1]);
 	   close (wpipe[0]);


When I try this, X_BACKUP is consistently empty.  Though if you add, say, 
a call to "sleep 0.2" to coproc X, then X_BACKUP consistently gets a copy 
of X's fds in time.

I am sorry if this sounds contrived, but I hope it demonstrates that 
closing fds and and unsetting the variable for a coproc automatically when 
it terminates is fundamentally flawed, because it depends on the arbitrary 
race timing between the two processes.



>> *Or* else add an option to the coproc keyword to explicitly close the 
>> coproc - which will close both fds and clear the variable.
>
> Not going to add any more options to reserved words; that does more 
> violence to the grammar than I want.

Not sure how you'd feel about using 'unset' on the coproc variable 
instead.  (Though as discussed, I think the coproc terminated + fds 
manually closed condition is also sufficient.)


.............


Anyway, as far as I'm concerned there's nothing urgent about all this, but 
(along with the multi-coproc support that you implemented), avoiding the 
current automatic deallocation behavior would seem to go a long way toward 
making coproc a correct and generally useful feature.


Thanks for your time!

Carl


PS Zack you're welcome :)

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-04 12:52                 ` Carl Edquist
@ 2024-04-04 23:23                   ` Martin D Kealey
  2024-04-08 19:50                     ` Chet Ramey
  2024-04-08 16:21                   ` Chet Ramey
  1 sibling, 1 reply; 54+ messages in thread
From: Martin D Kealey @ 2024-04-04 23:23 UTC (permalink / raw)
  To: Carl Edquist; +Cc: Chet Ramey, Zachary Santer, bug-bash, libc-alpha

[-- Attachment #1: Type: text/plain, Size: 417 bytes --]

I'm somewhat uneasy about having coprocs inaccessible to each other.
I can foresee reasonable cases where I'd want a coproc to utilize one or
more other coprocs.

In particular, I can see cases where a coproc is written to by one process,
and read from by another.

Can we at least have the auto-close behaviour be made optional, so that it
can be turned off when we want to do something more sophisticated?

-Martin

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-03 17:19                 ` Zachary Santer
@ 2024-04-08 15:07                   ` Chet Ramey
  2024-04-09  3:44                     ` Zachary Santer
  0 siblings, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-08 15:07 UTC (permalink / raw)
  To: Zachary Santer; +Cc: chet.ramey, Carl Edquist, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 1649 bytes --]

On 4/3/24 1:19 PM, Zachary Santer wrote:
> On Wed, Apr 3, 2024 at 10:32 AM Chet Ramey <chet.ramey@case.edu> wrote:
>>
>> How long should the shell defer deallocating the coproc after the process
>> terminates? What should it do to make sure that the variables don't hang
>> around with invalid file descriptors? Or should the user be responsible for
>> unsetting the array variable too? (That's never been a requirement,
>> obviously.)
> 
> For sake of comparison, and because I don't know the answer, what does
> bash do behind the scenes in this situation?
> 
> exec {fd}< <( some command )
> while IFS='' read -r line <&"${fd}"; do
>    # do stuff
> done
> {fd}<&-
> 
> Because the command in the process substitution isn't waiting for
> input, (I think) it could've exited at any point before all of its
> output has been consumed. Even so, bash appears to handle this
> seamlessly.

Bash doesn't close the file descriptor in $fd. Since it's used with `exec',
it's under the user's control.

The script here explicitly opens and closes the file descriptor, so it
can read until read returns failure. It doesn't really matter when the
process exits or whether the shell closes its ends of the pipe -- the
script has made a copy that it can use for its own purposes. (And you
need to use exec to close it when you're done.)

You can do the same thing with a coproc. The question is whether or
not scripts should have to.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-04 12:52                 ` Carl Edquist
  2024-04-04 23:23                   ` Martin D Kealey
@ 2024-04-08 16:21                   ` Chet Ramey
  2024-04-12 16:49                     ` Carl Edquist
  1 sibling, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-08 16:21 UTC (permalink / raw)
  To: Carl Edquist; +Cc: chet.ramey, Zachary Santer, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 7022 bytes --]

On 4/4/24 8:52 AM, Carl Edquist wrote:

> Zack illustrated basically the same point with his example:
> 
>>     exec {fd}< <( some command )
>>     while IFS='' read -r line <&"${fd}"; do
>>       # do stuff
>>     done
>>     {fd}<&-
> 
> A process-substitution open to the shell like this is effectively a 
> one-ended coproc (though not in the jobs list), and it behaves reliably 
> here because the user can count on {fd} to remain open even after the child 
> process terminates.

That exposes the fundamental difference. The procsub is essentially the
same kind of object as a coproc, but it exposes the pipe endpoint(s) as
filenames. The shell maintains open file descriptors to the child process
whose input or output it exposes as a FIFO or a file in /dev/fd, since
you have to have a reader and a writer. The shell closes the file
descriptor and, if necessary, removes the FIFO when the command for which
that was one of the word expansions (or a redirection) completes. coprocs
are designed to be longer-lived, and not associated with a particular
command or redirection.

But the important piece is that $fd is not the file descriptor the shell
keeps open to the procsub -- it's a new file descriptor, dup'd from the
original by the redirection. Since it was used with `exec', it persists
until the script explicitly closes it. It doesn't matter when the shell
reaps the procsub and closes the file descriptor(s) -- the copy in $fd
remains until the script explicitly closes it. You might get read returning
failure at some point, but the shell won't close $fd for you.

Since procsubs expand to filenames, even opening them is sufficient to
give you a new file descriptor (with the usual caveats about how different
OSs handle the /dev/fd device).

You can do this yourself with coprocs right now, with no changes to the
shell.


> So, the user can determine when the coproc fds are no longer needed, 
> whether that's when EOF is hit trying to read from the coproc, or whatever 
> other condition.

Duplicating the file descriptor will do that for you.


> Personally I like the idea of 'closing' a coproc explicitly, but if it's a 
> bother to add options to the coproc keyword, then I would say just let the 
> user be responsible for closing the fds.  Once the coproc has terminated 
> _and_ the coproc's fds are closed, then the coproc can be deallocated.

This is not backwards compatible. coprocs may be a little-used feature, but
you're adding a burden on the shell programmer that wasn't there
previously.


> Apparently there is already some detection in there for when the coproc fds 
> get closed, as the {NAME[@]} fd array members get set to -1 automatically 
> when when you do, eg, 'exec {NAME[0]}<&-'.  So perhaps this won't be a 
> radical change.

Yes, there is some limited checking in the redirection code, since the
shell is supposed to manage the coproc file descriptors for the user.

> 
> Alternatively (or, additionally), you could interpret 'unset NAME' for a 
> coproc to mean "deallocate the coproc."  That is, close the {NAME[@]} fds, 
> unset the NAME variable, and remove any coproc bookkeeping for NAME.

Hmmm. That's not unreasonable.


>> What should it do to make sure that the variables don't hang around with 
>> invalid file descriptors?
> 
> First, just to be clear, the fds to/from the coproc pipes are not invalid 
> when the coproc terminates (you can still read from them); they are only 
> invalid after they are closed.

That's only sort of true; writing to a pipe for which there is no
reader generates SIGPIPE, which is a fatal signal. If the coproc
terminates, the file descriptor to write to it becomes invalid because
it's implicitly closed. If you restrict yourself to reading from coprocs,
or doing one initial write and then only reading from there on, you can
avoid this, but it's not the general case.

> The surprising bit is when they become invalid unexpectedly (from the point 
> of view of the user) because the shell closes them automatically, at the 
> somewhat arbitrary timing when the coproc is reaped.

No real difference from procsubs.

> Second, why is it a problem if the variables keep their (invalid) fds after 
> closing them, if the user is the one that closed them anyway?
> 
> Isn't this how it works with the auto-assigned fd redirections?

Those are different file descriptors.

> 
>      $ exec {d}<.
>      $ echo $d
>      10
>      $ exec {d}<&-
>      $ echo $d
>      10

The shell doesn't try to manage that object in the same way it does a
coproc. The user has explicitly indicated they want to manage it.


> But, as noted, bash apparently already ensures that the variables don't 
> hang around with invalid file descriptors, as once you close them the 
> corresponding variable gets updated to "-1".

Yes, the shell trying to be helpful. It's a managed object.

> If the user has explicitly closed both fd ends for a coproc, it should not 
> be a surprise to the user either way - whether the variable gets unset 
> automatically, or whether it remains with (-1 -1).
> 
> Since you are already unsetting the variable when the coproc is deallocated 
> though, I'd say it's fine to keep doing that -- just don't deallocate the 
> coproc before the user has closed both fds.

It's just not backwards compatible. I might add an option to enable that
kind of management, but probably not for bash-5.3.



> *Except* that it's inherently a race condition whether the original 
> variables will still be intact to save them.
> 
> Even if you attempt to save them immediately:
> 
>      coproc X { exit; }
>      X_BACKUP=( ${X[@]} )
> 
> it's not guaranteed that X_BACKUP=(...) will run before coproc X has been 
> deallocated, and the X variable cleared.

That's not what I mean about saving the file descriptors. But there is a
window there where a short-lived coprocess could be reaped before you dup
the file descriptors. Since the original intent of the feature was that
coprocs were a way to communicate with long-lived processes -- something
more persistent than a process substitution -- it was not really a
concern at the time.


>>> *Or* else add an option to the coproc keyword to explicitly close the 
>>> coproc - which will close both fds and clear the variable.
>>
>> Not going to add any more options to reserved words; that does more 
>> violence to the grammar than I want.
> 
> Not sure how you'd feel about using 'unset' on the coproc variable 
> instead.  (Though as discussed, I think the coproc terminated + fds 
> manually closed condition is also sufficient.)

That does sound promising.

Chet
-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-04 23:23                   ` Martin D Kealey
@ 2024-04-08 19:50                     ` Chet Ramey
  2024-04-09 14:46                       ` Zachary Santer
  2024-04-09 15:58                       ` Carl Edquist
  0 siblings, 2 replies; 54+ messages in thread
From: Chet Ramey @ 2024-04-08 19:50 UTC (permalink / raw)
  To: Martin D Kealey, Carl Edquist
  Cc: chet.ramey, Zachary Santer, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 824 bytes --]

On 4/4/24 7:23 PM, Martin D Kealey wrote:
> I'm somewhat uneasy about having coprocs inaccessible to each other.
> I can foresee reasonable cases where I'd want a coproc to utilize one or 
> more other coprocs.

That's not the intended purpose, so I don't think not fixing a bug to
accommodate some future hypothetical use case is a good idea. That's
why there's a warning message when you try to use more than one coproc --
the shell doesn't keep track of more than one.

If you want two processes to communicate (really three), you might want
to build with the multiple coproc support and use the shell as the
arbiter.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-08 15:07                   ` Chet Ramey
@ 2024-04-09  3:44                     ` Zachary Santer
  2024-04-13 18:45                       ` Chet Ramey
  0 siblings, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-04-09  3:44 UTC (permalink / raw)
  To: chet.ramey; +Cc: Carl Edquist, bug-bash, libc-alpha

On Mon, Apr 8, 2024 at 11:07 AM Chet Ramey <chet.ramey@case.edu> wrote:
>
> Bash doesn't close the file descriptor in $fd. Since it's used with `exec',
> it's under the user's control.
>
> The script here explicitly opens and closes the file descriptor, so it
> can read until read returns failure. It doesn't really matter when the
> process exits or whether the shell closes its ends of the pipe -- the
> script has made a copy that it can use for its own purposes.

> (And you need to use exec to close it when you're done.)

Caught that shortly after sending the email. Yeah, I know.

> You can do the same thing with a coproc. The question is whether or
> not scripts should have to.

If there's a way to exec fds to read from and write to the same
background process without first using the coproc keyword or using
FIFOs I'm all ears. To me, coproc fills that gap. I'd be fine with
having to close the coproc fds in subshells myself. Heck, you still
have to use exec to close at least the writing coproc fd in the parent
process to get the coproc to exit, regardless.

The fact that the current implementation allows the coproc fds to get
into process substitutions is a little weird to me. A process
substitution, in combination with exec, is kind of the one other way
to communicate with background processes through fds without using
FIFOs. I still have to close the coproc fds there myself, right now.

Consider the following situation: I've got different kinds of
background processes going on, and I've got fds exec'd from process
substitutions, fds from coprocs, and fds exec'd from other things, and
I need to keep them all out of the various background processes. Now I
need different arrays of fds, so I can close all the fds that get into
a background process forked with & without trying to close the coproc
fds there; while still being able to close all the fds, including the
coproc fds, in process substitutions.

I'm curious what the reasoning was there.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-08 19:50                     ` Chet Ramey
@ 2024-04-09 14:46                       ` Zachary Santer
  2024-04-13 18:51                         ` Chet Ramey
  2024-04-09 15:58                       ` Carl Edquist
  1 sibling, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-04-09 14:46 UTC (permalink / raw)
  To: chet.ramey; +Cc: Martin D Kealey, Carl Edquist, bug-bash, libc-alpha

On Mon, Apr 8, 2024 at 3:50 PM Chet Ramey <chet.ramey@case.edu> wrote:
>
> On 4/4/24 7:23 PM, Martin D Kealey wrote:
> > I'm somewhat uneasy about having coprocs inaccessible to each other.
> > I can foresee reasonable cases where I'd want a coproc to utilize one or
> > more other coprocs.
>
> That's not the intended purpose, so I don't think not fixing a bug to
> accommodate some future hypothetical use case is a good idea. That's
> why there's a warning message when you try to use more than one coproc --
> the shell doesn't keep track of more than one.

That use case is always going to be hypothetical if the support for it
isn't really there, though, isn't it?

> If you want two processes to communicate (really three), you might want
> to build with the multiple coproc support and use the shell as the
> arbiter.

If you've written a script for other people than just yourself,
expecting all of them to build their own bash install with a
non-default preprocessor directive is pretty unreasonable.

The part that I've been missing this whole time is that using exec
with the fds provided by the coproc keyword is actually a complete
solution for my use case, if I'm willing to close all the resultant
fds myself in background processes where I don't want them to go.
Which I am.

$ coproc CAT1 { cat; }
[1] 1769
$ exec {CAT1_2[0]}<&"${CAT1[0]}" {CAT1_2[1]}>&"${CAT1[1]}"
{CAT1[0]}<&- {CAT1[1]}>&-
$ declare -p CAT1 CAT1_2
declare -a CAT1=([0]="-1" [1]="-1")
declare -a CAT1_2=([0]="10" [1]="11")
$ coproc CAT2 { exec {CAT1_2[0]}<&- {CAT1_2[1]}>&-; cat; }
[2] 1771
$ exec {CAT2_2[0]}<&"${CAT2[0]}" {CAT2_2[1]}>&"${CAT2[1]}"
{CAT2[0]}<&- {CAT2[1]}>&-
$ declare -p CAT2 CAT2_2
declare -a CAT2=([0]="-1" [1]="-1")
declare -a CAT2_2=([0]="12" [1]="13")
$ printf 'dog\ncat\nrabbit\ntortoise\n' >&"${CAT1_2[1]}"
$ IFS='' read -r -u "${CAT1_2[0]}" line; printf '%s\n' "${?}:${line}"
0:dog
$ exec {CAT1_2[1]}>&-
$ IFS='' read -r -u "${CAT1_2[0]}" line; printf '%s\n' "${?}:${line}"
0:cat
[1]-  Done                    coproc CAT1 { cat; }
$ IFS='' read -r -u "${CAT1_2[0]}" line; printf '%s\n' "${?}:${line}"
0:rabbit
$ IFS='' read -r -u "${CAT1_2[0]}" line; printf '%s\n' "${?}:${line}"
0:tortoise
$ IFS='' read -r -u "${CAT1_2[0]}" line; printf '%s\n' "${?}:${line}"
1:
$ exec {CAT1_2[0]}<&- {CAT2_2[0]}<&- {CAT2_2[1]}>&-
$
[2]+  Done

No warning message when creating the CAT2 coproc. I swear, I was so
close to getting this figured out three years ago, unless the behavior
when a coproc still exists only because other non-coproc fds are
pointing to it has changed since whatever version of bash I was
testing in at the time.

I am completely satisfied with this solution.

The trial and error aspect to figuring this kind of stuff out is
really frustrating. Maybe I'll take some time and write a Wooledge
Wiki article on this at some point, if there isn't one already.

Whether the coproc fds should be automatically kept out of most kinds
of subshells, like it is now; or out of more kinds than currently; is
kind of beside the point to me now. But, having a builtin to ensure
the same behavior is applied to any arbitrary fd might be useful to
people, especially if those fds get removed from process substitutions
as well. If the code for coproc fds gets applied to these fds, then
you've got more chances to see that the logic actually works
correctly, if nothing else.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-08 19:50                     ` Chet Ramey
  2024-04-09 14:46                       ` Zachary Santer
@ 2024-04-09 15:58                       ` Carl Edquist
  2024-04-13 20:10                         ` Chet Ramey
  2024-06-09  1:37                         ` Martin D Kealey
  1 sibling, 2 replies; 54+ messages in thread
From: Carl Edquist @ 2024-04-09 15:58 UTC (permalink / raw)
  To: Chet Ramey, Martin D Kealey; +Cc: Zachary Santer, bug-bash, libc-alpha

On 4/4/24 7:23 PM, Martin D Kealey wrote:

> I'm somewhat uneasy about having coprocs inaccessible to each other. I 
> can foresee reasonable cases where I'd want a coproc to utilize one or 
> more other coprocs.
>
> In particular, I can see cases where a coproc is written to by one 
> process, and read from by another.
>
> Can we at least have the auto-close behaviour be made optional, so that 
> it can be turned off when we want to do something more sophisticated?

With support for multiple coprocs, auto-closing the fds to other coprocs 
when creating new ones is important in order to avoid deadlocks.

But if you're willing to take on management of those coproc fds yourself, 
you can expose them to new coprocs by making your own copies with exec 
redirections.

But this only "kind of" works, because for some reason bash seems to close 
all pipe fds for external commands in coprocs, even the ones that the user 
explicitly copies with exec redirections.

(More on that in a bit.)


On Mon, 8 Apr 2024, Chet Ramey wrote:

> On 4/4/24 7:23 PM, Martin D Kealey wrote:
>> I'm somewhat uneasy about having coprocs inaccessible to each other. I 
>> can foresee reasonable cases where I'd want a coproc to utilize one or 
>> more other coprocs.
>
> That's not the intended purpose,

Just a bit of levity here - i can picture Doc from back to the future 
exclaiming, "Marty, it's perfect!  You're just not thinking 4th 
dimensionally!"

> so I don't think not fixing a bug to accommodate some future 
> hypothetical use case is a good idea. That's why there's a warning 
> message when you try to use more than one coproc -- the shell doesn't 
> keep track of more than one.
>
> If you want two processes to communicate (really three), you might want
> to build with the multiple coproc support and use the shell as the
> arbiter.


For what it's worth, my experience is that coprocesses in bash (rigged up 
by means other than the coproc keyword) become very fun and interesting 
when you allow for the possibility of communication between coprocesses. 
(Most of my use cases for coprocesses fall under this category, actually.)

The most basic commands for tying multiple coprocesses together are tee(1) 
and paste(1), for writing to or reading from multiple coprocesses at once.

You can do this already with process substitutions like

 	tee >(cmd1) >(cmd2)

 	paste <(cmd3) <(cmd4)


My claim here is that there are uses for this where these commands are all 
separate coprocesses; that is, you'd want to read the output from cmd1 and 
cmd2 separately, and provide input for cmd3 and cmd4 separately.

(I'll try to send some examples in a later email.)


Nevertheless it's still crucial to keep the shell's existing coprocess fds 
out of new coprocesses, otherwise you easily run yourself into deadlock.


Now, if you built bash with multiple coproc support, I would have expected 
you could still rig this up, by doing the redirection work explicitly 
yourself.  Something like this:

 	coproc UP   { stdbuf -oL tr a-z A-Z; }
 	coproc DOWN { stdbuf -oL tr A-Z a-z; }

 	# make user-managed backup copies of coproc fds
 	exec {up_r}<&${UP[0]} {up_w}>&${UP[1]}
 	exec {down_r}<&${DOWN[0]} {down_w}>&${DOWN[1]}

 	coproc THREEWAY { tee /dev/fd/$up_w  /dev/fd/$down_w; }


But the above doesn't actually work, as it seems that the coproc shell 
(THREEWAY) closes specifically all the pipe fds (beyond 0,1,2), even the 
user-managed ones explicitly copied with exec.

As a result, you get back errors like this:

 	tee: /dev/fd/11: No such file or directory
 	tee: /dev/fd/13: No such file or directory


That's the case even if you do something more explicit like:

 	coproc UP_AND_OUT { tee /dev/fd/99  99>&$up_w; }

the '99>&$up_w' redirection succeeds, showing that the coproc does have 
access to its backup fd $up_w (*), but apparently the shell closes fd 99 
(as well as $up_w) before exec'ing the tee command.

Note the coproc shell only does this with pipes; it leaves other user 
managed fds like files or directories alone.

I have no idea why that's the case, and i wonder whether it's intentional 
or an oversight.


But anyway, i imagine that if one wants to use multi coproc support (which 
requires automatically closing the shell's coproc fds for new coprocs), 
and wants to set up multiple coprocs to communicate amongst themselves, 
then the way to go would be explicit redirections.

(But again, this requires fixing this peculiar behavior where the coproc 
shell closes even the user managed copies of pipe fds before exec'ing 
external commands.)


(*) to prove that the coproc shell does have access to $up_w, we can make 
a shell-only replacement for tee(1) :  (actually works)

 	fdtee () {
 	  local line fd
 	  while read -r line; do
 	    for fd; do
 	      printf '%s\n' "$line" >&$fd;
 	    done;
 	  done;
 	}

 	coproc UP   { stdbuf -oL tr a-z A-Z; }
 	coproc DOWN { stdbuf -oL tr A-Z a-z; }

 	# make user-managed backup copies of coproc fds
 	exec {up_r}<&${UP[0]} {up_w}>&${UP[1]}
 	exec {down_r}<&${DOWN[0]} {down_w}>&${DOWN[1]}

 	stdout=1
 	coproc THREEWAY { fdtee $stdout $up_w $down_w; }

 	# save these too, for safe keeping
 	exec {tee_r}<&${THREEWAY[0]} {tee_w}>&${THREEWAY[1]}


Then:  (actually works)

 	$ echo 'Greetings!' >&$tee_w
 	$ read -u $tee_r  plain
 	$ read -u $up_r   upped
 	$ read -u $down_r downed
 	$ echo "[$plain] [$upped] [$downed]"
 	[Greetings!] [GREETINGS!] [greetings!]


This is a pretty trivial example just to demonstrate the concept.  But 
once you have the freedom to play with it, you find more interesting, 
useful applications.

Of course, for the above technique to be generally useful, external 
commands need access to these user-managed fds (copied with exec).  (I 
have no idea why the coproc shell closes them.)  The shell is crippled 
when limited to builtins.

(I'll try to tidy up some working examples with my coprocess management 
library this week, for the curious.)


Juicy thread hey?  I can hardly keep up!  :)

Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-08 16:21                   ` Chet Ramey
@ 2024-04-12 16:49                     ` Carl Edquist
  2024-04-16 15:48                       ` Chet Ramey
  0 siblings, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-04-12 16:49 UTC (permalink / raw)
  To: Chet Ramey; +Cc: Zachary Santer, bug-bash, libc-alpha

[-- Attachment #1: Type: text/plain, Size: 12139 bytes --]

On Mon, 8 Apr 2024, Chet Ramey wrote:

> On 4/4/24 8:52 AM, Carl Edquist wrote:
>
>>  Zack illustrated basically the same point with his example:
>>
>>>      exec {fd}< <( some command )
>>>      while IFS='' read -r line <&"${fd}"; do
>>>        # do stuff
>>>      done
>>>      {fd}<&-
>> 
>> A process-substitution open to the shell like this is effectively a 
>> one-ended coproc (though not in the jobs list), and it behaves reliably 
>> here because the user can count on {fd} to remain open even after the 
>> child process terminates.
>
> That exposes the fundamental difference. The procsub is essentially the 
> same kind of object as a coproc, but it exposes the pipe endpoint(s) as 
> filenames. The shell maintains open file descriptors to the child 
> process whose input or output it exposes as a FIFO or a file in /dev/fd, 
> since you have to have a reader and a writer. The shell closes the file 
> descriptor and, if necessary, removes the FIFO when the command for 
> which that was one of the word expansions (or a redirection) completes. 
> coprocs are designed to be longer-lived, and not associated with a 
> particular command or redirection.
>
> But the important piece is that $fd is not the file descriptor the shell 
> keeps open to the procsub -- it's a new file descriptor, dup'd from the 
> original by the redirection. Since it was used with `exec', it persists 
> until the script explicitly closes it. It doesn't matter when the shell 
> reaps the procsub and closes the file descriptor(s) -- the copy in $fd 
> remains until the script explicitly closes it. You might get read 
> returning failure at some point, but the shell won't close $fd for you.
>
> Since procsubs expand to filenames, even opening them is sufficient to 
> give you a new file descriptor (with the usual caveats about how 
> different OSs handle the /dev/fd device).
>
> You can do this yourself with coprocs right now, with no changes to the 
> shell.
>
>
>>  So, the user can determine when the coproc fds are no longer needed,
>>  whether that's when EOF is hit trying to read from the coproc, or
>>  whatever other condition.
>
> Duplicating the file descriptor will do that for you.

Thanks for the explanation, that all makes sense.

One technical difference in my mind is that doing this with a procsub is 
reliably safe:

 	exec {fd}< <( some command )

since the expanded pathname (/dev/fd/N or the fifo alternative) will stay 
around for the duration of the exec command, so there is no concern about 
whether or not the dup redirection will succeed.

Where with a coproc

 	coproc X { potentially short lived command with output; }
 	exec {xr}<&${X[0]} {xw}>&${X[1]}

there is technically the possibility that the coproc can finish and be 
reaped before the exec command gets a chance to run and duplicate the fds.

But, I also get what you said, that your design intent with coprocs was 
for them to be longer-lived, so immediate termination was not a concern.


>> Personally I like the idea of 'closing' a coproc explicitly, but if 
>> it's a bother to add options to the coproc keyword, then I would say 
>> just let the user be responsible for closing the fds.  Once the coproc 
>> has terminated _and_ the coproc's fds are closed, then the coproc can 
>> be deallocated.
>
> This is not backwards compatible. coprocs may be a little-used feature, 
> but you're adding a burden on the shell programmer that wasn't there 
> previously.

Ok, so, I'm trying to imagine a case where this would cause any problems 
or extra work for such an existing user.  Maybe you can provide an example 
from your own uses?  (Where it would cause trouble or require adding code 
if the coproc deallocation were deferred until the fds are closed 
explicitly.)

My first thought is that in the general case, the user doesn't really need 
to worry much about closing the fds for a terminated coproc anyway, as 
they will all be closed implicitly when the shell exits (either an 
interactive session or a script).

[This is a common model for using coprocs, by the way, where an auxiliary 
coprocess is left open for the lifetime of the shell session and never 
explicitly closed.  When the shell session exits, the fds are closed 
implicitly by the OS, and the coprocess sees EOF and exits on its own.]

If a user expects the coproc variable to go away automatically, that user 
won't be accessing a still-open fd from that variable for anything.

As for the forgotten-about half-closed pipe fds to the reaped coproc, I 
don't see how they could lead to deadlock, nor do I see how a shell 
programmer expecting the existing behavior would even attempt to access 
them at all, apart from programming error.

The only potential issue I can imagine is if a script (or a user at an 
interactive prompt) would start _so_ many of these longer-lived coprocs 
(more than 500??), one at a time in succession, in a single shell session, 
that all the available fds would be exhausted.  (That is, if the shell is 
not closing them automatically upon coproc termination.)  Is that the 
backwards compatibility concern?

Because otherwise it seems like stray fds for terminated coprocs would be 
benign.

...

Meanwhile, the bash man page does not specify the shell's behavior for 
when a coproc terminates, so you might say there's room for interpretation 
and the new deferring behavior would not break any promises.

And as it strikes me anyway, the real "burden" on the programmer with the 
existing behavior is having to make a copy of the coproc fds every time

 	coproc X { cmd; }
 	exec {xr}<&${X[0]} {xw}>&${X[1]}

and use the copies instead of the originals in order to reliably read the 
final output from the coproc.

...

Though I can hear Obi-Wan Kenobi gently saying to Luke,
"You must do what you feel is right, of course."


>>> What should it do to make sure that the variables don't hang around 
>>> with invalid file descriptors?
>>
>> First, just to be clear, the fds to/from the coproc pipes are not 
>> invalid when the coproc terminates (you can still read from them); they 
>> are only invalid after they are closed.
>
> That's only sort of true; writing to a pipe for which there is no reader 
> generates SIGPIPE, which is a fatal signal.

Eh, when I talk about an fd being "invalid" here I mean "fd is not a valid 
file descriptor" (to use the language for EBADF from the man page for 
various system calls like read(2), write(2), close(2)).  That's why I say 
the fds only become invalid after they are closed.

And of course the primary use I care about is reading the final output 
from a completed coproc.  (Which is generally after explicitly closing the 
write end.)  The shell's read fd is still open, and can be read - it'll 
either return data, or return EOF, but that's not an error and not 
invalid.

But since you mention it, writing to a broken pipe is still semantically 
meaningful also.  (I would even say valid.)  In the typical case it's 
expected behavior for a process to get killed when it attempts this and 
shell pipeline programming is designed with this in mind.

But when you try to write to a terminated coproc when you have the shell 
automatically closing its write end, you get an unpredictable situation:

- If the write happens after the coproc terminates but before the shell 
reaps it (and closes the fds), then you will generate a SIGPIPE, which 
by default gracefully kills the shell (as is normal for programs in a 
pipeline).

- On the other hand, if the write happens after the shell reaps it and 
closes the fds, you will get a bad (invalid) file descriptor error 
message, without killing the shell.

So even for write attempts, you introduce uncertain behavior by 
automatically closing the fds, when the normal, predictable, valid thing 
would be to die by SIGPIPE.

(That's my take anyway.)


> If the coproc terminates, the file descriptor to write to it becomes 
> invalid because it's implicitly closed.

Yes, but the distinction I was making is that they do not become invalid 
when or because the coproc terminates, they become invalid when and 
because the shell closes them.  (I'm saying that if the shell did not 
close them automatically, they would remain valid.)


>>  The surprising bit is when they become invalid unexpectedly (from the
>>  point of view of the user) because the shell closes them
>>  automatically, at the somewhat arbitrary timing when the coproc is
>>  reaped.
>
> No real difference from procsubs.

I think I disagree?  The difference is that the replacement string for a 
procsub (/dev/fd/N or a fifo path) remains valid for the command in 
question.  (Right?)  So the command in question can count on that path 
being valid.  And if a procsub is used in an exec redirection, in order to 
extend its use for future commands (and the redirection is guaranteed to 
work, since it is guaranteed to be valid for that exec command), then the 
newly opened pipe fd will not be subject to automatic closing either.

As far as I can tell there is no arbitrary timing for when the shell 
closes the fds for procsubs.  As far as I can tell, it closes them when 
the command in question completes, and that's the end of the story. 
(There's no waiting for the timing of the background procsub process to 
complete.)


>> Second, why is it a problem if the variables keep their (invalid) fds 
>> after closing them, if the user is the one that closed them anyway?
>> 
>> Isn't this how it works with the auto-assigned fd redirections?
>
> Those are different file descriptors.
>
>>
>>       $ exec {d}<.
>>       $ echo $d
>>       10
>>       $ exec {d}<&-
>>       $ echo $d
>>       10
>
> The shell doesn't try to manage that object in the same way it does a
> coproc. The user has explicitly indicated they want to manage it.

Ok - your intention makes sense then.  My reasoning was that 
auto-allocated redirection fds ( {x}>file or {x}>&$N ) are a way of asking 
the shell to automatically place fds in a variable for you to manage - and 
I imagined 'coproc X {...}' the same way.


>> If the user has explicitly closed both fd ends for a coproc, it should 
>> not be a surprise to the user either way - whether the variable gets 
>> unset automatically, or whether it remains with (-1 -1).
>>
>> Since you are already unsetting the variable when the coproc is 
>> deallocated though, I'd say it's fine to keep doing that -- just don't 
>> deallocate the coproc before the user has closed both fds.
>
> It's just not backwards compatible.  I might add an option to enable 
> that kind of management, but probably not for bash-5.3.

Ah, nice idea.  No hurry on my end - but yeah if you imagine the alternate 
behavior is somehow going to cause problems for existing uses (eg, the fd 
exhaustion mentioned earlier) then yeah a shell option for the 
deallocation behavior would at least be a way for users to get reliable 
behavior without the burden of duping the fds manually every time.


> But there is a window there where a short-lived coprocess could be 
> reaped before you dup the file descriptors. Since the original intent of 
> the feature was that coprocs were a way to communicate with long-lived 
> processes -- something more persistent than a process substitution -- it 
> was not really a concern at the time.

Makes sense.  For me, working with coprocesses is largely a more flexible 
way of setting up interesting pipelines - which is where the shell excels.

Once a 'pipework' is set up (I'm making up this word now to distinguish 
from a simple pipeline), the shell does not have to be in the middle 
shoveling data around - the external commands can do that on their own.

So in my mind, thinking about the "lifetime" of a coproc is often not so 
different from thinking about the lifetime of a regular pipeline, once you 
set up the plumbing for your commands.  The timing of individual parts of 
a pipeline finishing shouldn't really matter, as long as the pipes serve 
their purpose to deliver output from one part to the next.


Thanks for your time, and happy Friday :)

Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-09  3:44                     ` Zachary Santer
@ 2024-04-13 18:45                       ` Chet Ramey
  2024-04-14  2:09                         ` Zachary Santer
  0 siblings, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-13 18:45 UTC (permalink / raw)
  To: Zachary Santer; +Cc: chet.ramey, Carl Edquist, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 1115 bytes --]

On 4/8/24 11:44 PM, Zachary Santer wrote:

> The fact that the current implementation allows the coproc fds to get
> into process substitutions is a little weird to me. A process
> substitution, in combination with exec, is kind of the one other way
> to communicate with background processes through fds without using
> FIFOs. I still have to close the coproc fds there myself, right now.

So are you advocating for the shell to close coproc file descriptors
when forking children for command substitutions, process substitutions,
and subshells, in addition to additional coprocs? Right now, it closes
coproc file descriptors when forking subshells.

> 
> Consider the following situation: I've got different kinds of
> background processes going on, and I've got fds exec'd from process
> substitutions, fds from coprocs,

If you have more than one coproc, you have to manage all this yourself
already.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-09 14:46                       ` Zachary Santer
@ 2024-04-13 18:51                         ` Chet Ramey
  0 siblings, 0 replies; 54+ messages in thread
From: Chet Ramey @ 2024-04-13 18:51 UTC (permalink / raw)
  To: Zachary Santer
  Cc: chet.ramey, Martin D Kealey, Carl Edquist, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 1649 bytes --]

On 4/9/24 10:46 AM, Zachary Santer wrote:

>> If you want two processes to communicate (really three), you might want
>> to build with the multiple coproc support and use the shell as the
>> arbiter.
> 
> If you've written a script for other people than just yourself,
> expecting all of them to build their own bash install with a
> non-default preprocessor directive is pretty unreasonable.

This all started because I wasn't comfortable with the amount of testing
the multiple coprocs code had undergone. If we can get more people to
test these features, there's a better chance of making it the default.

> The part that I've been missing this whole time is that using exec
> with the fds provided by the coproc keyword is actually a complete
> solution for my use case, if I'm willing to close all the resultant
> fds myself in background processes where I don't want them to go.
> Which I am.

Good deal.

> Whether the coproc fds should be automatically kept out of most kinds
> of subshells, like it is now; or out of more kinds than currently; is
> kind of beside the point to me now.

Sure, but it's the potential for deadlock that we're trying to reduce.

> But, having a builtin to ensure
> the same behavior is applied to any arbitrary fd might be useful to
> people, especially if those fds get removed from process substitutions
> as well.

What does this mean? What kind of builtin? And what `same behavior'?

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-09 15:58                       ` Carl Edquist
@ 2024-04-13 20:10                         ` Chet Ramey
  2024-04-14 18:43                           ` Zachary Santer
  2024-04-15 17:01                           ` Carl Edquist
  2024-06-09  1:37                         ` Martin D Kealey
  1 sibling, 2 replies; 54+ messages in thread
From: Chet Ramey @ 2024-04-13 20:10 UTC (permalink / raw)
  To: Carl Edquist, Martin D Kealey
  Cc: chet.ramey, Zachary Santer, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 3386 bytes --]

On 4/9/24 11:58 AM, Carl Edquist wrote:
> On 4/4/24 7:23 PM, Martin D Kealey wrote:
> 
>> I'm somewhat uneasy about having coprocs inaccessible to each other. I 
>> can foresee reasonable cases where I'd want a coproc to utilize one or 
>> more other coprocs.
>>
>> In particular, I can see cases where a coproc is written to by one 
>> process, and read from by another.
>>
>> Can we at least have the auto-close behaviour be made optional, so that 
>> it can be turned off when we want to do something more sophisticated?
> 
> With support for multiple coprocs, auto-closing the fds to other coprocs 
> when creating new ones is important in order to avoid deadlocks.
> 
> But if you're willing to take on management of those coproc fds yourself, 
> you can expose them to new coprocs by making your own copies with exec 
> redirections.
> 
> But this only "kind of" works, because for some reason bash seems to close 
> all pipe fds for external commands in coprocs, even the ones that the user 
> explicitly copies with exec redirections.



> 
> (More on that in a bit.)
> 
> 
> On Mon, 8 Apr 2024, Chet Ramey wrote:
> 
>> On 4/4/24 7:23 PM, Martin D Kealey wrote:
>>> I'm somewhat uneasy about having coprocs inaccessible to each other. I 
>>> can foresee reasonable cases where I'd want a coproc to utilize one or 
>>> more other coprocs.

>>
>> That's not the intended purpose,

The original intent was to allow the shell to drive a long-running process
that ran more-or-less in parallel with it. Look at examples/scripts/bcalc
for an example of that kind of use.

> 
> For what it's worth, my experience is that coprocesses in bash (rigged up 
> by means other than the coproc keyword) become very fun and interesting 
> when you allow for the possibility of communication between coprocesses. 
> (Most of my use cases for coprocesses fall under this category, actually.)

Sure, as long as you're willing to take on file descriptor management
yourself. I just don't want to make it a new requirement, since it's never
been one before.

> Now, if you built bash with multiple coproc support, I would have expected 
> you could still rig this up, by doing the redirection work explicitly 
> yourself.  Something like this:
> 
>      coproc UP   { stdbuf -oL tr a-z A-Z; }
>      coproc DOWN { stdbuf -oL tr A-Z a-z; }
> 
>      # make user-managed backup copies of coproc fds
>      exec {up_r}<&${UP[0]} {up_w}>&${UP[1]}
>      exec {down_r}<&${DOWN[0]} {down_w}>&${DOWN[1]}
> 
>      coproc THREEWAY { tee /dev/fd/$up_w  /dev/fd/$down_w; }
> 
> 
> But the above doesn't actually work, as it seems that the coproc shell 
> (THREEWAY) closes specifically all the pipe fds (beyond 0,1,2), even the 
> user-managed ones explicitly copied with exec.

File descriptors the user saves with exec redirections beyond [0-2]
are set to close-on-exec. POSIX makes that behavior unspecified, but
bash has always done it.

Shells don't offer any standard way to modify the state of that flag,
but there is the `fdflags' loadable builtin you can experiment with
to change close-on-exec.

Chet

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-13 18:45                       ` Chet Ramey
@ 2024-04-14  2:09                         ` Zachary Santer
  0 siblings, 0 replies; 54+ messages in thread
From: Zachary Santer @ 2024-04-14  2:09 UTC (permalink / raw)
  To: chet.ramey; +Cc: Martin D Kealey, Carl Edquist, bug-bash, libc-alpha

On Sat, Apr 13, 2024 at 2:45 PM Chet Ramey <chet.ramey@case.edu> wrote:
>
> On 4/8/24 11:44 PM, Zachary Santer wrote:
>
> > The fact that the current implementation allows the coproc fds to get
> > into process substitutions is a little weird to me. A process
> > substitution, in combination with exec, is kind of the one other way
> > to communicate with background processes through fds without using
> > FIFOs. I still have to close the coproc fds there myself, right now.
>
> So are you advocating for the shell to close coproc file descriptors
> when forking children for command substitutions, process substitutions,
> and subshells, in addition to additional coprocs? Right now, it closes
> coproc file descriptors when forking subshells.

Yes. I couldn't come up with a way that letting the coproc fds into
command substitutions could cause a problem, in the same sense that
letting them into regular (  ) subshells doesn't seem like a problem.
That bit is at least good for my arbitrary standard of "consistency,"
though.

At least in my use case, trying to use the coproc file descriptors
directly in a pipeline forced the use of a process substitution,
because I needed the coproc fds accessible in the second segment of
what would've been a three-segment pipeline. (Obviously, I'm using
'shopt -s lastpipe' here.) I ultimately chose to do 'exec {fd}> >(
command )' and redirect from one command within the second segment
into ${fd} instead of ending the second segment with '> >( command );
wait "${?}"'. In the first case, you have all the same drawbacks as
allowing the coproc fds into a subshell forked with &. In the second
case, it's effectively the same as allowing the coproc fds into the
segments of a pipeline that become subshells. I guess that would be a
concern if the segment of the pipeline in the parent shell closes the
fds to the coproc while the pipeline is still executing. That seems
like an odd thing to do, but okay.

Now that I've got my own fds that I'm managing myself, I've turned
that bit of code into a plain, three-segment pipeline, at least for
now.

> > Consider the following situation: I've got different kinds of
> > background processes going on, and I've got fds exec'd from process
> > substitutions, fds from coprocs,
>
> If you have more than one coproc, you have to manage all this yourself
> already.

Not if we manage to convince you to turn MULTIPLE_COPROCS=1 on by
default. Or if someone builds bash that way for themselves.

On Sat, Apr 13, 2024 at 2:51 PM Chet Ramey <chet.ramey@case.edu> wrote:
>
> On 4/9/24 10:46 AM, Zachary Santer wrote:
>
> >> If you want two processes to communicate (really three), you might want
> >> to build with the multiple coproc support and use the shell as the
> >> arbiter.
> >
> > If you've written a script for other people than just yourself,
> > expecting all of them to build their own bash install with a
> > non-default preprocessor directive is pretty unreasonable.
>
> This all started because I wasn't comfortable with the amount of testing
> the multiple coprocs code had undergone. If we can get more people to
> test these features, there's a better chance of making it the default.
>
> > The part that I've been missing this whole time is that using exec
> > with the fds provided by the coproc keyword is actually a complete
> > solution for my use case, if I'm willing to close all the resultant
> > fds myself in background processes where I don't want them to go.
> > Which I am.
>
> Good deal.
>
> > Whether the coproc fds should be automatically kept out of most kinds
> > of subshells, like it is now; or out of more kinds than currently; is
> > kind of beside the point to me now.
>
> Sure, but it's the potential for deadlock that we're trying to reduce.

I hesitate to say to just set MULTIPLE_COPROCS=1 free and wait for
people to complain. I'm stunned at my luck in getting Carl Edquist's
attention directed at this. Hopefully there are other people who
aren't subscribed to this email list who are interested in using this
functionality, if it becomes more fully implemented.

> > But, having a builtin to ensure
> > the same behavior is applied to any arbitrary fd might be useful to
> > people, especially if those fds get removed from process substitutions
> > as well.
>
> What does this mean? What kind of builtin? And what `same behavior'?

Let's say it's called 'nosub', and takes fd arguments. It would make
the shell take responsibility for keeping those fds out of subshells.
Perhaps it could take a -u flag, to make it stop keeping the fd
arguments out of subshells. That would be a potential way to get bash
to quit closing coproc fds in subshells, as the user is declaring that
s/he is now responsible for those fds. Still a separate matter from
whether those fds get closed automatically in the parent shell at any
given point.

People who exec fds have to know to take responsibility for everywhere
they go, right now. Exec'ing fds like this is bound to be more common
than using coprocs, as is, I assume, exec'ing fds to process
substitutions. If there are benefits to keeping coproc fds out of
subshells like bash attempts to do, the same benefits would apply if
bash offers to take the burden of keeping other, user-specified fds
out of subshells.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-13 20:10                         ` Chet Ramey
@ 2024-04-14 18:43                           ` Zachary Santer
  2024-04-15 18:55                             ` Chet Ramey
  2024-04-15 17:01                           ` Carl Edquist
  1 sibling, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-04-14 18:43 UTC (permalink / raw)
  To: chet.ramey; +Cc: Carl Edquist, Martin D Kealey, bug-bash, libc-alpha

On Sat, Apr 13, 2024 at 4:10 PM Chet Ramey <chet.ramey@case.edu> wrote:
>
> The original intent was to allow the shell to drive a long-running process
> that ran more-or-less in parallel with it. Look at examples/scripts/bcalc
> for an example of that kind of use.

$ ./bcalc
equation: -12
./bcalc: line 94: history: -1: invalid option
history: usage: history [-c] [-d offset] [n] or history -anrw
[filename] or history -ps arg [arg...]
-12
equation: exit

diff --git a/examples/scripts/bcalc b/examples/scripts/bcalc
index bc7e2b40..826eca4f 100644
--- a/examples/scripts/bcalc
+++ b/examples/scripts/bcalc
@@ -91,7 +91,7 @@ do
        esac

        # save to the history list
-       history -s "$EQN"
+       history -s -- "$EQN"

        # run it through bc
        calc "$EQN"

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-13 20:10                         ` Chet Ramey
  2024-04-14 18:43                           ` Zachary Santer
@ 2024-04-15 17:01                           ` Carl Edquist
  2024-04-17 14:20                             ` Chet Ramey
  1 sibling, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-04-15 17:01 UTC (permalink / raw)
  To: Chet Ramey; +Cc: Martin D Kealey, Zachary Santer, bug-bash, libc-alpha

[-- Attachment #1: Type: text/plain, Size: 5088 bytes --]

On Sat, 13 Apr 2024, Chet Ramey wrote:

> The original intent was to allow the shell to drive a long-running 
> process that ran more-or-less in parallel with it. Look at 
> examples/scripts/bcalc for an example of that kind of use.

Thanks for mentioning this example.  As you understand, this model use 
case does not require closing the coproc fds when finished, because they 
will be closed implicitly when the shell exits.  (As bcalc itself admits.)

And if the coproc is left open for the lifetime of the shell, the 
alternate behavior of deferring the coproc deallocation (until both coproc 
fds are closed) would not require anything extra from the user.

The bcalc example does close both coproc fds though - both at the end, and 
whenever it resets.  And so in this example (which as you say, was the 
original intent), the user is already explicitly closing both coproc fds 
explicitly; so the alternate deferring behavior would not require anything 
extra from the user here either.

...

Yet another point brought to light by the bcalc example relates to the 
coproc pid variable.  The reset() function first closes the coproc pipe 
fds, then sleeps for a second to give the BC coproc some time to finish.

An alternative might be to 'wait' for the coproc to finish (likely faster 
than sleeping for a second).  But you have to make and use your 
$coproc_pid copy rather than $BC_PID directly, because 'wait $BC_PID' may 
happen before or after the coproc is reaped and BC_PID is unset.  (As the 
bcalc author seems to understand.)  So in general the coproc *_PID 
variable only seems usable for making a copy when starting the coproc.

The info page has the following:

> The process ID of the shell spawned to execute the coprocess is 
> available as the value of the variable 'NAME_PID'.  The 'wait' builtin 
> command may be used to wait for the coprocess to terminate.

But it seems to me that the copy is necessary, and it is never reliable to 
run 'wait $NAME_PID'.  Because any time the shell is in a position to wait 
for the coproc to finish, by that time it's going to be a race whether or 
not NAME_PID is still set.


So this is another example for me of why it would be handy if coproc 
deallocation were deferred until explicit user action (closing both coproc 
fds, or unsetting the coproc variable).  That way ${NAME[@]} and $NAME_PID 
could reliably be used directly without having to make copies.

Anyway, just food for thought if down the line you make a shell option for 
coproc deallocation behavior.


>> Now, if you built bash with multiple coproc support, I would have 
>> expected you could still rig this up, by doing the redirection work 
>> explicitly yourself.  Something like this:
>>
>>      coproc UP   { stdbuf -oL tr a-z A-Z; }
>>      coproc DOWN { stdbuf -oL tr A-Z a-z; }
>>
>>      # make user-managed backup copies of coproc fds
>>      exec {up_r}<&${UP[0]} {up_w}>&${UP[1]}
>>      exec {down_r}<&${DOWN[0]} {down_w}>&${DOWN[1]}
>>
>>      coproc THREEWAY { tee /dev/fd/$up_w  /dev/fd/$down_w; }
>> 
>> 
>> But the above doesn't actually work, as it seems that the coproc shell 
>> (THREEWAY) closes specifically all the pipe fds (beyond 0,1,2), even 
>> the user-managed ones explicitly copied with exec.
>
> File descriptors the user saves with exec redirections beyond [0-2] are 
> set to close-on-exec. POSIX makes that behavior unspecified, but bash 
> has always done it.

Ah, ok, thanks.  I believe I found where this gets set in 
do_redirection_internal() in redir.c.  (Whew, a big function.)

As far as I can tell the close-on-exec state is "duplicated" rather than 
set unconditionally.  That is, the new fd in a redirection is only set 
close-on-exec if the source is.  (Good, because in general I rely on 
redirections to be made available to external commands.)  But apparently 
coproc marks its pipe fds close-on-exec, so there's no way to expose 
manual copies of these fds to external commands.

So, that explains the behavior I was seeing ...

It's just a bit too bad for anyone that actually wants to do more 
elaborate coproc interconnections with manual redirections, as they're 
limited to shell builtins.

...

I might pose a question to ponder about this though:

With the multi-coproc support code, is it still necessary to set the 
coproc pipe fds close-on-exec?  (If, perhaps, they're already getting 
explicitly closed in the right places.)

Because if the new coproc fds are _not_ set close-on-exec, in general that 
would allow the user to do manual redirections for external commands (eg 
tee(1) or paste(1)) to communicate with multiple coproc fds together.


> Shells don't offer any standard way to modify the state of that flag, 
> but there is the `fdflags' loadable builtin you can experiment with to 
> change close-on-exec.

Thanks for the tip.  It's nice to know there is a workaround to leave 
copies of the coproc fds open across exec; though for now I will probably 
continue setting up pipes in the shell by methods other than the coproc 
keyword.


Cheers,

Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-14 18:43                           ` Zachary Santer
@ 2024-04-15 18:55                             ` Chet Ramey
  0 siblings, 0 replies; 54+ messages in thread
From: Chet Ramey @ 2024-04-15 18:55 UTC (permalink / raw)
  To: Zachary Santer
  Cc: chet.ramey, Carl Edquist, Martin D Kealey, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 617 bytes --]

On 4/14/24 2:43 PM, Zachary Santer wrote:
> On Sat, Apr 13, 2024 at 4:10 PM Chet Ramey <chet.ramey@case.edu> wrote:
>>
>> The original intent was to allow the shell to drive a long-running process
>> that ran more-or-less in parallel with it. Look at examples/scripts/bcalc
>> for an example of that kind of use.
> 
> $ ./bcalc
> equation: -12
> ./bcalc: line 94: history: -1: invalid option

Good catch, thanks.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-12 16:49                     ` Carl Edquist
@ 2024-04-16 15:48                       ` Chet Ramey
  2024-04-20 23:11                         ` Carl Edquist
  0 siblings, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-16 15:48 UTC (permalink / raw)
  To: Carl Edquist; +Cc: chet.ramey, Zachary Santer, bug-bash, libc-alpha

On 4/12/24 12:49 PM, Carl Edquist wrote:

> Where with a coproc
> 
>      coproc X { potentially short lived command with output; }
>      exec {xr}<&${X[0]} {xw}>&${X[1]}
> 
> there is technically the possibility that the coproc can finish and be 
> reaped before the exec command gets a chance to run and duplicate the fds.
> 
> But, I also get what you said, that your design intent with coprocs was for 
> them to be longer-lived, so immediate termination was not a concern.

The bigger concern was how to synchronize between the processes, but that's
something that the script writer has to do on their own.

>>> Personally I like the idea of 'closing' a coproc explicitly, but if it's 
>>> a bother to add options to the coproc keyword, then I would say just let 
>>> the user be responsible for closing the fds.  Once the coproc has 
>>> terminated _and_ the coproc's fds are closed, then the coproc can be 
>>> deallocated.
>>
>> This is not backwards compatible. coprocs may be a little-used feature, 
>> but you're adding a burden on the shell programmer that wasn't there 
>> previously.
> 
> Ok, so, I'm trying to imagine a case where this would cause any problems or 
> extra work for such an existing user.  Maybe you can provide an example 
> from your own uses?  (Where it would cause trouble or require adding code 
> if the coproc deallocation were deferred until the fds are closed explicitly.)

My concern was always coproc fds leaking into other processes, especially
pipelines. If someone has a coproc now and is `messy' about cleaning it up,
I feel like there's the possibility of deadlock. But I don't know how
extensively they're used, or all the use cases, so I'm not sure how likely
it is. I've learned there are users who do things with shell features I
never imagined. (People wanting to use coprocs without the shell as the
arbiter, for instance. :-) )

> My first thought is that in the general case, the user doesn't really need 
> to worry much about closing the fds for a terminated coproc anyway, as they 
> will all be closed implicitly when the shell exits (either an interactive 
> session or a script).

Yes.

> 
> [This is a common model for using coprocs, by the way, where an auxiliary 
> coprocess is left open for the lifetime of the shell session and never 
> explicitly closed.  When the shell session exits, the fds are closed 
> implicitly by the OS, and the coprocess sees EOF and exits on its own.]

That's one common model, yes. Another is that the shell process explicitly
sends a close or shutdown command to the coproc, so termination is
expected.

> If a user expects the coproc variable to go away automatically, that user 
> won't be accessing a still-open fd from that variable for anything.

I'm more concerned about a pipe with unread data that would potentially
cause problems. I suppose we just need more testing.


> As for the forgotten-about half-closed pipe fds to the reaped coproc, I 
> don't see how they could lead to deadlock, nor do I see how a shell 
> programmer expecting the existing behavior would even attempt to access 
> them at all, apart from programming error.

Probably not.

> 
> The only potential issue I can imagine is if a script (or a user at an 
> interactive prompt) would start _so_ many of these longer-lived coprocs 
> (more than 500??), one at a time in succession, in a single shell session, 
> that all the available fds would be exhausted.  (That is, if the shell is 
> not closing them automatically upon coproc termination.)  Is that the 
> backwards compatibility concern?

That's more of a "my arm hurts when I do this" situation. If a script
opened 500 fds using exec redirection, resource exhaustion would be their
own responsibility.


> Meanwhile, the bash man page does not specify the shell's behavior for when 
> a coproc terminates, so you might say there's room for interpretation and 
> the new deferring behavior would not break any promises.

I could always enable it in the devel branch and see what happens with the
folks who use that. It would be three years after any release when distros
would put it into production anyway.

> 
> And as it strikes me anyway, the real "burden" on the programmer with the 
> existing behavior is having to make a copy of the coproc fds every time
> 
>      coproc X { cmd; }
>      exec {xr}<&${X[0]} {xw}>&${X[1]}
> 
> and use the copies instead of the originals in order to reliably read the 
> final output from the coproc.

Maybe, though it's easy enough to wrap that in a shell function.


>>> First, just to be clear, the fds to/from the coproc pipes are not 
>>> invalid when the coproc terminates (you can still read from them); they 
>>> are only invalid after they are closed.
>>
>> That's only sort of true; writing to a pipe for which there is no reader 
>> generates SIGPIPE, which is a fatal signal.
> 
> Eh, when I talk about an fd being "invalid" here I mean "fd is not a valid 
> file descriptor" (to use the language for EBADF from the man page for 
> various system calls like read(2), write(2), close(2)).  That's why I say 
> the fds only become invalid after they are closed.
> 
> And of course the primary use I care about is reading the final output from 
> a completed coproc.  (Which is generally after explicitly closing the write 
> end.)  The shell's read fd is still open, and can be read - it'll either 
> return data, or return EOF, but that's not an error and not invalid.
> 
> But since you mention it, writing to a broken pipe is still semantically 
> meaningful also.  (I would even say valid.)  In the typical case it's 
> expected behavior for a process to get killed when it attempts this and 
> shell pipeline programming is designed with this in mind.

You'd be surprised at how often I get requests to put in an internal
SIGPIPE handler to avoid problems/shell termination with builtins writing
to closed pipes.


> So even for write attempts, you introduce uncertain behavior by 
> automatically closing the fds, when the normal, predictable, valid thing 
> would be to die by SIGPIPE.

Again, you might be surprised at how many people view that as a bug in
the shell.


>> If the coproc terminates, the file descriptor to write to it becomes 
>> invalid because it's implicitly closed.
> 
> Yes, but the distinction I was making is that they do not become invalid 
> when or because the coproc terminates, they become invalid when and because 
> the shell closes them.  (I'm saying that if the shell did not close them 
> automatically, they would remain valid.)
> 
> 
>>>  The surprising bit is when they become invalid unexpectedly (from the
>>>  point of view of the user) because the shell closes them
>>>  automatically, at the somewhat arbitrary timing when the coproc is
>>>  reaped.
>>
>> No real difference from procsubs.
> 
> I think I disagree?  The difference is that the replacement string for a 
> procsub (/dev/fd/N or a fifo path) remains valid for the command in 
> question.  (Right?) 

Using your definition of valid, I believe so, yes.

Avoiding SIGPIPE depends on how the OS handles opens on /dev/fd/N: an
internal dup or a handle to the same fd. In the latter case, I think the
file descriptor obtained when opening /dev/fd/N would become `invalid'
at the same time the process terminates.

I think we're talking about our different interpretations of `invalid'
(EBADF as opposed to EPIPE/SIGPIPE).


> So the command in question can count on that path 
> being valid.  And if a procsub is used in an exec redirection, in order to 
> extend its use for future commands (and the redirection is guaranteed to 
> work, since it is guaranteed to be valid for that exec command), then the 
> newly opened pipe fd will not be subject to automatic closing either.

Correct.

> 
> As far as I can tell there is no arbitrary timing for when the shell closes 
> the fds for procsubs.  As far as I can tell, it closes them when the 
> command in question completes, and that's the end of the story. (There's no 
> waiting for the timing of the background procsub process to complete.)

Right. There are reasonably well-defined rules for when redirections 
associated with commands are disposed, and exec redirections to procsubs
just follow from those. The shell closes file descriptors (and
potentially unlinks the FIFO) when it reaps the process substitution, but
it takes some care not to do that prematurely, and the user isn't using
those fds.

> 
> 
>>> Second, why is it a problem if the variables keep their (invalid) fds 
>>> after closing them, if the user is the one that closed them anyway?
>>>
>>> Isn't this how it works with the auto-assigned fd redirections?
>>
>> Those are different file descriptors.
>>
>>>
>>>       $ exec {d}<.
>>>       $ echo $d
>>>       10
>>>       $ exec {d}<&-
>>>       $ echo $d
>>>       10
>>
>> The shell doesn't try to manage that object in the same way it does a
>> coproc. The user has explicitly indicated they want to manage it.
> 
> Ok - your intention makes sense then.  My reasoning was that auto-allocated 
> redirection fds ( {x}>file or {x}>&$N ) are a way of asking the shell to 
> automatically place fds in a variable for you to manage - and I imagined 
> 'coproc X {...}' the same way.

The philosophy is the same as if you picked the file descriptor number
yourself and assigned it to the variable -- the shell just does some of
the bookkeeping for you so you don't have to worry about the file
descriptor resource limit. You still have to manage file descriptor $x the
same way you would if you had picked file descriptor 15 (for example).


>> But there is a window there where a short-lived coprocess could be reaped 
>> before you dup the file descriptors. Since the original intent of the 
>> feature was that coprocs were a way to communicate with long-lived 
>> processes -- something more persistent than a process substitution -- it 
>> was not really a concern at the time.
> 
> Makes sense.  For me, working with coprocesses is largely a more flexible 
> way of setting up interesting pipelines - which is where the shell excels.
> 
> Once a 'pipework' is set up (I'm making up this word now to distinguish 
> from a simple pipeline), the shell does not have to be in the middle 
> shoveling data around - the external commands can do that on their own.

My original intention for the coprocs (and Korn's from whence they came)
was that the shell would be in the middle -- it's another way for the shell
to do IPC.

Chet
-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-15 17:01                           ` Carl Edquist
@ 2024-04-17 14:20                             ` Chet Ramey
  2024-04-20 22:04                               ` Carl Edquist
  0 siblings, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-17 14:20 UTC (permalink / raw)
  To: Carl Edquist
  Cc: chet.ramey, Martin D Kealey, Zachary Santer, bug-bash, libc-alpha

On 4/15/24 1:01 PM, Carl Edquist wrote:
> On Sat, 13 Apr 2024, Chet Ramey wrote:
> 
>> The original intent was to allow the shell to drive a long-running 
>> process that ran more-or-less in parallel with it. Look at 
>> examples/scripts/bcalc for an example of that kind of use.
> 
> Thanks for mentioning this example.  As you understand, this model use case 
> does not require closing the coproc fds when finished, because they will be 
> closed implicitly when the shell exits.  (As bcalc itself admits.)
> 
> And if the coproc is left open for the lifetime of the shell, the alternate 
> behavior of deferring the coproc deallocation (until both coproc fds are 
> closed) would not require anything extra from the user.
> 
> The bcalc example does close both coproc fds though - both at the end, and 
> whenever it resets.  And so in this example (which as you say, was the 
> original intent), the user is already explicitly closing both coproc fds 
> explicitly; so the alternate deferring behavior would not require anything 
> extra from the user here either.
> 
> ...
> 
> Yet another point brought to light by the bcalc example relates to the 
> coproc pid variable.  The reset() function first closes the coproc pipe 
> fds, then sleeps for a second to give the BC coproc some time to finish.
> 
> An alternative might be to 'wait' for the coproc to finish (likely faster 
> than sleeping for a second). 

If the coproc has some problem and doesn't exit immediately, `wait' without
options will hang. That's why I opted for the sleep/kill-as-insurance
combo.

(And before you ask why I didn't use `wait -n', I wrote bcalc in 30 minutes
after someone asked me a question about doing floating point math with awk
in a shell script, and it worked.)

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-03-14  9:58             ` Carl Edquist
                                 ` (2 preceding siblings ...)
  2024-04-03 14:32               ` Chet Ramey
@ 2024-04-17 14:37               ` Chet Ramey
  2024-04-20 22:04                 ` Carl Edquist
  3 siblings, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-17 14:37 UTC (permalink / raw)
  To: Carl Edquist, Zachary Santer; +Cc: chet.ramey, bug-bash, libc-alpha

On 3/14/24 5:58 AM, Carl Edquist wrote:

> Separately, I consider the following coproc behavior to be weird, fragile, 
> and broken.

Yes, I agree that coprocs should survive being suspended. The most recent
devel branch push has code to prevent the coproc being reaped if it's
stopped and not terminated.

Chet

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Modify buffering of standard streams via environment variables (not LD_PRELOAD)?
  2024-03-20  8:55                     ` Carl Edquist
@ 2024-04-19  0:16                       ` Zachary Santer
  2024-04-19  9:32                         ` Pádraig Brady
  2024-04-20 16:00                         ` Carl Edquist
  0 siblings, 2 replies; 54+ messages in thread
From: Zachary Santer @ 2024-04-19  0:16 UTC (permalink / raw)
  To: Carl Edquist; +Cc: Kaz Kylheku, libc-alpha, coreutils, Pádraig Brady

Was "RFE: enable buffering on null-terminated data"

On Wed, Mar 20, 2024 at 4:54 AM Carl Edquist <edquist@cs.wisc.edu> wrote:
>
> However, if stdbuf's magic env vars are exported in your shell (either by
> doing a trick like 'export $(env -i stdbuf -oL env)', or else more simply
> by first starting a new shell with 'stdbuf -oL bash'), then every command
> in your pipelines will start with the new default line-buffered stdout.
> That way your line-items from build.sh should get passed all the way
> through the pipeline as they are produced.

Finally had a chance to try to build with 'stdbuf --output=L --error=L
--' in front of the build script, and it caused some crazy problems. I
was building Ada, though, so pretty good chance that part of the build
chain doesn't link against libc at all.

I got a bunch of
ERROR: ld.so: object '/usr/libexec/coreutils/libstdbuf.so' from
LD_PRELOAD cannot be preloaded: ignored.

And then it somehow caused compiler errors relating to the size of
what would be pointer types. Cleared out all the build products and
tried again without stdbuf and everything was fine.

From the original thread just within the coreutils email list, "stdbuf
feature request - line buffering but for null-terminated data":
On Tue, Mar 12, 2024 at 12:42 PM Kaz Kylheku <kaz@kylheku.com> wrote:
>
> I would say that if it is implemented, the programs which require
> it should all make provisions to set it up themselves.
>
> stdbuf is a hack/workaround for programs that ignore the
> issue of buffering. Specifically, programs which send information
> to one of the three standard streams, such that the information
> is required in a timely way.  Those streams become fully buffered
> when not connected to a terminal.

I think I've partially come around to this point of view. However,
instead of expecting all sorts of individual programs to implement
their own buffering mode command-line options, could this be handled
with environment variables, but without LD_PRELOAD? I don't know if
libc itself can check for those environment variables and adjust each
program's buffering on its own, but if so, that would be a much
simpler solution.

You could compare this to the various locale environment variables,
though I think a lot of commands whose behavior differ from locale to
locale do have to implement their own handling of that internally, at
least to some extent.

This seems like somewhat less of a hack, and if no part of a program
looks for those environment variables, it isn't going to find itself
getting broken by the dynamic linker. It's just not going to change
its buffering.

Additionally, things that don't link against libc could still honor
these environment variables, if the developers behind them care to put
in the effort.

Zack

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Modify buffering of standard streams via environment variables (not LD_PRELOAD)?
  2024-04-19  0:16                       ` Modify buffering of standard streams via environment variables (not LD_PRELOAD)? Zachary Santer
@ 2024-04-19  9:32                         ` Pádraig Brady
  2024-04-19 11:36                           ` Zachary Santer
  2024-04-20 16:00                         ` Carl Edquist
  1 sibling, 1 reply; 54+ messages in thread
From: Pádraig Brady @ 2024-04-19  9:32 UTC (permalink / raw)
  To: Zachary Santer, Carl Edquist; +Cc: Kaz Kylheku, libc-alpha, coreutils

On 19/04/2024 01:16, Zachary Santer wrote:
> Was "RFE: enable buffering on null-terminated data"
> 
> On Wed, Mar 20, 2024 at 4:54 AM Carl Edquist <edquist@cs.wisc.edu> wrote:
>>
>> However, if stdbuf's magic env vars are exported in your shell (either by
>> doing a trick like 'export $(env -i stdbuf -oL env)', or else more simply
>> by first starting a new shell with 'stdbuf -oL bash'), then every command
>> in your pipelines will start with the new default line-buffered stdout.
>> That way your line-items from build.sh should get passed all the way
>> through the pipeline as they are produced.
> 
> Finally had a chance to try to build with 'stdbuf --output=L --error=L
> --' in front of the build script, and it caused some crazy problems. I
> was building Ada, though, so pretty good chance that part of the build
> chain doesn't link against libc at all.
> 
> I got a bunch of
> ERROR: ld.so: object '/usr/libexec/coreutils/libstdbuf.so' from
> LD_PRELOAD cannot be preloaded: ignored.
> 
> And then it somehow caused compiler errors relating to the size of
> what would be pointer types. Cleared out all the build products and
> tried again without stdbuf and everything was fine.
> 
>>From the original thread just within the coreutils email list, "stdbuf
> feature request - line buffering but for null-terminated data":
> On Tue, Mar 12, 2024 at 12:42 PM Kaz Kylheku <kaz@kylheku.com> wrote:
>>
>> I would say that if it is implemented, the programs which require
>> it should all make provisions to set it up themselves.
>>
>> stdbuf is a hack/workaround for programs that ignore the
>> issue of buffering. Specifically, programs which send information
>> to one of the three standard streams, such that the information
>> is required in a timely way.  Those streams become fully buffered
>> when not connected to a terminal.
> 
> I think I've partially come around to this point of view. However,
> instead of expecting all sorts of individual programs to implement
> their own buffering mode command-line options, could this be handled
> with environment variables, but without LD_PRELOAD? I don't know if
> libc itself can check for those environment variables and adjust each
> program's buffering on its own, but if so, that would be a much
> simpler solution.
> 
> You could compare this to the various locale environment variables,
> though I think a lot of commands whose behavior differ from locale to
> locale do have to implement their own handling of that internally, at
> least to some extent.
> 
> This seems like somewhat less of a hack, and if no part of a program
> looks for those environment variables, it isn't going to find itself
> getting broken by the dynamic linker. It's just not going to change
> its buffering.
> 
> Additionally, things that don't link against libc could still honor
> these environment variables, if the developers behind them care to put
> in the effort.

env variables are what I proposed 18 years ago now:
https://sourceware.org/bugzilla/show_bug.cgi?id=2457

cheers,
Pádraig

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Modify buffering of standard streams via environment variables (not LD_PRELOAD)?
  2024-04-19  9:32                         ` Pádraig Brady
@ 2024-04-19 11:36                           ` Zachary Santer
  2024-04-19 12:26                             ` Pádraig Brady
  0 siblings, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-04-19 11:36 UTC (permalink / raw)
  To: Pádraig Brady; +Cc: Carl Edquist, Kaz Kylheku, libc-alpha, coreutils

On Fri, Apr 19, 2024 at 5:32 AM Pádraig Brady <P@draigbrady.com> wrote:
>
> env variables are what I proposed 18 years ago now:
> https://sourceware.org/bugzilla/show_bug.cgi?id=2457

And the "resistance to that" from the Red Hat people 24 years ago is
listed on a website that doesn't exist anymore.

If I'm to argue with a guy from 18 years ago...

Ulrich Drepper wrote:
> Hell, no.  Programs expect a certain buffer mode and perhaps would work
> unexpectedly if this changes.  By setting a mode to unbuffered, for instance,
> you can easily DoS a system.  I can think about enough other reasons why this is
> a terrible idea.  Programs explicitly must request a buffering scheme so that it
> matches the way the program uses the stream.

If buffering were set according to the env vars before the program
configures buffers on its end, if it chooses to, then the env vars
have no effect. This is how the stdbuf util works, right now. Would
programs that expect a certain buffer mode not set that mode
explicitly themselves? Are you allowing untrusted users to set env
vars for important daemons or something? How is this a valid concern?

This is specific to the standard streams, 0-2. Buffering of stdout and
stderr is already configured dynamically by libc. If it's going to a
terminal, it's line-buffered. If it's not, it's fully buffered.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Modify buffering of standard streams via environment variables (not LD_PRELOAD)?
  2024-04-19 11:36                           ` Zachary Santer
@ 2024-04-19 12:26                             ` Pádraig Brady
  2024-04-19 16:11                               ` Zachary Santer
  0 siblings, 1 reply; 54+ messages in thread
From: Pádraig Brady @ 2024-04-19 12:26 UTC (permalink / raw)
  To: Zachary Santer; +Cc: Carl Edquist, Kaz Kylheku, libc-alpha, coreutils

On 19/04/2024 12:36, Zachary Santer wrote:
> On Fri, Apr 19, 2024 at 5:32 AM Pádraig Brady <P@draigbrady.com> wrote:
>>
>> env variables are what I proposed 18 years ago now:
>> https://sourceware.org/bugzilla/show_bug.cgi?id=2457
> 
> And the "resistance to that" from the Red Hat people 24 years ago is
> listed on a website that doesn't exist anymore.
> 
> If I'm to argue with a guy from 18 years ago...
> 
> Ulrich Drepper wrote:
>> Hell, no.  Programs expect a certain buffer mode and perhaps would work
>> unexpectedly if this changes.  By setting a mode to unbuffered, for instance,
>> you can easily DoS a system.  I can think about enough other reasons why this is
>> a terrible idea.  Programs explicitly must request a buffering scheme so that it
>> matches the way the program uses the stream.
> 
> If buffering were set according to the env vars before the program
> configures buffers on its end, if it chooses to, then the env vars
> have no effect. This is how the stdbuf util works, right now. Would
> programs that expect a certain buffer mode not set that mode
> explicitly themselves? Are you allowing untrusted users to set env
> vars for important daemons or something? How is this a valid concern?
> 
> This is specific to the standard streams, 0-2. Buffering of stdout and
> stderr is already configured dynamically by libc. If it's going to a
> terminal, it's line-buffered. If it's not, it's fully buffered.

Playing devil's advocate, I guess programs may be depending
on the automatic buffering modes set.
I guess the thinking is that it was too easy to perturb
the system with env vars, though you can already do that with LD_PRELOAD.

Perhaps at this stage we should consider stdbuf ubiquitous enough to suffice,
noting that it's also supported on FreeBSD.
I'm surprised that the LD_PRELOAD setting is breaking your ada build,
and it would be interesting to determine the reason for that.

cheers,
Pádraig

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Modify buffering of standard streams via environment variables (not LD_PRELOAD)?
  2024-04-19 12:26                             ` Pádraig Brady
@ 2024-04-19 16:11                               ` Zachary Santer
  0 siblings, 0 replies; 54+ messages in thread
From: Zachary Santer @ 2024-04-19 16:11 UTC (permalink / raw)
  To: Pádraig Brady; +Cc: Carl Edquist, Kaz Kylheku, libc-alpha, coreutils

On Fri, Apr 19, 2024 at 8:26 AM Pádraig Brady <P@draigbrady.com> wrote:
>
> Perhaps at this stage we should consider stdbuf ubiquitous enough to suffice,
> noting that it's also supported on FreeBSD.

Alternatively, if glibc were modified to act on these hypothetical
environment variables, it would be trivial to have stdbuf simply set
those, to ensure backwards compatibility.

> I'm surprised that the LD_PRELOAD setting is breaking your ada build,
> and it would be interesting to determine the reason for that.

If I had that kind of time...

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Modify buffering of standard streams via environment variables (not LD_PRELOAD)?
  2024-04-19  0:16                       ` Modify buffering of standard streams via environment variables (not LD_PRELOAD)? Zachary Santer
  2024-04-19  9:32                         ` Pádraig Brady
@ 2024-04-20 16:00                         ` Carl Edquist
  2024-04-20 20:00                           ` Zachary Santer
  1 sibling, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-04-20 16:00 UTC (permalink / raw)
  To: Zachary Santer; +Cc: libc-alpha, coreutils, Pádraig Brady

[-- Attachment #1: Type: text/plain, Size: 1425 bytes --]

On Thu, 18 Apr 2024, Zachary Santer wrote:

> On Wed, Mar 20, 2024 at 4:54 AM Carl Edquist <edquist@cs.wisc.edu> wrote:
>>
>> However, if stdbuf's magic env vars are exported in your shell (either 
>> by doing a trick like 'export $(env -i stdbuf -oL env)', or else more 
>> simply by first starting a new shell with 'stdbuf -oL bash'), then 
>> every command in your pipelines will start with the new default 
>> line-buffered stdout. That way your line-items from build.sh should get 
>> passed all the way through the pipeline as they are produced.
>
> Finally had a chance to try to build with 'stdbuf --output=L --error=L 
> --' in front of the build script, and it caused some crazy problems.

For what it's worth, when I was trying that out msys2 (since that's what 
you said you were using), I also ran into some very weird errors when just 
trying to export LD_PRELOAD and _STDBUF_O to what stdbuf -oL sets.  It was 
weird because I didn't see issues when just running a command (including 
bash) directly under stdbuf.  I didn't get to the bottom of it though and 
I don't have access to a windows laptop any more to experiment.

Also I might ask, why are you setting "--error=L" ?

Not that this is the problem you're seeing, but in any case stderr is 
unbuffered by default, and you might mess up the output a bit by line 
buffering it, if it's expecting to output partial lines for progress or 
whatever.

Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Modify buffering of standard streams via environment variables (not LD_PRELOAD)?
  2024-04-20 16:00                         ` Carl Edquist
@ 2024-04-20 20:00                           ` Zachary Santer
  2024-04-20 21:45                             ` Carl Edquist
  0 siblings, 1 reply; 54+ messages in thread
From: Zachary Santer @ 2024-04-20 20:00 UTC (permalink / raw)
  To: Carl Edquist; +Cc: libc-alpha, coreutils, Pádraig Brady

On Sat, Apr 20, 2024 at 11:58 AM Carl Edquist <edquist@cs.wisc.edu> wrote:
>
> On Thu, 18 Apr 2024, Zachary Santer wrote:
> >
> > Finally had a chance to try to build with 'stdbuf --output=L --error=L
> > --' in front of the build script, and it caused some crazy problems.
>
> For what it's worth, when I was trying that out msys2 (since that's what
> you said you were using), I also ran into some very weird errors when just
> trying to export LD_PRELOAD and _STDBUF_O to what stdbuf -oL sets.  It was
> weird because I didn't see issues when just running a command (including
> bash) directly under stdbuf.  I didn't get to the bottom of it though and
> I don't have access to a windows laptop any more to experiment.

This was actually in RHEL 7.

stdbuf --output=L --error=L -- "${@}" 2>&1 |
  tee log-file |
    while IFS='' read -r line; do
      # do stuff
    done
#

And then obviously the arguments to this script give the command I
want it to run.

> Also I might ask, why are you setting "--error=L" ?
>
> Not that this is the problem you're seeing, but in any case stderr is
> unbuffered by default, and you might mess up the output a bit by line
> buffering it, if it's expecting to output partial lines for progress or
> whatever.

I don't know how buffering works when stdout and stderr get redirected
to the same pipe. You'd think, whatever it is, it would have to be
smart enough to keep them interleaved in the same order they were
printed to in. That in mind, I would assume they both get placed into
the same block buffer by default.

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Modify buffering of standard streams via environment variables (not LD_PRELOAD)?
  2024-04-20 20:00                           ` Zachary Santer
@ 2024-04-20 21:45                             ` Carl Edquist
  0 siblings, 0 replies; 54+ messages in thread
From: Carl Edquist @ 2024-04-20 21:45 UTC (permalink / raw)
  To: Zachary Santer; +Cc: libc-alpha, coreutils, Pádraig Brady

On Sat, 20 Apr 2024, Zachary Santer wrote:

> This was actually in RHEL 7.

Oh.  In that case it might be worth looking into ...


> I don't know how buffering works when stdout and stderr get redirected 
> to the same pipe. You'd think, whatever it is, it would have to be smart 
> enough to keep them interleaved in the same order they were printed to 
> in. That in mind, I would assume they both get placed into the same 
> block buffer by default.

My take is always to try it and find out.  Though in this case I think the 
default (without using stdbuf) is that the program's stderr is output to 
the pipe immediately (ie, unbuffered) on each library call (fprintf(3), 
fputs(3), putc(3), fwrite(3)), while stdout is written to the pipe at 
block boundaries - even though fd 1 and 2 refer to the same pipe.

If you force line buffering for stdout and stderr, that is likely what you 
want, and it will interleave _lines_ in the order that they were printed.

However, stdout and stderr are still separate streams even if they refer 
to the same output file/pipe/device, so partial lines are not interleaved 
in the order that they were printed.

For example:

 	#include <stdio.h>

 	int main()
 	{
 		putc('a', stderr);
 		putc('1', stdout);
 		putc('b', stderr);
 		putc('2', stdout);
 		putc('c', stderr);
 		putc('3', stdout);
 		putc('\n', stderr);
 		putc('\n', stdout);

 		return 0;
 	}

will output "abc\n123\n" instead of "a1b2c3\n\n", even if you run it as

 	$ ./abc123 2>&1 | cat
or
 	$ stdbuf -oL -eL ./abc123 2>&1 | cat


...

Not that that's relevant for what you're doing :)

Carl


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-17 14:20                             ` Chet Ramey
@ 2024-04-20 22:04                               ` Carl Edquist
  2024-04-22 16:06                                 ` Chet Ramey
  0 siblings, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-04-20 22:04 UTC (permalink / raw)
  To: Chet Ramey; +Cc: Zachary Santer, bug-bash, libc-alpha

On Wed, 17 Apr 2024, Chet Ramey wrote:

> On 4/15/24 1:01 PM, Carl Edquist wrote:
>>
>>  Yet another point brought to light by the bcalc example relates to the
>>  coproc pid variable.  The reset() function first closes the coproc
>>  pipe fds, then sleeps for a second to give the BC coproc some time to
>>  finish.
>>
>>  An alternative might be to 'wait' for the coproc to finish (likely
>>  faster than sleeping for a second).
>
> If the coproc has some problem and doesn't exit immediately, `wait' 
> without options will hang. That's why I opted for the 
> sleep/kill-as-insurance combo.

Yes that much was clear from the script itself.

I didn't mean any of that as a critique of the bcalc script.  I just meant 
it brought to light the point that the coproc pid variable is another 
thing in the current deallocate-on-terminate behavior, that needs to be 
copied before it can be used reliably.  (With the 'kill' or 'wait' 
builtins.)

Though I do suspect that the most common case with coprocs is that closing 
the shell's read and write fds to the coproc is enough to cause the coproc 
to finish promptly - as neither read attempts on its stdin nor write 
attempts on its stdout can block anymore.

I think this is _definitely_ true for the BC coproc in the bcalc example. 
But it's kind of a distraction to get hung up on that detail, because in 
the general case there may very well be other scenarios where it would be 
appropriate to, um, _nudge_ the coproc a bit with the kill command.


> (And before you ask why I didn't use `wait -n', I wrote bcalc in 30 
> minutes after someone asked me a question about doing floating point 
> math with awk in a shell script, and it worked.)

It's fine!  It's just an example, after all  :)


Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-17 14:37               ` Chet Ramey
@ 2024-04-20 22:04                 ` Carl Edquist
  0 siblings, 0 replies; 54+ messages in thread
From: Carl Edquist @ 2024-04-20 22:04 UTC (permalink / raw)
  To: Chet Ramey; +Cc: Zachary Santer, bug-bash, libc-alpha

On Wed, 17 Apr 2024, Chet Ramey wrote:

> Yes, I agree that coprocs should survive being suspended. The most 
> recent devel branch push has code to prevent the coproc being reaped if 
> it's stopped and not terminated.

Oh, nice!  :)


Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-16 15:48                       ` Chet Ramey
@ 2024-04-20 23:11                         ` Carl Edquist
  2024-04-22 16:12                           ` Chet Ramey
  0 siblings, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-04-20 23:11 UTC (permalink / raw)
  To: Chet Ramey; +Cc: Zachary Santer, bug-bash, libc-alpha

On Tue, 16 Apr 2024, Chet Ramey wrote:

> The bigger concern was how to synchronize between the processes, but 
> that's something that the script writer has to do on their own.

Right.  It can be tricky and depends entirely on what the user's up to.


> My concern was always coproc fds leaking into other processes, 
> especially pipelines. If someone has a coproc now and is `messy' about 
> cleaning it up, I feel like there's the possibility of deadlock.

I get where you're coming from with the concern.  I would welcome being 
shown otherwise, but as far as I can tell, deadlock is a ghost of a 
concern once the coproc is dead.


Maybe it helps to step through it ...


- First, where does deadlock start?  (In the context of pipes)

I think the answer is: When there is a read or write attempted on a pipe 
that blocks (indefinitely).


- What causes a read or a write on a pipe to block?

A pipe read blocks when a corresponding write-end is open, 
but there is no data available to read.

A pipe write blocks when a corresponding read-end is open, 
but the pipe is full.


- Are the coproc's corresponding ends of the shell's pipe fds open?

Well, not if the coproc is really dead.


- Will a read or write ever be attempted?

If the shell's stray coproc fds are left open, sure they will leak into 
pipelines too - but since they're forgotten, in theory no command will 
actually attempt to use them.


- What if a command attempts to use these stray fds anyway, by mistake?

If the coproc is really dead, then its side of the pipe fds will have been 
closed.  Thus read/write attempts on the fds on the shell's side (either 
from the shell itself, or from commands / pipelines that the fds leaked 
into) WILL NOT BLOCK, and thus will not result in deadlock.

(A read attempt will hit EOF, a write attempt will get SIGPIPE/EPIPE.)


HOPEFULLY that is enough to put any reasonable fears of deadlock to bed - 
at least in terms of the shell's leaked fds leading to deadlock.


- But what if the _coproc_ leaked its pipe fds before it died?

At this point I think perhaps we get into what you called a "my arm hurts 
when I do this" situation.  It kind of breaks the whole coproc model: if 
the stdin/stdout of a coproc are still open by one of the coproc's 
children, then I might say the coproc is not really dead.

But anyway I want to be a good sport, for completeness.


An existing use case that would lead to trouble would perhaps have to look 
something like this:

The shell sends a quit command to a coproc, without closing the shell's 
coproc fds.

The coproc has a child, then exits.  The coproc (parent) is dead.  The 
coproc's child has inherited the coproc's pipe fds.  The script author 
_expects_ that the coproc parent will exit, and expects that this will 
trigger the old behavior, that the shell will automatically close its fds 
to the coproc parent.  Thus the author _expects_ that the coproc exiting 
will, indirectly but automatically, cause any blocked reads/writes on 
stdin/stdout in the coproc's child to stop blocking.  Thus the author 
_expects_ the coproc's child to promptly complete, even though its output 
_will not be consumable_ (because the author _expects_ that its stdout 
will be attached to a broken pipe).

But [here's where the potential problem starts] with the new deferring 
behavior, the shell's coproc fds are not automatically closed, and thus 
the coproc's _child_ does not stop blocking, and thus the author's 
short-lived expectations for this coproc's useless child are dashed to the 
ground, while that child is left standing idle until the cows come home. 
(That is, until the shell exits.)

It really seems like a contrived and senseless scenario, doesn't it? 
(Even to me!)

[And an even more far-fetched scenario: a coproc transmits copies of its 
pipe fds to another process over a unix socket ancillary message 
(SCM_RIGHTS), instead of to a child by inheritance.  The rest of the story 
is the same, and equally senseless.]


> But I don't know how extensively they're used, or all the use cases, so 
> I'm not sure how likely it is. I've learned there are users who do 
> things with shell features I never imagined. (People wanting to use 
> coprocs without the shell as the arbiter, for instance. :-) )

Hehe...

Well, yeah, once you gift-wrap yourself a friendly, reliable interface and 
have the freedom to play with it to your heart's content - you find some 
fun things to do with coprocesses.  (Much like regular shell pipelines.)

I get your meaning though - without knowing all the potential uses, it's 
hard to say with absolute certainty that no user will be negatively 
affected by a new improvement or bug fix.


>> [This is a common model for using coprocs, by the way, where an 
>> auxiliary coprocess is left open for the lifetime of the shell session 
>> and never explicitly closed.  When the shell session exits, the fds are 
>> closed implicitly by the OS, and the coprocess sees EOF and exits on 
>> its own.]
>
> That's one common model, yes. Another is that the shell process 
> explicitly sends a close or shutdown command to the coproc, so 
> termination is expected.

Right, but here also (after sending a quit command) the conclusion is the 
same as my point just below - that if the user is expecting the coproc to 
terminate, and expecting the current behavior that as a result the coproc 
variable will go away automatically, then that variable is as good as 
forgotten to the user.


>> If a user expects the coproc variable to go away automatically, that 
>> user won't be accessing a still-open fd from that variable for 
>> anything.
>
> I'm more concerned about a pipe with unread data that would potentially 
> cause problems. I suppose we just need more testing.

If I understand you right, you are talking about a scenario like 
this:

- a coproc writes to its output pipe
- the coproc terminates
- the shell leaves its fd for the read end of this pipe open
- there is unread data left sitting in this pipe
- [theoretical concern here]

Is that right?

I can't imagine this possibly leading to deadlock.  Either (1) the user 
has forgotten about this pipe, and never attempts to read from it, or (2) 
the user attempts to read from this pipe, returning some or all of the 
data, and possibly hitting EOF, but in any case DOES NOT BLOCK.

(I'm sorry if this is basically restating what I've already said earlier.)


> That's more of a "my arm hurts when I do this" situation. If a script 
> opened 500 fds using exec redirection, resource exhaustion would be 
> their own responsibility.

Ha, good!

[I had a small fear that fd exhaustion might have been your actual 
concern.]


>> Meanwhile, the bash man page does not specify the shell's behavior for 
>> when a coproc terminates, so you might say there's room for 
>> interpretation and the new deferring behavior would not break any 
>> promises.
>
> I could always enable it in the devel branch and see what happens with 
> the folks who use that. It would be three years after any release when 
> distros would put it into production anyway.

Oh, fun  :)


>> But since you mention it, writing to a broken pipe is still 
>> semantically meaningful also.  (I would even say valid.)  In the 
>> typical case it's expected behavior for a process to get killed when it 
>> attempts this and shell pipeline programming is designed with this in 
>> mind.
>
> You'd be surprised at how often I get requests to put in an internal 
> SIGPIPE handler to avoid problems/shell termination with builtins 
> writing to closed pipes.

Ah, well, I get it though.  It _is_ a bit jarring to see your shell get 
blown away with something like this -

 	$ exec 9> >(typo)
 	$ ...
 	$ echo >&9  # Boom!


So it does not surprise me that you have some users puzzling over it.

But FWIW I do think it is the most consistent & correct behavior.

Plus, of course, the user can install their own shell handler code for 
that case, or downgrade the effect to a non-fatal error with

 	$ trap '' SIGPIPE


>> So even for write attempts, you introduce uncertain behavior by 
>> automatically closing the fds, when the normal, predictable, valid 
>> thing would be to die by SIGPIPE.
>
> Again, you might be surprised at how many people view that as a bug in 
> the shell.

I'm not terribly surprised, since at first (before reasoning about it) the 
behavior is admittedly alarming.  ("What happened to my terminal?!?!")

But I'd argue the alternative is worse, because then it's an unpredictable 
race between SIGPIPE (which they're complaining about) and EBADF.


> I think we're talking about our different interpretations of `invalid' 
> (EBADF as opposed to EPIPE/SIGPIPE).

Right - just explaining; I think by now we are on the same page.


> My original intention for the coprocs (and Korn's from whence they came) 
> was that the shell would be in the middle -- it's another way for the 
> shell to do IPC.

And coprocesses are great for this, too!

It's just that external commands in a sense are extensions of the shell. 
The arms and legs, you might say, for doing the heavy lifting.


Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-20 22:04                               ` Carl Edquist
@ 2024-04-22 16:06                                 ` Chet Ramey
  2024-04-27 16:56                                   ` Carl Edquist
  0 siblings, 1 reply; 54+ messages in thread
From: Chet Ramey @ 2024-04-22 16:06 UTC (permalink / raw)
  To: Carl Edquist; +Cc: chet.ramey, Zachary Santer, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 818 bytes --]

On 4/20/24 6:04 PM, Carl Edquist wrote:

> I think this is _definitely_ true for the BC coproc in the bcalc example. 
> But it's kind of a distraction to get hung up on that detail, because in 
> the general case there may very well be other scenarios where it would be 
> appropriate to, um, _nudge_ the coproc a bit with the kill command.

You might be surprised. The OP was sending thousands of calculations to (I
think) GNU bc, which had some resource consumption issue that resulted in
it eventually hanging, unresponsive. The kill was the solution there. I
imagine there are similar scenarios with other tools.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-20 23:11                         ` Carl Edquist
@ 2024-04-22 16:12                           ` Chet Ramey
  0 siblings, 0 replies; 54+ messages in thread
From: Chet Ramey @ 2024-04-22 16:12 UTC (permalink / raw)
  To: Carl Edquist; +Cc: chet.ramey, Zachary Santer, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 521 bytes --]

On 4/20/24 7:11 PM, Carl Edquist wrote:

>> I could always enable it in the devel branch and see what happens with 
>> the folks who use that. It would be three years after any release when 
>> distros would put it into production anyway.
> 
> Oh, fun  :)

I'll enable it in the next devel branch push, so within a week.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-22 16:06                                 ` Chet Ramey
@ 2024-04-27 16:56                                   ` Carl Edquist
  2024-04-28 17:50                                     ` Chet Ramey
  0 siblings, 1 reply; 54+ messages in thread
From: Carl Edquist @ 2024-04-27 16:56 UTC (permalink / raw)
  To: Chet Ramey; +Cc: Zachary Santer, bug-bash, libc-alpha


On Mon, 22 Apr 2024, Chet Ramey wrote:

> You might be surprised. The OP was sending thousands of calculations to 
> (I think) GNU bc, which had some resource consumption issue that 
> resulted in it eventually hanging, unresponsive. The kill was the 
> solution there. I imagine there are similar scenarios with other tools.

Ok, you got me!  I take it back.

I hadn't considered bc operations being cpu/memory intensive.  But that 
possibility makes sense - given that it's arbitrary precision I guess you 
can ask for a number to the billionth power and never see the end of it  :)

Carl

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-27 16:56                                   ` Carl Edquist
@ 2024-04-28 17:50                                     ` Chet Ramey
  0 siblings, 0 replies; 54+ messages in thread
From: Chet Ramey @ 2024-04-28 17:50 UTC (permalink / raw)
  To: Carl Edquist; +Cc: chet.ramey, Zachary Santer, bug-bash, libc-alpha


[-- Attachment #1.1: Type: text/plain, Size: 1025 bytes --]

On 4/27/24 12:56 PM, Carl Edquist wrote:
> 
> On Mon, 22 Apr 2024, Chet Ramey wrote:
> 
>> You might be surprised. The OP was sending thousands of calculations to 
>> (I think) GNU bc, which had some resource consumption issue that resulted 
>> in it eventually hanging, unresponsive. The kill was the solution there. 
>> I imagine there are similar scenarios with other tools.
> 
> Ok, you got me!  I take it back.
> 
> I hadn't considered bc operations being cpu/memory intensive.  But that 
> possibility makes sense - given that it's arbitrary precision I guess you 
> can ask for a number to the billionth power and never see the end of it  :)

I'm not sure it was that so much as the long-running nature of the coproc.
A resource leak might never be noticeable except in this (admittedly
uncommon) scenario.

-- 
``The lyf so short, the craft so long to lerne.'' - Chaucer
		 ``Ars longa, vita brevis'' - Hippocrates
Chet Ramey, UTech, CWRU    chet@case.edu    http://tiswww.cwru.edu/~chet/


[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 203 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: Examples of concurrent coproc usage?
  2024-04-09 15:58                       ` Carl Edquist
  2024-04-13 20:10                         ` Chet Ramey
@ 2024-06-09  1:37                         ` Martin D Kealey
  1 sibling, 0 replies; 54+ messages in thread
From: Martin D Kealey @ 2024-06-09  1:37 UTC (permalink / raw)
  To: Carl Edquist
  Cc: Chet Ramey, Martin D Kealey, Zachary Santer, bug-bash, libc-alpha

[-- Attachment #1: Type: text/plain, Size: 1279 bytes --]

On Wed, 10 Apr 2024 at 03:58, Carl Edquist <edquist@cs.wisc.edu> wrote:

> Note the coproc shell only does this with pipes; it leaves other user
> managed fds like files or directories alone.
>
> I have no idea why that's the case, and i wonder whether it's intentional
> or an oversight.
>

Simply closing all pipes is definitely a bug.

This is starting to feel like we really need explicit ways to control
attributes on filedescriptors.

It should be possible to arrange so that any new subshell will keep
"emphemal" filedescriptors until just before invoking a command.

One mechanism would be to add two new per-fd attributes: *inherited-by-fork*,
and *ephemeral*.

The *inherited-by-fork* attribute would be set on any fd that's carried
through a fork (especially the implicit fork to create a pipeline) and
reset on any fd that's the result of a redirection.

The *emphemal* attribute is set on any coproc fd (or at least, any that's a
pipe to the stdin of a coproc).

Then when *both* attributes are set on an fd, it would be closed just
before launching any inner command, after any redirections have been
done.That way we could simply turn off the close-after-fork attribute on a
coproc fd if that's desired, but otherwise avoid deadlocks in the simple
cases.

-Martin

^ permalink raw reply	[flat|nested] 54+ messages in thread

end of thread, other threads:[~2024-06-09  1:38 UTC | newest]

Thread overview: 54+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
     [not found] <CABkLJULa8c0zr1BkzWLTpAxHBcpb15Xms0-Q2OOVCHiAHuL0uA@mail.gmail.com>
     [not found] ` <9831afe6-958a-fbd3-9434-05dd0c9b602a@draigBrady.com>
2024-03-10 15:29   ` RFE: enable buffering on null-terminated data Zachary Santer
2024-03-10 20:36     ` Carl Edquist
2024-03-11  3:48       ` Zachary Santer
2024-03-11 11:54         ` Carl Edquist
2024-03-11 15:12           ` Examples of concurrent coproc usage? Zachary Santer
2024-03-14  9:58             ` Carl Edquist
2024-03-17 19:40               ` Zachary Santer
2024-04-01 19:24               ` Chet Ramey
2024-04-01 19:31                 ` Chet Ramey
2024-04-02 16:22                   ` Carl Edquist
2024-04-03 13:54                     ` Chet Ramey
2024-04-03 14:32               ` Chet Ramey
2024-04-03 17:19                 ` Zachary Santer
2024-04-08 15:07                   ` Chet Ramey
2024-04-09  3:44                     ` Zachary Santer
2024-04-13 18:45                       ` Chet Ramey
2024-04-14  2:09                         ` Zachary Santer
2024-04-04 12:52                 ` Carl Edquist
2024-04-04 23:23                   ` Martin D Kealey
2024-04-08 19:50                     ` Chet Ramey
2024-04-09 14:46                       ` Zachary Santer
2024-04-13 18:51                         ` Chet Ramey
2024-04-09 15:58                       ` Carl Edquist
2024-04-13 20:10                         ` Chet Ramey
2024-04-14 18:43                           ` Zachary Santer
2024-04-15 18:55                             ` Chet Ramey
2024-04-15 17:01                           ` Carl Edquist
2024-04-17 14:20                             ` Chet Ramey
2024-04-20 22:04                               ` Carl Edquist
2024-04-22 16:06                                 ` Chet Ramey
2024-04-27 16:56                                   ` Carl Edquist
2024-04-28 17:50                                     ` Chet Ramey
2024-06-09  1:37                         ` Martin D Kealey
2024-04-08 16:21                   ` Chet Ramey
2024-04-12 16:49                     ` Carl Edquist
2024-04-16 15:48                       ` Chet Ramey
2024-04-20 23:11                         ` Carl Edquist
2024-04-22 16:12                           ` Chet Ramey
2024-04-17 14:37               ` Chet Ramey
2024-04-20 22:04                 ` Carl Edquist
2024-03-12  3:34           ` RFE: enable buffering on null-terminated data Zachary Santer
2024-03-14 14:15             ` Carl Edquist
2024-03-18  0:12               ` Zachary Santer
2024-03-19  5:24                 ` Kaz Kylheku
2024-03-19 12:50                   ` Zachary Santer
2024-03-20  8:55                     ` Carl Edquist
2024-04-19  0:16                       ` Modify buffering of standard streams via environment variables (not LD_PRELOAD)? Zachary Santer
2024-04-19  9:32                         ` Pádraig Brady
2024-04-19 11:36                           ` Zachary Santer
2024-04-19 12:26                             ` Pádraig Brady
2024-04-19 16:11                               ` Zachary Santer
2024-04-20 16:00                         ` Carl Edquist
2024-04-20 20:00                           ` Zachary Santer
2024-04-20 21:45                             ` Carl Edquist

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).