{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Library exposing internal functions uses by 'Eval.Worker'
-- useful work writing your own workers
module System.Restricted.Worker.Internal
    (
      -- * Worker related
      killWorker
    , workerAlive
    , workerTimeout
    , forkWorker
      -- * Connection related
    , connectToWorker
    , mkSock
      -- * Useful util functions
    , removeFileIfExists
    , processAlive
    ) where

import Control.Concurrent             (threadDelay)
import Control.Exception              (IOException, catch, handle, throwIO)
import Control.Monad                  (void, when)
import Data.Maybe                     (fromJust)
import Network                        (PortID (..), Socket, connectTo, listenOn)
import Network.Socket                 (close)
import System.Directory               (removeFile)
import System.IO                      (Handle)
import System.IO.Error                (isDoesNotExistError, isPermissionError)
import System.Mem.Weak                (addFinalizer)
import System.Posix.IO                (dupTo, handleToFd)
import System.Posix.Process           (forkProcess, getProcessStatus)
import System.Posix.Signals           (Handler (..), installHandler,
                                       killProcess, processStatusChanged,
                                       setStoppedChildFlag, signalProcess)
import System.Posix.Types             (Fd (..), ProcessID)

import System.Restricted.Limits
import System.Restricted.Worker.Types

-- | Connect to the worker's socket and return a handle
connectToWorker :: Worker a -> IO Handle
connectToWorker Worker{..} = connectTo "localhost" (UnixSocket workerSocket)

-- | Remove a file if it exists. Should be thread-safe.
removeFileIfExists :: FilePath -> IO ()
removeFileIfExists f = removeFile f `catch` handleE
  where handleE e
            | isDoesNotExistError e = return ()
            | isPermissionError   e = return ()
            | otherwise             =  putStrLn ("removeFileIfExists " ++ show e)
                                    >> throwIO e

-- | Create a new unix socket
mkSock :: FilePath -> IO Socket
mkSock sf = do
    removeFileIfExists sf
    listenOn (UnixSocket sf)

-- | Fork a worker process
forkWorker :: Worker a
           -> Maybe (IO Handle)  -- ^ Where to redirect stdout
           -> (Socket -> IO ())  -- ^ Callback funcion
           -> IO ProcessID
forkWorker Worker{..} out cb = do
    _ <- setStoppedChildFlag True
    _ <- installHandler processStatusChanged Ignore Nothing
    soc <- mkSock workerSocket
    addFinalizer soc (close soc)
    forkProcess $ do
        _ <- setStoppedChildFlag False
        _ <- installHandler processStatusChanged Default Nothing
        setLimits workerLimits
        case out of
            Nothing -> return ()
            Just x  -> do
                fd <- handleToFd =<< x
                void $ dupTo fd (Fd 1)
        cb soc



-- | Kill a worker. Takes an initialized worker,
-- returns non-initialized one.
killWorker :: Worker a -> IO (Worker a)
killWorker w@Worker{..} = do
    when (initialized w) $ do
        alive <- processAlive (fromJust workerPid)
        when alive $ do
            signalProcess killProcess (fromJust workerPid)
            tc <- getProcessStatus False False (fromJust workerPid)
            case tc of
                Just _  -> return ()
                Nothing -> signalProcess killProcess (fromJust workerPid)
    return (w { workerPid = Nothing })

-- | Waits for a certain period of time
-- and then kills the worker
workerTimeout :: Worker a -- ^ ID of a process to be killed
              -> Int      -- ^ Time limit (in seconds)
              -> IO (Worker a)
workerTimeout w lim = do
  threadDelay (lim * 1000000)
  killWorker w


-----------------------

-- | Checks whether the process is alive
-- /hacky/
processAlive :: ProcessID -> IO Bool
processAlive pid = handle (\(_ :: IOException) -> return False) $ do
    _ <- getProcessStatus False False pid
    return True

-- | Checks whether the worker is alive
workerAlive :: Worker a -> IO Bool
workerAlive w = do
    case (workerPid w) of
        Nothing  -> return False
        Just pid -> processAlive pid