381
|
1 #!/usr/bin/env zsh |
|
2 |
|
3 # |
|
4 # zsh-async |
|
5 # |
497
|
6 # version: v1.8.5 |
381
|
7 # author: Mathias Fredriksson |
|
8 # url: https://github.com/mafredri/zsh-async |
|
9 # |
|
10 |
497
|
11 typeset -g ASYNC_VERSION=1.8.5 |
495
|
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 |
381
|
26 # Wrapper for jobs executed by the async worker, gives output in parseable format with execution time |
|
27 _async_job() { |
495
|
28 # Disable xtrace as it would mangle the output. |
|
29 setopt localoptions noxtrace |
|
30 |
|
31 # Store start time for job. |
381
|
32 float -F duration=$EPOCHREALTIME |
|
33 |
495
|
34 # Run the command and capture both stdout (`eval`) and stderr (`cat`) in |
|
35 # separate subshells. When the command is complete, we grab write lock |
|
36 # (mutex token) and output everything except stderr inside the command |
|
37 # block, after the command block has completed, the stdin for `cat` is |
|
38 # closed, causing stderr to be appended with a $'\0' at the end to mark the |
|
39 # end of output from this job. |
|
40 local jobname=${ASYNC_JOB_NAME:-$1} out |
|
41 out="$( |
|
42 local stdout stderr ret tok |
381
|
43 { |
|
44 stdout=$(eval "$@") |
|
45 ret=$? |
495
|
46 duration=$(( EPOCHREALTIME - duration )) # Calculate duration. |
381
|
47 |
495
|
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 |
381
|
55 |
495
|
56 # Grab mutex lock, stalls until token is available. |
|
57 read -r -k 1 -p tok || return 1 |
381
|
58 |
495
|
59 # Return output (<job_name> <return_code> <stdout> <duration> <stderr>). |
|
60 print -r -n - "$out" |
381
|
61 |
495
|
62 # Unlock mutex by inserting a token. |
|
63 print -n -p $tok |
381
|
64 } |
|
65 |
|
66 # The background worker manages all tasks and runs them without interfering with other processes |
|
67 _async_worker() { |
495
|
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 |
381
|
92 local -A storage |
|
93 local unique=0 |
495
|
94 local notify_parent=0 |
|
95 local parent_pid=0 |
|
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 } |
381
|
131 |
495
|
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 |
381
|
137 case $opt in |
495
|
138 n) notify_parent=1;; |
|
139 p) parent_pid=$OPTARG;; |
381
|
140 u) unique=1;; |
495
|
141 z) notify_parent=0;; # Uses ZLE watcher instead. |
381
|
142 esac |
|
143 done |
|
144 |
495
|
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 } |
381
|
161 |
495
|
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'} |
381
|
202 |
|
203 # Check for non-job commands sent to worker |
495
|
204 case $request in |
|
205 _killjobs) killjobs; continue;; |
|
206 _async_eval*) do_eval=1;; |
381
|
207 esac |
|
208 |
495
|
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). |
|
214 local job=$cmd[1] |
|
215 |
|
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. |
381
|
221 for pid in ${${(v)jobstates##*:*:}%\=*}; do |
|
222 if [[ ${storage[$job]} == $pid ]]; then |
|
223 continue 2 |
|
224 fi |
|
225 done |
|
226 fi |
|
227 |
495
|
228 # Guard against closing coproc from trap before command has started. |
|
229 processing=1 |
|
230 |
|
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 |
381
|
260 done |
|
261 } |
|
262 |
|
263 # |
495
|
264 # Get results from finished jobs and pass it to the to callback function. This is the only way to reliably return the |
|
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). |
381
|
269 # |
|
270 # usage: |
|
271 # async_process_results <worker_name> <callback_function> |
|
272 # |
|
273 # callback_function is called with the following parameters: |
|
274 # $1 = job name, e.g. the function passed to async_job |
|
275 # $2 = return code |
|
276 # $3 = resulting stdout from execution |
|
277 # $4 = execution time, floating point e.g. 2.05 seconds |
|
278 # $5 = resulting stderr from execution |
495
|
279 # $6 = has next result in buffer (0 = buffer empty, 1 = yes) |
381
|
280 # |
|
281 async_process_results() { |
495
|
282 setopt localoptions unset noshwordsplit noksharrays noposixidentifiers noposixstrings |
381
|
283 |
|
284 local worker=$1 |
|
285 local callback=$2 |
495
|
286 local caller=$3 |
381
|
287 local -a items |
495
|
288 local null=$'\0' data |
|
289 integer -l len pos num_processed has_next |
381
|
290 |
|
291 typeset -gA ASYNC_PROCESS_BUFFER |
|
292 |
495
|
293 # Read output from zpty and parse it if available. |
|
294 while zpty -r -t $worker data 2>/dev/null; do |
|
295 ASYNC_PROCESS_BUFFER[$worker]+=$data |
|
296 len=${#ASYNC_PROCESS_BUFFER[$worker]} |
|
297 pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter). |
|
298 |
|
299 # Keep going until we find a NULL-character. |
|
300 if (( ! len )) || (( pos > len )); then |
|
301 continue |
|
302 fi |
|
303 |
|
304 while (( pos <= len )); do |
|
305 # Take the content from the beginning, until the NULL-character and |
|
306 # perform shell parsing (z) and unquoting (Q) as an array (@). |
|
307 items=("${(@Q)${(z)ASYNC_PROCESS_BUFFER[$worker][1,$pos-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 |
|
330 done |
381
|
331 done |
|
332 |
495
|
333 (( num_processed )) && return 0 |
|
334 |
|
335 # Avoid printing exit value when `setopt printexitvalue` is active.` |
|
336 [[ $caller = trap || $caller = watcher ]] && return 0 |
381
|
337 |
|
338 # No results were processed |
|
339 return 1 |
|
340 } |
|
341 |
|
342 # Watch worker for output |
|
343 _async_zle_watcher() { |
|
344 setopt localoptions noshwordsplit |
|
345 typeset -gA ASYNC_PTYS ASYNC_CALLBACKS |
|
346 local worker=$ASYNC_PTYS[$1] |
|
347 local callback=$ASYNC_CALLBACKS[$worker] |
|
348 |
495
|
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 |
381
|
365 if [[ -n $callback ]]; then |
495
|
366 async_process_results $worker $callback watcher |
381
|
367 fi |
|
368 } |
|
369 |
495
|
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 |
381
|
392 # |
|
393 # Start a new asynchronous job on specified worker, assumes the worker is running. |
|
394 # |
|
395 # usage: |
|
396 # async_job <worker_name> <my_function> [<function_params>] |
|
397 # |
|
398 async_job() { |
495
|
399 setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings |
381
|
400 |
|
401 local worker=$1; shift |
495
|
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" |
381
|
434 } |
|
435 |
|
436 # This function traps notification signals and calls all registered callbacks |
|
437 _async_notify_trap() { |
|
438 setopt localoptions noshwordsplit |
|
439 |
495
|
440 local k |
381
|
441 for k in ${(k)ASYNC_CALLBACKS}; do |
495
|
442 async_process_results $k ${ASYNC_CALLBACKS[$k]} trap |
381
|
443 done |
|
444 } |
|
445 |
|
446 # |
|
447 # Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the |
|
448 # specified callback function. This requires that a worker is initialized with the -n (notify) option. |
|
449 # |
|
450 # usage: |
|
451 # async_register_callback <worker_name> <callback_function> |
|
452 # |
|
453 async_register_callback() { |
|
454 setopt localoptions noshwordsplit nolocaltraps |
|
455 |
495
|
456 typeset -gA ASYNC_PTYS ASYNC_CALLBACKS |
381
|
457 local worker=$1; shift |
|
458 |
|
459 ASYNC_CALLBACKS[$worker]="$*" |
|
460 |
495
|
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 |
381
|
464 trap '_async_notify_trap' WINCH |
495
|
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 |
381
|
473 fi |
|
474 } |
|
475 |
|
476 # |
|
477 # Unregister the callback for a specific worker. |
|
478 # |
|
479 # usage: |
|
480 # async_unregister_callback <worker_name> |
|
481 # |
|
482 async_unregister_callback() { |
|
483 typeset -gA ASYNC_CALLBACKS |
|
484 |
|
485 unset "ASYNC_CALLBACKS[$1]" |
|
486 } |
|
487 |
|
488 # |
|
489 # Flush all current jobs running on a worker. This will terminate any and all running processes under the worker, use |
|
490 # with caution. |
|
491 # |
|
492 # usage: |
|
493 # async_flush_jobs <worker_name> |
|
494 # |
|
495 async_flush_jobs() { |
|
496 setopt localoptions noshwordsplit |
|
497 |
|
498 local worker=$1; shift |
|
499 |
|
500 # Check if the worker exists |
|
501 zpty -t $worker &>/dev/null || return 1 |
|
502 |
|
503 # Send kill command to worker |
495
|
504 async_job $worker "_killjobs" |
|
505 |
|
506 # Clear the zpty buffer. |
|
507 local junk |
|
508 if zpty -r -t $worker junk '*'; then |
|
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 |
381
|
515 |
495
|
516 # Finally, clear the process buffer in case of partially parsed responses. |
381
|
517 typeset -gA ASYNC_PROCESS_BUFFER |
|
518 unset "ASYNC_PROCESS_BUFFER[$worker]" |
|
519 } |
|
520 |
|
521 # |
|
522 # Start a new async worker with optional parameters, a worker can be told to only run unique tasks and to notify a |
|
523 # process when tasks are complete. |
|
524 # |
|
525 # usage: |
|
526 # async_start_worker <worker_name> [-u] [-n] [-p <pid>] |
|
527 # |
|
528 # opts: |
|
529 # -u unique (only unique job names can run) |
|
530 # -n notify through SIGWINCH signal |
|
531 # -p pid to notify (defaults to current pid) |
|
532 # |
|
533 async_start_worker() { |
495
|
534 setopt localoptions noshwordsplit noclobber |
381
|
535 |
|
536 local worker=$1; shift |
495
|
537 local -a args |
|
538 args=("$@") |
381
|
539 zpty -t $worker &>/dev/null && return |
|
540 |
|
541 typeset -gA ASYNC_PTYS |
|
542 typeset -h REPLY |
495
|
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 |
497
|
564 |
|
565 # Redirect of errfd is broken on zsh 5.0.2. |
|
566 if is-at-least 5.0.8; then |
|
567 exec {errfd}>&2 |
|
568 fi |
495
|
569 |
|
570 # Make sure async worker is started without xtrace |
|
571 # (the trace output interferes with the worker). |
|
572 [[ -o xtrace ]] && { |
|
573 has_xtrace=1 |
|
574 unsetopt xtrace |
|
575 } |
|
576 |
497
|
577 if (( errfd != -1 )); then |
|
578 zpty -b $worker _async_worker -p $$ $args 2>&$errfd |
|
579 else |
|
580 zpty -b $worker _async_worker -p $$ $args |
|
581 fi |
495
|
582 local ret=$? |
|
583 |
|
584 # Re-enable it if it was enabled, for debugging. |
|
585 (( has_xtrace )) && setopt xtrace |
497
|
586 (( errfd != -1 )) && exec {errfd}>& - |
495
|
587 |
|
588 if (( ret )); then |
381
|
589 async_stop_worker $worker |
|
590 return 1 |
495
|
591 fi |
381
|
592 |
495
|
593 if ! is-at-least 5.0.8; then |
|
594 # For ZSH versions older than 5.0.8 we delay a bit to give |
|
595 # time for the worker to start before issuing commands, |
|
596 # otherwise it will not be ready to receive them. |
|
597 sleep 0.001 |
|
598 fi |
|
599 |
|
600 if [[ -o interactive ]] && [[ -o zle ]]; then |
|
601 if (( ! ASYNC_ZPTY_RETURNS_FD )); then |
|
602 REPLY=$zptyfd # Use the guessed value for the file desciptor. |
|
603 fi |
381
|
604 |
495
|
605 ASYNC_PTYS[$REPLY]=$worker # Map the file desciptor to the worker. |
381
|
606 fi |
|
607 } |
|
608 |
|
609 # |
|
610 # Stop one or multiple workers that are running, all unfetched and incomplete work will be lost. |
|
611 # |
|
612 # usage: |
|
613 # async_stop_worker <worker_name_1> [<worker_name_2>] |
|
614 # |
|
615 async_stop_worker() { |
|
616 setopt localoptions noshwordsplit |
|
617 |
495
|
618 local ret=0 worker k v |
381
|
619 for worker in $@; do |
|
620 # Find and unregister the zle handler for the worker |
|
621 for k v in ${(@kv)ASYNC_PTYS}; do |
|
622 if [[ $v == $worker ]]; then |
|
623 zle -F $k |
|
624 unset "ASYNC_PTYS[$k]" |
|
625 fi |
|
626 done |
|
627 async_unregister_callback $worker |
|
628 zpty -d $worker 2>/dev/null || ret=$? |
495
|
629 |
|
630 # Clear any partial buffers. |
|
631 typeset -gA ASYNC_PROCESS_BUFFER |
|
632 unset "ASYNC_PROCESS_BUFFER[$worker]" |
381
|
633 done |
|
634 |
|
635 return $ret |
|
636 } |
|
637 |
|
638 # |
|
639 # Initialize the required modules for zsh-async. To be called before using the zsh-async library. |
|
640 # |
|
641 # usage: |
|
642 # async_init |
|
643 # |
|
644 async_init() { |
|
645 (( ASYNC_INIT_DONE )) && return |
495
|
646 typeset -g ASYNC_INIT_DONE=1 |
381
|
647 |
|
648 zmodload zsh/zpty |
|
649 zmodload zsh/datetime |
|
650 |
495
|
651 # Load is-at-least for reliable version check. |
|
652 autoload -Uz is-at-least |
|
653 |
|
654 # Check if zsh/zpty returns a file descriptor or not, |
|
655 # shell must also be interactive with zle enabled. |
|
656 typeset -g ASYNC_ZPTY_RETURNS_FD=0 |
|
657 [[ -o interactive ]] && [[ -o zle ]] && { |
381
|
658 typeset -h REPLY |
495
|
659 zpty _async_test : |
|
660 (( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1 |
381
|
661 zpty -d _async_test |
|
662 } |
|
663 } |
|
664 |
|
665 async() { |
|
666 async_init |
|
667 } |
|
668 |
|
669 async "$@" |