Skip to content

Commit

Permalink
util (ble/util/msleep): work around the bash-4.3 bug of "read -t"
Browse files Browse the repository at this point in the history
  • Loading branch information
akinomyoga committed Feb 20, 2021
1 parent e886883 commit 4ca9b2e
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 19 deletions.
9 changes: 4 additions & 5 deletions lib/init-msleep.sh
@@ -1,6 +1,6 @@
#!/bin/bash

function ble/util/msleep/.use-compiled-builtin/compile {
function ble/util/msleep/.load-compiled-builtin/compile {
local builtin_path=$1
[[ -x $builtin_path && $builtin_path -nt $_ble_base/lib/init-msleep.sh ]] && return 0

Expand All @@ -14,16 +14,15 @@ EOF
[[ -x $builtin_path ]]
} &>/dev/null

function ble/util/msleep/.use-compiled-builtin {
function ble/util/msleep/.load-compiled-builtin {
local basename=$_ble_edit_io_fname2
local fname_buff=$basename.buff

local builtin_path=$_ble_base_cache/$HOSTNAME.init-msleep.so
local builtin_path=$_ble_base_cache/init-msleep.$_ble_bash.$HOSTNAME.so
local builtin_runpath=$_ble_base_run/$$.init-msleep.so
ble/util/msleep/.use-compiled-builtin/compile "$builtin_path" &&
ble/util/msleep/.load-compiled-builtin/compile "$builtin_path" &&
ble/bin/cp "$builtin_path" "$builtin_runpath" || return 1

enable -f "$builtin_runpath" msleep || return 1
blehook unload+='enable -d ble/builtin/msleep &>/dev/null'
function ble/util/msleep { ble/builtin/msleep "$1"; }
}
3 changes: 2 additions & 1 deletion memo/ChangeLog.md
Expand Up @@ -41,6 +41,7 @@
- edit (`kill-forward-logical-line`): fix a bug not deleting newline at the end of the line `#D1443` 09cf7f1
- complete (mandb): fix an encoding prpblem of utf8 manuals `#D1446` 7a4a480
- util (`ble/util/msleep`): fix hang in Cygwin by swithing from `/dev/udp/0.0.0.0/80` to `/dev/zero` `#D1452` d4d718a
- util (`ble/util/msleep`): work around the bash-4.3 bug of `read -t` (reported by 3ximus) `#D1468` `#D1469` 0000000
- syntax: fix broken AST with `[[` keyword `#D1454` 69658ef
- benchmark (`ble-measure`): work around a locale-dependent decimal point of `EPOCHREALTIME` (reported by 3ximus) `#D1460` 1aa471b
- global:work around bash-4.2 bug of `declare -gA` (reported by 0xC0ncord) `#D1470` 8856a04
Expand All @@ -57,7 +58,7 @@
- global: clean up helps of user functions `#D1459` 33c283e
- benchmark (`ble-measure`): support `-T TIME` and `-B TIME` option `#D1460` 1aa471b
- util, color (`bleopt`, `blehook`, `ble-color-setface`): support `--color` and fix `sgr0` contamination in non-color output `#D1466` 69248ff
- global: fix status check for read timeout `#D1467` 0000000
- global: fix status check for read timeout `#D1467` e886883

<!---------------------------------------------------------------------------->
# ble-0.4.0-devel2
Expand Down
139 changes: 139 additions & 0 deletions note.txt
Expand Up @@ -3811,6 +3811,145 @@ bash_tips

全て 40300 に書き換えた。まあ、これで何も起こらないだろう。

2021-02-09

* 2021-02-01: spike branch で tab completion で crash する (reported by 3ximus) [#D1469]
https://github.com/akinomyoga/ble.sh/issues/82#issuecomment-770390986

Ref: これは #D1452 / #D1468 と同じ問題。詳細は #D1468 で議論。

eval-pathname-expansion の中で死んでいるのだろうか。failglob などの判定に失
敗している可能性もある。つまり、shopt -s failglob が設定されている時に、
globpat が含まれているのに含まれていないと判定された時に、set -f 等の操作を
行わずにパス名展開を直接実行して、それにより強制的に終了してしまっている可
能性。

→でも元々 failglob があっても大丈夫な様に eval を使って評価していた筈→本
当だろうか。eval だけで failglob を回避する事ができていたのだろうか。
syntax の方で .set-result という関数を用意したのは何か理由があったのでは
ないか。と思ったが、.set-result は failglob を避ける為の物ではなくて展開
結果を格納する為の物だった。

→更に、現在の判定方法で globpat を見逃すとも考えづらい。

実際に手元で failglob の設定下で補完を試みたが変な事は起こっていない。bash
version の違いかもしれないとも思ったが別に 5.1 でも dev でも変な事はない。
3.2 や 4.0 4.3 4.4 で試しても違いはない。

→hangは観測できたという話をしたらどうやら正確にはcrash ではなくて hang し
ていた様だ。そして振る舞いを見る限りに於いては自分の手元で観測した hang と
全く同じである。

* 2021-02-03 TAB completion に於いて conditional-sync で hang する [#D1468]
Ref: #D1452 と #D1469 は実はこの問題と同一であった。

chatoyancy で再現している。

3ximus の報告した crash は観測されていないが代わりに linux 上で hang すると
いう現象は起こっている。親 shell が固まっていて一方で子 shell はいつまで経っ
ても終了しないという状態になる。これはどういう状態だろうか。子 shell に対し
て kill を実行するとその時点で終了して親 shell も使える様になる。時々、再び
子 shell を生成してそれが固まるという現象は発生している。subshell が発生し
ている状態で固まっているという様子を見る限りは恐らく conditional-sync で変
な問題が起こっているのだと思われるが分からない。

* 親shellの CPU が微妙に動いている。
* ユーザー入力しても何故か親シェルが中断しない

改めて condtional sync の実装を確認してみると2回 fork している。1つの fork
はジョブ管理によって変なメッセージが発生するのを避ける為。2つ目の fork が実
際に & で bg job を起動する為の物。症状として見られていた hanging shell は
実は worker ではなくて最初の subshell の様だ。更に詳しく調べてみるとどうや
ら msleep の中で停止してしまっているようだ。つまりこれは Cygwin 上で発生し
ていた #D1452 のフリーズと全く同じ問題である。

Cygwin のテストケースと同じ物で再現できるだろうか。

( echo {1..1000} & builtin read -t 0.000100 v < /dev/udp/0.0.0.0/80 ) >/dev/null

Linux では /dev/udp ではなくて fifo を使っている筈。

OK 再現した。

$ mkfifo a.pipe
$ exec 9<>a.pipe
$ test1() { (eval "echo {0..$count}" & builtin read -u 9 -t 0.001 v) >/dev/null; echo ok $((count++)); }
$ bind -t '"\C-t":test1'

これは結局 bash のバグなので別の場所で議論する事にした。

workaround を考える。

A 一つの方向は先ず変な事が起こる発生確率を下げるという事。
然し、それでも read -t がブロックする可能性は 0 ではない。
これは実の所 /dev/zero を使っていても同様の筈である。
とにかく変な事が発生する確率は 0 ではないという事。

変な事が起こる確率を下げる為に。もう一つの可能性は以下の様に、
$() を使って fork して、その後で read timeout を
一番外側で実行するという事。

{ pid=$({ echo OK >&3; sleep 10; } &>/dev/null & echo $!); } 3>&1

疑問としは孫シェルを wait できるのかという事。→うーん。駄目だった。

bash: wait: pid 9447 はこのシェルの子プロセスではありません

wait できない。という事は終了ステータスを検知する方法がない。或いは
ble/util/assign と同様に終了ステータスは自前のファイル経由で読み取るとい
う手もあるのかもしれないが…。とにかく面倒になってしまうという事は避けら
れない。

そもそもこの方向で問題を軽減できるのかどうかも非自明である。

B read timeout を使わない方法を模索するべきなのかもしれない。
然し read timeout が使えないとなるとやはり方法は絞られてくる。
一つの方法は普通にコマンドの sleep を呼び出すという事。

然し、spawn のコストと、更に sleep の分解能が秒単位しか無いという可能性が
ある。そう考えると read timeout はどのシステムでも使う事ができるかなり便
利な機能だったのである。

c というよりそもそも conditional-sync ではデフォルトでユーザの入力を待って
いるのだから、read -t を sleep に使うのではなくて、直接ユーザー入力を検出
するのに使えば良いのではないのか? と思ったが駄目だ。read -t 0 ならばユー
ザー入力を読み取らずに検査する事ができるが、read -t 0.001 だとユーザー入
力を消費してしまう。そうなると気軽に実行できない。或いは、ユーザー入力を
読み取る事ができた暁にはその場で ble-decode-char するという手もある?

うーん。然し、_ble_bind_hook の中で実行した bind/.tail の中で
ble-decode-char するとすると、例えば其処でコマンド実行が _ble_bind_hook
に大して設定された時に、それがその場で実行されない可能性がある。更に、設
定した物が実行される前に消滅してしまう可能性すらある。ble-decode-char を
その場で実行する方式にすると色々木にしなければならない事が多い。

d conditional-sync に限っては ALRM を subshell から投げれば良いのでは。と思っ
て試してみたが、何も設置していない状態で kill -ALRM $$ すると親シェルが終
了してしまう。ALRM に何か無視する様に ble.sh でハンドラを設置しても良いが、
それはそれでまた面倒事が増える。

うーん。二つの方向性がある。全般に msleep の問題を解決するという事と
conditional-sync の問題を解決するという事。今までの発生頻度から考えると
conditional-sync の対策だけをしても当面は大丈夫な気もするが、何れは msleep
の問題を完全解決したい。そうなった暁には conditional-sync の対策は不要にな
るし、また conditional-sync の問題だけ解決するのは臭いものに蓋をしているだ
けで本質的問題の解決にはなっていない。それよりは msleep 自体の問題を解決す
る様に考えたいのである。

仕方がないので cygwin と同じ手法で回避する事にした。

2021-02-13 /dev/zero による workaround を追加したがよく考えたら /dev/zero
が全ての環境にあるとは限らないし、ある環境でも read をブロックしてくれるか
は微妙である。と思ったが /dev/zero で止まるかどうかは bash の側の実装の問題
なので、もし /dev/zero が 0 を出力し続けるのであれば、ちゃんとブロックはす
るだろうと思われる。

/dev/zero が存在する環境について一度確認はしておきたい。Solaris, Minix に存
在する。FreeBSD, Haiku にも存在した。当然 Cygwin と Linux にも存在する。
MSYS2 でも OK だった。何れも read -t 0.2 < /dev/zero がちゃんと期待通りに動
いた。という事を考えれば基本的に全ての環境で /dev/zero が使えると考えて良い
のではないだろうか。

2021-02-04

* global: read timeout は $?==142 とは限らない [#D1467]
Expand Down
45 changes: 32 additions & 13 deletions src/util.sh
Expand Up @@ -2760,19 +2760,38 @@ if ((_ble_bash>=40400)) && ble/util/msleep/.check-builtin-sleep; then
fi
}
function sleep { ble/builtin/sleep "$@"; }
elif ((_ble_bash>=40000)) && [[ $OSTYPE != haiku* && $OSTYPE != minix* ]]; then
if [[ $OSTYPE == cygwin* || $OSTYPE == msys* ]]; then
# Note: #D1452 socket (/dev/udp) で Cygwin が hang する。他の実装も全般に
# read -t は Cygwin では固まる可能性がある様だ。但し、発生する頻度は方法に
# よって異なる。仕方が無いので自前で loadable builtin をコンパイルする事に
# した。と思ったがライセンスの問題でこれを有効にする訳には行かない。
[[ -f $_ble_base/lib/init-msleep.sh ]] &&
source "$_ble_base/lib/init-msleep.sh" &&
ble/util/msleep/.use-compiled-builtin ||
ble/util/msleep/.use-read-timeout zero.exec1-coreutil
else
ble/util/msleep/.use-read-timeout fifo.exec2
fi
elif [[ -f $_ble_base/lib/init-msleep.sh ]] &&
source "$_ble_base/lib/init-msleep.sh" &&
ble/util/msleep/.load-compiled-builtin
then
# 自前で sleep.so をコンパイルする。
#
# Note: #D1452 #D1468 #D1469 元々使っていた read -t による手法が
# Bash のバグでブロックする事が分かった。bash 4.3..5.1 ならばどの OS
# でも再現する。仕方が無いので自前で loadable builtin をコンパイルす
# る事にした。と思ったがライセンスの問題でこれを有効にする訳には行か
# ない。
function ble/util/msleep { ble/builtin/msleep "$1"; }
elif ((40000<=_ble_bash&&!(40300<=_ble_bash&&_ble_bash<50200))) &&
[[ $OSTYPE != cygwin* && $OSTYPE != mingw* && $OSTYPE != haiku* && $OSTYPE != minix* ]]
then
# FIFO (mkfifo) を予め読み書き両用で開いて置き read -t する方法。
#
# Note: #D1452 #D1468 #D1469 Bash 4.3 以降では一般に read -t が
# SIGALRM との race condition で固まる可能性がある。socket
# (/dev/udp) や fifo で特に問題が発生しやすい。特に Cygwin で顕著。
# 但し、発生する頻度は環境や用法・手法によって異なる。Cygwin/MSYS,
# Haiku 及び Minix では fifo は思う様に動かない。
ble/util/msleep/.use-read-timeout fifo.exec2
elif ((_ble_bash>=40000)) && [[ -c /dev/zero ]]; then
# /dev/zero に対して read -t する方法。
#
# Note: #D1452 #D1468 #D1469 元々使っていた FIFO に対する方法が安全
# でない時は /dev/zero に対して read -t する。0 を読み続ける事になる
# ので CPU を使う事になるが短時間の sleep の時のみに使う事にして我慢
# する事にする。確認した全ての OS で /dev/zero は存在した (Linux,
# Cygwin, FreeBSD, Solaris, Minix, Haiku, MSYS2)。
ble/util/msleep/.use-read-timeout zero.exec1-coreutil
elif ble/bin/.freeze-utility-path sleepenh; then
function ble/util/msleep/.core { ble/bin/sleepenh "$1" &>/dev/null; }
elif ble/bin/.freeze-utility-path usleep; then
Expand Down

0 comments on commit 4ca9b2e

Please sign in to comment.