remote: more error handling, port rest of the logger to Serializer

This commit is contained in:
sorki 2023-11-29 14:47:10 +01:00
parent 922f5bbf8c
commit c853163346
2 changed files with 202 additions and 64 deletions

View File

@ -10,8 +10,7 @@ module System.Nix.Store.Remote.Serializer
-- * NixSerializer
NixSerializer
-- * Errors
, GetError(..)
, LoggerError(..)
, PrimError(..)
-- ** Runners
, runSerialT
, runG
@ -36,6 +35,7 @@ module System.Nix.Store.Remote.Serializer
-- * StorePath
, path
-- ** Logger
, LoggerError(..)
, activityID
, maybeActivity
, activityResult
@ -48,7 +48,7 @@ module System.Nix.Store.Remote.Serializer
, verbosity
) where
import Control.Monad.Except (MonadError, throwError)
import Control.Monad.Except (MonadError, throwError, withExceptT)
import Control.Monad.Reader (MonadReader)
import Control.Monad.Trans (MonadTrans, lift)
import Control.Monad.Trans.Reader (ReaderT, runReaderT)
@ -60,15 +60,17 @@ import Data.Map (Map)
import Data.Set (Set)
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.Word (Word64)
import Data.Word (Word8, Word64)
import GHC.Generics (Generic)
import qualified Control.Monad
import qualified Control.Monad.Reader
import qualified Data.HashSet
import qualified Data.Map.Strict
import qualified Data.Serialize.Get
import qualified Data.Set
import qualified Data.Text
import qualified Data.Text.Encoding
import Data.Serializer
import System.Nix.Build (BuildResult)
@ -104,24 +106,33 @@ runSerialT r =
. runExceptT
. _unSerialT
mapError
:: Functor m
=> (e -> e')
-> SerialT r e m a
-> SerialT r e' m a
mapError f =
SerialT
. withExceptT f
. _unSerialT
-- * NixSerializer
type NixSerializer r e = Serializer (SerialT r e)
-- * Errors
data GetError
= GetError
| GetError_EnumOutOfMinBound Int
| GetError_EnumOutOfMaxBound Int
| GetError_IllegalBool Word64
| GetError_Path InvalidPathError
deriving (Eq, Ord, Generic, Show)
data LoggerError
= LoggerError_Get GetError
| LoggerError_TooOldForErrorInfo
| LoggerError_TooNewForBasicError
data PrimError
= PrimError
| PrimError_BadPadding
{ badPaddingStr :: ByteString
, badPaddingLen :: Int
, badPaddingPads :: [Word8]
}
| PrimError_EnumOutOfMinBound Int
| PrimError_EnumOutOfMaxBound Int
| PrimError_IllegalBool Word64
| PrimError_Path InvalidPathError
deriving (Eq, Ord, Generic, Show)
-- ** Runners
@ -153,31 +164,58 @@ runP serializer r =
int :: Integral a => NixSerializer r e a
int = lift2 getInt putInt
bool :: NixSerializer r GetError Bool
bool :: NixSerializer r PrimError Bool
bool = Serializer
{ getS = getS (int @Word64) >>= \case
0 -> pure False
1 -> pure True
x -> throwError $ GetError_IllegalBool x
x -> throwError $ PrimError_IllegalBool x
, putS = lift . putBool
}
byteString :: NixSerializer r e ByteString
byteString = lift2 getByteString putByteString
byteString :: NixSerializer r PrimError ByteString
byteString = Serializer
{ getS = do
len <- getS int
st <- lift $ Data.Serialize.Get.getByteString len
Control.Monad.when (len `mod` 8 /= 0) $ do
pads <- lift $ unpad $ fromIntegral $ 8 - (len `mod` 8)
Control.Monad.unless
(all (== 0) pads)
$ throwError
$ PrimError_BadPadding st len pads
pure st
, putS = lift . putByteString
}
where
unpad x =
Control.Monad.replicateM x Data.Serialize.Get.getWord8
enum :: Enum a => NixSerializer r GetError a
-- | Utility toEnum version checking bounds using Bounded class
toEnumCheckBoundsM
:: ( Enum a
, MonadError PrimError m
)
=> Int
-> m a
toEnumCheckBoundsM = \case
x | x < minBound -> throwError $ PrimError_EnumOutOfMinBound x
x | x > maxBound -> throwError $ PrimError_EnumOutOfMaxBound x
x | otherwise -> pure $ toEnum x
enum :: Enum a => NixSerializer r PrimError a
enum = Serializer
{ getS = getS int >>= \case
x | x < minBound -> throwError $ GetError_EnumOutOfMinBound x
x | x > maxBound -> throwError $ GetError_EnumOutOfMaxBound x
x | otherwise -> pure $ toEnum x
{ getS = getS int >>= toEnumCheckBoundsM
, putS = lift . putEnum
}
text :: NixSerializer r e Text
text = liftSerialize
text :: NixSerializer r PrimError Text
text = mapIsoSerializer
Data.Text.Encoding.decodeUtf8
Data.Text.Encoding.encodeUtf8
byteString
maybeText :: NixSerializer r e (Maybe Text)
maybeText :: NixSerializer r PrimError (Maybe Text)
maybeText = mapIsoSerializer
(\case
t | Data.Text.null t -> Nothing
@ -247,14 +285,14 @@ protoVersion = liftSerialize
-- * StorePath
path :: HasStoreDir r => NixSerializer r GetError StorePath
path :: HasStoreDir r => NixSerializer r PrimError StorePath
path = Serializer
{ getS = do
sd <- Control.Monad.Reader.asks hasStoreDir
lift (getPath sd)
>>=
either
(throwError . GetError_Path)
(throwError . PrimError_Path)
pure
, putS = \p -> do
sd <- Control.Monad.Reader.asks hasStoreDir
@ -266,39 +304,114 @@ derivation sd = lift2 (getDerivation sd) (putDerivation sd)
-- ** Logger
maybeActivity :: NixSerializer r e (Maybe Activity)
data LoggerError
= LoggerError_Prim PrimError
| LoggerError_InvalidOpCode Int
| LoggerError_TooOldForErrorInfo
| LoggerError_TooNewForBasicError
| LoggerError_UnknownLogFieldType Word8
deriving (Eq, Ord, Generic, Show)
mapPrimE
:: Functor m
=> SerialT r PrimError m a
-> SerialT r LoggerError m a
mapPrimE = mapError LoggerError_Prim
maybeActivity :: NixSerializer r LoggerError (Maybe Activity)
maybeActivity = Serializer
{ getS = getS (int @Int) >>= \case
0 -> pure Nothing
x -> either fail (pure . Just) $ toEnumCheckBounds (x - 100)
x -> mapPrimE $ toEnumCheckBoundsM (x - 100) >>= pure . Just
, putS = \case
Nothing -> putS (int @Int) 0
Just act -> putS activity act
}
where
activity :: NixSerializer r e Activity
activity = liftSerialize
activity :: NixSerializer r LoggerError Activity
activity = Serializer
{ getS = mapPrimE $ getS int >>= toEnumCheckBoundsM . (+(-100))
, putS = putS int . (+100) . fromEnum
}
activityID :: NixSerializer r e ActivityID
activityID = liftSerialize
activityID :: NixSerializer r LoggerError ActivityID
activityID = mapIsoSerializer ActivityID unActivityID int
activityResult :: NixSerializer r e ActivityResult
activityResult = liftSerialize
activityResult :: NixSerializer r LoggerError ActivityResult
activityResult = Serializer
{ getS = mapPrimE $ getS int >>= toEnumCheckBoundsM . (+(-100))
, putS = putS int . (+100) . fromEnum
}
field :: NixSerializer r e Field
field = liftSerialize
field :: NixSerializer r LoggerError Field
field = Serializer
{ getS = getS (int @Word8) >>= \case
0 -> Field_LogInt <$> getS int
1 -> Field_LogStr <$> mapPrimE (getS text)
x -> throwError $ LoggerError_UnknownLogFieldType x
, putS = \case
Field_LogInt x -> putS int (0 :: Word8) >> putS int x
Field_LogStr x -> putS int (1 :: Word8) >> mapPrimE (putS text x)
}
trace :: NixSerializer r e Trace
trace = liftSerialize
trace :: NixSerializer r LoggerError Trace
trace = Serializer
{ getS = do
tracePosition <- (\case 0 -> Nothing; x -> Just x) <$> getS (int @Int)
traceHint <- mapPrimE $ getS text
pure Trace{..}
, putS = \Trace{..} -> do
maybe (putS (int @Int) 0) (putS int) $ tracePosition
mapPrimE $ putS text traceHint
}
basicError :: NixSerializer r e BasicError
basicError = liftSerialize
basicError :: NixSerializer r LoggerError BasicError
basicError = Serializer
{ getS = do
basicErrorMessage <- mapPrimE $ getS text
basicErrorExitStatus <- getS int
pure BasicError{..}
errorInfo :: NixSerializer r e ErrorInfo
errorInfo = liftSerialize
, putS = \BasicError{..} -> do
mapPrimE $ putS text basicErrorMessage
putS int basicErrorExitStatus
}
loggerOpCode :: NixSerializer r e LoggerOpCode
loggerOpCode = liftSerialize
errorInfo :: NixSerializer r LoggerError ErrorInfo
errorInfo = Serializer
{ getS = do
etyp <- mapPrimE $ getS text
Control.Monad.unless (etyp == Data.Text.pack "Error")
$ fail
$ "get ErrorInfo: received unknown error type" ++ show etyp
errorInfoLevel <- getS verbosity
_name <- mapPrimE $ getS text -- removed error name
errorInfoMessage <- mapPrimE $ getS text
errorInfoPosition <- (\case 0 -> Nothing; x -> Just x) <$> getS int
errorInfoTraces <- getS (list trace)
pure ErrorInfo{..}
, putS = \ErrorInfo{..} -> do
mapPrimE $ do
putS text $ Data.Text.pack "Error"
putS verbosity errorInfoLevel
mapPrimE $ do
putS text $ Data.Text.pack "Error" -- removed error name
putS text errorInfoMessage
maybe (putS (int @Word8) 0) (putS int) errorInfoPosition
putS (list trace) errorInfoTraces
}
loggerOpCode :: NixSerializer r LoggerError LoggerOpCode
loggerOpCode = Serializer
{ getS = do
c <- getS int
either
(pure $ throwError (LoggerError_InvalidOpCode c))
pure
$ intToLoggerOpCode c
, putS = putS int . loggerOpCodeToInt
}
logger
:: HasProtoVersion r
@ -306,47 +419,62 @@ logger
logger = Serializer
{ getS = getS loggerOpCode >>= \case
LoggerOpCode_Next ->
Logger_Next <$> getS text
mapPrimE $
Logger_Next <$> getS text
LoggerOpCode_Read ->
Logger_Read <$> getS int
LoggerOpCode_Write ->
Logger_Write <$> getS byteString
mapPrimE $
Logger_Write <$> getS byteString
LoggerOpCode_Last ->
pure Logger_Last
LoggerOpCode_Error -> do
pv <- Control.Monad.Reader.asks hasProtoVersion
Logger_Error <$>
if protoVersion_minor pv >= 26
then Right <$> getS errorInfo
else Left <$> getS basicError
LoggerOpCode_StartActivity -> do
startActivityID <- getS activityID
startActivityVerbosity <- getS verbosity
startActivityType <- getS maybeActivity
startActivityString <- getS byteString
startActivityString <- mapPrimE $ getS byteString
startActivityFields <- getS (list field)
startActivityParentID <- getS activityID
pure Logger_StartActivity{..}
LoggerOpCode_StopActivity -> do
stopActivityID <- getS activityID
pure Logger_StopActivity{..}
LoggerOpCode_Result -> do
resultActivityID <- getS activityID
resultType <- getS activityResult
resultFields <- getS (list field)
pure Logger_Result {..}
, putS = \case
Logger_Next s -> do
putS loggerOpCode LoggerOpCode_Next
putS text s
mapError LoggerError_Prim $
putS text s
Logger_Read i -> do
putS loggerOpCode LoggerOpCode_Read
putS int i
Logger_Write s -> do
putS loggerOpCode LoggerOpCode_Write
putS byteString s
mapPrimE $ putS byteString s
Logger_Last ->
putS loggerOpCode LoggerOpCode_Last
Logger_Error basicOrInfo -> do
putS loggerOpCode LoggerOpCode_Error
@ -363,12 +491,15 @@ logger = Serializer
putS activityID startActivityID
putS verbosity startActivityVerbosity
putS maybeActivity startActivityType
putS byteString startActivityString
mapPrimE $
putS byteString startActivityString
putS (list field) startActivityFields
putS activityID startActivityParentID
Logger_StopActivity{..} -> do
putS loggerOpCode LoggerOpCode_StopActivity
putS activityID stopActivityID
Logger_Result{..} -> do
putS loggerOpCode LoggerOpCode_Result
putS activityID resultActivityID
@ -376,5 +507,8 @@ logger = Serializer
putS (list field) resultFields
}
verbosity :: NixSerializer r e Verbosity
verbosity = liftSerialize
verbosity :: NixSerializer r LoggerError Verbosity
verbosity = Serializer
{ getS = mapPrimE $ getS enum
, putS = mapPrimE . putS enum
}

View File

@ -4,6 +4,7 @@ module NixSerializerSpec (spec) where
import Data.Fixed (Uni)
import Data.Time (NominalDiffTime)
import Data.Text (Text)
import Test.Hspec (Expectation, Spec, describe, parallel, shouldBe)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck (Gen, arbitrary, forAll, suchThat)
@ -14,8 +15,9 @@ import qualified Data.Serializer
import qualified System.Nix.Build
import System.Nix.Arbitrary ()
import System.Nix.Build (BuildResult)
import System.Nix.Derivation (Derivation(inputDrvs))
import System.Nix.StorePath (StoreDir)
import System.Nix.StorePath (StoreDir, StorePath)
import System.Nix.Store.Remote.Arbitrary ()
import System.Nix.Store.Remote.Serializer
import System.Nix.Store.Remote.Types (ErrorInfo(..), Logger(..), ProtoVersion(..), Trace(..))
@ -40,8 +42,10 @@ roundtripSReader serializer readerVal a =
roundtripS
:: ( Eq a
, Show a
, Eq e
, Show e
)
=> NixSerializer () GetError a
=> NixSerializer () e a
-> a
-> Expectation
roundtripS serializer = roundtripSReader serializer ()
@ -49,7 +53,7 @@ roundtripS serializer = roundtripSReader serializer ()
spec :: Spec
spec = parallel $ do
describe "Prim" $ do
prop "Int" $ roundtripS @Int int
prop "Int" $ roundtripS @Int @() int
prop "Bool" $ roundtripS bool
prop "ByteString" $ roundtripS byteString
prop "Text" $ roundtripS text
@ -64,14 +68,14 @@ spec = parallel $ do
fromSeconds :: NominalDiffTime -> Int
fromSeconds = (fromEnum :: Uni -> Int) . realToFrac
roundtripS $
roundtripS @Int @() $
Data.Serializer.mapIsoSerializer
(fromSeconds . Data.Time.Clock.POSIX.utcTimeToPOSIXSeconds)
(Data.Time.Clock.POSIX.posixSecondsToUTCTime . toSeconds)
time
describe "Combinators" $ do
prop "list" $ roundtripS @[Int] (list int)
prop "list" $ roundtripS @[Int] @() (list int)
prop "set" $ roundtripS (set byteString)
prop "hashSet" $ roundtripS (hashSet byteString)
prop "mapS" $ roundtripS (mapS (int @Int) byteString)
@ -80,7 +84,7 @@ spec = parallel $ do
prop "BuildResult"
$ forAll (arbitrary `suchThat` ((/= Just "") . System.Nix.Build.errorMessage))
$ \br ->
roundtripS buildResult
roundtripS @BuildResult @() buildResult
-- fix time to 0 as we test UTCTime above
$ br { System.Nix.Build.startTime = Data.Time.Clock.POSIX.posixSecondsToUTCTime 0
, System.Nix.Build.stopTime = Data.Time.Clock.POSIX.posixSecondsToUTCTime 0
@ -90,10 +94,10 @@ spec = parallel $ do
roundtripSReader @StoreDir path sd
prop "Derivation" $ \sd ->
roundtripS (derivation sd)
roundtripS @(Derivation StorePath Text) @() (derivation sd)
. (\drv -> drv { inputDrvs = mempty })
prop "ProtoVersion" $ roundtripS protoVersion
prop "ProtoVersion" $ roundtripS @ProtoVersion @() protoVersion
describe "Logger" $ do
prop "ActivityID" $ roundtripS activityID