{-# LANGUAGE ScopedTypeVariables, RecordWildCards, FlexibleInstances #-} {-# LANGUAGE BangPatterns #-} {- This module stores the meta-data so its very important its always accurate We can't rely on getting any exceptions or termination at the end, so we'd better write out a journal We store a series of records, and if they contain twice as many records as needed, we compress -} module Development.Shake.Internal.Core.Storage( withStorage ) where import General.Chunks import General.Binary import General.Intern import Development.Shake.Internal.Options import General.Timing import General.FileLock import qualified General.Ids as Ids import Control.Exception.Extra import Control.Monad.Extra import Data.Monoid import Data.Either.Extra import Data.Time import Data.Char import Data.Word import Development.Shake.Classes import Numeric import General.Extra import Data.List.Extra import Data.Maybe import System.FilePath import qualified Data.ByteString.UTF8 as UTF8 import qualified Data.HashMap.Strict as Map import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString as BS8 import Data.Functor import Prelude -- Increment every time the on-disk format/semantics change, -- @x@ is for the users version number databaseVersion :: String -> String -- THINGS I WANT TO DO ON THE NEXT CHANGE -- * Change filepaths to store a 1 byte prefix saying 8bit ASCII or UTF8 -- * Duration and Time should be stored as number of 1/10000th seconds Int32 databaseVersion x = "SHAKE-DATABASE-13-" ++ s ++ "\r\n" where s = tail $ init $ show x -- call show, then take off the leading/trailing quotes -- ensures we do not get \r or \n in the user portion -- | Storage of heterogeneous things. In the particular case of Shake, -- k ~ TypeRep, v ~ (Key, Status{Value}). -- -- The storage starts with a witness table saying what can be contained. -- If any entries in the witness table don't have a current Witness then a fake -- error witness is manufactured. If the witness ever changes the entire DB is -- rewritten. withStorage :: (Show k, Eq k, Hashable k, NFData k, Show v, NFData v) => ShakeOptions -- ^ Storage options -> (IO String -> IO ()) -- ^ Logging function -> Map.HashMap k (BinaryOp v) -- ^ Witnesses -> (Ids.Ids v -> (k -> Id -> v -> IO ()) -> IO a) -- ^ Execute -> IO a withStorage ShakeOptions{..} diagnostic witness act = withLockFileDiagnostic diagnostic (shakeFiles ".shake.lock") $ do let dbfile = shakeFiles ".shake.database" createDirectoryRecursive shakeFiles -- complete a partially failed compress whenM (restoreChunksBackup dbfile) $ do unexpected "Backup file exists, restoring over the previous file\n" diagnostic $ return "Backup file move to original" addTiming "Database read" withChunks dbfile shakeFlush $ \h -> do let corrupt | not shakeStorageLog = resetChunksCorrupt Nothing h | otherwise = do let file = dbfile <.> "corrupt" resetChunksCorrupt (Just file) h unexpected $ "Backup of corrupted file stored at " ++ file ++ "\n" -- check the version information matches let ver = BS.pack $ databaseVersion shakeVersion oldVer <- readChunkMax h $ fromIntegral $ BS.length ver + 100000 let verEq = Right ver == oldVer when (not shakeVersionIgnore && not verEq && oldVer /= Left BS.empty) $ do let limit x = let (a,b) = splitAt 200 x in a ++ (if null b then "" else "...") let disp = map (\x -> if isPrint x && isAscii x then x else '?') . takeWhile (`notElem` "\r\n") outputErr $ unlines ["Error when reading Shake database - invalid version stamp detected:" ," File: " ++ dbfile ," Expected: " ++ disp (BS.unpack ver) ," Found: " ++ disp (limit $ BS.unpack $ fromEither oldVer) ,"All rules will be rebuilt"] corrupt let (witnessNew, save) = putWitness witness evaluate save witnessOld <- readChunk h ids <- case witnessOld of Left _ -> do resetChunksCorrupt Nothing h return Nothing Right witnessOld -> handleBool (not . isAsyncException) (\err -> do msg <- showException err outputErr $ unlines $ ("Error when reading Shake database " ++ dbfile) : map (" "++) (lines msg) ++ ["All files will be rebuilt"] corrupt return Nothing) $ do let load = getWitness witnessOld witness evaluate load ids <- Ids.empty let go !i = do v <- readChunk h case v of Left e -> do let slop = fromIntegral $ BS.length e when (slop > 0) $ unexpected $ "Last " ++ show slop ++ " bytes do not form a whole record\n" diagnostic $ return $ "Read " ++ show i ++ " chunks, plus " ++ show slop ++ " slop" return i Right bs -> do let (k,id,v) = load bs evaluate $ rnf k evaluate $ rnf v Ids.insert ids id (k,v) diagnostic $ do let raw x = "[len " ++ show (BS.length bs) ++ "] " ++ concat [['0' | length c == 1] ++ c | x <- BS8.unpack bs, let c = showHex x ""] let pretty (Left x) = "FAILURE: " ++ show x pretty (Right x) = x x2 <- try_ $ evaluate $ let s = show v in rnf s `seq` s return $ "Chunk " ++ show i ++ " " ++ raw bs ++ " " ++ show id ++ " = " ++ pretty x2 go $ i+1 countItems <- go 0 countDistinct <- Ids.sizeUpperBound ids diagnostic $ return $ "Found at most " ++ show countDistinct ++ " distinct entries out of " ++ show countItems when (countItems > countDistinct*2 || not verEq || witnessOld /= witnessNew) $ do addTiming "Database compression" resetChunksCompact h $ \out -> do out $ putEx ver out $ putEx witnessNew Ids.forWithKeyM_ ids $ \i (k,v) -> out $ save k i v Just <$> Ids.for ids snd ids <- case ids of Just ids -> return ids Nothing -> do writeChunk h $ putEx ver writeChunk h $ putEx witnessNew Ids.empty addTiming "With database" writeChunks h $ \out -> act ids $ \k i v -> out $ save k i v where unexpected x = when shakeStorageLog $ do t <- getCurrentTime appendFile (shakeFiles ".shake.storage.log") $ "\n[" ++ show t ++ "]: " ++ trimEnd x ++ "\n" outputErr x = do when (shakeVerbosity >= Quiet) $ shakeOutput Quiet x unexpected x keyName :: Show k => k -> BS.ByteString keyName = UTF8.fromString . show getWitness :: Show k => BS.ByteString -> Map.HashMap k (BinaryOp v) -> (BS.ByteString -> (k, Id, v)) getWitness bs mp | length ws > limit || Map.size mp > limit = error "Number of distinct witness types exceeds limit" | otherwise = ind `seq` mp2 `seq` \bs -> let (k :: Word16,bs2) = binarySplit bs in case ind (fromIntegral k) of Nothing -> error $ "Witness type out of bounds, " ++ show k Just f -> f bs2 where limit = fromIntegral (maxBound :: Word16) ws :: [BS.ByteString] = getEx bs mp2 = Map.fromList [(keyName k, (k, v)) | (k,v) <- Map.toList mp] ind = fastAt [ case Map.lookup w mp2 of Nothing -> error $ "Witness type has disappeared, " ++ UTF8.toString w Just (k, BinaryOp{..}) -> \bs -> let (i, bs2) = binarySplit bs v = getOp bs2 in (k, i, v) | w <- ws] putWitness :: (Eq k, Hashable k, Show k) => Map.HashMap k (BinaryOp v) -> (BS.ByteString, k -> Id -> v -> Builder) putWitness mp = (runBuilder $ putEx (ws :: [BS.ByteString]), mp2 `seq` \k -> fromMaybe (error $ "Don't know how to save, " ++ show k) $ Map.lookup k mp2) where ws = sort $ map keyName $ Map.keys mp wsMp = Map.fromList $ zip ws [0 :: Word16 ..] mp2 = Map.mapWithKey (\k BinaryOp{..} -> let tag = putEx $ wsMp Map.! keyName k in \(Id w) v -> tag <> putEx w <> putOp v) mp withLockFileDiagnostic :: (IO String -> IO ()) -> FilePath -> IO a -> IO a withLockFileDiagnostic diagnostic file act = do diagnostic $ return $ "Before withLockFile on " ++ file res <- withLockFile file $ do diagnostic $ return "Inside withLockFile" act diagnostic $ return "After withLockFile" return res