packages feed

ztail-1.0.1: TailHandle.hs

{-# LANGUAGE CPP, DeriveDataTypeable #-}
module TailHandle (
  runTail
  ) where

import Control.Monad
import System.Posix.Types
import System.Posix.Files
import System.Posix.IO
import System.IO.Error
import qualified Data.Maybe
import GHC.IO.Handle (SeekMode(AbsoluteSeek))
import Control.Concurrent
import qualified Control.Exception
import qualified Data.Typeable
#ifdef INOTIFY
import qualified System.INotify as INotify
#endif
import Data.IORef

import Util
import TailTypes

data TailHandle = TailHandle{
  thTail :: Tail,
  thRuntime :: TailRuntime,
  thPoll :: ThreadId,
  thReopen :: Maybe ThreadId,
  thFd :: Maybe Fd,
  thPos :: FileOffset,
  thIno :: Maybe FileID,
  thBuf :: String,
  thAgain :: Bool,
#ifdef INOTIFY
  thPollWatch :: Maybe INotify.WatchDescriptor,
  thReopenWatch :: Maybe INotify.WatchDescriptor,
#endif
  thSigPending :: IORef (Maybe TailSignal),
  thSigHandler :: IO () -> IO ()
}

data TailSignal = PollSignal | ReopenSignal deriving (Show, Data.Typeable.Typeable, Eq, Ord)
instance Control.Exception.Exception TailSignal

thErrMsg = tailErrMsg . thTail

catchDoesNotExist :: IO a -> IO (Maybe a)
catchDoesNotExist f = catchWhen isDoesNotExistError (liftM Just f) (return Nothing)

bad = ioError . userError

closeTail :: TailHandle -> IO TailHandle
closeTail th@TailHandle{ thFd = Nothing } = return th
closeTail th@TailHandle{ thFd = Just fd } = do
#ifdef INOTIFY
  whenJust rm_watch (thPollWatch th)
  whenJust rm_watch (thReopenWatch th)
#endif
  closeFd fd
  return th{
    thFd = Nothing
  , thPos = 0
  , thIno = Nothing
#ifdef INOTIFY
  , thPollWatch = Nothing
  , thReopenWatch = Nothing 
#endif
  }
#ifdef INOTIFY
  where
    rm_watch = INotify.removeWatch (Data.Maybe.fromJust (trINotify (thRuntime th)))
#endif

seekTail :: FileOffset -> TailHandle -> IO TailHandle
seekTail _ TailHandle{ thFd = Nothing } = bad "seek on closed fd"
seekTail c th@TailHandle{ thFd = Just fd } =
  fdSeek fd AbsoluteSeek c >. th{ thPos = c }

inotifyTail :: TailHandle -> IO TailHandle
#ifdef INOTIFY
inotifyTail th@TailHandle{ 
    thRuntime = TailRuntime{ trINotify = Just inotify },
    thPoll = tid,
    thTail = Tail{
      tailTarg = TailPath path,
      tailPollINotify = ipoll,
      tailReopenINotify = ireopen
    } } = do
  poll <- justWhen ipoll $
    INotify.addWatch inotify [INotify.Modify] path 
      (\_ -> Control.Exception.throwTo tid PollSignal)
  reopen <- justWhen ireopen $
    INotify.addWatch inotify [INotify.MoveSelf] path 
      (\_ -> Control.Exception.throwTo tid ReopenSignal)
  return th{
    thPollWatch = poll,
    thReopenWatch = reopen
  }
#endif
inotifyTail th = return th

openTail :: TailHandle -> IO TailHandle
openTail th@TailHandle{ thFd = Nothing } = get (tailTarg (thTail th)) where
  get (TailFd fd) = got (Just fd)
  get (TailPath path) = got =<<
    catchDoesNotExist (
      openFd path ReadOnly Nothing OpenFileFlags{
	append = False, exclusive = False, noctty = False, nonBlock = True, trunc = False
      })
  got Nothing = thErrMsg th "No such file or directory" >. th{ thPos = 0 }
  got (Just fd) = do
    setFdOption fd NonBlockingRead True
    go fd =<< getFdStatus fd
  go fd stat
    | isBlockDevice stat || isDirectory stat || isSymbolicLink stat =
      closeFd fd >> bad "unsupported file type"
    | isRegularFile stat =
      inotifyTail th' >>= seekTail (if pos < 0 then max 0 $ sz + 1 + pos else min sz pos)
    | otherwise =
      return th'{ thPos = -1 }
    where
      th' = th{ thFd = Just fd, thIno = Just (fileID stat), thAgain = True }
      sz = fileSize stat
      pos = thPos th
openTail _ = bad "open on opened tail"

reopenTail :: TailHandle -> IO TailHandle
reopenTail th@TailHandle{ thTail = Tail{ tailTarg = TailPath path }, thIno = ino } = do
  stat <- catchDoesNotExist $ getFileStatus path
  case stat of
    Nothing -> return th
    Just _ | ino == Nothing -> openTail th
    Just stat | ino == Just (fileID stat) -> return th
    Just stat | fileSize stat == 0 -> return th
    _ -> do
      thErrMsg th "Following new file"
      closeTail th >>= openTail
reopenTail th = return th

noRead :: TailHandle -> (TailHandle, [String])
noRead th = (th{ thAgain = False }, [])

bufsiz :: ByteCount
bufsiz = 8192

readTail :: TailHandle -> IO (TailHandle, [String])
readTail th@TailHandle{ thFd = Nothing } = return (noRead th)
readTail th@TailHandle{ thFd = Just fd, thPos = pos } =
  if pos == -1
    then catchWhen isEOFError readsock $ do
      checkbuf th >.= noRead
      -- thErrMsg th "closed?"
      -- closeTail th >.= noRead th
    else getFdStatus fd >.= fileSize >>= gotlen
  where
    checkbuf th@TailHandle{ thBuf = buf } = do
      when (buf /= "") $
	thErrMsg th ("Unterminated line: " ++ buf)
      return th{ thBuf = "" }
    gotlen len
      | len < pos = do
          thErrMsg th ("File truncated to " ++ show len)
	  seekTail 0 th >>= readTail
      | len == pos = do
	  checkbuf th >.= noRead
      | otherwise = do
	  let count = min (fromIntegral (len - pos)) bufsiz
	  (buf, buflen) <- readit (fromIntegral count)
	  when (buflen /= count) $
	    thErrMsg th ("Short read (" ++ show buflen ++ "/" ++ show count ++ ")")
	  return $ gotbuf th{ thPos = pos + fromIntegral buflen, thAgain = buflen == bufsiz } buf
    readsock =
      readit bufsiz >.= fst >.= gotbuf th{ thAgain = True }
    gotbuf th "" = noRead th
    gotbuf th@TailHandle{ thBuf = oldbuf } buf =
      case initlast $ split (== '\n') buf of
	([], r) ->
	  (th{ thBuf = oldbuf ++ r }, [])
	(l1 : l, r) ->
	  (th{ thBuf = r }, (oldbuf ++ l1) : l)
    readit len = catchWhen isFullError
      (fdRead fd len)
      (return ("", 0))

pause :: IO ()
pause = threadDelay (30*60*1000000) -- fixme how? (-1) doesn't work

waitTail :: TailHandle -> IO ()
waitTail TailHandle{ thFd = Nothing } = pause
waitTail TailHandle{ thFd = Just fd, thPos = -1 } = threadWaitRead fd
waitTail TailHandle{ thTail = t, thAgain = again } = do
  if again
    then yield 
    else if i == 0
      then pause
      else threadDelay i
  where 
    i = fromInterval (tailPollInterval t)

reopenThread :: Int -> ThreadId -> IO ()
reopenThread ri tid = forever $ do
  threadDelay ri
  Control.Exception.throwTo tid ReopenSignal

newTail :: TailRuntime -> Tail -> IO TailHandle
newTail tr tail = do
  tid <- myThreadId
  rid <- justWhen (ri /= 0) $ forkIO (reopenThread ri tid)
  sigpend <- newIORef Nothing
  return TailHandle{
    thTail = tail,
    thRuntime = tr,
    thPoll = tid,
    thReopen = rid,
    thFd = Nothing,
    thPos = if tailBegin tail then 0 else -1,
    thIno = Nothing,
    thBuf = [],
    thAgain = True,
#ifdef INOTIFY
    thPollWatch = Nothing,
    thReopenWatch = Nothing,
#endif
    thSigPending = sigpend,
    thSigHandler = Control.Exception.handle (\s ->
      modifyIORef sigpend (max (Just s)))
  }
  where 
    ri = fromInterval (tailReopenInterval tail)

runTail :: TailRuntime -> Tail -> IO ()
runTail tr tail = Control.Exception.block $
  newTail tr tail >>=
  openTail >>= go >>= 
  \TailHandle{ thReopen = wid } -> whenJust killThread wid
  where
    catch th ReopenSignal = reopenTail th -- >>= wait
    catch th PollSignal = return th
    wait th = do
      sig <- readIORef (thSigPending th)
      writeIORef (thSigPending th) Nothing
      maybe 
	(Control.Exception.catch
	  (trUnlock tr $ waitTail th >. th) 
	  (catch th))
	(catch th)
	sig
    poll th
      | thReopen th == Nothing
	&& (thFd th == Nothing
	  || (thAgain th == False && thPos th /= -1
	    && fromInterval (tailPollInterval (thTail th)) == 0
#ifdef INOTIFY
	    && thPollWatch th == Nothing 
	    && thReopenWatch th == Nothing
#endif
	    ))
	= return th
      | otherwise = wait th >>= go
    go th = readTail th >>= proc >>= poll
    proc (th, s) = mapM_ (thSigHandler th . fun) s >. th
    fun = (trText tr) tail