33#
44# zsh-async
55#
6- # version: 1.7.2
6+ # version: 1.8.0
77# author: Mathias Fredriksson
88# url: https://github.com/mafredri/zsh-async
99#
1010
11- typeset -g ASYNC_VERSION=1.7.2
11+ typeset -g ASYNC_VERSION=1.8.0
1212# Produce debug output from zsh-async when set to 1.
1313typeset -g ASYNC_DEBUG=${ASYNC_DEBUG:- 0}
1414
@@ -37,19 +37,27 @@ _async_job() {
3737 # block, after the command block has completed, the stdin for `cat` is
3838 # closed, causing stderr to be appended with a $'\0' at the end to mark the
3939 # end of output from this job.
40- local jobname=${ASYNC_JOB_NAME:- $1 }
41- local stdout stderr ret tok
42- {
43- stdout=$( eval " $@ " )
44- ret=$?
45- duration=$(( EPOCHREALTIME - duration )) # Calculate duration.
40+ local jobname=${ASYNC_JOB_NAME:- $1 } out
41+ out=" $(
42+ local stdout stderr ret tok
43+ {
44+ stdout=$( eval " $@ " )
45+ ret=$?
46+ duration=$(( EPOCHREALTIME - duration )) # Calculate duration.
47+
48+ print -r -n - $' \0' ${(q)jobname} $ret ${(q)stdout} $duration
49+ } 2> >( stderr=$( cat) && print -r -n - " " ${(q)stderr} $' \0' )
50+ ) "
51+ if [[ $out != $' \0' * $' \0' ]]; then
52+ # Corrupted output (aborted job?), skipping.
53+ return
54+ fi
4655
47- # Grab mutex lock, stalls until token is available.
48- read -r -k 1 -p tok || exit 1
56+ # Grab mutex lock, stalls until token is available.
57+ read -r -k 1 -p tok || return 1
4958
50- # Return output (<job_name> <return_code> <stdout> <duration> <stderr>).
51- print -r -n - $' \0' ${(q)jobname} $ret ${(q)stdout} $duration
52- } 2> >( stderr=$( cat) && print -r -n - " " ${(q)stderr} $' \0' )
59+ # Return output (<job_name> <return_code> <stdout> <duration> <stderr>).
60+ print -r -n - " $out "
5361
5462 # Unlock mutex by inserting a token.
5563 print -n -p $tok
@@ -73,10 +81,13 @@ _async_worker() {
7381 # When a zpty is deleted (using -d) all the zpty instances created before
7482 # the one being deleted receive a SIGHUP, unless we catch it, the async
7583 # worker would simply exit (stop working) even though visible in the list
76- # of zpty's (zpty -L).
77- TRAPHUP () {
78- return 0 # Return 0, indicating signal was handled.
79- }
84+ # of zpty's (zpty -L). This has been fixed around the time of Zsh 5.4
85+ # (not released).
86+ if ! is-at-least 5.4.1; then
87+ TRAPHUP () {
88+ return 0 # Return 0, indicating signal was handled.
89+ }
90+ fi
8091
8192 local -A storage
8293 local unique=0
@@ -121,15 +132,33 @@ _async_worker() {
121132 # Register a SIGCHLD trap to handle the completion of child processes.
122133 trap child_exit CHLD
123134
124- # Process option parameters passed to worker
125- while getopts " np:u " opt; do
135+ # Process option parameters passed to worker.
136+ while getopts " np:uz " opt; do
126137 case $opt in
127138 n) notify_parent=1;;
128139 p) parent_pid=$OPTARG ;;
129140 u) unique=1;;
141+ z) notify_parent=0;; # Uses ZLE watcher instead.
130142 esac
131143 done
132144
145+ # Terminate all running jobs, note that this function does not
146+ # reinstall the child trap.
147+ terminate_jobs () {
148+ trap - CHLD # Ignore child exits during kill.
149+ coproc : # Quit coproc.
150+ coproc_pid=0 # Reset pid.
151+
152+ if is-at-least 5.4.1; then
153+ trap ' ' HUP # Catch the HUP sent to this process.
154+ kill -HUP -$$ # Send to entire process group.
155+ trap - HUP # Disable HUP trap.
156+ else
157+ # We already handle HUP for Zsh < 5.4.1.
158+ kill -HUP -$$ # Send to entire process group.
159+ fi
160+ }
161+
133162 killjobs () {
134163 local tok
135164 local -a pids
@@ -143,27 +172,36 @@ _async_worker() {
143172 # process is in the middle of writing to stdin during kill.
144173 (( coproc_pid )) && read -r -k 1 -p tok
145174
146- kill -HUP -$$ # Send to entire process group.
147- coproc : # Quit coproc.
148- coproc_pid=0 # Reset pid.
175+ terminate_jobs
176+ trap child_exit CHLD # Reinstall child trap.
149177 }
150178
151179 local request do_eval=0
152180 local -a cmd
153181 while : ; do
154182 # Wait for jobs sent by async_job.
155183 read -r -d $' \0' request || {
156- # Since we handle SIGHUP above (and thus do not know when `zpty -d`)
157- # occurs, a failure to read probably indicates that stdin has
158- # closed. This is why we propagate the signal to all children and
159- # exit manually.
160- kill -HUP -$$ # Send SIGHUP to all jobs.
161- exit 0
184+ # Unknown error occurred while reading from stdin, the zpty
185+ # worker is likely in a broken state, so we shut down.
186+ terminate_jobs
187+
188+ # Stdin is broken and in case this was an unintended
189+ # crash, we try to report it as a last hurrah.
190+ print -r -n $' \0' " '[async]'" $(( 127 + 3 )) " ''" 0 " '$0 :$LINENO : zpty fd died, exiting'" $' \0'
191+
192+ # We use `return` to abort here because using `exit` may
193+ # result in an infinite loop that never exits and, as a
194+ # result, high CPU utilization.
195+ return $(( 127 + 1 ))
162196 }
163197
198+ # We need to clean the input here because sometimes when a zpty
199+ # has died and been respawned, messages will be prefixed with a
200+ # carraige return (\r, or \C-M).
201+ request=${request# $' \C-M' }
202+
164203 # Check for non-job commands sent to worker
165204 case $request in
166- _unset_trap) notify_parent=0; continue ;;
167205 _killjobs) killjobs; continue ;;
168206 _async_eval* ) do_eval=1;;
169207 esac
@@ -175,9 +213,11 @@ _async_worker() {
175213 # Name of the job (first argument).
176214 local job=$cmd [1]
177215
178- # If worker should perform unique jobs
179- if (( unique )) ; then
180- # Check if a previous job is still running, if yes, let it finnish
216+ # Check if a worker should perform unique jobs, unless
217+ # this is an eval since they run synchronously.
218+ if (( ! do_eval )) && (( unique )) ; then
219+ # Check if a previous job is still running, if yes,
220+ # skip this job and let the previous one finish.
181221 for pid in ${${(v)jobstates##*:*: } % \= * } ; do
182222 if [[ ${storage[$job]} == $pid ]]; then
183223 continue 2
@@ -317,7 +357,7 @@ _async_zle_watcher() {
317357 async_stop_worker $worker
318358
319359 if [[ -n $callback ]]; then
320- $callback ' [async]' 2 " " 0 " $worker : zle -F $1 returned error $2 " 0
360+ $callback ' [async]' 2 " " 0 " $0 : $LINENO : error: fd for $ worker failed: zle -F $1 returned error $2 " 0
321361 fi
322362 return
323363 fi ;
@@ -327,6 +367,28 @@ _async_zle_watcher() {
327367 fi
328368}
329369
370+ _async_send_job () {
371+ setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
372+
373+ local caller=$1
374+ local worker=$2
375+ shift 2
376+
377+ zpty -t $worker & > /dev/null || {
378+ typeset -gA ASYNC_CALLBACKS
379+ local callback=$ASYNC_CALLBACKS [$worker ]
380+
381+ if [[ -n $callback ]]; then
382+ $callback ' [async]' 3 " " 0 " $0 :$LINENO : error: no such worker: $worker " 0
383+ else
384+ print -u2 " $caller : no such async worker: $worker "
385+ fi
386+ return 1
387+ }
388+
389+ zpty -w $worker " $@ " $' \0'
390+ }
391+
330392#
331393# Start a new asynchronous job on specified worker, assumes the worker is running.
332394#
@@ -344,8 +406,7 @@ async_job() {
344406 cmd=(${(q)cmd} ) # Quote special characters in multi argument commands.
345407 fi
346408
347- # Quote the cmd in case RC_EXPAND_PARAM is set.
348- zpty -w $worker " $cmd " $' \0'
409+ _async_send_job $0 $worker " $cmd "
349410}
350411
351412#
@@ -369,7 +430,7 @@ async_worker_eval() {
369430 fi
370431
371432 # Quote the cmd in case RC_EXPAND_PARAM is set.
372- zpty -w $worker " _async_eval $cmd " $' \0 '
433+ _async_send_job $0 $worker " _async_eval $cmd "
373434}
374435
375436# This function traps notification signals and calls all registered callbacks
@@ -392,7 +453,7 @@ _async_notify_trap() {
392453async_register_callback () {
393454 setopt localoptions noshwordsplit nolocaltraps
394455
395- typeset -gA ASYNC_CALLBACKS
456+ typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
396457 local worker=$1 ; shift
397458
398459 ASYNC_CALLBACKS[$worker ]=" $* "
@@ -401,6 +462,14 @@ async_register_callback() {
401462 # workers to notify (via -n) when a job is done.
402463 if [[ ! -o interactive ]] || [[ ! -o zle ]]; then
403464 trap ' _async_notify_trap' WINCH
465+ elif [[ -o interactive ]] && [[ -o zle ]]; then
466+ local fd w
467+ for fd w in ${(@ kv)ASYNC_PTYS} ; do
468+ if [[ $w == $worker ]]; then
469+ zle -F $fd _async_zle_watcher # Register the ZLE handler.
470+ break
471+ fi
472+ done
404473 fi
405474}
406475
@@ -465,6 +534,8 @@ async_start_worker() {
465534 setopt localoptions noshwordsplit
466535
467536 local worker=$1 ; shift
537+ local -a args
538+ args=(" $@ " )
468539 zpty -t $worker & > /dev/null && return
469540
470541 typeset -gA ASYNC_PTYS
@@ -478,23 +549,29 @@ async_start_worker() {
478549 unsetopt xtrace
479550 }
480551
481- if (( ! ASYNC_ZPTY_RETURNS_FD )) && [[ -o interactive ]] && [[ -o zle ]]; then
482- # When zpty doesn't return a file descriptor (on older versions of zsh)
483- # we try to guess it anyway.
484- integer -l zptyfd
485- exec {zptyfd}>&1 # Open a new file descriptor (above 10).
486- exec {zptyfd}>& - # Close it so it's free to be used by zpty.
552+ if [[ -o interactive ]] && [[ -o zle ]]; then
553+ # Inform the worker to ignore the notify flag and that we're
554+ # using a ZLE watcher instead.
555+ args+=(-z)
556+
557+ if (( ! ASYNC_ZPTY_RETURNS_FD )) ; then
558+ # When zpty doesn't return a file descriptor (on older versions of zsh)
559+ # we try to guess it anyway.
560+ integer -l zptyfd
561+ exec {zptyfd}>&1 # Open a new file descriptor (above 10).
562+ exec {zptyfd}>& - # Close it so it's free to be used by zpty.
563+ fi
487564 fi
488565
489- zpty -b $worker _async_worker -p $$ $@ || {
566+ zpty -b $worker _async_worker -p $$ $args || {
490567 async_stop_worker $worker
491568 return 1
492569 }
493570
494571 # Re-enable it if it was enabled, for debugging.
495572 (( has_xtrace )) && setopt xtrace
496573
497- if [[ $ZSH_VERSION < 5.0.8 ]] ; then
574+ if ! is-at-least 5.0.8; then
498575 # For ZSH versions older than 5.0.8 we delay a bit to give
499576 # time for the worker to start before issuing commands,
500577 # otherwise it will not be ready to receive them.
@@ -506,11 +583,7 @@ async_start_worker() {
506583 REPLY=$zptyfd # Use the guessed value for the file desciptor.
507584 fi
508585
509- ASYNC_PTYS[$REPLY ]=$worker # Map the file desciptor to the worker.
510- zle -F $REPLY _async_zle_watcher # Register the ZLE handler.
511-
512- # Disable trap in favor of ZLE handler when notify is enabled (-n).
513- async_job $worker _unset_trap
586+ ASYNC_PTYS[$REPLY ]=$worker # Map the file desciptor to the worker.
514587 fi
515588}
516589
@@ -556,6 +629,9 @@ async_init() {
556629 zmodload zsh/zpty
557630 zmodload zsh/datetime
558631
632+ # Load is-at-least for reliable version check.
633+ autoload -Uz is-at-least
634+
559635 # Check if zsh/zpty returns a file descriptor or not,
560636 # shell must also be interactive with zle enabled.
561637 typeset -g ASYNC_ZPTY_RETURNS_FD=0
0 commit comments