I think I've found two problems in fhandler_termios::bg_check():

  * Cygwin's tcsetpgrp function will return EIO when the process
    group for the calling process has no leader.

  * This appears to be caused by a leaderless process group being
    interpreted as an orphaned process group.

Please find a plain text file attachment that includes a changelog
entry and a patch.

Please find a plain text file attachment that includes a small Perl
script that can be used as a test case.

Rationale:
----------

When the first process in a pipeline exits but a second process
keeps running, the process group for this pipeline will no longer
have a leader.  This is what happens when my test script is run like
so:

  cat </dev/null | /usr/bin/perl /tmp/test_bg_check.pl

The leader ('cat') has exited by the time the test script invokes
tcsetpgrp.  The following snippet in fhandler_termios::bg_check gets
triggered when this happens:

  /* If the process group is no more ... */
  int pgid_gone = !pid_exists (myself->pgid);

  if (pgid_gone)
    goto setEIO;

Here we see that fhandler_termios::bg_check will not tolerate a
leaderless process group.

The comment indicates that the test is looking for a process group
that is "no more", however, the process group still exists and has a
process in it (the calling process); it just happens to not have a
leader at this time.

I've not been able to find any documentation that says a leaderless
process group is problematic in some way.  Also, the script runs
fine in pipeline mode on both Linux and OpenBSD.

POSIX does talk about returning EIO for tcsetattr when:

  The process group of the writing process is orphaned, and the
  writing process is not ignoring or blocking SIGTTOU.

But SIGTTOU is ignored in my test script, so the above should not
apply (and I'm not even calling tcsetattr).  Parenthetically, SunOS
will return EIO for tcsetpgrp under the circumstance described
above, so I think EIO as a possible return value for tcsetpgrp is
not a problem per se...

Also, a leaderless process group is not the same thing as an
orphaned process group, the latter being defined by POSIX as:

  A process group in which the parent of every member is either
  itself a member of the group or is not a member of the group's
  session.

Out of curiosity, I researched how the EIO test is handled in both
Linux and OpenBSD.  The approaches are quite different.  In Linux,
we see code that loops through the relevant pids to see if the
process group is orphaned (edited for readability):

  vi /usr/src/linux-2.6.32/drivers/char/tty_io.c +/is_current_pgrp_orphaned
  vi /usr/src/linux-2.6.32/kernel/exit.c         +/will_become_orphaned_pgrp

    static int
    will_become_orphaned_pgrp (struct pid *pgrp, struct task_struct 
*ignored_task)
    {
      struct task_struct *p;
      do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
        if (task_pgrp(p->real_parent) != pgrp &&
            task_session(p->real_parent) == task_session(p))
          return 0;
      } while_each_pid_task(pgrp, PIDTYPE_PGID, p);
      return 1;
    }

In OpenBSD we see that it maintains a count of "qualified" jobs per
process group.  A qualified job is one with a parent in a different
process group of the same session.  The EIO test then becomes a
matter of looking for a zero qualified jobs count (also edited):

  vi /usr/src/sys/kern/kern_proc.c +/fixjobc
  vi /usr/src/sys/kern/tty.c       +/isbackground

    while (isbackground(pr, tp) &&
        (p->p_flag & P_PPWAIT) == 0 &&
        (p->p_sigignore & sigmask(SIGTTOU)) == 0 &&
        (p->p_sigmask & sigmask(SIGTTOU)) == 0) {
      if (pr->ps_pgrp->pg_jobc == 0)                  // <-- here
        return (EIO);
      pgsignal(pr->ps_pgrp, SIGTTOU, 1);
    } // continue on - allow the ioctl

Neither approach cares if a process group has a leader or not...

My patch adds a new function to test for orphaned process groups.
It also modifies the decision tree to take advantage of the new
function.  I think it will cause fhandler_termios::bg_check to be
more correct and hopefully not introduce any regressions.  It is, at
least, a fix for my test case...

Thanks for your consideration and thanks for such a wonderful system!

- Tor

2011-03-28  Tor Perkins
  
  * fhandler_termios.cc (fhandler_termios::bg_check): Do not return EIO
  when a process group has no leader as this is allowed and does not imply
  an orphaned process group.  Add a test for orphaned process groups.

Index: cygwin/fhandler_termios.cc
===================================================================
RCS file: /cvs/src/src/winsup/cygwin/fhandler_termios.cc,v
retrieving revision 1.78
diff -u -p -r1.78 fhandler_termios.cc
--- cygwin/fhandler_termios.cc  23 Oct 2010 18:07:08 -0000      1.78
+++ cygwin/fhandler_termios.cc  2 Apr 2011 23:20:49 -0000
@@ -111,6 +111,26 @@ fhandler_pty_master::tcgetpgrp ()
   return tc->pgid;
 }
 
