help / color / mirror / code / Atom feed
From: Bart Schaefer <schaefer@brasslantern.com>
To: Zsh hackers list <zsh-workers@zsh.org>
Subject: [PATCH] zargs and signal handling
Date: Sat, 4 Jun 2022 21:45:41 -0700	[thread overview]
Message-ID: <CAH+w=7az_Vw-S4=wJk6ydw0Q+JQRRZqtk7VOWBr064LVsQkdOQ@mail.gmail.com> (raw)

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

Whilst experimenting with something else, I discovered that "zargs -P"
would often abandon its backgrounded jobs when receiving
terminal-driven signals.  This led me down several rat-holes trying to
figure out what was going on.  In fact I'm still not sure what all the
reasons were or how many of them were related to "setopt nomonitor"
(which is removed in the attached patch).

I discovered for example that even when INT and QUIT were explicitly
trapped, a shell function passed as the command for zargs to run would
execute with those signals either reset to their defaults or simply
ignored, and I haven't been able to work out the circumstances when
each of those cases occurs.  E.g., with

f() { sleep $1; print $1 }
zargs -n 1 -P 3 -- 2 3 4 5 6 -- f

Interrupting zargs with ^C would leave all the background "f" running,
waiting for "sleep".  On the other hand, with

zargs -n 1 -P 3 -- 2 3 4 5 6 -- /bin/sleep

the ^C terminated the sleeps as expected but attempting to stop zargs
with ^Z would cause zargs to return, leaving orphan stopped sleep
processes behind.  That part was due to "nomonitor".

Here then is a patch which:
-- as discussed in the "Removing subshell from zargs" thread, removes
the preliminary "wait" for all the process
-- as noted above, removes "nomonitor" (because it was only needed for
that "wait")
-- explicitly adds traps to exit for tty-generated signals plus TERM,
except that it ...
-- captures the global signal trap context and restores it in the
background jobs
-- wraps the whole thing in an "always" block to clean up local helper functions
-- updates the comments to note another buglet and drop support for zsh 4.x.

As further speculated in the other thread, this doesn't yet remove the
subshell around the whole mess.

I'd love to hear explanations of why the signal handling behaves as it
does.  Every time I thought I had the logic mapped onto subshell entry
conditions or whatever, it'd behave a different way the next time I
tried, until I ended up with the rather brute-force approach in the

[-- Attachment #2: zargs_1_8.txt --]
[-- Type: text/plain, Size: 2918 bytes --]

diff --git a/Functions/Misc/zargs b/Functions/Misc/zargs
index 81916a3ac..782d6811e 100644
--- a/Functions/Misc/zargs
+++ b/Functions/Misc/zargs
@@ -39,9 +39,14 @@
 # "Killed by a signal" is determined by the usual shell rule that $? is
 # the signal number plus 128, so zargs can be fooled by a command that
-# explicitly exits with 129+.  Also, zsh prior to 4.1.x returns 1 rather
-# than 127 for "command not found" so this function incorrectly returns
-# 123 in that case if used with zsh 4.0.x.
+# explicitly exits with 129+.  If the command passed to zargs is a shell
+# function which uses "exit" instead of "return", zsh interprets 129+ as
+# a signal sent to the process group and may terminate zargs with that
+# status.  This is avoided when running zargs -P 2 or greater.
+# ZARGS_VERSION 1.5 is the last to support zsh 4.x.  Also, zsh prior to
+# 4.1.x returns 1 rather than 127 for "command not found" so zargs
+# incorrectly returned 123 in that case if used with zsh 4.0.x.
 # Because of "wait" limitations, --max-procs spawns max-procs jobs, then
 # waits for all of those, then spawns another batch, etc.
@@ -71,6 +76,10 @@
 # * The use of SIGUSR1 and SIGUSR2 to change the number of parallel jobs
 #   is not supported.
+{ # Begin "always" block to reset locally defined functions
+local ZARGS_VERSION="1.8"
 # First, capture the current setopts as "sticky emulation"
 if zmodload zsh/parameter
@@ -84,11 +93,20 @@ else
   emulate $(emulate -l) -c '_zarun() { eval "$@" }'
+local _zaTRAPS="$(trap)"
+_zatraps() {
+  # In children, these traps may be reset to default behavior, even
+  # if the calling shell has traps.  Restore to surrounding context,
+  # but assure that if zargs itself is signaled, children will exit.
+  [[ -o interactive ]] &&
+    function TRAP{HUP,INT,QUIT,TERM} { exit $((128 + $1)) }
+  [[ -n "$_zaTRAPS" ]] && eval "$_zaTRAPS"
+  unset _zaTRAPS
 emulate -L zsh || return 1
 local -a opts eof n s l P i
-local ZARGS_VERSION="1.7"
 if zparseopts -a opts -D -- \
 	-eof::=eof e::=eof \
 	-exit x \
@@ -193,14 +211,14 @@ then (( c = $#command - 1 ))
 else command=( print -r -- )
-local wait bg
-local execute='
+local bg execute='
     if (( $opts[(I)-(-interactive|p)] ))
     then read -q "?$call?..." || continue
     elif (( $opts[(I)-(-verbose|t)] ))
     then print -u2 -r -- "$call"
     _zarun "{
+	_zatraps
     } $bg"'
 local ret=0 analyze='
@@ -263,12 +281,10 @@ fi
 if (( P != 1 && ARGC > 1 ))
-    # These setopts are necessary for "wait" on multiple jobs to work.
-    setopt nonotify nomonitor
+    setopt nonotify	# Do not report each exiting job
     local -a _zajobs
     local j
     bg='& _zajobs+=( $! )'
-    wait='wait'
     for j in $_zajobs; do
       wait $j
@@ -316,4 +332,8 @@ return $ret
+} always {
+  builtin unfunction _zarun _zatraps
 # }

             reply	other threads:[~2022-06-05  4:46 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-06-05  4:45 Bart Schaefer [this message]
2022-06-09  3:44 ` Bart Schaefer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='CAH+w=7az_Vw-S4=wJk6ydw0Q+JQRRZqtk7VOWBr064LVsQkdOQ@mail.gmail.com' \
    --to=schaefer@brasslantern.com \
    --cc=zsh-workers@zsh.org \


* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox


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 NNTP newsgroup(s).