Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions config/shared/zsh/completion.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ _selfishell_completion_files_changed() {
return 1
}

# zcompile unlinks and recreates the read-only .zwc in place, so concurrent
# shells fail with "can't write zwc file"; compile privately, then rename.
# mv would move the file into a directory occupying the target, so skip one.
_selfishell_completion_compile() {
local temporary="$ZCOMPDUMP.tmp.$$.$RANDOM.zwc"

[[ ! -d "$ZCOMPDUMP.zwc" ]] || return 1
zcompile "$temporary" "$ZCOMPDUMP" && command mv -f "$temporary" "$ZCOMPDUMP.zwc" || {
command rm -f "$temporary"
return 1
}
}

if [[ -o interactive ]] && { _selfishell_completion_needs_audit "$ZCOMPDUMP" ||
_selfishell_completion_files_changed "$ZCOMPDUMP"; }; then
# -i excludes insecure entries; -D rebuilds even when an unaudited dump has
Expand All @@ -56,19 +69,20 @@ if [[ -o interactive ]] && { _selfishell_completion_needs_audit "$ZCOMPDUMP" ||
fi
# Invalidate the old marker before rebuilding; a failed write must not
# let the next shell skip the audit and restore insecure fpath entries.
if compdump && zcompile "$ZCOMPDUMP" && [[ "${_comp_secure:-}" != yes &&
if compdump && _selfishell_completion_compile && [[ "${_comp_secure:-}" != yes &&
! -e "$ZCOMPDUMP.audit" && ! -L "$ZCOMPDUMP.audit" ]]; then
command touch "$ZCOMPDUMP.audit" 2>/dev/null
fi
fi
else
compinit -C -d "$ZCOMPDUMP"
fi
unfunction _selfishell_completion_needs_audit _selfishell_completion_files_changed

if [[ -s "$ZCOMPDUMP" && ( ! -s "$ZCOMPDUMP.zwc" || "$ZCOMPDUMP" -nt "$ZCOMPDUMP.zwc" ) ]]; then
zcompile "$ZCOMPDUMP"
_selfishell_completion_compile
fi
unfunction _selfishell_completion_needs_audit _selfishell_completion_files_changed \
_selfishell_completion_compile

if (( $+functions[zinit] )); then
zinit cdreplay -q
Expand Down
19 changes: 19 additions & 0 deletions tests/integration/native_completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,25 @@ func TestNativeCompletionAudit(t *testing.T) {
}
}

// Terminals restoring several tabs start shells that recompile the dump at once.
// A directory occupying the bytecode path must not receive the compiled file.
func TestNativeConcurrentDumpCompile(t *testing.T) {
t.Parallel()
home := nativeHome(t)
r := nativeRun(t, home, `startup='_selfishell_command_path() { return 1; }; source "$SELFISHELL_SOURCE"'
zsh -fc "$startup"
for i in {1..8}; do
zsh -fc "repeat 20; do command touch \"\$HOME/.zcompdump\"; $startup; done" &
done
wait
command rm -f "$HOME/.zcompdump.zwc" && command mkdir "$HOME/.zcompdump.zwc" &&
command touch -t 202001010000 "$HOME/.zcompdump.zwc" || exit 10
zsh -fc "$startup"
leftovers=("$HOME"/.zcompdump.tmp*(DN) "$HOME"/.zcompdump.zwc/*(DN))
(( ! $#leftovers )) || print -rl -- $leftovers`, "ZDOTDIR="+home, "SELFISHELL_SOURCE="+nativeCompletion())
nativeQuiet(t, r)
}

func nativeCompletionProbe(t *testing.T, home, dir, mode, compile string) capture {
t.Helper()
if mode == "" {
Expand Down
Loading