From 543d8bffaff9f152cb2e90b7954d26a735849ef7 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 13 May 2026 21:30:25 +0700 Subject: [PATCH] Fix async worker error messages leaking to stderr When the async worker dies and recovery fails, subsequent async calls in the same `prompt_pure_async_tasks` invocation would print error messages to stderr because the callback gets unregistered during recovery. Fixes #639 --- pure.zsh | 21 +++++++----- tests/async-worker-failure.zsh | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/pure.zsh b/pure.zsh index 1e5a51f..78e778f 100644 --- a/pure.zsh +++ b/pure.zsh @@ -505,11 +505,14 @@ prompt_pure_async_tasks() { fi # Update the current working directory of the async worker. - async_worker_eval "prompt_pure" builtin cd -q $PWD + # If any call fails (dead worker), bail out. The callback's recovery + # mechanism handles restarting; continuing here would print errors + # to stderr because the callback gets unregistered during recovery. + async_worker_eval "prompt_pure" builtin cd -q $PWD || return # Sync git environment variables to the async worker. - async_worker_eval "prompt_pure" "${${GIT_DIR:+export GIT_DIR=${(q)GIT_DIR}}:-unset GIT_DIR}" - async_worker_eval "prompt_pure" "${${GIT_WORK_TREE:+export GIT_WORK_TREE=${(q)GIT_WORK_TREE}}:-unset GIT_WORK_TREE}" + async_worker_eval "prompt_pure" "${${GIT_DIR:+export GIT_DIR=${(q)GIT_DIR}}:-unset GIT_DIR}" || return + async_worker_eval "prompt_pure" "${${GIT_WORK_TREE:+export GIT_WORK_TREE=${(q)GIT_WORK_TREE}}:-unset GIT_WORK_TREE}" || return typeset -gA prompt_pure_vcs_info @@ -529,7 +532,7 @@ prompt_pure_async_tasks() { fi unset MATCH MBEGIN MEND - async_job "prompt_pure" prompt_pure_async_vcs_info + async_job "prompt_pure" prompt_pure_async_vcs_info || return # Only perform tasks inside a Git working tree. [[ -n $prompt_pure_vcs_info[top] ]] || return @@ -544,16 +547,16 @@ prompt_pure_async_refresh() { # We set the pattern here to avoid redoing the pattern check until the # working tree has changed. Pull and fetch are always valid patterns. typeset -g prompt_pure_git_fetch_pattern="pull|fetch" - async_job "prompt_pure" prompt_pure_async_git_aliases + async_job "prompt_pure" prompt_pure_async_git_aliases || return fi - async_job "prompt_pure" prompt_pure_async_git_arrows + async_job "prompt_pure" prompt_pure_async_git_arrows || return # Do not perform `git fetch` if it is disabled or in home folder. if (( ${PURE_GIT_PULL:-1} )) && [[ $prompt_pure_vcs_info[top] != $HOME ]]; then zstyle -t :prompt:pure:git:fetch only_upstream local only_upstream=$((? == 0)) - async_job "prompt_pure" prompt_pure_async_git_fetch $only_upstream + async_job "prompt_pure" prompt_pure_async_git_fetch $only_upstream || return fi # If dirty checking is sufficiently fast, @@ -562,12 +565,12 @@ prompt_pure_async_refresh() { if (( time_since_last_dirty_check > ${PURE_GIT_DELAY_DIRTY_CHECK:-1800} )); then unset prompt_pure_git_last_dirty_check_timestamp # Check check if there is anything to pull. - async_job "prompt_pure" prompt_pure_async_git_dirty ${PURE_GIT_UNTRACKED_DIRTY:-1} + async_job "prompt_pure" prompt_pure_async_git_dirty ${PURE_GIT_UNTRACKED_DIRTY:-1} || return fi # If stash is enabled, tell async worker to count stashes if zstyle -t ":prompt:pure:git:stash" show; then - async_job "prompt_pure" prompt_pure_async_git_stash + async_job "prompt_pure" prompt_pure_async_git_stash || return else unset prompt_pure_git_stash fi diff --git a/tests/async-worker-failure.zsh b/tests/async-worker-failure.zsh index e968990..60467da 100644 --- a/tests/async-worker-failure.zsh +++ b/tests/async-worker-failure.zsh @@ -9,6 +9,7 @@ main() { test_callback_no_recursion_on_worker_failure || return test_callback_failed_recovery_clears_git_state || return test_callback_recovery_calls_tasks_on_success || return + test_dead_worker_no_stderr_leakage || return print -- "async-worker-failure tests passed" } @@ -148,4 +149,64 @@ test_callback_recovery_calls_tasks_on_success() { unfunction async_start_worker async_stop_worker async_register_callback async_worker_eval prompt_pure_async_tasks } +test_dead_worker_no_stderr_leakage() { + # Restore prompt_pure_async_tasks (earlier tests unfunction it). + source ./pure.zsh >/dev/null 2>&1 + prompt_pure_preprompt_render() { : } + + # Simulate: worker was previously started but is now dead. + # When recovery fails, the remaining async calls in prompt_pure_async_tasks + # must not leak error messages to stderr. (GitHub issue #639) + typeset -g prompt_pure_async_inited=1 + + # Recovery will fail because the worker cannot be restarted. + async_start_worker() { return 1 } + async_stop_worker() { + typeset -gA ASYNC_CALLBACKS + unset "ASYNC_CALLBACKS[$1]" + } + async_register_callback() { : } + async_flush_jobs() { : } + + # Register the callback (as prompt_pure_async_init would have done). + typeset -gA ASYNC_CALLBACKS + ASYNC_CALLBACKS[prompt_pure]="prompt_pure_async_callback" + + # Simulate dead worker behavior: the first call with a registered callback + # invokes recovery (which fails and unregisters the callback). Subsequent + # calls find no callback and print errors to stderr. + async_worker_eval() { + local worker=$1; shift + typeset -gA ASYNC_CALLBACKS + local callback= + (( ${+ASYNC_CALLBACKS[$worker]} )) && callback=$ASYNC_CALLBACKS[$worker] + if [[ -n $callback ]]; then + $callback '[async]' 3 "" 0 "error: no such worker: $worker" 0 + else + print -u2 "async_worker_eval: no such async worker: $worker" + fi + return 1 + } + async_job() { + local worker=$1; shift + typeset -gA ASYNC_CALLBACKS + local callback= + (( ${+ASYNC_CALLBACKS[$worker]} )) && callback=$ASYNC_CALLBACKS[$worker] + if [[ -n $callback ]]; then + $callback '[async]' 3 "" 0 "error: no such worker: $worker" 0 + else + print -u2 "async_job: no such async worker: $worker" + fi + return 1 + } + + local stderr_file=$TMPDIR/pure-test-stderr-$$ + prompt_pure_async_tasks 2>$stderr_file + local stderr_output=$(<$stderr_file 2>/dev/null) + + assert_empty "$stderr_output" "prompt_pure_async_tasks must not leak error messages to stderr when worker is dead" || return + + unfunction async_start_worker async_stop_worker async_register_callback async_flush_jobs async_worker_eval async_job +} + main "$@" -- 2.51.2