+int
+tty_min::is_orphaned_process_group (int pgid)
+{
+  /* An orphaned process group is a process group in which the parent
+     of every member is either itself a member of the group or is not
+     a member of the group's session. */
+  winpids pids ((DWORD) PID_MAP_RW);
+  for (unsigned i = 0; i < pids.npids; i++)
+    {
+      _pinfo *p = pids[i];
+      if (!p->exists () || p->pgid != pgid)
+        continue;
+      pinfo ppid (p->ppid);
+      if (ppid->pgid != pgid &&
+          ppid->sid == myself->sid)
+        return 0;
+    }
+  return 1;
+}
+
 void
 tty_min::kill_pgrp (int sig)
 {
@@ -157,36 +177,41 @@ fhandler_termios::bg_check (int sig)
       return bg_eof;
     }
 
-  /* If the process group is no more or if process is ignoring or blocks 'sig',
-     return with error */
-  int pgid_gone = !pid_exists (myself->pgid);
+  /* Determine if process ignores or blocks 'sig' */
   int sigs_ignored =
     ((void *) global_sigs[sig].sa_handler == (void *) SIG_IGN) ||
     (_main_tls->sigmask & SIGTOMASK (sig));
 
-  if (pgid_gone)
-    goto setEIO;
-  else if (!sigs_ignored)
-    /* nothing */;
-  else if (sig == SIGTTOU)
-    return bg_ok;              /* Just allow the output */
-  else
-    goto setEIO;       /* This is an output error */
-
-  /* Don't raise a SIGTT* signal if we have already been interrupted
-     by another signal. */
-  if (WaitForSingleObject (signal_arrived, 0) != WAIT_OBJECT_0)
-    {
-      siginfo_t si = {0};
-      si.si_signo = sig;
-      si.si_code = SI_KERNEL;
-      kill_pgrp (myself->pgid, si);
-    }
-  return bg_signalled;
-
-setEIO:
-  set_errno (EIO);
-  return bg_error;
+  /* If the process is ignoring SIGTT*, then background IO is OK.  If
+     the process is not ignoring SIGTT*, then the sig is to be sent to
+     all processes in the process group (unless the process group of the
+     process is orphaned, in which case we return EIO). */
+  if ( sigs_ignored )
+    { 
+      return bg_ok;   /* Just allow the IO */
+    } 
+  else 
+    { 
+      if ( tc->is_orphaned_process_group (myself->pgid) )
+        {
+          termios_printf ("process group is orphaned");
+          set_errno (EIO);   /* This is an IO error */
+          return bg_error;
+        }
+      else
+        {
+          /* Don't raise a SIGTT* signal if we have already been
+             interrupted by another signal. */
+          if (WaitForSingleObject (signal_arrived, 0) != WAIT_OBJECT_0)
+            {
+              siginfo_t si = {0};
+              si.si_signo = sig;
+              si.si_code = SI_KERNEL;
+              kill_pgrp (myself->pgid, si);
+            }
+          return bg_signalled;
+        } 
+    } 
 }
 
 #define set_input_done(x) input_done = input_done || (x)
Index: cygwin/tty.h
===================================================================
RCS file: /cvs/src/src/winsup/cygwin/tty.h,v
retrieving revision 1.26
diff -u -p -r1.26 tty.h
--- cygwin/tty.h        19 Apr 2010 19:52:43 -0000      1.26
+++ cygwin/tty.h        2 Apr 2011 23:20:49 -0000
@@ -76,6 +76,7 @@ public:
   int getsid () {return sid;}
   void setsid (pid_t tsid) {sid = tsid;}
   void kill_pgrp (int sig);
+  int is_orphaned_process_group (int pgid);
   HWND gethwnd () {return hwnd;}
   void sethwnd (HWND wnd) {hwnd = wnd;}
 };
#!/usr/bin/perl
#
# Usage note:
#
#                    /usr/bin/perl /tmp/test_bg_check.pl # <-- no error
#   cat </dev/null | /usr/bin/perl /tmp/test_bg_check.pl # <--    error
#

$SIG{'TTOU'} = 'IGNORE';          # ignore SIGTTOU for tcsetpgrp

use POSIX qw(tcsetpgrp);          # bring in that system function

unless ( -t STDIN ) {             # if pipe, read input
  my @junk = <STDIN>;             # then close/redirect
  open(STDIN, "</dev/tty");       # 'cat' exits cleanly
}

unless ( $pid = fork ) {                 # child leads new pgrp
  setpgrp();                             # (does: setpgid(0,0))
  tcsetpgrp(fileno(*STDIN), getpgrp());  # then forwards itself
  exit;
}

waitpid($pid, 0);                 # parent waits, then forwards itself
tcsetpgrp(fileno(*STDIN), getpgrp()) || die "tcsetpgrp: EIO: $!";

$|++;                             # autoflush STDOUT
while ( sleep 1 ) { print "." }   # test job control (ctrl-z / fg)

Reply via email to