def testWaitpid(self): if posix_.environ.get('EINTR_TEST'): # Now we can do kill -TERM PID can get EINTR. signal.signal(signal.SIGTERM, _Handler) p = subprocess.Popen(['sleep', '5']) log('started sleep pid %d', p.pid) log('Hanging on waitpid in pid %d', posix_.getpid()) posix_.waitpid(-1, 0)
def WaitForOne(self): # type: () -> bool """Wait until the next process returns (or maybe Ctrl-C). Returns: True if we got a notification, or False if there was nothing to wait for. In the interactive shell, we return True if we get a Ctrl-C, so the caller will try again. """ # This is a list of async jobs try: # -1 makes it like wait(), which waits for any process. # NOTE: WUNTRACED is necessary to get stopped jobs. What about # WCONTINUED? pid, status = posix.waitpid(-1, WUNTRACED) except OSError as e: #log('wait() error: %s', e) if e.errno == errno_.ECHILD: return False # nothing to wait for caller should stop else: # We should never get here. EINTR was handled by the 'posix' # module. The only other error is EINVAL, which doesn't apply to # this call. raise except KeyboardInterrupt: # NOTE: Another way to handle this is to disable SIGINT when a process is # running. Not sure if there's any real difference. bash and dash # handle SIGINT pretty differently. if self.exec_opts.interactive(): # Caller should keep waiting. If we run 'sleep 3' and hit Ctrl-C, both # processes will get SIGINT, but the shell has to wait again to get the # exit code. return True else: raise # abort a batch script #log('WAIT got %s %s', pid, status) # All child processes are supposed to be in this dict. But this may # legitimately happen if a grandchild outlives the child (its parent). # Then it is reparented under this process, so we might receive # notification of its exit, even though we didn't start it. We can't have # any knowledge of such processes, so print a warning. if pid not in self.job_state.child_procs: stderr_line("osh: PID %d stopped, but osh didn't start it", pid) return True # caller should keep waiting proc = self.job_state.child_procs[pid] if WIFSIGNALED(status): term_sig = WTERMSIG(status) status = 128 + term_sig # Print newline after Ctrl-C. if term_sig == signal_.SIGINT: print('') proc.WhenDone(pid, status) elif WIFEXITED(status): status = WEXITSTATUS(status) #log('exit status: %s', status) proc.WhenDone(pid, status) elif WIFSTOPPED(status): #sig = posix.WSTOPSIG(status) # TODO: Do something nicer here. Implement 'fg' command. # Show in jobs list. log('') log('[PID %d] Stopped', pid) self.job_state.NotifyStopped( pid) # show in 'jobs' list, enable 'fg' proc.WhenStopped() self.last_status = status # for wait -n return True # caller should keep waiting
def WaitForOne(self, eintr_retry): # type: (bool) -> int """Wait until the next process returns (or maybe Ctrl-C). Args: Should be True to prevent zombies Returns: -1 Nothing to wait for 0 OK, job mutated >= 128 Interrupted with signal In the interactive shell, we return 0 if we get a Ctrl-C, so the caller will try again. Callers: wait -n -- WaitForOne() just once wait -- WaitForOne() in a loop wait $! -- job.JobWait() Comparisons: bash: jobs.c waitchld() Has a special case macro(!) CHECK_WAIT_INTR for the wait builtin dash: jobs.c waitproc() uses sigfillset(), sigprocmask(), etc. Runs in a loop while (gotsigchld), but that might be a hack for System V! You could imagine a clean API like posix::wait_for_one() wait_result = ECHILD -- nothing to wait for | Done(int pid, int status) -- process done | EINTR(bool sigint) -- may or may not retry But I think we want to keep KeyboardInterrupt as an exception for now. """ # This is a list of async jobs try: # Notes: # - The arg -1 makes it like wait(), which waits for any process. # - WUNTRACED is necessary to get stopped jobs. What about WCONTINUED? # - Unlike other syscalls, we do NOT try on EINTR, because the 'wait' # builtin should be interruptable. This doesn't appear to cause any # problems for other WaitForOne callers? pid, status = posix.waitpid(-1, WUNTRACED) except OSError as e: #log('wait() error: %s', e) if e.errno == errno_.ECHILD: return -1 # nothing to wait for caller should stop elif e.errno == errno_.EINTR: # Bug #858 fix # Examples: # - 128 + SIGUSR1 = 128 + 10 = 138 # - 128 + SIGUSR2 = 128 + 12 = 140 return 0 if eintr_retry else (128 + self.sig_state.last_sig_num) else: # The signature of waitpid() means this shouldn't happen raise AssertionError() except KeyboardInterrupt: # NOTE: Another way to handle this is to disable SIGINT when a process is # running. Not sure if there's any real difference. bash and dash # handle SIGINT pretty differently. if self.exec_opts.interactive(): # Caller should keep waiting. If we run 'sleep 3' and hit Ctrl-C, both # processes will get SIGINT, but the shell has to wait again to get the # exit code. return 0 else: raise # abort a batch script # All child processes are supposed to be in this dict. But this may # legitimately happen if a grandchild outlives the child (its parent). # Then it is reparented under this process, so we might receive # notification of its exit, even though we didn't start it. We can't have # any knowledge of such processes, so print a warning. if pid not in self.job_state.child_procs: stderr_line("osh: PID %d stopped, but osh didn't start it", pid) return True # caller should keep waiting proc = self.job_state.child_procs[pid] if WIFSIGNALED(status): term_sig = WTERMSIG(status) status = 128 + term_sig # Print newline after Ctrl-C. if term_sig == signal_.SIGINT: print('') proc.WhenDone(pid, status) elif WIFEXITED(status): status = WEXITSTATUS(status) #log('exit status: %s', status) proc.WhenDone(pid, status) elif WIFSTOPPED(status): #sig = posix.WSTOPSIG(status) # TODO: Do something nicer here. Implement 'fg' command. # Show in jobs list. log('') log('[PID %d] Stopped', pid) self.job_state.NotifyStopped( pid) # show in 'jobs' list, enable 'fg' proc.WhenStopped() else: raise AssertionError(status) self.last_status = status # for wait -n self.tracer.OnProcessEnd(pid, status) return 0 # caller should keep waiting