packages feed

netlines-1.0.0: Data/Enumerator/NetLines/IO.hs

-- |
-- Module:     Data.Enumerator.NetLines.IO
-- Copyright:  (c) 2011 Ertugrul Soeylemez
-- License:    BSD3
-- Maintainer: Ertugrul Soeylemez <es@ertes.de>
--
-- Efficient input and output with timeouts.

{-# LANGUAGE ScopedTypeVariables #-}

module Data.Enumerator.NetLines.IO
    ( -- * Enumerators (input)
      enumHandleSession,
      enumHandleTimeout,

      -- * Iteratees (output)
      iterHandleTimeout
    )
    where

import Control.ContStuff as Monad
import Data.Enumerator as E
import Data.Enumerator.NetLines.Class as NL
import Data.Enumerator.NetLines.Error
import Data.Time.Clock
import System.IO
import System.IO.Error as IO
import System.Timeout


-- | Enumerate from a handle with the given buffer size (first
-- argument), read timeout in milliseconds (second argument) and session
-- timeout in milliseconds (third argument).  If either timeout is
-- exceeded a 'TimeoutError' exception is thrown via 'throwError'.

enumHandleSession ::
    forall b m str. (MonadIO m, Splittable str) =>
    Int -> Int -> Int -> Handle -> Enumerator str m b
enumHandleSession bufSize readTime sessionTime h step = do
    startTime <- liftIO getCurrentTime
    loop startTime step

    where
    loop :: UTCTime -> Enumerator str m b
    loop startTime (Continue k) = do
        now <- liftIO getCurrentTime
        let timeoutErr = TimeoutError "Read timeout"
            diff = sessionTime - round (1000 * diffUTCTime now startTime)
            timeout = min diff readTime
        when (timeout <= 0) $ throwError timeoutErr
        mHaveInput <- liftIO $ IO.try (hWaitForInput h timeout)
        case mHaveInput of
          Left err
              | isEOFError err -> continue k
              | otherwise      -> throwError err
          Right False -> throwError timeoutErr
          Right True  -> do
              str <- tryIO $ NL.hGetNonBlocking h bufSize
              if NL.null str
                then continue k
                else k (Chunks [str]) >>== loop startTime
    loop _ step = returnI step


-- | Enumerate from a handle with the given buffer size (first argument)
-- and timeout in milliseconds (second argument).  If the timeout is
-- exceeded a 'TimeoutError' exception is thrown via 'throwError'.
--
-- Note that this timeout is not a timeout for the whole enumeration,
-- but for each individual read operation.  In other words, this timeout
-- protects against dead/unresponsive peers, but not against (perhaps
-- intentionally) slowly sending peers.

enumHandleTimeout ::
    forall b m str. (MonadIO m, Splittable str) =>
    Int -> Int -> Handle -> Enumerator str m b
enumHandleTimeout bufSize timeout h = loop
    where
    loop :: Enumerator str m b
    loop (Continue k) = do
        mHaveInput <- liftIO $ IO.try (hWaitForInput h timeout)
        case mHaveInput of
          Left err
              | isEOFError err -> continue k
              | otherwise      -> throwError err
          Right False -> throwError $ TimeoutError "Read timeout"
          Right True  -> do
              str <- tryIO $ NL.hGetNonBlocking h bufSize
              if NL.null str
                then continue k
                else k (Chunks [str]) >>== loop
    loop step = returnI step


-- | Writes its inputs to the given handle.  Times out after the given
-- number of milliseconds with a 'TimeoutError' iteratee exception.  The
-- handle should be unbuffered and in binary mode.  See 'hSetBuffering'
-- and 'hSetBinaryMode'.
--
-- Please note that only the write operations themselves are timed.
-- Most notably the operation of the data source enumerator is *not*
-- timed.  Hence the operation may time out later than the given time
-- margin, but never earlier.

iterHandleTimeout ::
    forall m str. (MonadIO m, Splittable str) =>
    Int -> Handle -> Iteratee str m ()
iterHandleTimeout maxTime h = do
    startTime <- tryIO getCurrentTime
    continue (loop startTime)

    where
    loop :: UTCTime -> Stream str -> Iteratee str m ()
    loop _ EOF = yield () EOF
    loop startTime (Chunks []) = continue (loop startTime)
    loop startTime (Chunks strs) = do
        let timeoutErr = TimeoutError "Write timeout"
        now <- tryIO getCurrentTime
        let restMs = 1000*maxTime - round (1000000 * diffUTCTime now startTime)
        when (restMs <= 0) (throwError timeoutErr)
        tryIO (timeout restMs (mapM_ (NL.hPutStr h) strs)) >>=
            maybe (throwError timeoutErr) return
        continue (loop startTime)