Mercurial > dotfiles
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() { |
