From 0dd5f7dd788c81e76f989e4149b9c8582c5de591 Mon Sep 17 00:00:00 2001 From: vnz <1267662+vnz@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:21:10 +0200 Subject: [PATCH] global: write cache files atomically Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01R2ZY4aNfZ2gnhKw9UBJovV Co-Authored-By: Koichi Murase --- ble.pp | 10 +++++-- docs/ChangeLog.md | 1 + lib/init-bind.sh | 12 ++++++--- lib/init-cmap.sh | 10 ++++--- lib/init-term.sh | 6 ++++- lib/keymap.emacs.sh | 3 ++- lib/keymap.vi.sh | 3 ++- lib/keymap.vi_digraph.sh | 4 +-- note.txt | 56 ++++++++++++++++++++++++++++++++++++++++ src/decode.sh | 29 ++++++++++++--------- src/util.sh | 5 ++-- 11 files changed, 111 insertions(+), 28 deletions(-) diff --git a/ble.pp b/ble.pp index 4e9c84f4..0b9cdc02 100644 --- a/ble.pp +++ b/ble.pp @@ -2000,11 +2000,17 @@ function ble/base/clean-up-runtime-directory { [[ :$opts: == *:finalize:* ]] && alive[$$]=0 local file pid iremoved=0 ibgpid=0 - for file in "$_ble_base_run"/[1-9]*.*; do + for file in "$_ble_base_run"/[1-9]*.* "$_ble_base_cache"/*.[1-9]*.part; do [[ -e $file || -h $file ]] || continue # extract pid (skip if it is not a number) - pid=${file##*/}; pid=${pid%%.*} + pid=${file##*/} + if [[ $file == "$_ble_base_cache"/*.part ]]; then + pid=${pid%.part} + pid=${pid##*.} + else + pid=${pid%%.*} + fi [[ $pid && ! ${pid//[0-9]} ]] || continue if [[ ! ${alive[pid]+set} ]]; then diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 715c9b4f..e614d6c2 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -214,6 +214,7 @@ - import (`ble/util/import/is-loaded`): skip the file existence check for an absolute path `#D2423` d8ad40e7 - util (`ble/path#canonicalize`): fix infinite loops (fixup d8ad40e7) (reported by chill-nemesis) `#D2426` 63c23e99 - nsearch: fix the search position after a forward mismatch (reported by NitramO-YT, fixed by dfherr) `#D2430` 5fe06d62 +- global: generate cache atomically (reported by RakibFiha, fixed by vnz) `#D2432` xxxxxxxx ## Compatibility diff --git a/lib/init-bind.sh b/lib/init-bind.sh index ea383144..ceda8b7c 100644 --- a/lib/init-bind.sh +++ b/lib/init-bind.sh @@ -1,5 +1,9 @@ #!/bin/bash +## @var[in] ble_decode_bind_cache +## @var[in] ble_decode_unbind_cache +## The filenames to write the caches for bind/unbind the ble.sh keybindings. + # 現在の bash の version に従って以下の二つのファイルを生成します: # # $_ble_base_cache/decode.bind.$_ble_bash.$bleopt_input_encoding.bind @@ -205,12 +209,14 @@ function ble/init:bind/.generate { } function ble/init:bind/generate-binder { - local fbind1=$_ble_base_cache/decode.bind.$_ble_bash.$bleopt_input_encoding.bind - local fbind2=$_ble_base_cache/decode.bind.$_ble_bash.$bleopt_input_encoding.unbind + local fbind1=$ble_decode_bind_cache + local fbind2=$ble_decode_unbind_cache ble/edit/info/show text "ble.sh: updating binders..." - ble/init:bind/.generate 3>| "$fbind1" 4>| "$fbind2" + ble/init:bind/.generate 3>| "$fbind1.$$.part" 4>| "$fbind2.$$.part" && + ble/bin/mv -f "$fbind1"{".$$.part",} && + ble/bin/mv -f "$fbind2"{".$$.part",} ble/edit/info/immediate-show text "ble.sh: updating binders... done" } diff --git a/lib/init-cmap.sh b/lib/init-cmap.sh index 9e82c81a..07abdc05 100644 --- a/lib/init-cmap.sh +++ b/lib/init-cmap.sh @@ -1,5 +1,8 @@ #!/bin/bash +## @var[in] ble_decode_cmap_cache +## The filename to write the cache + # # 以下は ble-decode.sh にて既定で定義される特殊キー # @@ -494,10 +497,8 @@ function ble/init:cmap/initialize-keys { } ## @fn ble/init:cmap/initialize -## @var[in] dump -## The filename to write the cache function ble/init:cmap/initialize { - ble/edit/info/immediate-show text 'ble.sh: generating "'"$dump"'"...' + ble/edit/info/immediate-show text 'ble.sh: generating "'"$ble_decode_cmap_cache"'"...' ble/init:cmap/initialize-kbd ble/init:cmap/initialize-keys #%$ hash=$(bash ./make_command.sh hash lib/init-cmap.sh) && [ -n "$hash" ] && echo " local hash='$hash'" @@ -514,7 +515,8 @@ function ble/init:cmap/initialize { END { print "_ble_decode_cmap_cache_hash='\''" hash "'\''"; } - ' >| "$dump" + ' >| "$ble_decode_cmap_cache.$$.part" && + ble/bin/mv -f "$ble_decode_cmap_cache"{".$$.part",} # If ble.sh has not yet attached, we want to clear the processing message. [[ $_ble_attached ]] || ble/edit/info/immediate-clear diff --git a/lib/init-term.sh b/lib/init-term.sh index a1cad098..8b99971a 100644 --- a/lib/init-term.sh +++ b/lib/init-term.sh @@ -1,5 +1,8 @@ #!/bin/bash +## @var[in] ble_term_cache +## The filename to write the cache for the present TERM. + # 2020-02-07 #D12MSYS2 の CR 対策のため更新の必要あり _ble_term_tput= @@ -339,7 +342,8 @@ function ble/init:term/initialize { ble/init:term/register-varname "_ble_term_sgr_ab" # save - ble/util/declare-print-definitions "${varnames[@]}" >| "$_ble_base_cache/term.$TERM" + ble/util/declare-print-definitions "${varnames[@]}" >| "$ble_term_cache.$$.part" && + ble/bin/mv -f "$ble_term_cache"{".$$.part",} } ble/util/put "ble/term.sh: updating tput cache for TERM=$TERM... " >&2 diff --git a/lib/keymap.emacs.sh b/lib/keymap.emacs.sh index 8e5b2db7..20202d92 100644 --- a/lib/keymap.emacs.sh +++ b/lib/keymap.emacs.sh @@ -282,7 +282,8 @@ function ble-decode/keymap:emacs/initialize { ble/decode/keymap#load isearch dump ble/decode/keymap#load nsearch dump ble/decode/keymap#load emacs dump - } 3>| "$fname_keymap_cache" + } 3>| "$fname_keymap_cache.$$.part" && + ble/bin/mv -f "$fname_keymap_cache"{".$$.part",} ble/edit/info/immediate-show text "ble.sh: updating cache/keymap.emacs... done" } diff --git a/lib/keymap.vi.sh b/lib/keymap.vi.sh index 438e78e1..d325949b 100644 --- a/lib/keymap.vi.sh +++ b/lib/keymap.vi.sh @@ -8662,7 +8662,8 @@ function ble-decode/keymap:vi/initialize { ble/decode/keymap#load vi_omap dump ble/decode/keymap#load vi_xmap dump ble/decode/keymap#load vi_cmap dump - } 3>| "$fname_keymap_cache" + } 3>| "$fname_keymap_cache.$$.part" && + ble/bin/mv -f "$fname_keymap_cache"{".$$.part",} ble/edit/info/immediate-show text "ble.sh: updating cache/keymap.vi... done" } diff --git a/lib/keymap.vi_digraph.sh b/lib/keymap.vi_digraph.sh index 05037f98..719f1970 100644 --- a/lib/keymap.vi_digraph.sh +++ b/lib/keymap.vi_digraph.sh @@ -59,8 +59,8 @@ function ble-decode/keymap:vi_digraph/initialize { ble/edit/info/immediate-show text "ble.sh: updating cache/keymap.vi_digraph..." - >| "$fname_keymap_cache" - ble/decode/keymap#load vi_digraph dump 3>> "$fname_keymap_cache" + ble/decode/keymap#load vi_digraph dump 3>| "$fname_keymap_cache.$$.part" && + ble/bin/mv -f "$fname_keymap_cache"{".$$.part",} ble/edit/info/immediate-show text "ble.sh: updating cache/keymap.vi_digraph... done" } diff --git a/note.txt b/note.txt index f6e2a831..ed099628 100644 --- a/note.txt +++ b/note.txt @@ -8231,6 +8231,62 @@ bash_tips 2026-09-07 + * global: generate cache atomically (reported by RakibFiha, fixed by vnz) [#D2432] + https://github.com/akinomyoga/ble.sh/issues/109 + https://github.com/akinomyoga/ble.sh/pull/727 + + > The cache files under $_ble_base_cache were written in place with ">|". + > When many shells start at the same time with a stale cache (e.g. a + > terminal session restore right after "ble-update" or a bash upgrade), one + > shell truncates and rewrites a cache file while others already pass the + > "-s"/"-nt" checks and source it. A shell that sources a half-written + > "keymap.emacs" fails with "ble.sh: The keymap 'emacs' is empty." and + > ble-attach aborts; a half-written "decode.bind.*" fails with a Bash + > syntax error. + > + > Write every attach-time cache to ".$$.part" first and rename it + > into place with "ble/bin/mv -f", so readers only ever see complete files. + > "decode.readline.*" already used a temporary file but with a name shared + > by all shells; use the per-process name there as well. + + 同時にシェルを複数人の手で立ち上げる様な事はないと想定していたが、 + tmux-concurrent の様な物は同時に複数のシェルを立ち上げるという事の様だ。確 + かに、それで全ての説明が付く。 + + init-*.sh に関しては全部呼び出し元からキャッシュファイルの書き込み先のファ + イル名を指定することにした。変数名は一様に ble_*_cache という事にする。 + + 元々現在の様な設計になっていた一つの理由には外部プロセスの立ち上げを抑制し + て起動速度を可能な限り短くするという物だった。然し、結局 mv を呼び出さなけ + ればならないのだとしたらもっと別の方法はあるだろうか。取り敢えず何れの実行 + パスもキャッシュが存在しない時にのみ実行されるところだという所は確認した。 + なので、少なくとも毎回実行されるという訳でもない。 + + * reject: 例えば何らかの方法で mutex か何かを取得して同期をするなど。しかし、 + その様な事をする方がやはり時間がかかるだろう。mkdir による方法を用いると + しても、少なくとも mkdir のコストは存在する事になるし例えば NFS 上でうま + くその様な処理が動くのかも分からない。 + + * done: 毎回 ble/bin/mv || ble/bin/rm を実行しているのは汚い。 + + a 毎回 ble/bin/rm を呼び出しているのはコストがかかるし、これは + $_ble_base_run/$$.* と同様に終了時に一発 rm で cleanup すれば良い。 + cache は少なくとも in-memory filesystem に置くつもりはないので、中身の + 消去も即座にしなくても大丈夫だろう。 + + b ble/bin/mv || ble/bin/rm もしくは ble/bin/mv "$file"{".$$.part",} || >| + "$file.$$.part"を行う様な関数を作ってもいいのかもしれないが今のところは + 其処までして即座に削除しなくても良い様な気がする。 + + * done: 孤立リダイレクション >| "$cache" によって中身を空にする操作は + atomic ではないか? と思ったが、どうだろうか。例えば他の人がそのファイルに + 書き込みをしている間に >| をすると、>| する事自体は atomic であったとして + も、他の人が続きを書き込むに従ってファイルの前半が NUL 埋めされてしまうの + ではないか? と思ったが、そもそも有限の大きさの内容を書き込む場合には + in-place ではなく .$$.part で行っているのでその様な事は起こらないはずであ + る。mv と >| が前後したり重なったりしても別に変な内容のファイルにはならな + い筈である。 + * keymap.vi: support vi_nmap/accept-and-next (requested by sharpchen) [#D2431] https://github.com/akinomyoga/ble.sh/discussions/726 diff --git a/src/decode.sh b/src/decode.sh index 6409d669..ef2659e5 100644 --- a/src/decode.sh +++ b/src/decode.sh @@ -2844,11 +2844,11 @@ function ble/decode/cmap/initialize { function ble/decode/cmap/initialize { return 0; } local init=$_ble_base/lib/init-cmap.sh - local dump=$_ble_base_cache/decode.cmap.$_ble_decode_kbd_ver.$TERM.dump + local ble_decode_cmap_cache=$_ble_base_cache/decode.cmap.$_ble_decode_kbd_ver.$TERM.dump #%$ hash=$(bash ./make_command.sh hash lib/init-cmap.sh) && [ -n "$hash" ] && echo " local hash='$hash'" - if [[ -s $dump && $dump -nt $init ]]; then + if [[ -s $ble_decode_cmap_cache && $ble_decode_cmap_cache -nt $init ]]; then local _ble_decode_cmap_cache_hash=__uninitialized__ - source -- "$dump" + source -- "$ble_decode_cmap_cache" [[ $_ble_decode_cmap_cache_hash == "$hash" ]] && return 0 fi @@ -3091,7 +3091,6 @@ _ble_decode_bind_encoding= function ble/decode/readline/bind { _ble_decode_bind_encoding=$bleopt_input_encoding - local file=$_ble_base_cache/decode.bind.$_ble_bash.$_ble_decode_bind_encoding.bind # * 一時的に 'set convert-meta off' にする。 # @@ -3105,21 +3104,25 @@ function ble/decode/readline/bind { # ble/term/rl-convert-meta/enter + local ble_decode_bind_cache=$_ble_base_cache/decode.bind.$_ble_bash.$_ble_decode_bind_encoding.bind + local ble_decode_unbind_cache=$_ble_base_cache/decode.bind.$_ble_bash.$_ble_decode_bind_encoding.unbind #%$ hash=$(bash ./make_command.sh hash lib/init-bind.sh) && [ -n "$hash" ] && echo " local hash='$hash'" local _ble_decode_bind_cache_hash=__uninitialized__ - [[ -s $file && $file -nt $_ble_base/lib/init-bind.sh ]] && source -- "$file" + [[ -s $ble_decode_bind_cache && $ble_decode_bind_cache -nt $_ble_base/lib/init-bind.sh ]] && + source -- "$ble_decode_bind_cache" if [[ $_ble_decode_bind_cache_hash != "$hash" ]]; then source -- "$_ble_base/lib/init-bind.sh" - source -- "$file" + source -- "$ble_decode_bind_cache" fi _ble_decode_bind__uvwflag= ble/util/assign _ble_decode_bind_bindp 'builtin bind -p' # TERM 変更検出用 } function ble/decode/readline/unbind { + local ble_decode_unbind_cache=$_ble_base_cache/decode.bind.$_ble_bash.$_ble_decode_bind_encoding.unbind ble/function#try ble/encoding:"$bleopt_input_encoding"/clear - source -- "$_ble_base_cache/decode.bind.$_ble_bash.$_ble_decode_bind_encoding.unbind" + source -- "$ble_decode_unbind_cache" } function ble/decode/readline/rebind { [[ $_ble_decode_bind_state == none ]] && return 0 @@ -4235,8 +4238,8 @@ function ble/builtin/bind/read-user-settings/.collect { local cache=$_ble_base_cache/decode.readline.$_ble_bash.$map.txt if ! [[ -s $cache && $cache -nt $_ble_base/ble.sh ]]; then INPUTRC=/dev/null "$BASH" --noprofile --norc -i -c "builtin bind -m $map -p" 2>/dev/null | - LC_ALL= LC_CTYPE=C ble/bin/sed '/^#/d;s/"\\M-/"\\e/' >| "$cache.part" && - ble/bin/mv "$cache.part" "$cache" || continue + LC_ALL= LC_CTYPE=C ble/bin/sed '/^#/d;s/"\\M-/"\\e/' >| "$cache.$$.part" && + ble/bin/mv -f "$cache"{".$$.part",} || continue fi local cache_content ble/util/readfile cache_content "$cache" @@ -4385,16 +4388,18 @@ function ble/builtin/bind/read-user-settings/.cache-alive { ## @var[in] delay_prefix ## @var[in] cache_prefix function ble/builtin/bind/read-user-settings/.cache-save { - local keymap content fail= + local keymap fail= for keymap in emacs vi_imap vi_nmap; do if [[ -s $delay_prefix.$keymap ]]; then - ble/util/copyfile "$delay_prefix.$keymap" "$cache_prefix.$keymap" + ble/util/copyfile "$delay_prefix.$keymap" "$cache_prefix.$keymap.$$.part" && + ble/bin/mv -f "$cache_prefix.$keymap"{".$$.part",} else >| "$cache_prefix.$keymap" fi || fail=1 done [[ $fail ]] && return 1 - ble/util/print "$settings" >| "$cache_prefix.settings" + ble/util/print "$settings" >| "$cache_prefix.settings.$$.part" && + ble/bin/mv -f "$cache_prefix.settings"{".$$.part",} } ## @fn ble/builtin/bind/read-user-settings/.cache-load ## @var[in] delay_prefix diff --git a/src/util.sh b/src/util.sh index 03884341..4679a656 100644 --- a/src/util.sh +++ b/src/util.sh @@ -6934,8 +6934,9 @@ function ble/term/DA2R.hook { esac } function ble/term/.initialize { - if [[ -s $_ble_base_cache/term.$TERM && $_ble_base_cache/term.$TERM -nt $_ble_base/lib/init-term.sh ]]; then - source -- "$_ble_base_cache/term.$TERM" + local ble_term_cache=$_ble_base_cache/term.$TERM + if [[ -s $ble_term_cache && $ble_term_cache -nt $_ble_base/lib/init-term.sh ]]; then + source -- "$ble_term_cache" else source -- "$_ble_base/lib/init-term.sh" fi