Catching Control-C исключение в GHC (Haskell)

Я построил очень простой цикл read-eval-print в Haskell, который ловит Control-C (UserInterrupt). Однако всякий раз, когда я компилирую и запускаю эту программу, она всегда захватывает первый Control-C и всегда прерывает второй Control-C с кодом выхода 130. Неважно, сколько строк ввода я даю ему до и между двумя Control-Cs, так всегда бывает. Я знаю, что я должен пропустить что-то простое... пожалуйста, помогите, спасибо!

Примечание: это с исключениями base-4, поэтому Control.Exception и не Control.OldException.

import Control.Exception as E
import System.IO

main :: IO ()
main = do hSetBuffering stdout NoBuffering
          hSetBuffering stdin NoBuffering
          repLoop

repLoop :: IO ()
repLoop
  = do putStr "> "
       line <- interruptible "<interrupted>" getLine
       if line == "exit"
          then putStrLn "goodbye"
          else do putStrLn $ "input was: " ++ line
                  repLoop

interruptible :: a -> IO a -> IO a
interruptible a m
  = E.handleJust f return m
  where
    f UserInterrupt
      = Just a
    f _
      = Nothing

Ответы

Ответ 1

Вэй Ху правильно; система времени исполнения Haskell преднамеренно прерывает программу при нажатии второго элемента управления-C. Чтобы получить такое поведение, можно ожидать:

import Control.Exception as E
import Control.Concurrent
import System.Posix.Signals

main = do
  tid <- myThreadId
  installHandler keyboardSignal (Catch (throwTo tid UserInterrupt)) Nothing
  ... -- rest of program

Ответ 2

Отказ от ответственности: я не знаком с внутренними компонентами GHC, и мой ответ основан на grepping исходном коде, чтении комментариев и угадывании.

Определяемая вами функция main фактически обернута runMainIO, определенной в GHC.TopHandler (это подтверждается, посмотрев TcRnDriver.lhs):

-- | 'runMainIO' is wrapped around 'Main.main' (or whatever main is
-- called in the program).  It catches otherwise uncaught exceptions,
-- and also flushes stdout\/stderr before exiting.
runMainIO :: IO a -> IO a
runMainIO main = 
    do 
      main_thread_id <- myThreadId
      weak_tid <- mkWeakThreadId main_thread_id
      install_interrupt_handler $ do
           m <- deRefWeak weak_tid 
           case m of
               Nothing  -> return ()
               Just tid -> throwTo tid (toException UserInterrupt)
      a <- main
      cleanUp
      return a
    `catch`
      topHandler

И install_interrupt_handler определяется как:

install_interrupt_handler :: IO () -> IO ()
#ifdef mingw32_HOST_OS
install_interrupt_handler handler = do
  _ <- GHC.ConsoleHandler.installHandler $
     Catch $ \event -> 
        case event of
           ControlC -> handler
           Break    -> handler
           Close    -> handler
           _ -> return ()
  return ()
#else
#include "rts/Signals.h"
-- specialised version of System.Posix.Signals.installHandler, which
-- isn't available here.
install_interrupt_handler handler = do
   let sig = CONST_SIGINT :: CInt
   _ <- setHandler sig (Just (const handler, toDyn handler))
   _ <- stg_sig_install sig STG_SIG_RST nullPtr
     -- STG_SIG_RST: the second ^C kills us for real, just in case the
     -- RTS or program is unresponsive.
   return ()

В Linux stg_sig_install - это функция C, вызывающая sigaction. Параметр STG_SIG_RST переводится на SA_RESETHAND. В Windows все делается по-другому, что, вероятно, объясняет ja наблюдение.

Ответ 3

Самое надежное решение для меня (по крайней мере, в Linux) заключалось в установке обработчика сигналов с использованием System.Posix.Signals. Я надеялся на решение, которое не требовало бы этого, но настоящая причина, по которой я поставил вопрос, заключалась в том, что я хотел знать, почему GHC вел себя так, как это делал. Как объясняется в #haskell, вероятным объяснением является то, что GHC ведет себя таким образом, чтобы пользователь всегда мог контролировать приложение C, если оно зависает. Тем не менее, было бы неплохо, если бы GHC обеспечил способ повлиять на это поведение без метода с более низким уровнем, к которому мы прибегали:).