Closed30

nyagos が Ctrl-C で終了してしまう件

Ctrl-C を効かなくするには二種類の方法がある

  1. SetConsoleMode で、ENABLE_PROCESSED_INPUT をオンだかオフだかする(どっちやねん)
    • Ctrl-C を押下しても、SIGINT が送られなくなる
    • プロセスではなくコンソールの設定をいじるので、自プロセスだけでなく子プロセスも Ctrl-C が効かなくなる。
    • Windows 独自の設定なので、移植性がちょっと落ちる
  2. signal 関数で SIGINT を無視する感じにする

nyagosは当初 1. の方を使っていて、これをやると落ちるなんてことは絶対なくなるんだけど、逆に子プロセスを kill できなくなるので、現在は 2. の方を採用している。ただし、SIGINT を完全に無視するんじゃなくて、SIGINT をキャッチしたら子プロセスを kill するというような動作をする goroutine を走らせている。

SIGINT をキャッチしたら子プロセスを kill する

あ、厳密にいうと、

  • SIGINT をキャッチしたら、context.WithCancel を使って現在やっている処理にキャンセル処理を通知する( shell/loop.go の (*Shell)Loop 関数内 )
  • 外部プロセスの終了待ちをしている goroutine がキャンセルを検知すると、外部プロセスに kill する( shell/interpret.go の (*Shell)startAndWaitProcess 関数内 )

という動作をしています

