module Mueval.Parallel where

import Control.Concurrent (forkIO, killThread, myThreadId, threadDelay, throwTo, ThreadId)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar, MVar)
import Control.Exception.Extensible as E (ErrorCall(..),SomeException,catch)
import Control.Monad (void)
import System.IO (hSetBuffering, stdout, BufferMode(NoBuffering))
import System.Posix.Signals (sigXCPU, installHandler, Handler(CatchOnce))

import Mueval.Interpreter
import Mueval.ArgsParse

-- | Fork off a thread which will sleep and then kill off the specified thread.
watchDog :: Int -> ThreadId -> IO ()
watchDog tout tid = do _ <- installHandler sigXCPU
                                           $ throwTo tid $ ErrorCall "Time limit exceeded.") Nothing
                       _ <- forkIO $ do
                                   threadDelay (tout * 700000)
                                   -- Time's up. It's a good day to die.
                                   throwTo tid (ErrorCall "Time limit exceeded")
                                   killThread tid -- Die now, srsly.
                                   error "Time expired"
                       return () -- Never reached. Either we error out here
                                 -- or the evaluation thread finishes.

-- | A basic blocking operation.
block :: (t -> MVar a -> IO t1) -> t -> IO a
block f opts = do  mvar <- newEmptyMVar
                   _ <- f opts mvar
                   takeMVar mvar -- block until ErrorCall, or forkedMain succeeds

-- | Using MVars, block on 'forkedMain' until it finishes.
forkedMain :: Options -> IO ()
forkedMain opts = void (block forkedMain' opts)

-- | Set a 'watchDog' on this thread, and then continue on with whatever.
forkedMain' :: Options -> MVar String -> IO ThreadId
forkedMain' opts mvar = do mainId <- myThreadId
                           watchDog (timeLimit opts) mainId
                           hSetBuffering stdout NoBuffering

                           -- Our modules and expression are set up. Let's do stuff.
                           forkIO $ (interpreterSession (checkImport opts)
                                                            >> putMVar mvar "Done.")
                                      `E.catch` \e -> throwTo mainId (e::SomeException)
                                                             -- bounce exceptions to the main thread,
                                                             -- so they are reliably printed out
          where checkImport x = if noImports x then x{modules=Nothing} else x