comparison .zfun/async @ 495:d3d52766cfcd

zsh: update async to 1.8.4
author Augie Fackler <raf@durin42.com>
date Sun, 27 Sep 2020 16:10:50 -0400
parents e84b6da69ea0
children 2ea29b487b06
comparison
equal deleted inserted replaced
494:ef22f075b638 495:d3d52766cfcd
1 #!/usr/bin/env zsh 1 #!/usr/bin/env zsh
2 2
3 # 3 #
4 # zsh-async 4 # zsh-async
5 # 5 #
6 # version: 1.1.0 6 # version: v1.8.4
7 # author: Mathias Fredriksson 7 # author: Mathias Fredriksson
8 # url: https://github.com/mafredri/zsh-async 8 # url: https://github.com/mafredri/zsh-async
9 # 9 #
10 10
11 typeset -g ASYNC_VERSION=1.8.4
12 # Produce debug output from zsh-async when set to 1.
13 typeset -g ASYNC_DEBUG=${ASYNC_DEBUG:-0}
14
15 # Execute commands that can manipulate the environment inside the async worker. Return output via callback.
16 _async_eval() {
17 local ASYNC_JOB_NAME
18 # Rename job to _async_eval and redirect all eval output to cat running
19 # in _async_job. Here, stdout and stderr are not separated for
20 # simplicity, this could be improved in the future.
21 {
22 eval "$@"
23 } &> >(ASYNC_JOB_NAME=[async/eval] _async_job 'cat')
24 }
25
11 # Wrapper for jobs executed by the async worker, gives output in parseable format with execution time 26 # Wrapper for jobs executed by the async worker, gives output in parseable format with execution time
12 _async_job() { 27 _async_job() {
13 # Store start time as double precision (+E disables scientific notation) 28 # Disable xtrace as it would mangle the output.
29 setopt localoptions noxtrace
30
31 # Store start time for job.
14 float -F duration=$EPOCHREALTIME 32 float -F duration=$EPOCHREALTIME
15 33
16 # Run the command 34 # Run the command and capture both stdout (`eval`) and stderr (`cat`) in
17 # 35 # separate subshells. When the command is complete, we grab write lock
18 # What is happening here is that we are assigning stdout, stderr and ret to 36 # (mutex token) and output everything except stderr inside the command
19 # variables, and then we are printing out the variable assignment through 37 # block, after the command block has completed, the stdin for `cat` is
20 # typeset -p. This way when we run eval we get something along the lines of: 38 # closed, causing stderr to be appended with a $'\0' at the end to mark the
21 # eval " 39 # end of output from this job.
22 # typeset stdout=' M async.test.sh\n M async.zsh' 40 local jobname=${ASYNC_JOB_NAME:-$1} out
23 # typeset ret=0 41 out="$(
24 # typeset stderr='' 42 local stdout stderr ret tok
25 # "
26 unset stdout stderr ret
27 eval "$(
28 { 43 {
29 stdout=$(eval "$@") 44 stdout=$(eval "$@")
30 ret=$? 45 ret=$?
31 typeset -p stdout ret 46 duration=$(( EPOCHREALTIME - duration )) # Calculate duration.
32 } 2> >(stderr=$(cat); typeset -p stderr) 47
48 print -r -n - $'\0'${(q)jobname} $ret ${(q)stdout} $duration
49 } 2> >(stderr=$(cat) && print -r -n - " "${(q)stderr}$'\0')
33 )" 50 )"
34 51 if [[ $out != $'\0'*$'\0' ]]; then
35 # Calculate duration 52 # Corrupted output (aborted job?), skipping.
36 duration=$(( EPOCHREALTIME - duration )) 53 return
37 54 fi
38 # stip all null-characters from stdout and stderr 55
39 stdout=${stdout//$'\0'/} 56 # Grab mutex lock, stalls until token is available.
40 stderr=${stderr//$'\0'/} 57 read -r -k 1 -p tok || return 1
41 58
42 # if ret is missing for some unknown reason, set it to -1 to indicate we 59 # Return output (<job_name> <return_code> <stdout> <duration> <stderr>).
43 # have run into a bug 60 print -r -n - "$out"
44 ret=${ret:--1} 61
45 62 # Unlock mutex by inserting a token.
46 # Grab mutex lock 63 print -n -p $tok
47 read -ep >/dev/null
48
49 # return output (<job_name> <return_code> <stdout> <duration> <stderr>)
50 print -r -N -n -- "$1" "$ret" "$stdout" "$duration" "$stderr"$'\0'
51
52 # Unlock mutex
53 print -p "t"
54 } 64 }
55 65
56 # The background worker manages all tasks and runs them without interfering with other processes 66 # The background worker manages all tasks and runs them without interfering with other processes
57 _async_worker() { 67 _async_worker() {
68 # Reset all options to defaults inside async worker.
69 emulate -R zsh
70
71 # Make sure monitor is unset to avoid printing the
72 # pids of child processes.
73 unsetopt monitor
74
75 # Redirect stderr to `/dev/null` in case unforseen errors produced by the
76 # worker. For example: `fork failed: resource temporarily unavailable`.
77 # Some older versions of zsh might also print malloc errors (know to happen
78 # on at least zsh 5.0.2 and 5.0.8) likely due to kill signals.
79 exec 2>/dev/null
80
81 # When a zpty is deleted (using -d) all the zpty instances created before
82 # the one being deleted receive a SIGHUP, unless we catch it, the async
83 # worker would simply exit (stop working) even though visible in the list
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
91
58 local -A storage 92 local -A storage
59 local unique=0 93 local unique=0
60 94 local notify_parent=0
61 # Process option parameters passed to worker 95 local parent_pid=0
62 while getopts "np:u" opt; do 96 local coproc_pid=0
97 local processing=0
98
99 local -a zsh_hooks zsh_hook_functions
100 zsh_hooks=(chpwd periodic precmd preexec zshexit zshaddhistory)
101 zsh_hook_functions=(${^zsh_hooks}_functions)
102 unfunction $zsh_hooks &>/dev/null # Deactivate all zsh hooks inside the worker.
103 unset $zsh_hook_functions # And hooks with registered functions.
104 unset zsh_hooks zsh_hook_functions # Cleanup.
105
106 close_idle_coproc() {
107 local -a pids
108 pids=(${${(v)jobstates##*:*:}%\=*})
109
110 # If coproc (cat) is the only child running, we close it to avoid
111 # leaving it running indefinitely and cluttering the process tree.
112 if (( ! processing )) && [[ $#pids = 1 ]] && [[ $coproc_pid = $pids[1] ]]; then
113 coproc :
114 coproc_pid=0
115 fi
116 }
117
118 child_exit() {
119 close_idle_coproc
120
121 # On older version of zsh (pre 5.2) we notify the parent through a
122 # SIGWINCH signal because `zpty` did not return a file descriptor (fd)
123 # prior to that.
124 if (( notify_parent )); then
125 # We use SIGWINCH for compatibility with older versions of zsh
126 # (pre 5.1.1) where other signals (INFO, ALRM, USR1, etc.) could
127 # cause a deadlock in the shell under certain circumstances.
128 kill -WINCH $parent_pid
129 fi
130 }
131
132 # Register a SIGCHLD trap to handle the completion of child processes.
133 trap child_exit CHLD
134
135 # Process option parameters passed to worker.
136 while getopts "np:uz" opt; do
63 case $opt in 137 case $opt in
64 # Use SIGWINCH since many others seem to cause zsh to freeze, e.g. ALRM, INFO, etc. 138 n) notify_parent=1;;
65 n) trap 'kill -WINCH $ASYNC_WORKER_PARENT_PID' CHLD;; 139 p) parent_pid=$OPTARG;;
66 p) ASYNC_WORKER_PARENT_PID=$OPTARG;;
67 u) unique=1;; 140 u) unique=1;;
141 z) notify_parent=0;; # Uses ZLE watcher instead.
68 esac 142 esac
69 done 143 done
70 144
71 # Create a mutex for writing to the terminal through coproc 145 # Terminate all running jobs, note that this function does not
72 coproc cat 146 # reinstall the child trap.
73 # Insert token into coproc 147 terminate_jobs() {
74 print -p "t" 148 trap - CHLD # Ignore child exits during kill.
75 149 coproc : # Quit coproc.
76 while read -r cmd; do 150 coproc_pid=0 # Reset pid.
77 # Separate on spaces into an array 151
78 cmd=(${=cmd}) 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
162 killjobs() {
163 local tok
164 local -a pids
165 pids=(${${(v)jobstates##*:*:}%\=*})
166
167 # No need to send SIGHUP if no jobs are running.
168 (( $#pids == 0 )) && continue
169 (( $#pids == 1 )) && [[ $coproc_pid = $pids[1] ]] && continue
170
171 # Grab lock to prevent half-written output in case a child
172 # process is in the middle of writing to stdin during kill.
173 (( coproc_pid )) && read -r -k 1 -p tok
174
175 terminate_jobs
176 trap child_exit CHLD # Reinstall child trap.
177 }
178
179 local request do_eval=0
180 local -a cmd
181 while :; do
182 # Wait for jobs sent by async_job.
183 read -r -d $'\0' request || {
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 ))
196 }
197
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
203 # Check for non-job commands sent to worker
204 case $request in
205 _killjobs) killjobs; continue;;
206 _async_eval*) do_eval=1;;
207 esac
208
209 # Parse the request using shell parsing (z) to allow commands
210 # to be parsed from single strings and multi-args alike.
211 cmd=("${(z)request}")
212
213 # Name of the job (first argument).
79 local job=$cmd[1] 214 local job=$cmd[1]
80 215
81 # Check for non-job commands sent to worker 216 # Check if a worker should perform unique jobs, unless
82 case $job in 217 # this is an eval since they run synchronously.
83 _unset_trap) 218 if (( !do_eval )) && (( unique )); then
84 trap - CHLD; continue;; 219 # Check if a previous job is still running, if yes,
85 _killjobs) 220 # skip this job and let the previous one finish.
86 # Do nothing in the worker when receiving the TERM signal
87 trap '' TERM
88 # Send TERM to the entire process group (PID and all children)
89 kill -TERM -$$ &>/dev/null
90 # Reset trap
91 trap - TERM
92 continue
93 ;;
94 esac
95
96 # If worker should perform unique jobs
97 if (( unique )); then
98 # Check if a previous job is still running, if yes, let it finnish
99 for pid in ${${(v)jobstates##*:*:}%\=*}; do 221 for pid in ${${(v)jobstates##*:*:}%\=*}; do
100 if [[ ${storage[$job]} == $pid ]]; then 222 if [[ ${storage[$job]} == $pid ]]; then
101 continue 2 223 continue 2
102 fi 224 fi
103 done 225 done
104 fi 226 fi
105 227
106 # Run task in background 228 # Guard against closing coproc from trap before command has started.
107 _async_job $cmd & 229 processing=1
108 # Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')... 230
109 storage[$job]=$! 231 # Because we close the coproc after the last job has completed, we must
232 # recreate it when there are no other jobs running.
233 if (( ! coproc_pid )); then
234 # Use coproc as a mutex for synchronized output between children.
235 coproc cat
236 coproc_pid="$!"
237 # Insert token into coproc
238 print -n -p "t"
239 fi
240
241 if (( do_eval )); then
242 shift cmd # Strip _async_eval from cmd.
243 _async_eval $cmd
244 else
245 # Run job in background, completed jobs are printed to stdout.
246 _async_job $cmd &
247 # Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')...
248 storage[$job]="$!"
249 fi
250
251 processing=0 # Disable guard.
252
253 if (( do_eval )); then
254 do_eval=0
255
256 # When there are no active jobs we can't rely on the CHLD trap to
257 # manage the coproc lifetime.
258 close_idle_coproc
259 fi
110 done 260 done
111 } 261 }
112 262
113 # 263 #
114 # Get results from finnished jobs and pass it to the to callback function. This is the only way to reliably return the 264 # Get results from finished jobs and pass it to the to callback function. This is the only way to reliably return the
115 # job name, return code, output and execution time and with minimal effort. 265 # job name, return code, output and execution time and with minimal effort.
266 #
267 # If the async process buffer becomes corrupt, the callback will be invoked with the first argument being `[async]` (job
268 # name), non-zero return code and fifth argument describing the error (stderr).
116 # 269 #
117 # usage: 270 # usage:
118 # async_process_results <worker_name> <callback_function> 271 # async_process_results <worker_name> <callback_function>
119 # 272 #
120 # callback_function is called with the following parameters: 273 # callback_function is called with the following parameters:
121 # $1 = job name, e.g. the function passed to async_job 274 # $1 = job name, e.g. the function passed to async_job
122 # $2 = return code 275 # $2 = return code
123 # $3 = resulting stdout from execution 276 # $3 = resulting stdout from execution
124 # $4 = execution time, floating point e.g. 2.05 seconds 277 # $4 = execution time, floating point e.g. 2.05 seconds
125 # $5 = resulting stderr from execution 278 # $5 = resulting stderr from execution
279 # $6 = has next result in buffer (0 = buffer empty, 1 = yes)
126 # 280 #
127 async_process_results() { 281 async_process_results() {
128 setopt localoptions noshwordsplit 282 setopt localoptions unset noshwordsplit noksharrays noposixidentifiers noposixstrings
129 283
130 integer count=0
131 local worker=$1 284 local worker=$1
132 local callback=$2 285 local callback=$2
286 local caller=$3
133 local -a items 287 local -a items
134 local IFS=$'\0' 288 local null=$'\0' data
289 integer -l len pos num_processed has_next
135 290
136 typeset -gA ASYNC_PROCESS_BUFFER 291 typeset -gA ASYNC_PROCESS_BUFFER
137 # Read output from zpty and parse it if available 292
138 while zpty -rt $worker line 2>/dev/null; do 293 # Read output from zpty and parse it if available.
139 # Remove unwanted \r from output 294 while zpty -r -t $worker data 2>/dev/null; do
140 ASYNC_PROCESS_BUFFER[$worker]+=${line//$'\r'$'\n'/$'\n'} 295 ASYNC_PROCESS_BUFFER[$worker]+=$data
141 # Split buffer on null characters, preserve empty elements 296 len=${#ASYNC_PROCESS_BUFFER[$worker]}
142 items=("${(@)=ASYNC_PROCESS_BUFFER[$worker]}") 297 pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
143 # Remove last element since it's due to the return string separator structure 298
144 items=("${(@)items[1,${#items}-1]}") 299 # Keep going until we find a NULL-character.
145 300 if (( ! len )) || (( pos > len )); then
146 # Continue until we receive all information 301 continue
147 (( ${#items} % 5 )) && continue 302 fi
148 303
149 # Work through all results 304 while (( pos <= len )); do
150 while (( ${#items} > 0 )); do 305 # Take the content from the beginning, until the NULL-character and
151 $callback "${(@)=items[1,5]}" 306 # perform shell parsing (z) and unquoting (Q) as an array (@).
152 shift 5 items 307 items=("${(@Q)${(z)ASYNC_PROCESS_BUFFER[$worker][1,$pos-1]}}")
153 count+=1 308
309 # Remove the extracted items from the buffer.
310 ASYNC_PROCESS_BUFFER[$worker]=${ASYNC_PROCESS_BUFFER[$worker][$pos+1,$len]}
311
312 len=${#ASYNC_PROCESS_BUFFER[$worker]}
313 if (( len > 1 )); then
314 pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
315 fi
316
317 has_next=$(( len != 0 ))
318 if (( $#items == 5 )); then
319 items+=($has_next)
320 $callback "${(@)items}" # Send all parsed items to the callback.
321 (( num_processed++ ))
322 elif [[ -z $items ]]; then
323 # Empty items occur between results due to double-null ($'\0\0')
324 # caused by commands being both pre and suffixed with null.
325 else
326 # In case of corrupt data, invoke callback with *async* as job
327 # name, non-zero exit status and an error message on stderr.
328 $callback "[async]" 1 "" 0 "$0:$LINENO: error: bad format, got ${#items} items (${(q)items})" $has_next
329 fi
154 done 330 done
155
156 # Empty the buffer
157 unset "ASYNC_PROCESS_BUFFER[$worker]"
158 done 331 done
159 332
160 # If we processed any results, return success 333 (( num_processed )) && return 0
161 (( count )) && return 0 334
335 # Avoid printing exit value when `setopt printexitvalue` is active.`
336 [[ $caller = trap || $caller = watcher ]] && return 0
162 337
163 # No results were processed 338 # No results were processed
164 return 1 339 return 1
165 } 340 }
166 341
169 setopt localoptions noshwordsplit 344 setopt localoptions noshwordsplit
170 typeset -gA ASYNC_PTYS ASYNC_CALLBACKS 345 typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
171 local worker=$ASYNC_PTYS[$1] 346 local worker=$ASYNC_PTYS[$1]
172 local callback=$ASYNC_CALLBACKS[$worker] 347 local callback=$ASYNC_CALLBACKS[$worker]
173 348
349 if [[ -n $2 ]]; then
350 # from man zshzle(1):
351 # `hup' for a disconnect, `nval' for a closed or otherwise
352 # invalid descriptor, or `err' for any other condition.
353 # Systems that support only the `select' system call always use
354 # `err'.
355
356 # this has the side effect to unregister the broken file descriptor
357 async_stop_worker $worker
358
359 if [[ -n $callback ]]; then
360 $callback '[async]' 2 "" 0 "$0:$LINENO: error: fd for $worker failed: zle -F $1 returned error $2" 0
361 fi
362 return
363 fi;
364
174 if [[ -n $callback ]]; then 365 if [[ -n $callback ]]; then
175 async_process_results $worker $callback 366 async_process_results $worker $callback watcher
176 fi 367 fi
368 }
369
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'
177 } 390 }
178 391
179 # 392 #
180 # Start a new asynchronous job on specified worker, assumes the worker is running. 393 # Start a new asynchronous job on specified worker, assumes the worker is running.
181 # 394 #
182 # usage: 395 # usage:
183 # async_job <worker_name> <my_function> [<function_params>] 396 # async_job <worker_name> <my_function> [<function_params>]
184 # 397 #
185 async_job() { 398 async_job() {
186 setopt localoptions noshwordsplit 399 setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
187 400
188 local worker=$1; shift 401 local worker=$1; shift
189 zpty -w $worker $@ 402
403 local -a cmd
404 cmd=("$@")
405 if (( $#cmd > 1 )); then
406 cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
407 fi
408
409 _async_send_job $0 $worker "$cmd"
410 }
411
412 #
413 # Evaluate a command (like async_job) inside the async worker, then worker environment can be manipulated. For example,
414 # issuing a cd command will change the PWD of the worker which will then be inherited by all future async jobs.
415 #
416 # Output will be returned via callback, job name will be [async/eval].
417 #
418 # usage:
419 # async_worker_eval <worker_name> <my_function> [<function_params>]
420 #
421 async_worker_eval() {
422 setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
423
424 local worker=$1; shift
425
426 local -a cmd
427 cmd=("$@")
428 if (( $#cmd > 1 )); then
429 cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
430 fi
431
432 # Quote the cmd in case RC_EXPAND_PARAM is set.
433 _async_send_job $0 $worker "_async_eval $cmd"
190 } 434 }
191 435
192 # This function traps notification signals and calls all registered callbacks 436 # This function traps notification signals and calls all registered callbacks
193 _async_notify_trap() { 437 _async_notify_trap() {
194 setopt localoptions noshwordsplit 438 setopt localoptions noshwordsplit
195 439
440 local k
196 for k in ${(k)ASYNC_CALLBACKS}; do 441 for k in ${(k)ASYNC_CALLBACKS}; do
197 async_process_results $k ${ASYNC_CALLBACKS[$k]} 442 async_process_results $k ${ASYNC_CALLBACKS[$k]} trap
198 done 443 done
199 } 444 }
200 445
201 # 446 #
202 # Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the 447 # Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the
206 # async_register_callback <worker_name> <callback_function> 451 # async_register_callback <worker_name> <callback_function>
207 # 452 #
208 async_register_callback() { 453 async_register_callback() {
209 setopt localoptions noshwordsplit nolocaltraps 454 setopt localoptions noshwordsplit nolocaltraps
210 455
211 typeset -gA ASYNC_CALLBACKS 456 typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
212 local worker=$1; shift 457 local worker=$1; shift
213 458
214 ASYNC_CALLBACKS[$worker]="$*" 459 ASYNC_CALLBACKS[$worker]="$*"
215 460
216 if (( ! ASYNC_USE_ZLE_HANDLER )); then 461 # Enable trap when the ZLE watcher is unavailable, allows
462 # workers to notify (via -n) when a job is done.
463 if [[ ! -o interactive ]] || [[ ! -o zle ]]; then
217 trap '_async_notify_trap' WINCH 464 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
218 fi 473 fi
219 } 474 }
220 475
221 # 476 #
222 # Unregister the callback for a specific worker. 477 # Unregister the callback for a specific worker.
244 499
245 # Check if the worker exists 500 # Check if the worker exists
246 zpty -t $worker &>/dev/null || return 1 501 zpty -t $worker &>/dev/null || return 1
247 502
248 # Send kill command to worker 503 # Send kill command to worker
249 zpty -w $worker "_killjobs" 504 async_job $worker "_killjobs"
250 505
251 # Clear all output buffers 506 # Clear the zpty buffer.
252 while zpty -r $worker line; do true; done 507 local junk
253 508 if zpty -r -t $worker junk '*'; then
254 # Clear any partial buffers 509 (( ASYNC_DEBUG )) && print -n "async_flush_jobs $worker: ${(V)junk}"
510 while zpty -r -t $worker junk '*'; do
511 (( ASYNC_DEBUG )) && print -n "${(V)junk}"
512 done
513 (( ASYNC_DEBUG )) && print
514 fi
515
516 # Finally, clear the process buffer in case of partially parsed responses.
255 typeset -gA ASYNC_PROCESS_BUFFER 517 typeset -gA ASYNC_PROCESS_BUFFER
256 unset "ASYNC_PROCESS_BUFFER[$worker]" 518 unset "ASYNC_PROCESS_BUFFER[$worker]"
257 } 519 }
258 520
259 # 521 #
267 # -u unique (only unique job names can run) 529 # -u unique (only unique job names can run)
268 # -n notify through SIGWINCH signal 530 # -n notify through SIGWINCH signal
269 # -p pid to notify (defaults to current pid) 531 # -p pid to notify (defaults to current pid)
270 # 532 #
271 async_start_worker() { 533 async_start_worker() {
272 setopt localoptions noshwordsplit 534 setopt localoptions noshwordsplit noclobber
273 535
274 local worker=$1; shift 536 local worker=$1; shift
537 local -a args
538 args=("$@")
275 zpty -t $worker &>/dev/null && return 539 zpty -t $worker &>/dev/null && return
276 540
277 typeset -gA ASYNC_PTYS 541 typeset -gA ASYNC_PTYS
278 typeset -h REPLY 542 typeset -h REPLY
279 zpty -b $worker _async_worker -p $$ $@ || { 543 typeset has_xtrace=0
544
545 if [[ -o interactive ]] && [[ -o zle ]]; then
546 # Inform the worker to ignore the notify flag and that we're
547 # using a ZLE watcher instead.
548 args+=(-z)
549
550 if (( ! ASYNC_ZPTY_RETURNS_FD )); then
551 # When zpty doesn't return a file descriptor (on older versions of zsh)
552 # we try to guess it anyway.
553 integer -l zptyfd
554 exec {zptyfd}>&1 # Open a new file descriptor (above 10).
555 exec {zptyfd}>&- # Close it so it's free to be used by zpty.
556 fi
557 fi
558
559 # Workaround for stderr in the main shell sometimes (incorrectly) being
560 # reassigned to /dev/null by the reassignment done inside the async
561 # worker.
562 # See https://github.com/mafredri/zsh-async/issues/35.
563 integer errfd=-1
564 exec {errfd}>&2
565
566 # Make sure async worker is started without xtrace
567 # (the trace output interferes with the worker).
568 [[ -o xtrace ]] && {
569 has_xtrace=1
570 unsetopt xtrace
571 }
572
573 zpty -b $worker _async_worker -p $$ $args 2>&$errfd
574 local ret=$?
575
576 # Re-enable it if it was enabled, for debugging.
577 (( has_xtrace )) && setopt xtrace
578 exec {errfd}>& -
579
580 if (( ret )); then
280 async_stop_worker $worker 581 async_stop_worker $worker
281 return 1 582 return 1
282 } 583 fi
283 584
284 if (( ASYNC_USE_ZLE_HANDLER )); then 585 if ! is-at-least 5.0.8; then
285 ASYNC_PTYS[$REPLY]=$worker 586 # For ZSH versions older than 5.0.8 we delay a bit to give
286 zle -F $REPLY _async_zle_watcher 587 # time for the worker to start before issuing commands,
287 588 # otherwise it will not be ready to receive them.
288 # If worker was called with -n, disable trap in favor of zle handler 589 sleep 0.001
289 async_job $worker _unset_trap 590 fi
591
592 if [[ -o interactive ]] && [[ -o zle ]]; then
593 if (( ! ASYNC_ZPTY_RETURNS_FD )); then
594 REPLY=$zptyfd # Use the guessed value for the file desciptor.
595 fi
596
597 ASYNC_PTYS[$REPLY]=$worker # Map the file desciptor to the worker.
290 fi 598 fi
291 } 599 }
292 600
293 # 601 #
294 # Stop one or multiple workers that are running, all unfetched and incomplete work will be lost. 602 # Stop one or multiple workers that are running, all unfetched and incomplete work will be lost.
297 # async_stop_worker <worker_name_1> [<worker_name_2>] 605 # async_stop_worker <worker_name_1> [<worker_name_2>]
298 # 606 #
299 async_stop_worker() { 607 async_stop_worker() {
300 setopt localoptions noshwordsplit 608 setopt localoptions noshwordsplit
301 609
302 local ret=0 610 local ret=0 worker k v
303 for worker in $@; do 611 for worker in $@; do
304 # Find and unregister the zle handler for the worker 612 # Find and unregister the zle handler for the worker
305 for k v in ${(@kv)ASYNC_PTYS}; do 613 for k v in ${(@kv)ASYNC_PTYS}; do
306 if [[ $v == $worker ]]; then 614 if [[ $v == $worker ]]; then
307 zle -F $k 615 zle -F $k
308 unset "ASYNC_PTYS[$k]" 616 unset "ASYNC_PTYS[$k]"
309 fi 617 fi
310 done 618 done
311 async_unregister_callback $worker 619 async_unregister_callback $worker
312 zpty -d $worker 2>/dev/null || ret=$? 620 zpty -d $worker 2>/dev/null || ret=$?
621
622 # Clear any partial buffers.
623 typeset -gA ASYNC_PROCESS_BUFFER
624 unset "ASYNC_PROCESS_BUFFER[$worker]"
313 done 625 done
314 626
315 return $ret 627 return $ret
316 } 628 }
317 629
321 # usage: 633 # usage:
322 # async_init 634 # async_init
323 # 635 #
324 async_init() { 636 async_init() {
325 (( ASYNC_INIT_DONE )) && return 637 (( ASYNC_INIT_DONE )) && return
326 ASYNC_INIT_DONE=1 638 typeset -g ASYNC_INIT_DONE=1
327 639
328 zmodload zsh/zpty 640 zmodload zsh/zpty
329 zmodload zsh/datetime 641 zmodload zsh/datetime
330 642
331 # Check if zsh/zpty returns a file descriptor or not, shell must also be interactive 643 # Load is-at-least for reliable version check.
332 ASYNC_USE_ZLE_HANDLER=0 644 autoload -Uz is-at-least
333 [[ -o interactive ]] && { 645
646 # Check if zsh/zpty returns a file descriptor or not,
647 # shell must also be interactive with zle enabled.
648 typeset -g ASYNC_ZPTY_RETURNS_FD=0
649 [[ -o interactive ]] && [[ -o zle ]] && {
334 typeset -h REPLY 650 typeset -h REPLY
335 zpty _async_test cat 651 zpty _async_test :
336 (( REPLY )) && ASYNC_USE_ZLE_HANDLER=1 652 (( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1
337 zpty -d _async_test 653 zpty -d _async_test
338 } 654 }
339 } 655 }
340 656
341 async() { 657 async() {