{- Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. -} {-# LANGUAGE TypeApplications, ConstraintKinds, UndecidableInstances #-} module Glean.Util.ShellPrint ( DbVerbosity(..) , StatsFormatOpts(..) , ShellFormat(..) , ShellPrint , ShellPrintFormat(..) , PrintOpts(..) , getTerminalWidth , hPutShellPrintLn , putShellPrintLn , shellFormatOpt , shellPrint , withFormatOpts ) where import Prelude hiding ((<>)) import Control.Monad import Data.Aeson import qualified Data.Aeson as J import qualified Data.Aeson.Encode.Pretty as J import qualified Data.ByteString.Lazy.Char8 as BS import Data.Char import Data.Int import Data.Maybe import Data.Text (Text) import qualified Data.Text as Text import qualified Data.HashMap.Strict as HashMap import Data.Time.Clock.POSIX import Data.List hiding (span) import Data.Void import Options.Applicative as O import Text.Printf hiding (parseFormat) import System.Exit import System.IO import System.Process import System.Timeout import Compat.Prettyprinter import Prettyprinter.Render.Terminal as Pretty import Util.Aeson import Util.Control.Exception (catchAll) import Util.TimeSec import Util.Timing import Glean.Backend.Types (dbShard) import qualified Glean.Types as Thrift import Glean.Repo.Text (showRepo) data ShellPrintFormat = TTY | PlainText | Json | CompactJson deriving (Eq,Show) data Context = Context { ctxFormat :: ShellPrintFormat , ctxNow :: Time } parseFormat :: String -> Maybe ShellPrintFormat parseFormat "tty" = Just TTY parseFormat "plain" = Just PlainText parseFormat "json" = Just Json parseFormat "compact-json" = Just CompactJson parseFormat _ = Nothing shellFormatOpt :: Parser (Maybe ShellPrintFormat) shellFormatOpt = O.optional $ O.option (maybeReader parseFormat) $ mconcat [ O.long "format" , O.metavar "(tty|plain|json|compact-json)" , O.help "Output format" ] data PrintOpts = PrintOpts { poFormat :: ShellPrintFormat , poNow :: Time , poWidth :: Maybe PageWidth } type ShellPrint a = ShellFormat () a -- we might prefer this to be "forall o . ShellFormat o a" but that -- requires QuantifiedConstraints which isn't available until GHC 8.6.x -- and we're keeping GHC 8.4.x compatibility for now. putShellPrintLn :: ShellPrint a => Maybe ShellPrintFormat -> a -> IO () putShellPrintLn = hPutShellPrintLn stdout hPutShellPrintLn :: ShellPrint a => Handle -> Maybe ShellPrintFormat -> a -> IO () hPutShellPrintLn outh opt x = do tty <- hIsTerminalDevice stdout now <- utcTimeToPOSIXSeconds <$> getCurrentTime width <- fromMaybe 80 <$> getTerminalWidth let t0 = Time (round now) format = fromMaybe (if tty then TTY else PlainText) opt opts = PrintOpts { poFormat = format , poNow = t0 , poWidth = Just $ AvailablePerLine width 1 } shellPrint outh opts x shellPrint :: forall a. ShellFormat () a => Handle -> PrintOpts -> a -> IO () shellPrint handle PrintOpts{..} x = Pretty.renderIO handle sds where sds = layoutPretty layout $ doc <> hardline doc = case poFormat of CompactJson -> pretty $ BS.unpack $ J.encode $ shellFormatJson context () x Json -> pretty $ BS.unpack $ J.encodePretty $ shellFormatJson context () x TTY -> shellFormatText context () x PlainText -> unAnnotate $ shellFormatText context () x context = Context { ctxFormat = poFormat , ctxNow = poNow } layout = LayoutOptions { layoutPageWidth = fromMaybe Unbounded poWidth } type Ann = AnsiStyle -- | Format data of type 'a' with format options of type 'o' class ShellFormat o a where shellFormatText :: Context -> o -> a -> Doc Ann shellFormatJson :: Context -> o -> a -> Value data WithFormatOpts o a = WithFormatOpts a o instance ShellFormat o a => ShellFormat d (WithFormatOpts o a) where shellFormatText ctx _ (WithFormatOpts x opts) = shellFormatText ctx opts x shellFormatJson ctx _ (WithFormatOpts x opts) = shellFormatJson ctx opts x withFormatOpts :: ShellFormat o a => a -> o -> WithFormatOpts o a withFormatOpts = WithFormatOpts instance ShellFormat o Void where shellFormatText _ctx _ v = case v of {} shellFormatJson _ctx _ v = case v of {} instance ShellFormat o String where shellFormatText _ctx _ s = pretty s shellFormatJson _ctx _ s = J.toJSON s instance ShellFormat o Thrift.DatabaseProperties where shellFormatText _ctx _ props = vsep [ pretty name <> ":" <+> pretty value | (name,value) <- sortOn fst $ HashMap.toList props ] shellFormatJson _ctx _ props = J.toJSON props instance ShellFormat o Thrift.DatabaseStatus where shellFormatText _ctx _ status = case status of Thrift.DatabaseStatus_Complete -> parens "complete" Thrift.DatabaseStatus_Available -> parens "available elsewhere" Thrift.DatabaseStatus_Finalizing -> parens "finalizing" Thrift.DatabaseStatus_Incomplete -> parens "incomplete" Thrift.DatabaseStatus_Restoring -> parens "restoring" Thrift.DatabaseStatus_Broken -> parens "broken" Thrift.DatabaseStatus_Restorable -> parens "restorable" Thrift.DatabaseStatus_Missing -> parens "missing deps" shellFormatJson _ctx _ status = J.toJSON @String $ case status of Thrift.DatabaseStatus_Complete -> "COMPLETE" Thrift.DatabaseStatus_Available-> "AVAILABLE" Thrift.DatabaseStatus_Finalizing -> "FINALIZING" Thrift.DatabaseStatus_Incomplete -> "INCOMPLETE" Thrift.DatabaseStatus_Restoring -> "RESTORING" Thrift.DatabaseStatus_Broken -> "BROKEN" Thrift.DatabaseStatus_Restorable -> "RESTORABLE" Thrift.DatabaseStatus_Missing -> "MISSING" instance ShellFormat o Thrift.Dependencies where shellFormatText _ctx _ dependency = case dependency of Thrift.Dependencies_stacked Thrift.Stacked{..} -> pretty (showRepo $ Thrift.Repo stacked_name stacked_hash) Thrift.Dependencies_pruned (Thrift.Pruned baseRepo _ _ _) -> pretty (showRepo baseRepo) shellFormatJson _ctx _ dependency = case dependency of Thrift.Dependencies_stacked Thrift.Stacked{..} -> J.toJSON (showRepo $ Thrift.Repo stacked_name stacked_hash) Thrift.Dependencies_pruned (Thrift.Pruned baseRepo _ _ _) -> J.toJSON (showRepo baseRepo) instance ShellFormat o Thrift.Repo where shellFormatText _ctx _ repo = pretty (showRepo repo) shellFormatJson _ctx _ repo = J.toJSON (showRepo repo) statusColour :: Thrift.DatabaseStatus -> Color statusColour status = case status of Thrift.DatabaseStatus_Complete -> Green Thrift.DatabaseStatus_Available -> Green Thrift.DatabaseStatus_Finalizing -> Green Thrift.DatabaseStatus_Incomplete -> Blue Thrift.DatabaseStatus_Restoring -> Black Thrift.DatabaseStatus_Broken -> Red Thrift.DatabaseStatus_Restorable -> Black Thrift.DatabaseStatus_Missing -> Black statusStyle :: Thrift.DatabaseStatus -> AnsiStyle statusStyle = color . statusColour data DbVerbosity = DbSummarise | DbDescribe deriving (Eq) instance ShellFormat DbVerbosity Thrift.Database where shellFormatText ctx opts db = shellFormatText ctx opts (db, []::[(String, Void)]) shellFormatJson ctx opts db = shellFormatJson ctx opts (db, []::[(String, Void)]) instance (ShellFormat DbVerbosity v) => ShellFormat DbVerbosity (Thrift.Database, [(String, v)]) where shellFormatText ctx@Context{..} opts (db, extras) = nest 2 $ vsep $ [ annotate (statusStyle status) (shellFormatText ctx () repo) <+> shellFormatText ctx () status] ++ [ "Source:" <+> showWhen t | Just t <- [Thrift.database_repo_hash_time db] ] ++ [ "Created:" <+> showWhen (Thrift.database_created_since_epoch db) ] ++ [ "Completed:" <+> showWhen databaseComplete_time | Just Thrift.DatabaseComplete{..} <- [Thrift.database_complete db] ] ++ [ "Size:" <+> pretty(showSizeBytes (fromIntegral s)) | Just Thrift.DatabaseComplete{..} <- [Thrift.database_complete db] , Just s <- [databaseComplete_bytes] ] ++ [ "Broken:" <+> (if Text.null task then emptyDoc else pretty task <> ": ") <> pretty reason | Just (Thrift.DatabaseBroken task reason) <- [Thrift.database_broken db] ] ++ [ pretty key <> ":" <+> shellFormatText ctx opts value | (key, value) <- extras] ++ [ "Dependencies:" <+> shellFormatText ctx () dependency | Just dependency <- [Thrift.database_dependencies db] ] ++ [ "Backup:" <+> pretty loc | verbosity == DbDescribe || Thrift.database_status db == Thrift.DatabaseStatus_Restorable , Just loc <- [Thrift.database_location db] ] ++ [ "Expires in:" <+> pretty (ppTimeSpan timeSpan) | verbosity == DbDescribe , Just expiresEpochTime <- [Thrift.unPosixEpochTime <$> Thrift.database_expire_time db] , let expires = Time $ fromIntegral expiresEpochTime , let timeSpan = expires `timeDiff` ctxNow ] ++ [ "Shard:" <+> pretty (dbShard repo) | verbosity == DbDescribe ] ++ [ nest 2 $ vsep [ "Properties:" , shellFormatText ctx () (Thrift.database_properties db) ] | verbosity == DbDescribe ] where showWhen (Thrift.PosixEpochTime t) = pretty (show (posixSecondsToUTCTime (fromIntegral t))) <+> parens (pretty (Text.unpack age) <+> "ago") where age = ppTimeSpanWithGranularity Hour $ ctxNow `timeDiff` Time (fromIntegral t) showSizeBytes :: Double -> String showSizeBytes b | b > 1e9 = printf "%.2f GB" (b / 1e9) | otherwise = printf "%.2f MB" (b / 1e6) status = Thrift.database_status db repo = Thrift.database_repo db verbosity = opts shellFormatJson ctx opts (db, extras) = J.object $ [ "repo" .= shellFormatJson ctx () repo , "status" .= shellFormatJson ctx () status , "created" .= jsonTime (Thrift.database_created_since_epoch db) , "completed" .= jsonMaybeTime (Thrift.databaseComplete_time <$> Thrift.database_complete db) , "backup" .= maybe J.Null J.toJSON (Thrift.database_location db) , "expires" .= jsonMaybeTime (Thrift.database_expire_time db) , "shard" .= J.toJSON (dbShard $ Thrift.database_repo db) , "source" .= maybe J.Null jsonTime (Thrift.database_repo_hash_time db) , "properties" .= shellFormatJson ctx () (Thrift.database_properties db) , "dependencies" .= maybe J.Null J.toJSON (Thrift.database_dependencies db) , "size" .= maybe J.Null J.toJSON (Thrift.databaseComplete_bytes <$> Thrift.database_complete db) ] ++ [ keyFromText (jsonKeyFrom key) .= shellFormatJson ctx opts value | (key, value) <- extras] where status = Thrift.database_status db repo = Thrift.database_repo db jsonMaybeTime = maybe J.Null jsonTime jsonTime (Thrift.PosixEpochTime t) = J.toJSON $ posixSecondsToUTCTime (fromIntegral t) jsonKeyFrom s = Text.pack $ map f s where f ' ' = '_' f c = toLower c instance ShellFormat DbVerbosity [Thrift.Database] where shellFormatText ctx opts dbs = vsep $ concatMap f $ sortOn Thrift.database_created_since_epoch dbs where f db = [shellFormatText ctx opts db, pretty (" "::Text)] shellFormatJson ctx v dbs = J.toJSON $ map (shellFormatJson ctx v) dbs instance (ShellFormat DbVerbosity v) => ShellFormat DbVerbosity [(Thrift.Database, [(String, v)])] where shellFormatText ctx opts dbs = vsep $ concatMap f $ sortOn (Thrift.database_created_since_epoch . fst) dbs where f x = [shellFormatText ctx opts x, pretty (" "::Text)] shellFormatJson ctx v dbs = J.toJSON $ map (shellFormatJson ctx v) dbs type PredStatsList = [(Either Thrift.Id Thrift.PredicateRef, Thrift.PredicateStats)] type PredStatsFilter = Either Thrift.Id Thrift.PredicateRef -> Bool data StatsFormatOpts = StatsFormatOpts { showTotal :: Bool , sortBySize :: Bool } deriving (Eq) instance ShellFormat StatsFormatOpts (PredStatsFilter, PredStatsList) where shellFormatText _context opts (filterPred, preds) = vsep $ [ nest 2 $ vsep [ case ref of Left id -> pretty id Right pref -> pretty pref , "count:" <+> pretty (Thrift.predicateStats_count stats) , "size: " <+> pretty (getSizeInfo (Thrift.predicateStats_size stats) totalSizeBytes) ] | (ref, stats) <- sort $ filter (filterPred . fst) preds ] ++ if showTotal opts then [ "" , "Total: " <> pretty totalFacts <+> "facts" <+> parens (pretty (showAllocs totalSizeBytes)) ] else [] where predicate_count = Thrift.predicateStats_count . snd predicate_size = Thrift.predicateStats_size . snd totalFacts = foldl' (+) 0 $ map predicate_count preds totalSizeBytes = foldl' (+) 0 $ map predicate_size preds sort = if sortBySize opts then sortOn $ negate . predicate_size else sortOn fst shellFormatJson _ _ (filterPred, preds) = J.toJSON $ filter (filterPred . fst) preds getSizeInfo :: Int64 -> Int64 -> String getSizeInfo bytes total = printf "%d (%s) %.4f%%" bytes humanReadableSize percentage_x where percentage_x :: Double percentage_x = 100 * fromIntegral bytes / fromIntegral total humanReadableSize = showAllocs bytes -- | Get the terminal width getTerminalWidth :: IO (Maybe Int) getTerminalWidth = fmap join $ -- FIXME: This is a terrible way to get the terminal size but we don't -- seem to have any packages which can do this. System.Timeout.timeout 100000 (withCreateProcess (proc "stty" ["size"]){std_out = CreatePipe, std_err = CreatePipe} (\_ mouth merrh ph -> do let outh = fromMaybe (error "outh") mouth let errh = fromMaybe (error "errh") merrh out <- hGetContents outh err <- hGetContents errh length out `seq` length err `seq` return () hClose outh hClose errh ex <- waitForProcess ph return $ case ex of ExitSuccess | [[(_,"")],[(w,"")]] <- map reads $ words out -> Just w _ -> Nothing )) `catchAll` \_ -> return Nothing