一つの可能性としては、signal.Notify (禁止)と signal.Stop (解除)が頻繁すぎて、そこにスキが出来ているというのもあるかもしれない

  • シェルの基本ループ (shell/Loop.go の (*Shell)Loop関数
    • 一行入力 in "go-readline-ny"
      • 1文字キー入力( "mattn/go-tty" )
        • (tty.*TTY)Raw()実行時にENABLE_PROCESSED_INPUT がリセットされ、Raw()解除時に元に戻される
        • この間、Ctrl-C で割り込み(SIGINT)自体が発生しない
    • コマンド実行部 (shell/Interpreter.go)
      • 1文ごとに signal.Notify → コマンド実行 → signal.Stop を繰り返す
      • siginal.Notify~Stopの間、SIGINT は context のキャンセルに解釈されなおす

一行入力とコマンド実行部との遷移の間が割り込みを受け付ける隙になっている可能性が高い。signal 操作と go-tty は衝突しないので、ループの外側に signal.Notify~Stop の範囲を広げてみるか

トップレベルで、signal.Ignore(os.Interrpt) を入れてみました。多分、下位の signal.Notify/Stop や ENABLE_PROCESSED_INPUTのリセットと干渉しないので、これで直るはず…

この修正は "frame" というサブパッケージでやってますが、これは何かというと main から数えて呼び出し階層の2番目くらいにあるサブパッケージです。

  • main - 形式だけのトップレベル(frameとmainsを呼ぶだけ)
    • frame - Lua に依存しない部分の初期化処理と終了処理を担う
      • mains - Lua に依存する部分の初期化処理と終了処理とメインルーチン
        • shell - シェルのメインルーチン(loop.go)とコマンドインタープリタ(interpreter.go)
        • "go-readline-ny" - 一行入力のパッケージ(元々はサブパッケージだったが独立させた)

複雑な階層構造になってますが、これは Lua に依存する箇所を mains だけに限定したかったためです。これをしなければ、lua53.dll → GopherLua へのスムーズな切り替えが出来なかったと思います。

直っていない。

start nyagos.exe
more
[Ctrl-C]

で、ターミナルがクローズしてしまう。別のウインドウなので、親の nyagos が kill しているわけではない

more を実行する時に一時的に ENABLE_PROCESSED_INPUT をリセットして、Ctrl-C を押されても割り込みが発生しないようにした。

が、そうすると、キーボード入力の Enter が LF のない CR だけになってしまって、ずっと同じ行で入力し続ける形になっておかしい!
(が、落ちるよりマシなので、とりあえず commit してる。しかし、コレ、どうすりゃいいんだ?)

Windows Terminal や VSCode などでターミナルを開くとき、pwsh や nyagos の中で時間のかかる処理をCtrl+Cするとターミナルのプロセスごと落ちてしまうときがある

対処法
scoop/shimsではなくscoop/apps以下の .exe を指定する

scoop/shims 以下にある実行ファイルは、scoop/apps 以下にある本物の EXE ファイルを呼ぶためのブースター。下記がソースだが、ぱっと見、子プロセスを kill するようなコードは見受けられない。プロセスツリーのトップが殺されたら連鎖的に子供も殺されるような仕組みでもあるのかなぁ

https://github.com/lukesampson/scoop/blob/master/supporting/shimexe/shim.cs

zenn.dev の Book 「scoop / nyagos で始めるコマンドライン生活」が ~\scoop/apps/nyagos.exe を使うような記載になっていたのを、~\scoop\apps\nyagos\current\nyagos.exe を指定するよう修正した。

この記事が迷える人を増やす要因になっていた。もっと早く修正すべきだった。判断が遅い!

clone コマンドで、新ターミナルを起動し、プロンプトが出るか出ないかするタイミングで Ctrl-C を押下したら落ちた!

ASCII.jp:Go言語で知るプロセス(3)

signal.Stop(signals)
これを呼び出すと、それ以降Notify()で指定したシグナルを受け取らなくなるわけではなく、デフォルトに戻るようです。 Notify()でSIGINT(Ctrl + C)を指定していた場合、呼び出し後はブロックせずにデフォルトでプロセスを終了するようになります

な、なんだと…
(でも、Stop の後、ほとんど間もなく、readline(go-tty) 中で SetConsoleMode( ENABLE_PROCESSED_INPUT のリセット) とかしているので、これがすべての根源とも言い切れない。
が、対処しておく必要はあるだろう。

// 余談だが、context でキャンセルさせるようにしてから、Ctrl-C の面倒事が増えてきたような気がする。まぁ、context がなかった当時は内蔵 more もなかったけど

Ctrl-C 関連を改造中の nyagos にて

$ more
f
f
d
d
w
w
<DESKTOP-LGGUCRA:~/go/src/github.com/zetamatta/nyagos>
$
<DESKTOP-LGGUCRA:~/go/src/github.com/zetamatta/nyagos>
$ s
C:\Users\hymko\go\src\github.com\zetamatta\nyagos\nyagos.d\brace.lua:8: context canceled
stack traceback:
        C:\Users\hymko\go\src\github.com\zetamatta\nyagos\nyagos.d\brace.lua:8: in main chunk
        [G]: ?

more の中で Ctrl-C で止めると… 時間差で Cancel される。

(brace.lua というのは、A{B,C}D を ABD ACD へ変換するコマンドラインフィルターの Lua で、それが context.WithCancel の cancel で止められてる。なんでや!

もうかなり行き詰まってる

あー、context.WithCancel の .Done() と signal のチャネルを同じ場所で拾わせるという手があるのか

    signal.Notify(c,
        syscall.SIGINT,
        os.Interrupt)
 go func() {
        select {
        case <-c:
            // シグナルをキャッチ
            fmt.Println("signal")
            // robot() を終了させる
            cancel()
        case <-ctx.Done():
            // cancel() コール時に呼ばれる、ただしシグナルをキャッチした際の cancel() ではここは通らない
            debug()
            fmt.Println("ctx.Done()")
        }
    }()

なるほど

お、これがベストソリューションだったかも!?

これが決定版かもしれぬ。more の中で Ctrl-C を押しても、more だけが止まって、その後に引きずらないようになった。

ただし、signal.Notify~signal.Stop の範囲がまだ狭いので

で追加報告いただいている事象がこれでは治らない可能性が高い。
signal.Notify~signal.Stopの範囲の見直しする必要がありそうだ。

signal.Notify~signal.Stopの範囲の見直しする必要がありそうだ。

signal.Notify の後、signal.Stop をすると、Ctrl-C でのプロセス終了状態に戻ってしまうという記事があった。では、signal.Notify の入れ子はどうすればよいのか。検証してみよう

単純に channel をクローズしてはどうか?

package main

import (
	"time"
	"os/signal"
	"os"
)

func main(){
	c1 := make(chan os.Signal,1)
	c2 := make(chan os.Signal,1)

	go func(){
		for _ = range c1{
			println("first signal")
		}
	}()

	go func(){
		for _ = range c2{
			println("second signal")
		}
	}()

	signal.Notify(c1,os.Interrupt)
	signal.Notify(c2,os.Interrupt)

	println("Type Ctrl-C in 10 seconds")
	time.Sleep(time.Second*10)

	close(c1)

	println("Type Ctrl-C in 10 seconds")
	time.Sleep(time.Second*10)

	close(c2)
}
$ signalnotify.exe
Type Ctrl-C in 10 seconds
first signal
second signal
Type Ctrl-C in 10 seconds
panic: send on closed channel

goroutine 8 [running]:
os/signal.process(0x64ae00, 0x697710)
        C:/go/src/os/signal/signal.go:244 +0x17f
os/signal.loop()
        C:/go/src/os/signal/signal_unix.go:23 +0x45
created by os/signal.Notify.func1.1
        C:/go/src/os/signal/signal.go:150 +0x4b

ダメに決まってるだろ!

close ではなく、普通に signal.Stop を使ってみた。

	signal.Notify(c1,os.Interrupt)
	signal.Notify(c2,os.Interrupt)

	println("Type Ctrl-C in 10 seconds")
	time.Sleep(time.Second*10)

	signal.Stop(c2)

	println("Type Ctrl-C in 10 seconds")
	time.Sleep(time.Second*10)
	signal.Stop(c1)
	println("All done")
$ signalnotify.exe
Type Ctrl-C in 10 seconds
second signal
first signal
Type Ctrl-C in 10 seconds
first signal
All done

普通に行けた。signal.Notify は多重に機能する。

さて、全部の signal.Notify を解除した時の挙動は、何もしなかった時の状況に戻るわけだが、その時の挙動はどうか?

	signal.Notify(c1,os.Interrupt)
	signal.Notify(c2,os.Interrupt)

	println("Type Ctrl-C in 5 seconds")
	time.Sleep(time.Second*5)

	signal.Stop(c2)

	println("Type Ctrl-C in 5 seconds")
	time.Sleep(time.Second*5)
	signal.Stop(c1)

	println("Type Ctrl-C in 5 seconds")
	time.Sleep(time.Second*5)

	println("All done")
$ signalnotify.exe
Type Ctrl-C in 5 seconds
second signal
first signal
Type Ctrl-C in 5 seconds
first signal
Type Ctrl-C in 5 seconds

3回 Ctrl-C を押下した時、Notify をすべて解除した時はプロセスが殺されている( All done が表示されない )。

では、最初に signal.Ignore を設定してはどうか?

	signal.Ignore(os.Interrupt)
	signal.Notify(c1, os.Interrupt)
	signal.Notify(c2, os.Interrupt)

	println("Type Ctrl-C in 5 seconds")
	time.Sleep(time.Second * 5)

	signal.Stop(c2)

	println("Type Ctrl-C in 5 seconds")
	time.Sleep(time.Second * 5)
	signal.Stop(c1)

	println("Type Ctrl-C in 5 seconds")
	time.Sleep(time.Second * 5)

	println("All done")
$ signalnotify.exe
Type Ctrl-C in 5 seconds
second signal
first signal
Type Ctrl-C in 5 seconds
first signal
Type Ctrl-C in 5 seconds

うん、ダメっぽい!

signal.Notify が一つでも残っていれば Ctrl-C で強制停止されないわけだから、まぁ、大丈夫と見てよいかな…

(一つも signal.Notify が設定されていない状態というのは、シェルのメイン処理ループ外にいるということなので。そういうタイミングはほとんどない)

処理しない signal.Notify を用意してもいいけど、そちらの channel が Ctrl-C で満タンになると panic になるので、消し込むためだけの goroutine が必要になって、もったいない。

対処法
scoop/shimsではなくscoop/apps以下の .exe を指定する

これをやらなくても、落ちなくならなくなっているだろうか?

いけてそうな感じではあるけれども、疑似テスト(自分で、~/scoop/app/nyagos/4.4.8_0/nyagos を差し替えた)結果なので、まぁ、直ったとは断言できぬ状況

落ちなくはなったけど、more がエコーバックしないという問題がまだ残っていた。

どうも、more を実行する前に ls を実行していたりすると、SetConsoleMode がエラー終了していることが分かった。成功する時は

windows.SetConsoleMode(標準入力のコンソールハンドル,0x7)

となっているが、失敗する時は次のようになっていた。

windows.SetConsoleMode(標準入力のコンソールハンドル,0x5)

なので今まで UNIX の Cocked Mode 相当に遷移する際:

  • 0x1 = ENABLE_PROCESSED_INPUT
  • 0x4 = ENABLE_ECHO_INPUT

だけを明示的にセットしていたのを

  • 0x2 = ENABLE_LINE_INPUT

も合わせてセットしなければいけない(組み合わせ上認められない)ということになりそうだ。

more がエコーバックしないという問題は、前述の方法で解決した。
だが、2回目以降の more で、なぜか先頭行にいきなり空行が入ってしまうという問題が…なぜ?

詳しくは Ctrl-C kills nyagos.exe itself when more runs without redirect · Issue #342 · zetamatta/nyagos 以下に書きましたが

  • context.WithCancel の .Done() と signal のチャネルを同じ場所で拾わせる
  • signal.Notify ~ signal.Stop の範囲を見直し(シグナルを拾い損ねる隙をなくす)
  • context.WithCancel のキャンセルが来ても、外部プロセスを kill させず、ターミナルからの各プロセスへの sigint の送信に任せる

ようにしたところ、Ctrl-C まわりの諸問題は一応解決したようです。これらの対応はnyagos 4.4.9_2 に反映されています。

本スクラップはこれにてクローズしたいと思います。ありがとうございました。

このスクラップは2021/01/10にクローズされました
ログインするとコメントできます