Ctrl-C 等の割り込みの扱い方です。
tl;dr
System.Win32.Console.CtrlHandler を使います。
Ctrl-C
が押されたらクロージングの処理を伴って終了するプログラムを書いてみます。
import Control.Concurrent import Control.Monad import System.Exit import System.IO import System.Win32.Console.CtrlHandler main :: IO () main = do tid <- myThreadId let handler event = do if event == cTRL_C_EVENT then do putStrLn "goodbye!" killThread tid pure True else pure False pHandler <- mkHandler handler success <- c_SetConsoleCtrlHandler pHandler True when (not success) $ do putStrLn "SetConsoleCtrlHandler failed" exitFailure let loop n = do putStr $ show n ++ ", " hFlush stdout threadDelay 1000000 loop (n+1) loop 0
実行してみましょう
> runhaskell Main.hs 0, 1, 2, 3, 4, 5, goodbye! Main.hs: thread killed
ちゃんと goodbye と出力されて終了しました!killThread
は例外を伴って終了するので Main.hs: thread killed
というメッセージが出てしまっています。もし気になるなら例外を握りつぶすか MVar
を使って終了を監視する仕組みを作るといいでしょう。
肝心の割り込みを制御する関数は c_SetConsoleCtrlHandler
です。
c_SetConsoleCtrlHandler :: PHANDLER_ROUTINE -> BOOL -> IO BOOL mkHandler :: Handler -> IO PHANDLER_ROUTINE type BOOL = Bool type Handler = CtrlEvent -> IO BOOL type PHANDLER_ROUTINE = FunPtr Handler
c_SetConsoleCtrlHandler
の第1引数にはコールバック関数(のポインター)を、第2引数には追加時に True
を、削除時に False
を指定します。
CtrlEvent
の値は予め用意されていて下記があります。
cTRL_C_EVENT :: CtrlEvent cTRL_BREAK_EVENT :: CtrlEvent
Handler
の返り値は、イベントを処理して次のハンドラーにイベントを伝播させないなら True
を、そうでないなら False
を指定します。上の例では handler
という関数にメッセージの表示とメインスレッドの停止の処理を書いて Handler
として渡していました。
この記事は HaskellでCtrl-Cを制御する - Qiita のオマージュです。
Help wanted
runhaskell を使うと確かにこの動作をするのですが、ビルドして実行バイナリーを作ると Ctrl-C を2回打たないと止まりません。原因と背景を説明できる方いませんか?
> stack exec windows-interruption-exe 0, 1, 2, 3, goodbye! goodbye! windows-interruption-exe.EXE: thread killed
あと、Ctrl-Break だとハンドルせず終了するはずだけどしないような……
ちゃんと C のドキュメント読もう。→ 読んだ。理解は合ってそう。(追記)
議論は Haskell-jp Slack で。参加方法はこちら。