Skip to content

Commit 3142e20

Browse files
committed
zsh-async v1.8.0
1 parent 83ec46e commit 3142e20

File tree

1 file changed

+126
-50
lines changed

1 file changed

+126
-50
lines changed

lib/async.zsh

Lines changed: 126 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
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.
1313
typeset -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() {
392453
async_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

Comments
 (0)