ipedb-0.2.0.0: app-ipedb/Main.hs
module Main (main) where
import Codec.Compression.GZip qualified as GZip
import Control.Applicative (asum)
import Control.Exception (Exception (..), SomeException)
import Control.Monad (unless)
import Control.Monad.IO.Class (MonadIO (..))
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BSL
import Data.Default (Default (..))
import Data.Foldable (for_)
import Data.Machine ((~>))
import Data.Machine qualified as M
import Data.Maybe (fromMaybe)
import Data.String (IsString (..))
import Data.Vector qualified as V
import Data.Version qualified as V (showVersion)
import Data.Word (Word32)
import GHC.RTS.Events (Event)
import GHC.RTS.Events.Incremental (Decoder (..), decodeEventLog)
import IpeDB.Database qualified as DB
import IpeDB.Types.InfoProv qualified as IP
import Options.Applicative qualified as O
import Paths_ipedb (version)
import System.Exit (ExitCode (..), exitFailure, exitWith)
import System.IO qualified as IO
main :: IO ()
main = do
O.execParser ipeDBOptionsParserInfo >>= \case
IndexCommand options -> runIndex options
QueryCommand options -> runQuery options
ListCommand options -> runList options
CheckCommand options -> runCheck options
--------------------------------------------------------------------------------
-- Index
runIndex :: IndexOptions -> IO ()
runIndex IndexOptions{..} = do
withEventlogSource eventlogSource $ \eventlogSourceHandle -> do
DB.withNewSession def $ \session ->
DB.withNewTable @IP.InfoProvId @IP.InfoProv session def $ \table -> do
M.runT_ $
fromHandle eventlogEncoding eventlogSourceHandle
~> decodeEvent
~> DB.indexer IP.toInfoProv def{DB.indexerBufferSize = bufferSize} table
let saveOptions = def{DB.flatten = fromMaybe (shouldFlatten ipeDBTableFormat) ipeDBFlatten}
DB.saveTable saveOptions table ipeDBOutputPath ipeDBTableFormat
--------------------------------------------------------------------------------
-- Query
runQuery :: QueryOptions -> IO ()
runQuery QueryOptions{..} = do
DB.withNewSession def $ \session ->
DB.withTableFrom @IP.InfoProvId @IP.InfoProv session ipeDBPath ipeDBTableFormat $ \table -> do
infoProvs <- fmap V.toList . DB.lookups table . V.fromList $ infoProvIds
for_ (zip infoProvIds infoProvs) $ \(ipId, ip) ->
putStrLn $ show ipId <> ": " <> show ip
--------------------------------------------------------------------------------
-- List
runList :: ListOptions -> IO ()
runList ListOptions{..} = do
DB.withNewSession def $ \session ->
DB.withTableFrom @IP.InfoProvId @IP.InfoProv session ipeDBPath ipeDBTableFormat $ \table -> do
DB.withIterator def{DB.iteratorBufferSize = bufferSize} table $ \iterator ->
M.runT_ $ iterator ~> M.traversing (\(ipId, ip) -> putStrLn $ show ipId <> ": " <> show ip)
--------------------------------------------------------------------------------
-- Check
data MissingInfoProvEntry = MissingInfoProvEntry
{ table1Path :: !FilePath
, table2Path :: !FilePath
, infoProvId :: !IP.InfoProvId
}
deriving (Show)
instance Exception MissingInfoProvEntry where
displayException :: MissingInfoProvEntry -> String
displayException = flip showsException ""
where
showsException :: MissingInfoProvEntry -> ShowS
showsException MissingInfoProvEntry{..} =
shows infoProvId <> showString ": missing in " <> showString table2Path
data MismatchedInfoProvEntry = MismatchedInfoProvEntry
{ table1Path :: !FilePath
, table2Path :: !FilePath
, infoProvId :: !IP.InfoProvId
, infoProv1 :: !IP.InfoProv
, infoProv2 :: !IP.InfoProv
}
deriving (Show)
instance Exception MismatchedInfoProvEntry where
displayException :: MismatchedInfoProvEntry -> String
displayException = flip showsException ""
where
showsException :: MismatchedInfoProvEntry -> ShowS
showsException MismatchedInfoProvEntry{..} =
shows infoProvId <> showString ": mismatch between " <> showString table1Path <> showString " and " <> showString table2Path
runCheck :: CheckOptions -> IO ()
runCheck CheckOptions{..} = do
DB.withNewSession def $ \session ->
DB.withTableFrom @IP.InfoProvId @IP.InfoProv session ipeDB1Path ipeDB1TableFormat $ \table1 ->
DB.withTableFrom @IP.InfoProvId @IP.InfoProv session ipeDB2Path ipeDB2TableFormat $ \table2 -> do
let runCheck' = \case
CheckSubset -> checkSubset
CheckEqual -> checkEqual
exceptionCount <- runCheck' check (ipeDB1Path, table1) (ipeDB2Path, table2)
unless (exceptionCount == 0) $
exitWith (ExitFailure exceptionCount)
where
checkEqual ::
(FilePath, DB.Table IP.InfoProvId IP.InfoProv) ->
(FilePath, DB.Table IP.InfoProvId IP.InfoProv) ->
IO Int
checkEqual table1Info table2Info = do
exceptionCount1 <- checkSubset' True table1Info table2Info
exceptionCount2 <- checkSubset' False table2Info table1Info
pure (exceptionCount1 + exceptionCount2)
checkSubset ::
(FilePath, DB.Table IP.InfoProvId IP.InfoProv) ->
(FilePath, DB.Table IP.InfoProvId IP.InfoProv) ->
IO Int
checkSubset = checkSubset' True
checkSubset' ::
Bool ->
(FilePath, DB.Table IP.InfoProvId IP.InfoProv) ->
(FilePath, DB.Table IP.InfoProvId IP.InfoProv) ->
IO Int
checkSubset' checkPresentAndEqual (table1Path, table1) (table2Path, table2) =
DB.withIterator def{DB.iteratorBufferSize = bufferSize} table1 $ \iterator1 -> do
fmap sum . M.runT $
iterator1
~> M.buffered (fromIntegral bufferSize)
~> M.traversing resolve
~> M.asParts
~> M.mapping assertEqual
~> M.asParts
~> M.traversing (\e -> IO.hPutStr IO.stderr (displayException e) >> pure (1 :: Int))
where
resolve :: [(IP.InfoProvId, IP.InfoProv)] -> IO [(IP.InfoProvId, IP.InfoProv, Maybe IP.InfoProv)]
resolve entries1 = do
maybeInfoProvs2 <- DB.lookups table2 (V.fromList (fst <$> entries1))
let addToEntry (infoProvId, infoProv1) maybeInfoProv2 = (infoProvId, infoProv1, maybeInfoProv2)
pure $ zipWith addToEntry entries1 (V.toList maybeInfoProvs2)
assertEqual :: (IP.InfoProvId, IP.InfoProv, Maybe IP.InfoProv) -> Maybe SomeException
assertEqual (infoProvId, infoProv1, maybeInfoProv2) =
case maybeInfoProv2 of
Nothing ->
Just $ toException MissingInfoProvEntry{..}
Just infoProv2
| checkPresentAndEqual && infoProv1 /= infoProv2 ->
Just $ toException MismatchedInfoProvEntry{..}
| otherwise -> Nothing
--------------------------------------------------------------------------------
-- Eventlog Processing
--------------------------------------------------------------------------------
{- |
Stream a file as chunks.
-}
fromHandle :: EventlogEncoding -> IO.Handle -> M.SourceT IO BS.ByteString
fromHandle eventlogEncoding h =
M.MachineT $ do
chunks <- liftIO (BSL.toChunks . decodeEventlog eventlogEncoding <$> BSL.hGetContents h)
M.runMachineT $ M.source chunks
{- |
Parse t`Event`s from a stream of `BS.ByteString` chunks.
-}
decodeEvent :: M.Process BS.ByteString Event
decodeEvent = M.construct $ loop decodeEventLog
where
loop :: Decoder a -> M.PlanT (M.Is BS.ByteString) a m ()
loop Done{} = pure ()
loop (Consume k) = M.await >>= \chunk -> loop (k chunk)
loop (Produce a d') = M.yield a >> loop d'
loop (Error _ err) = error err
--------------------------------------------------------------------------------
-- database Options
--------------------------------------------------------------------------------
data IpeDBOptions
= IndexCommand IndexOptions
| QueryCommand QueryOptions
| ListCommand ListOptions
| CheckCommand CheckOptions
ipeDBOptionsParserInfo :: O.ParserInfo IpeDBOptions
ipeDBOptionsParserInfo =
O.info (ipeDBOptionsParser O.<**> O.helper O.<**> ipeDBVersioner) . mconcat $
[ O.progDesc "Build and query IPE databases."
]
ipeDBVersioner :: O.Parser (a -> a)
ipeDBVersioner =
O.simpleVersioner $! "ipedb " <> V.showVersion version
ipeDBOptionsParser :: O.Parser IpeDBOptions
ipeDBOptionsParser =
O.subparser . mconcat $
[ O.command "index" (IndexCommand <$> indexOptionsParserInfo)
, O.command "query" (QueryCommand <$> queryOptionsParserInfo)
, O.command "list" (ListCommand <$> listOptionsParserInfo)
, O.command "check" (CheckCommand <$> checkOptionsParserInfo)
]
--------------------------------------------------------------------------------
-- Index Options
data IndexOptions = IndexOptions
{ eventlogSource :: EventlogSource
, eventlogEncoding :: EventlogEncoding
, ipeDBOutputPath :: !FilePath
, ipeDBTableFormat :: !DB.TableFormat
, ipeDBFlatten :: !(Maybe Bool)
, bufferSize :: !Word32
}
indexOptionsParserInfo :: O.ParserInfo IndexOptions
indexOptionsParserInfo =
O.info (indexOptionsParser O.<**> O.helper) . mconcat $
[ O.progDesc "Build an IPE database from an eventlog."
]
indexOptionsParser :: O.Parser IndexOptions
indexOptionsParser =
IndexOptions
<$> eventlogSourceParser
<*> eventlogEncodingParser
<*> ipeDBOutputPathParser
<*> tableFormatParser
<*> O.optional (ipeDBFlattenParser O.<|> ipeDBNoFlattenParser)
<*> indexBufferSizeParser
shouldFlatten :: DB.TableFormat -> Bool
shouldFlatten = \case
DB.LSMTreeSnapshotV2 -> False
DB.LSMTreeSnapshotV2Tar -> True
DB.LSMTreeSnapshotV2TarGz -> True
ipeDBFlattenParser :: O.Parser Bool
ipeDBFlattenParser =
O.flag' True . mconcat $
[ O.long "flatten"
, O.help "Flatten the lsm-tree database. This improves the speed of lookups. By default, this is disabled for the 'lsm' format and enabled for the 'tar' and 'tgz' formats."
]
ipeDBNoFlattenParser :: O.Parser Bool
ipeDBNoFlattenParser =
O.flag' False . mconcat $
[ O.long "no-flatten"
, O.hidden
, O.help "Do not flatten the lsm-tree database."
]
--------------------------------------------------------------------------------
-- Query Options
data QueryOptions = QueryOptions
{ ipeDBPath :: !FilePath
, ipeDBTableFormat :: !DB.TableFormat
, infoProvIds :: ![IP.InfoProvId]
}
queryOptionsParserInfo :: O.ParserInfo QueryOptions
queryOptionsParserInfo =
O.info (queryOptionsParser O.<**> O.helper) . mconcat $
[ O.progDesc "Query an IPE database with IPE pointers."
]
queryOptionsParser :: O.Parser QueryOptions
queryOptionsParser =
QueryOptions
<$> ipeDBPathParser
<*> tableFormatParser
<*> O.some infoProvPtrParser
--------------------------------------------------------------------------------
-- List Options
data ListOptions = ListOptions
{ ipeDBPath :: !FilePath
, ipeDBTableFormat :: !DB.TableFormat
, bufferSize :: !Word32
}
listOptionsParserInfo :: O.ParserInfo ListOptions
listOptionsParserInfo =
O.info (listOptionsParser O.<**> O.helper) . mconcat $
[ O.progDesc "List all IPE entries in an IPE database."
]
listOptionsParser :: O.Parser ListOptions
listOptionsParser =
ListOptions
<$> ipeDBPathParser
<*> tableFormatParser
<*> listBufferSizeParser
--------------------------------------------------------------------------------
-- Check Options
data CheckOptions = CheckOptions
{ check :: !Check
, ipeDB1Path :: !FilePath
, ipeDB1TableFormat :: !DB.TableFormat
, ipeDB2Path :: !FilePath
, ipeDB2TableFormat :: !DB.TableFormat
, bufferSize :: !Word32
}
checkOptionsParserInfo :: O.ParserInfo CheckOptions
checkOptionsParserInfo =
O.info (checkOptionsParser O.<**> O.helper) . mconcat $
[ O.progDesc "Compare two tables."
]
checkOptionsParser :: O.Parser CheckOptions
checkOptionsParser =
CheckOptions
<$> checkParser
<*> ipeDBPathParser
<*> table1FormatParser
<*> ipeDBPathParser
<*> table2FormatParser
<*> listBufferSizeParser
data Check
= CheckSubset
| CheckEqual
checkParser :: O.Parser Check
checkParser =
asum
[ O.flag' CheckSubset . mconcat $
[ O.long "check-subset"
, O.help "Check that the first table is a subset of the second table."
]
, O.flag' CheckEqual . mconcat $
[ O.long "check-equal"
, O.help "Check that the first table and the second table are equal."
]
, pure CheckEqual
]
--------------------------------------------------------------------------------
-- Eventlog Source
data EventlogSource
= EventlogFromStdin
| EventlogFromFile {file :: !FilePath}
instance IsString EventlogSource where
fromString :: String -> EventlogSource
fromString x = if x == "-" then EventlogFromStdin else EventlogFromFile x
eventlogSourceParser :: O.Parser EventlogSource
eventlogSourceParser =
O.strArgument . mconcat $
[ O.metavar "EVENTLOG"
, O.completer (O.bashCompleter "file")
]
withEventlogSource :: EventlogSource -> (IO.Handle -> IO a) -> IO a
withEventlogSource EventlogFromStdin action = do
stdinIsTerminalDevice <- IO.hIsTerminalDevice IO.stdin
if stdinIsTerminalDevice
then do
putStrLn "Cannot read eventlog from terminal"
exitFailure
else do
IO.hSetBinaryMode IO.stdin True
action IO.stdin
withEventlogSource EventlogFromFile{..} action =
IO.withBinaryFile file IO.ReadMode action
--------------------------------------------------------------------------------
-- Eventlog Format
data EventlogEncoding
= EventlogEncodingNone
| EventlogEncodingGZip
readEventlogEncoding :: String -> Either String EventlogEncoding
readEventlogEncoding = \case
"none" -> Right EventlogEncodingNone
"gzip" -> Right EventlogEncodingGZip
eventlogEncodingString -> Left $! "Unknown eventlog encoding " <> eventlogEncodingString
eventlogEncodingParser :: O.Parser EventlogEncoding
eventlogEncodingParser =
O.option (O.eitherReader readEventlogEncoding) . mconcat $
[ O.short 'e'
, O.long "eventlog-encoding"
, O.metavar "FORMAT"
, O.completeWith ["none", "gzip"]
, O.help "The eventlog encoding (none, gzip)."
, O.value EventlogEncodingNone
]
decodeEventlog :: EventlogEncoding -> BSL.ByteString -> BSL.ByteString
decodeEventlog = \case
EventlogEncodingNone -> id
EventlogEncodingGZip -> GZip.decompress
--------------------------------------------------------------------------------
-- database
ipeDBOutputPathParser :: O.Parser FilePath
ipeDBOutputPathParser =
O.strOption . mconcat $
[ O.short 'o'
, O.long "output"
, O.metavar "IPEDB_PATH"
, O.completer (O.bashCompleter "file")
, O.help "The database output path."
]
ipeDBPathParser :: O.Parser FilePath
ipeDBPathParser =
O.strArgument . mconcat $
[ O.metavar "IPEDB_PATH"
, O.completer (O.bashCompleter "file")
]
--------------------------------------------------------------------------------
-- TableFormat
readTableFormat :: String -> Either String DB.TableFormat
readTableFormat = \case
"lsm" -> Right DB.LSMTreeSnapshotV2
"tar" -> Right DB.LSMTreeSnapshotV2Tar
"tgz" -> Right DB.LSMTreeSnapshotV2TarGz
tableFormatString -> Left $! "Unknown table format " <> tableFormatString
tableFormatParser :: O.Parser DB.TableFormat
tableFormatParser =
genericTableFormatParser . mconcat $
[ O.short 't'
, O.long "table-format"
]
table1FormatParser :: O.Parser DB.TableFormat
table1FormatParser =
genericTableFormatParser $
O.long "table1-format"
table2FormatParser :: O.Parser DB.TableFormat
table2FormatParser =
genericTableFormatParser $
O.long "table2-format"
genericTableFormatParser :: O.Mod O.OptionFields DB.TableFormat -> O.Parser DB.TableFormat
genericTableFormatParser mods =
O.option (O.eitherReader readTableFormat) . mconcat $
[ O.metavar "FORMAT"
, O.completeWith ["lsm", "tar", "tgz"]
, O.help "The database table format (lsm, tar, tgz)."
, O.value def
, mods
]
--------------------------------------------------------------------------------
-- Buffer Size
indexBufferSizeParser :: O.Parser Word32
indexBufferSizeParser =
bufferSizeParser . mconcat $
[ O.help "The size of the index buffer in number of elements."
, O.value DB.defaultIndexerOptions.indexerBufferSize
]
listBufferSizeParser :: O.Parser Word32
listBufferSizeParser =
bufferSizeParser . mconcat $
[ O.help "The size of the lookup buffer in number of elements."
, O.value DB.defaultIteratorOptions.iteratorBufferSize
]
bufferSizeParser :: O.Mod O.OptionFields Word32 -> O.Parser Word32
bufferSizeParser mods =
O.option O.auto . mconcat $
[ O.long "buffer-size"
, O.metavar "N"
, mods
]
--------------------------------------------------------------------------------
-- InfoProvId
infoProvPtrParser :: O.Parser IP.InfoProvId
infoProvPtrParser =
O.argument O.auto . mconcat $
[ O.metavar "IPE_PTR"
]