sstephenson / bats

Bash Automated Testing System
MIT License
7.12k stars 519 forks source link

`$cmd & echo foo | cat` freezes bats because pipe waits for backgrounded command to die #267

Closed jolmg closed 5 years ago

jolmg commented 5 years ago

Hello. I'm not sure if this is a bash issue or a bats issue, but since I can't get this to replicate outside of bats, I figured I'd try here first.

Here is an example file (I'll call it t.bats):

@test "foo" {
  sleep 10h &
  trap 'kill $!' EXIT
  echo begin > /dev/tty
  echo bar | cat > /dev/tty
  echo end > /dev/tty
}

On running bats t.bats I get the output:

 begin                                                                      1/1
bar

and then it stays frozen. I can't Ctrl-C out of it, either. I have to do Ctrl-Z, kill %.

If I remove | cat or remove the backgrounded process, it doesn't freeze.

Checking strace, it seems it's stuck waiting for all child processes to die:

$ strace -f bats t.bats
[...]
[pid 19663] wait4(-1, 

I got a backtrace of bash from gdb: bash-bats-freeze-backtrace.txt. Here are the first few lines:

#0  0x00007f5da855206b in waitpid () from /usr/lib/libc.so.6
#1  0x000055f62663f906 in waitchld (wpid=wpid@entry=22893, block=block@entry=1) at jobs.c:3599
#2  0x000055f626640a10 in wait_for (pid=22893) at jobs.c:2804
#3  0x000055f62662d9a2 in execute_command_internal (command=command@entry=0x55f62703f180, asynchronous=asynchronous@entry=0, pipe_in=pipe_in@entry=4, pipe_out=pipe_out@entry=-1, fds_to_close=fds_to_close@entry=0x55f62706c810) at execute_cmd.c:887
#4  0x000055f6266309db in execute_pipeline (command=command@entry=0x55f62706bed0, asynchronous=asynchronous@entry=0, pipe_in=pipe_in@entry=-1, pipe_out=pipe_out@entry=-1, fds_to_close=fds_to_close@entry=0x55f62706c810) at execute_cmd.c:2589

This is apparently waiting for the pipeline to die. It calls wait_for with a particular pid, but for some reason that results in it waiting on all child processes. wait_for calls waitchld with the pid and asking it to block, and waitchld decides clean up existing dead child processes while respecting the block parameter, so it blocks until all child processes are dead.

A comment on waitchld says (emphasis mine):

waitchld() reaps dead or stopped children. It's called by wait_for and sigchld_handler, and runs until there aren't any children terminating any more. If BLOCK is 1, this is to be a blocking wait for a single child, although an arriving SIGCHLD could cause the wait to be non-blocking. It returns the number of children reaped, or -1 if there are no unwaited-for child processes.

and then follows with the code:

      if (sigchld || block == 0)
    waitpid_flags |= WNOHANG;
...
      pid = WAITPID (-1, &status, waitpid_flags);

In the freezing execution, waitpid_flags is 0, and WNOHANG is 1.

It looks inconsistent with the comment, which makes me suspect the problem is with bash. However, I'd like to be able to replicate this without the use of bats before trying to bring this up with the bash developers. I don't know what bats does that makes this behavior turn up. Can someone here confirm where the problem is?

jolmg commented 5 years ago

As a workaround, pipes can be put in subshells, like so:

@test "foo" {
  sleep 10h &
  trap 'kill $!' EXIT
  echo begin > /dev/tty
  ( echo bar | cat ) > /dev/tty
  echo end > /dev/tty
}

That avoids the freezing.

jolmg commented 5 years ago

I forgot to mention the versions I'm working with:

$ bats --version
Bats 0.4.0
$ bash --version
GNU bash, version 5.0.7(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
jolmg commented 5 years ago

I just found out this is an obsolete repo. I tried to replicate this bug in bats-core v1.1.0 and it doesn't happen, so I'm going to close this.