ztail-1.1: TailHandle.hs
{-# LANGUAGE CPP, DeriveDataTypeable, ForeignFunctionInterface #-}
module TailHandle (
runTail
) where
import Control.Monad
import System.Posix.Types
import System.Posix.Files
import System.Posix.Directory
import System.Posix.IO
import System.IO.Error
import Data.List (genericTake, genericLength)
import Data.Maybe
import qualified Data.Set as Set
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 Foreign.Ptr (Ptr)
import Foreign.C.Types (CInt(..))
import Foreign.C.Error (throwErrnoIfNull)
import qualified Unsafe.Coerce
import System.FilePath ((</>))
import Util
import TailTypes
data TailHandle = TailHandle
{ thTail :: Tail
, thRuntime :: TailRuntime
, thPoll :: ThreadId
, thReopen :: Maybe ThreadId
, thFd :: Maybe Fd
, thPos :: FileOffset -- or -1 for blocking fd
, thIno :: Maybe FileID
, thBuf :: String
, thDirStream :: Maybe DirStream
, thDirList :: Set.Set FilePath
, thAgain :: Bool
#ifdef INOTIFY
, thPollWatch :: Maybe INotify.WatchDescriptor
, thReopenWatch :: Maybe INotify.WatchDescriptor
#endif
}
data TailSignal
= SignalPoll
| SignalReopen
| SignalInsert String Bool
| SignalDelete String
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
#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
, thPos = pos
, thTail = Tail
{ tailTarg = TailPath path
, tailPollINotify = ipoll
, tailReopenINotify = ireopen
} } = do
let sig (INotify.Modified {}) = Just SignalPoll
sig (INotify.MovedSelf {}) = Just SignalReopen
sig (INotify.MovedOut { INotify.filePath = f }) | notdot f = Just $ SignalDelete f
sig (INotify.MovedIn { INotify.filePath = f }) | notdot f = Just $ SignalInsert f False
sig (INotify.Created { INotify.filePath = f }) | notdot f = Just $ SignalInsert f True
sig (INotify.Deleted { INotify.filePath = f }) | notdot f = Just $ SignalDelete f
sig _ = Nothing
notdot "" = False
notdot ('.':_) = False
notdot _ = True
add l = INotify.addWatch inotify l path (whenJust (Control.Exception.throwTo tid) . sig)
poll <- justWhen (ipoll && pos >= 0) $
if isJust (thDirStream th)
then add [INotify.OnlyDir, INotify.Move, INotify.Create, INotify.Delete]
else add [INotify.Modify]
reopen <- justWhen ireopen $ add [INotify.MoveSelf, INotify.DeleteSelf]
return th{
thPollWatch = poll,
thReopenWatch = reopen
}
#endif
inotifyTail th = return th
foreign import ccall unsafe fdopendir :: CInt -> IO (Ptr ())
fdOpenDirStream :: Fd -> IO DirStream
fdOpenDirStream (Fd d) = (Unsafe.Coerce.unsafeCoerce :: Ptr () -> DirStream) =.< throwErrnoIfNull "fdOpenDirStream" (fdopendir d)
readDirStreamAll :: DirStream -> IO [FilePath]
readDirStreamAll d = readDirStream d >>= c where
c [] = return []
c ('.':_) = readDirStreamAll d
c f = (f :) =.< readDirStreamAll d
subTail :: TailHandle -> Bool -> FilePath -> IO ()
subTail th@TailHandle{ thRuntime = tr, thTail = t@Tail{ tailTarg = TailPath p } } new f | tailDirTail t || tailDirList t =
trAddTail tr t
{ tailTarg = TailPath (p </> f)
, tailFileTail = tailDirTail t
, tailDirList = tailDirList t && tailDirRecursive t
, tailDirTail = tailDirTail t && tailDirRecursive t
, tailBegin = tailBegin t || new
}
subTail _ _ _ = nop
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
inotifyTail =<< go fd =<< getFdStatus fd
go fd stat
| isRegularFile stat =
seekTail (if pos < 0 then max 0 $ sz + 1 + pos else min sz pos) th'
| isNamedPipe stat || isSocket stat || isCharacterDevice stat =
return th'{ thPos = -1 }
| isDirectory stat && (tailDirList (thTail th) || tailDirTail (thTail th)) = do
ds <- fdOpenDirStream fd
dl <- readDirStreamAll ds
let nl = genericTake (if pos < 0 then genericLength dl + 1 + pos else pos) dl
th'' = th'{ thDirStream = Just ds, thDirList = Set.fromList nl }
mapM_ (subTail th'' False) nl
return th''
| otherwise =
closeFd fd >> thErrMsg th "Unsupported file type" >. th
where
th' = th{ thFd = Just fd, thIno = Just (fileID stat), thAgain = True, thPos = 0 }
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
insertMsg :: String -> String
insertMsg = ('+':)
deleteMsg :: String -> String
deleteMsg = ('-':)
readTail :: TailHandle -> IO (TailHandle, [String])
readTail th@TailHandle{ thFd = Nothing } = return (noRead th)
readTail th@TailHandle{ thDirStream = Just ds } = do
dl <- rewindDirStream ds >> readDirStreamAll ds
let df f (n, o, s)
| Set.member f o = (n, Set.delete f o, s)
| otherwise = (f : n, o, Set.insert f s)
(nl, os, ds) = foldr df ([], thDirList th, thDirList th) dl
mapM_ (subTail th True) nl
return (th{ thDirList = Set.difference ds os, thAgain = False },
guard (tailDirList (thTail th)) >> map insertMsg nl ++ map deleteMsg (Set.toList os))
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{ thAgain = True } = yield
waitTail TailHandle{ thTail = Tail{ tailPollInterval = i } }
| i == 0 = pause
| otherwise = threadDelay (fromInterval i)
reopenThread :: Int -> ThreadId -> IO ()
reopenThread ri tid = forever $ do
threadDelay ri
Control.Exception.throwTo tid SignalReopen
newTail :: TailRuntime -> Tail -> IO TailHandle
newTail tr tail = do
tid <- myThreadId
rid <- justWhen (ri /= 0) $ forkIO (reopenThread ri tid)
return TailHandle
{ thTail = tail
, thRuntime = tr
, thPoll = tid
, thReopen = rid
, thFd = Nothing
, thPos = if tailBegin tail then 0 else -1
, thIno = Nothing
, thBuf = []
, thDirStream = Nothing
, thDirList = Set.empty
, thAgain = True
#ifdef INOTIFY
, thPollWatch = Nothing
, thReopenWatch = Nothing
#endif
}
where
ri = fromInterval (tailReopenInterval tail)
runTail :: TailRuntime -> Tail -> IO ()
runTail tr tail = Control.Exception.mask $ \unmask ->
let
catch th SignalReopen = reopenTail th -- >>= wait
catch th SignalPoll = return th{ thAgain = True }
catch th@TailHandle{ thDirStream = Just _, thDirList = l, thTail = t } (SignalInsert f new) = do
subTail th new f
if tailDirList t
then proc th' [insertMsg f]
else return th'
where th' = th{ thDirList = Set.insert f l }
catch th@TailHandle{ thDirStream = Just _, thDirList = l, thTail = t } (SignalDelete f) =
if tailDirList t
then proc th' [deleteMsg f]
else return th'
where th' = th{ thDirList = Set.delete f l }
wait th = Control.Exception.catch
(trUnlock tr $ unmask $ waitTail th >. th)
(catch th >=> wait)
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 >>= uncurry proc >>= poll
proc th s = mapM_ fun s >. th
fun = (trText tr) tail
in
newTail tr tail >>=
openTail >>= go >>=
\TailHandle{ thReopen = wid } -> whenJust killThread wid