{-# LANGUAGE ScopedTypeVariables, RecordWildCards, FlexibleInstances #-}
{-# LANGUAGE BangPatterns #-}
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
databaseVersion :: String -> String
databaseVersion x = "SHAKE-DATABASE-13-" ++ s ++ "\r\n"
where s = tail $ init $ show x
withStorage
:: (Show k, Eq k, Hashable k, NFData k, Show v, NFData v)
=> ShakeOptions
-> (IO String -> IO ())
-> Map.HashMap k (BinaryOp v)
-> (Ids.Ids v -> (k -> Id -> v -> IO ()) -> IO a)
-> IO a
withStorage ShakeOptions{..} diagnostic witness act = withLockFileDiagnostic diagnostic (shakeFiles </> ".shake.lock") $ do
let dbfile = shakeFiles </> ".shake.database"
createDirectoryRecursive shakeFiles
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"
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