----------------------------------------------------------------------------- -- | -- Module : Control.Concurrent.TFile -- Copyright : Peter Robinson 2009 -- License : LGPL -- -- Maintainer : Peter Robinson -- Stability : experimental -- Portability : non-portable (requires STM) -- -- -- A transactional variable that writes its content to a file when updated. -- Due to the atomicity guarantees of the 'AdvSTM' monad, an update to a -- 'TFile' is only committed when the file operation succeeds. -- -- This module should be imported qualified. -- ----------------------------------------------------------------------------- module Control.Concurrent.TFile( -- * Data type TFile, -- -- * Construction -- newIO, -- new, new, newIO, newFromFileIO, newEmpty, newEmptyIO, newEmptyFromFileIO, -- * Operations read, write, clear, isEmpty, -- * Utilities basedir, ) where import Control.Concurrent.TBox import Data.Binary import Data.Typeable import Control.Concurrent.AdvSTM.TMVar import Control.Concurrent.AdvSTM import Control.Concurrent.AdvSTM.TVar import Control.Monad import Control.Monad.IfElse(unlessM) import Control.Exception import System.FilePath((),takeFileName) import System.Directory import System.IO.Error hiding(catch) import System.IO.Cautious(writeFileL) import Prelude hiding(lookup,catch,null,read,readIO,writeFile) import qualified Prelude as P import qualified Safe.Failure as Safe -------------------------------------------------------------------------------- -- | A transactional variable that writes its content to a file on each update. -- The file is created in directory \".\/_TFile\/\". -- -- * The 'onCommit' hook of the 'AdvSTM' monad guarantee that the updated memory content -- of the TFile is only visible to other threads /iff/ the file has been written -- successfully. -- -- * If the 'TFile' is \"dirty\", the content is (re)read from the file on the next -- 'read'. -- data TFile k a = TF { filepath :: FilePath , tfileTVar :: TVar (Maybe a) , dirtyTVar :: TVar Bool , fileLock :: TMVar () } deriving(Typeable) -- | Currently set to \".\/_TFile\" -- TODO: provide interface for updating base directory within a transaction(?) basedir :: FilePath basedir = "_TFile" -- | Tries to construct a 'TFile' from a given filepath. -- Reads the content of the file into memory. newFromFileIO :: (Read k,TBox TFile k a) => FilePath -> IO (TFile k a,k) newFromFileIO fp = do (t,k) <- newEmptyFromFileIO fp _ <- atomically $ read t return (t,k) -- | Tries to construct a 'TFile' from a given filepath. -- Note that the content of the file is read into memory only on demand, i.e., -- when executing 'TBox.read'. -- Throws 'AssertionFailed' if the filename could not be parsed. newEmptyFromFileIO :: (Read k,TBox TFile k a) => FilePath -> IO (TFile k a,k) newEmptyFromFileIO fp = do k <- Safe.read (takeFileName fp) `catch` (\(_::Safe.SafeException) -> throw $ AssertionFailed ("Could not parse filename: " ++ fp)) t <- newEmptyIO k return (t,k) {- maybe (throw $ AssertionFailed ("Could not parse filename: " ++ fp)) (\k -> do t <- newEmptyIO k return (t,k)) $ Safe.read (takeFileName fp) -} instance (Binary a,Show k) => TBox TFile k a where newEmpty k = let fp = basedir show k in TF fp `liftM` newTVar Nothing `ap` newTVar True `ap` newTMVar () newEmptyIO k = do unlessM (doesDirectoryExist basedir) $ createDirectory basedir let fp = basedir show k TF fp `liftM` newTVarIO Nothing `ap` (newTVarIO =<< doesFileExist fp) `ap` newTMVarIO () new k a = do let fp = basedir show k tbox <- TF fp `liftM` newTVar (Just a) `ap` newTVar False `ap` newTMVar () write tbox a return tbox writeSTM tfile = writeTVar (tfileTVar tfile) . Just writeIO (TF fp _ _ lock) a = do unlessM (doesDirectoryExist basedir) $ createDirectory basedir withTMVar lock $ const $ writeFileL fp $ encode a readSTM = readTVar . tfileTVar readIO tmap = do a <- withTMVar (fileLock tmap) $ const $ decodeFile (filepath tmap) return (Just a) `catch` \(e::IOException) -> if isDoesNotExistError e then return Nothing else throw e clearSTM tbox = writeTVar (tfileTVar tbox) Nothing clearIO (TF fp _ _ lock) = withTMVar lock $ const $ removeFile fp `catch` \(e::IOException) -> unless (isDoesNotExistError e) $ throw e isDirty = readTVar . dirtyTVar setDirty = writeTVar . dirtyTVar withTMVar :: TMVar a -> (a -> IO b) -> IO b withTMVar tmvar ioac = do a <- atomically $ takeTMVar tmvar res <- ioac a atomically $ putTMVar tmvar a return res --------------------------------------------------------------------------------