async.zsh 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. #!/usr/bin/env zsh
  2. #
  3. # zsh-async
  4. #
  5. # version: v1.8.6
  6. # author: Mathias Fredriksson
  7. # url: https://github.com/mafredri/zsh-async
  8. #
  9. typeset -g ASYNC_VERSION=1.8.6
  10. # Produce debug output from zsh-async when set to 1.
  11. typeset -g ASYNC_DEBUG=${ASYNC_DEBUG:-0}
  12. # Execute commands that can manipulate the environment inside the async worker. Return output via callback.
  13. _async_eval() {
  14. local ASYNC_JOB_NAME
  15. # Rename job to _async_eval and redirect all eval output to cat running
  16. # in _async_job. Here, stdout and stderr are not separated for
  17. # simplicity, this could be improved in the future.
  18. {
  19. eval "$@"
  20. } &> >(ASYNC_JOB_NAME=[async/eval] _async_job 'command -p cat')
  21. }
  22. # Wrapper for jobs executed by the async worker, gives output in parseable format with execution time
  23. _async_job() {
  24. # Disable xtrace as it would mangle the output.
  25. setopt localoptions noxtrace
  26. # Store start time for job.
  27. float -F duration=$EPOCHREALTIME
  28. # Run the command and capture both stdout (`eval`) and stderr (`cat`) in
  29. # separate subshells. When the command is complete, we grab write lock
  30. # (mutex token) and output everything except stderr inside the command
  31. # block, after the command block has completed, the stdin for `cat` is
  32. # closed, causing stderr to be appended with a $'\0' at the end to mark the
  33. # end of output from this job.
  34. local jobname=${ASYNC_JOB_NAME:-$1} out
  35. out="$(
  36. local stdout stderr ret tok
  37. {
  38. stdout=$(eval "$@")
  39. ret=$?
  40. duration=$(( EPOCHREALTIME - duration )) # Calculate duration.
  41. print -r -n - $'\0'${(q)jobname} $ret ${(q)stdout} $duration
  42. } 2> >(stderr=$(command -p cat) && print -r -n - " "${(q)stderr}$'\0')
  43. )"
  44. if [[ $out != $'\0'*$'\0' ]]; then
  45. # Corrupted output (aborted job?), skipping.
  46. return
  47. fi
  48. # Grab mutex lock, stalls until token is available.
  49. read -r -k 1 -p tok || return 1
  50. # Return output (<job_name> <return_code> <stdout> <duration> <stderr>).
  51. print -r -n - "$out"
  52. # Unlock mutex by inserting a token.
  53. print -n -p $tok
  54. }
  55. # The background worker manages all tasks and runs them without interfering with other processes
  56. _async_worker() {
  57. # Reset all options to defaults inside async worker.
  58. emulate -R zsh
  59. # Make sure monitor is unset to avoid printing the
  60. # pids of child processes.
  61. unsetopt monitor
  62. # Redirect stderr to `/dev/null` in case unforseen errors produced by the
  63. # worker. For example: `fork failed: resource temporarily unavailable`.
  64. # Some older versions of zsh might also print malloc errors (know to happen
  65. # on at least zsh 5.0.2 and 5.0.8) likely due to kill signals.
  66. exec 2>/dev/null
  67. # When a zpty is deleted (using -d) all the zpty instances created before
  68. # the one being deleted receive a SIGHUP, unless we catch it, the async
  69. # worker would simply exit (stop working) even though visible in the list
  70. # of zpty's (zpty -L). This has been fixed around the time of Zsh 5.4
  71. # (not released).
  72. if ! is-at-least 5.4.1; then
  73. TRAPHUP() {
  74. return 0 # Return 0, indicating signal was handled.
  75. }
  76. fi
  77. local -A storage
  78. local unique=0
  79. local notify_parent=0
  80. local parent_pid=0
  81. local coproc_pid=0
  82. local processing=0
  83. local -a zsh_hooks zsh_hook_functions
  84. zsh_hooks=(chpwd periodic precmd preexec zshexit zshaddhistory)
  85. zsh_hook_functions=(${^zsh_hooks}_functions)
  86. unfunction $zsh_hooks &>/dev/null # Deactivate all zsh hooks inside the worker.
  87. unset $zsh_hook_functions # And hooks with registered functions.
  88. unset zsh_hooks zsh_hook_functions # Cleanup.
  89. close_idle_coproc() {
  90. local -a pids
  91. pids=(${${(v)jobstates##*:*:}%\=*})
  92. # If coproc (cat) is the only child running, we close it to avoid
  93. # leaving it running indefinitely and cluttering the process tree.
  94. if (( ! processing )) && [[ $#pids = 1 ]] && [[ $coproc_pid = $pids[1] ]]; then
  95. coproc :
  96. coproc_pid=0
  97. fi
  98. }
  99. child_exit() {
  100. close_idle_coproc
  101. # On older version of zsh (pre 5.2) we notify the parent through a
  102. # SIGWINCH signal because `zpty` did not return a file descriptor (fd)
  103. # prior to that.
  104. if (( notify_parent )); then
  105. # We use SIGWINCH for compatibility with older versions of zsh
  106. # (pre 5.1.1) where other signals (INFO, ALRM, USR1, etc.) could
  107. # cause a deadlock in the shell under certain circumstances.
  108. kill -WINCH $parent_pid
  109. fi
  110. }
  111. # Register a SIGCHLD trap to handle the completion of child processes.
  112. trap child_exit CHLD
  113. # Process option parameters passed to worker.
  114. while getopts "np:uz" opt; do
  115. case $opt in
  116. n) notify_parent=1;;
  117. p) parent_pid=$OPTARG;;
  118. u) unique=1;;
  119. z) notify_parent=0;; # Uses ZLE watcher instead.
  120. esac
  121. done
  122. # Terminate all running jobs, note that this function does not
  123. # reinstall the child trap.
  124. terminate_jobs() {
  125. trap - CHLD # Ignore child exits during kill.
  126. coproc : # Quit coproc.
  127. coproc_pid=0 # Reset pid.
  128. if is-at-least 5.4.1; then
  129. trap '' HUP # Catch the HUP sent to this process.
  130. kill -HUP -$$ # Send to entire process group.
  131. trap - HUP # Disable HUP trap.
  132. else
  133. # We already handle HUP for Zsh < 5.4.1.
  134. kill -HUP -$$ # Send to entire process group.
  135. fi
  136. }
  137. killjobs() {
  138. local tok
  139. local -a pids
  140. pids=(${${(v)jobstates##*:*:}%\=*})
  141. # No need to send SIGHUP if no jobs are running.
  142. (( $#pids == 0 )) && continue
  143. (( $#pids == 1 )) && [[ $coproc_pid = $pids[1] ]] && continue
  144. # Grab lock to prevent half-written output in case a child
  145. # process is in the middle of writing to stdin during kill.
  146. (( coproc_pid )) && read -r -k 1 -p tok
  147. terminate_jobs
  148. trap child_exit CHLD # Reinstall child trap.
  149. }
  150. local request do_eval=0
  151. local -a cmd
  152. while :; do
  153. # Wait for jobs sent by async_job.
  154. read -r -d $'\0' request || {
  155. # Unknown error occurred while reading from stdin, the zpty
  156. # worker is likely in a broken state, so we shut down.
  157. terminate_jobs
  158. # Stdin is broken and in case this was an unintended
  159. # crash, we try to report it as a last hurrah.
  160. print -r -n $'\0'"'[async]'" $(( 127 + 3 )) "''" 0 "'$0:$LINENO: zpty fd died, exiting'"$'\0'
  161. # We use `return` to abort here because using `exit` may
  162. # result in an infinite loop that never exits and, as a
  163. # result, high CPU utilization.
  164. return $(( 127 + 1 ))
  165. }
  166. # We need to clean the input here because sometimes when a zpty
  167. # has died and been respawned, messages will be prefixed with a
  168. # carraige return (\r, or \C-M).
  169. request=${request#$'\C-M'}
  170. # Check for non-job commands sent to worker
  171. case $request in
  172. _killjobs) killjobs; continue;;
  173. _async_eval*) do_eval=1;;
  174. esac
  175. # Parse the request using shell parsing (z) to allow commands
  176. # to be parsed from single strings and multi-args alike.
  177. cmd=("${(z)request}")
  178. # Name of the job (first argument).
  179. local job=$cmd[1]
  180. # Check if a worker should perform unique jobs, unless
  181. # this is an eval since they run synchronously.
  182. if (( !do_eval )) && (( unique )); then
  183. # Check if a previous job is still running, if yes,
  184. # skip this job and let the previous one finish.
  185. for pid in ${${(v)jobstates##*:*:}%\=*}; do
  186. if [[ ${storage[$job]} == $pid ]]; then
  187. continue 2
  188. fi
  189. done
  190. fi
  191. # Guard against closing coproc from trap before command has started.
  192. processing=1
  193. # Because we close the coproc after the last job has completed, we must
  194. # recreate it when there are no other jobs running.
  195. if (( ! coproc_pid )); then
  196. # Use coproc as a mutex for synchronized output between children.
  197. coproc command -p cat
  198. coproc_pid="$!"
  199. # Insert token into coproc
  200. print -n -p "t"
  201. fi
  202. if (( do_eval )); then
  203. shift cmd # Strip _async_eval from cmd.
  204. _async_eval $cmd
  205. else
  206. # Run job in background, completed jobs are printed to stdout.
  207. _async_job $cmd &
  208. # Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')...
  209. storage[$job]="$!"
  210. fi
  211. processing=0 # Disable guard.
  212. if (( do_eval )); then
  213. do_eval=0
  214. # When there are no active jobs we can't rely on the CHLD trap to
  215. # manage the coproc lifetime.
  216. close_idle_coproc
  217. fi
  218. done
  219. }
  220. #
  221. # Get results from finished jobs and pass it to the to callback function. This is the only way to reliably return the
  222. # job name, return code, output and execution time and with minimal effort.
  223. #
  224. # If the async process buffer becomes corrupt, the callback will be invoked with the first argument being `[async]` (job
  225. # name), non-zero return code and fifth argument describing the error (stderr).
  226. #
  227. # usage:
  228. # async_process_results <worker_name> <callback_function>
  229. #
  230. # callback_function is called with the following parameters:
  231. # $1 = job name, e.g. the function passed to async_job
  232. # $2 = return code
  233. # $3 = resulting stdout from execution
  234. # $4 = execution time, floating point e.g. 2.05 seconds
  235. # $5 = resulting stderr from execution
  236. # $6 = has next result in buffer (0 = buffer empty, 1 = yes)
  237. #
  238. async_process_results() {
  239. setopt localoptions unset noshwordsplit noksharrays noposixidentifiers noposixstrings
  240. local worker=$1
  241. local callback=$2
  242. local caller=$3
  243. local -a items
  244. local null=$'\0' data
  245. integer -l len pos num_processed has_next
  246. typeset -gA ASYNC_PROCESS_BUFFER
  247. # Read output from zpty and parse it if available.
  248. while zpty -r -t $worker data 2>/dev/null; do
  249. ASYNC_PROCESS_BUFFER[$worker]+=$data
  250. len=${#ASYNC_PROCESS_BUFFER[$worker]}
  251. pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
  252. # Keep going until we find a NULL-character.
  253. if (( ! len )) || (( pos > len )); then
  254. continue
  255. fi
  256. while (( pos <= len )); do
  257. # Take the content from the beginning, until the NULL-character and
  258. # perform shell parsing (z) and unquoting (Q) as an array (@).
  259. items=("${(@Q)${(z)ASYNC_PROCESS_BUFFER[$worker][1,$pos-1]}}")
  260. # Remove the extracted items from the buffer.
  261. ASYNC_PROCESS_BUFFER[$worker]=${ASYNC_PROCESS_BUFFER[$worker][$pos+1,$len]}
  262. len=${#ASYNC_PROCESS_BUFFER[$worker]}
  263. if (( len > 1 )); then
  264. pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
  265. fi
  266. has_next=$(( len != 0 ))
  267. if (( $#items == 5 )); then
  268. items+=($has_next)
  269. $callback "${(@)items}" # Send all parsed items to the callback.
  270. (( num_processed++ ))
  271. elif [[ -z $items ]]; then
  272. # Empty items occur between results due to double-null ($'\0\0')
  273. # caused by commands being both pre and suffixed with null.
  274. else
  275. # In case of corrupt data, invoke callback with *async* as job
  276. # name, non-zero exit status and an error message on stderr.
  277. $callback "[async]" 1 "" 0 "$0:$LINENO: error: bad format, got ${#items} items (${(q)items})" $has_next
  278. fi
  279. done
  280. done
  281. (( num_processed )) && return 0
  282. # Avoid printing exit value when `setopt printexitvalue` is active.`
  283. [[ $caller = trap || $caller = watcher ]] && return 0
  284. # No results were processed
  285. return 1
  286. }
  287. # Watch worker for output
  288. _async_zle_watcher() {
  289. setopt localoptions noshwordsplit
  290. typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
  291. local worker=$ASYNC_PTYS[$1]
  292. local callback=$ASYNC_CALLBACKS[$worker]
  293. if [[ -n $2 ]]; then
  294. # from man zshzle(1):
  295. # `hup' for a disconnect, `nval' for a closed or otherwise
  296. # invalid descriptor, or `err' for any other condition.
  297. # Systems that support only the `select' system call always use
  298. # `err'.
  299. # this has the side effect to unregister the broken file descriptor
  300. async_stop_worker $worker
  301. if [[ -n $callback ]]; then
  302. $callback '[async]' 2 "" 0 "$0:$LINENO: error: fd for $worker failed: zle -F $1 returned error $2" 0
  303. fi
  304. return
  305. fi;
  306. if [[ -n $callback ]]; then
  307. async_process_results $worker $callback watcher
  308. fi
  309. }
  310. _async_send_job() {
  311. setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
  312. local caller=$1
  313. local worker=$2
  314. shift 2
  315. zpty -t $worker &>/dev/null || {
  316. typeset -gA ASYNC_CALLBACKS
  317. local callback=$ASYNC_CALLBACKS[$worker]
  318. if [[ -n $callback ]]; then
  319. $callback '[async]' 3 "" 0 "$0:$LINENO: error: no such worker: $worker" 0
  320. else
  321. print -u2 "$caller: no such async worker: $worker"
  322. fi
  323. return 1
  324. }
  325. zpty -w $worker "$@"$'\0'
  326. }
  327. #
  328. # Start a new asynchronous job on specified worker, assumes the worker is running.
  329. #
  330. # Note if you are using a function for the job, it must have been defined before the worker was
  331. # started or you will get a `command not found` error.
  332. #
  333. # usage:
  334. # async_job <worker_name> <my_function> [<function_params>]
  335. #
  336. async_job() {
  337. setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
  338. local worker=$1; shift
  339. local -a cmd
  340. cmd=("$@")
  341. if (( $#cmd > 1 )); then
  342. cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
  343. fi
  344. _async_send_job $0 $worker "$cmd"
  345. }
  346. #
  347. # Evaluate a command (like async_job) inside the async worker, then worker environment can be manipulated. For example,
  348. # issuing a cd command will change the PWD of the worker which will then be inherited by all future async jobs.
  349. #
  350. # Output will be returned via callback, job name will be [async/eval].
  351. #
  352. # usage:
  353. # async_worker_eval <worker_name> <my_function> [<function_params>]
  354. #
  355. async_worker_eval() {
  356. setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
  357. local worker=$1; shift
  358. local -a cmd
  359. cmd=("$@")
  360. if (( $#cmd > 1 )); then
  361. cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
  362. fi
  363. # Quote the cmd in case RC_EXPAND_PARAM is set.
  364. _async_send_job $0 $worker "_async_eval $cmd"
  365. }
  366. # This function traps notification signals and calls all registered callbacks
  367. _async_notify_trap() {
  368. setopt localoptions noshwordsplit
  369. local k
  370. for k in ${(k)ASYNC_CALLBACKS}; do
  371. async_process_results $k ${ASYNC_CALLBACKS[$k]} trap
  372. done
  373. }
  374. #
  375. # Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the
  376. # specified callback function. This requires that a worker is initialized with the -n (notify) option.
  377. #
  378. # usage:
  379. # async_register_callback <worker_name> <callback_function>
  380. #
  381. async_register_callback() {
  382. setopt localoptions noshwordsplit nolocaltraps
  383. typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
  384. local worker=$1; shift
  385. ASYNC_CALLBACKS[$worker]="$*"
  386. # Enable trap when the ZLE watcher is unavailable, allows
  387. # workers to notify (via -n) when a job is done.
  388. if [[ ! -o interactive ]] || [[ ! -o zle ]]; then
  389. trap '_async_notify_trap' WINCH
  390. elif [[ -o interactive ]] && [[ -o zle ]]; then
  391. local fd w
  392. for fd w in ${(@kv)ASYNC_PTYS}; do
  393. if [[ $w == $worker ]]; then
  394. zle -F $fd _async_zle_watcher # Register the ZLE handler.
  395. break
  396. fi
  397. done
  398. fi
  399. }
  400. #
  401. # Unregister the callback for a specific worker.
  402. #
  403. # usage:
  404. # async_unregister_callback <worker_name>
  405. #
  406. async_unregister_callback() {
  407. typeset -gA ASYNC_CALLBACKS
  408. unset "ASYNC_CALLBACKS[$1]"
  409. }
  410. #
  411. # Flush all current jobs running on a worker. This will terminate any and all running processes under the worker, use
  412. # with caution.
  413. #
  414. # usage:
  415. # async_flush_jobs <worker_name>
  416. #
  417. async_flush_jobs() {
  418. setopt localoptions noshwordsplit
  419. local worker=$1; shift
  420. # Check if the worker exists
  421. zpty -t $worker &>/dev/null || return 1
  422. # Send kill command to worker
  423. async_job $worker "_killjobs"
  424. # Clear the zpty buffer.
  425. local junk
  426. if zpty -r -t $worker junk '*'; then
  427. (( ASYNC_DEBUG )) && print -n "async_flush_jobs $worker: ${(V)junk}"
  428. while zpty -r -t $worker junk '*'; do
  429. (( ASYNC_DEBUG )) && print -n "${(V)junk}"
  430. done
  431. (( ASYNC_DEBUG )) && print
  432. fi
  433. # Finally, clear the process buffer in case of partially parsed responses.
  434. typeset -gA ASYNC_PROCESS_BUFFER
  435. unset "ASYNC_PROCESS_BUFFER[$worker]"
  436. }
  437. #
  438. # Start a new async worker with optional parameters, a worker can be told to only run unique tasks and to notify a
  439. # process when tasks are complete.
  440. #
  441. # usage:
  442. # async_start_worker <worker_name> [-u] [-n] [-p <pid>]
  443. #
  444. # opts:
  445. # -u unique (only unique job names can run)
  446. # -n notify through SIGWINCH signal
  447. # -p pid to notify (defaults to current pid)
  448. #
  449. async_start_worker() {
  450. setopt localoptions noshwordsplit noclobber
  451. local worker=$1; shift
  452. local -a args
  453. args=("$@")
  454. zpty -t $worker &>/dev/null && return
  455. typeset -gA ASYNC_PTYS
  456. typeset -h REPLY
  457. typeset has_xtrace=0
  458. if [[ -o interactive ]] && [[ -o zle ]]; then
  459. # Inform the worker to ignore the notify flag and that we're
  460. # using a ZLE watcher instead.
  461. args+=(-z)
  462. if (( ! ASYNC_ZPTY_RETURNS_FD )); then
  463. # When zpty doesn't return a file descriptor (on older versions of zsh)
  464. # we try to guess it anyway.
  465. integer -l zptyfd
  466. exec {zptyfd}>&1 # Open a new file descriptor (above 10).
  467. exec {zptyfd}>&- # Close it so it's free to be used by zpty.
  468. fi
  469. fi
  470. # Workaround for stderr in the main shell sometimes (incorrectly) being
  471. # reassigned to /dev/null by the reassignment done inside the async
  472. # worker.
  473. # See https://github.com/mafredri/zsh-async/issues/35.
  474. integer errfd=-1
  475. # Redirect of errfd is broken on zsh 5.0.2.
  476. if is-at-least 5.0.8; then
  477. exec {errfd}>&2
  478. fi
  479. # Make sure async worker is started without xtrace
  480. # (the trace output interferes with the worker).
  481. [[ -o xtrace ]] && {
  482. has_xtrace=1
  483. unsetopt xtrace
  484. }
  485. if (( errfd != -1 )); then
  486. zpty -b $worker _async_worker -p $$ $args 2>&$errfd
  487. else
  488. zpty -b $worker _async_worker -p $$ $args
  489. fi
  490. local ret=$?
  491. # Re-enable it if it was enabled, for debugging.
  492. (( has_xtrace )) && setopt xtrace
  493. (( errfd != -1 )) && exec {errfd}>& -
  494. if (( ret )); then
  495. async_stop_worker $worker
  496. return 1
  497. fi
  498. if ! is-at-least 5.0.8; then
  499. # For ZSH versions older than 5.0.8 we delay a bit to give
  500. # time for the worker to start before issuing commands,
  501. # otherwise it will not be ready to receive them.
  502. sleep 0.001
  503. fi
  504. if [[ -o interactive ]] && [[ -o zle ]]; then
  505. if (( ! ASYNC_ZPTY_RETURNS_FD )); then
  506. REPLY=$zptyfd # Use the guessed value for the file desciptor.
  507. fi
  508. ASYNC_PTYS[$REPLY]=$worker # Map the file desciptor to the worker.
  509. fi
  510. }
  511. #
  512. # Stop one or multiple workers that are running, all unfetched and incomplete work will be lost.
  513. #
  514. # usage:
  515. # async_stop_worker <worker_name_1> [<worker_name_2>]
  516. #
  517. async_stop_worker() {
  518. setopt localoptions noshwordsplit
  519. local ret=0 worker k v
  520. for worker in $@; do
  521. # Find and unregister the zle handler for the worker
  522. for k v in ${(@kv)ASYNC_PTYS}; do
  523. if [[ $v == $worker ]]; then
  524. zle -F $k
  525. unset "ASYNC_PTYS[$k]"
  526. fi
  527. done
  528. async_unregister_callback $worker
  529. zpty -d $worker 2>/dev/null || ret=$?
  530. # Clear any partial buffers.
  531. typeset -gA ASYNC_PROCESS_BUFFER
  532. unset "ASYNC_PROCESS_BUFFER[$worker]"
  533. done
  534. return $ret
  535. }
  536. #
  537. # Initialize the required modules for zsh-async. To be called before using the zsh-async library.
  538. #
  539. # usage:
  540. # async_init
  541. #
  542. async_init() {
  543. (( ASYNC_INIT_DONE )) && return
  544. typeset -g ASYNC_INIT_DONE=1
  545. zmodload zsh/zpty
  546. zmodload zsh/datetime
  547. # Load is-at-least for reliable version check.
  548. autoload -Uz is-at-least
  549. # Check if zsh/zpty returns a file descriptor or not,
  550. # shell must also be interactive with zle enabled.
  551. typeset -g ASYNC_ZPTY_RETURNS_FD=0
  552. [[ -o interactive ]] && [[ -o zle ]] && {
  553. typeset -h REPLY
  554. zpty _async_test :
  555. (( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1
  556. zpty -d _async_test
  557. }
  558. }
  559. async() {
  560. async_init
  561. }
  562. async "$@"