Haskell で Ctrl-C を制御する(Windows)

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 で。参加方法はこちら