packages feed

arbtt-0.12: src/TimeLog.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE PatternGuards #-}
module TimeLog where

import Data

import Control.Applicative
import System.IO
import Control.Concurrent
import Control.Monad
import Data.Time
import Data.Binary
import Data.Binary.StringRef
import Data.Binary.Get
import Data.Function
import Data.Char
import System.Directory
import Control.DeepSeq
#ifndef mingw32_HOST_OS
import System.Posix.Files
#endif
import System.IO.Unsafe (unsafeInterleaveIO)

import qualified Data.ByteString.Lazy as BS
import Data.Maybe

magic = BS.pack $ map (fromIntegral.ord) "arbtt-timelog-v1\n"

mkTimeLogEntry :: Integer -> a -> IO (TimeLogEntry a)
mkTimeLogEntry delay entry = do
    date <- getCurrentTime
    return $ TimeLogEntry date delay entry


-- | Runs the given action each delay milliseconds and appends the TimeLog to the
-- given file.
runLogger :: ListOfStringable a => FilePath -> Integer -> IO a -> IO ()
runLogger filename delay action = flip fix Nothing $ \loop prev -> do
        entry <- action
        tle <- mkTimeLogEntry delay entry

        createTimeLog False filename
#ifndef mingw32_HOST_OS
        setFileMode filename (ownerReadMode `unionFileModes` ownerWriteMode)
#endif
        appendTimeLog filename prev tle
        threadDelay (fromIntegral delay * 1000)
        loop (Just entry)


createTimeLog :: Bool -> FilePath -> IO ()
createTimeLog force filename = do
        ex <- doesFileExist filename
        when (not ex || force) $ BS.writeFile filename magic

appendTimeLog :: ListOfStringable a => FilePath -> Maybe a -> TimeLogEntry a -> IO ()
appendTimeLog filename prev = BS.appendFile filename . ls_encode strs
  where strs = maybe [] listOfStrings prev

writeTimeLog :: ListOfStringable a => FilePath -> TimeLog a -> IO ()
writeTimeLog filename tl = do
        createTimeLog True filename
        foldM_ go  Nothing tl
  where go prev v = do appendTimeLog filename prev v
                       return (Just (tlData v))

-- | This might be very bad style, and it hogs memory, but it might help in some situations...
-- Use of unsafeInterleaveIO should be replaced by conduit, pipe or something the like
recoverTimeLog :: ListOfStringable a => FilePath -> IO (TimeLog a)
recoverTimeLog filename = do
        content <- BS.readFile filename
        start content
  where start content = case runGetOrFail (getLazyByteString (BS.length magic)) content of
            Right (rest, off, startString)
                | startString /= magic -> do
                    putStrLn $ "WARNING: Timelog starts with unknown marker " ++
                        show (map (chr.fromIntegral) (BS.unpack startString))
                    go Nothing rest off
                | otherwise -> do
                    putStrLn $ "Found header, continuing... (" ++ show (BS.length rest) ++ " bytes to go)"
                    go Nothing rest off
            Left _ -> do
                    putStrLn $ "WARNING: Timelog file shorter than a header marker"
                    return []

        go prev input off = do
                mb <- trySkip prev input off off
                flip (maybe (return [])) mb $ \(v,rest,off') ->
                        if BS.null rest
                        then return [v]
                        else (v:) <$> (unsafeInterleaveIO $ go (Just (tlData v)) rest off')

        trySkip prev input off orig_off
            | Just i <- BS.findIndex validTimeLogEntryTag input = do
               when (i > 0) $ do
                   putStrLn $ "At position " ++ show off ++ " skipping " ++ show i ++ " bytes to next valid TimeLogEntry tag"
               tryGet prev (BS.drop i input) (off + i) orig_off
            | otherwise = do
               putStrLn "No valid TimeLogEntry tag bytes remaining"
               return Nothing

        tryGet prev input off orig_off = case runGetOrFail (ls_get strs) input of
            Right (rest, off', v) -> do
                when (off /= orig_off) $
                    putStrLn $ "Skipped from " ++ show orig_off ++ ", succesful read at position " ++ show off ++ ", lost " ++ show (off - orig_off) ++ " bytes."
                return (Just (v, rest, off + off'))
            Left (_, _, e) -> do
                putStrLn $ "Failed to read value at position " ++ show off ++ ":"
                putStrLn $ "   " ++ e
                if BS.length input <= 1
                    then do putStrLn $ "End of file reached"
                            return Nothing
                    else do trySkip prev (BS.tail input) (off+1) orig_off
          where strs = maybe [] listOfStrings prev

readTimeLog :: (NFData a, ListOfStringable a) => FilePath -> IO (TimeLog a)
readTimeLog filename = do
        content <- BS.readFile filename
        return $ parseTimeLog content

parseTimeLog :: (NFData a, ListOfStringable a) => BS.ByteString -> TimeLog a
parseTimeLog input =
    if startString == magic
       then go Nothing rest off
       else error $
            "Timelog starts with unknown marker " ++
            show (map (chr.fromIntegral) (BS.unpack startString))
  where
    (startString, rest, off) = case runGetOrFail (getLazyByteString (BS.length magic)) input of
        Right (rest, off, x) -> (x, rest, off)
        Left (_, off, e) -> error $ "Timelog parse error at " ++ show off ++ ": " ++ e
    go prev input off
        | BS.null input = []
        | otherwise = case runGetOrFail (ls_get strs) input of
            Right (rest, off', v) ->
                v `deepseq` v : go (Just (tlData v)) rest (off + off')
            Left (_, off', e) ->
                error $ "Timelog parse error at " ++ show (off + off') ++ ": " ++ e
      where strs = maybe [] listOfStrings prev