packages feed

ghcup-0.2.1.0: lib/GHCup/Command/HealthCheck.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE MultiWayIf #-}

module GHCup.Command.HealthCheck where

import GHCup.Errors
import GHCup.Legacy.Utils
import GHCup.Prelude
import GHCup.Query.DB
import GHCup.Query.GHCupDirs
import GHCup.Types
import GHCup.Types.Optics

import Control.Exception
    (SomeException )
import Control.Exception.Safe         ( try, handleIO )
import Control.Monad                  ( forM )
import Control.Monad.IO.Class         ( liftIO )
import Control.Monad.Reader           ( MonadReader )
import Data.Functor                   ( (<&>) )
import Data.List                      ( foldl', sort )
import Data.Variant.Excepts
    ( pattern V, pattern VLeft, pattern VRight, runE )
import System.Directory               ( doesFileExist )
import System.FilePath                ( pathSeparator, (</>) )
import Text.PrettyPrint.HughesPJClass (Pretty(..), text, nest, vcat, ($$), ($+$))
import Witherable                     ( catMaybes, forMaybe )


data SymlinkState = Fine FilePath FilePath
                  | DestinationDoesn'tExist FilePath FilePath
                  | WrongDestination FilePath FilePath FilePath
                  | AllWrong FilePath FilePath FilePath
                  | Missing FilePath
  deriving (Show, Ord, Eq)

instance Pretty SymlinkState where
  pPrint (DestinationDoesn'tExist f dest) = vcat $ fmap text
    [ "- Target does not exist:"
    , "  * Symlink: " <> f
    , "  * Target: "  <> dest
    ]
  pPrint (WrongDestination f should actual) = vcat $ fmap text
    [ "- Wrong target:"
    , "  * Symlink: " <> f
    , "  * Target (should): "  <> should
    , "  * Target (actual): "  <> actual
    ]
  pPrint (AllWrong f should actual) = vcat $ fmap text
    [ "- Wrong and broken target:"
    , "  * Symlink: " <> f
    , "  * Target (should): "  <> should
    , "  * Target (actual and broken): "  <> actual
    ]
  pPrint (Missing f) = vcat $ fmap text
    [ "- Symlink missing:"
    , "  * Symlink: " <> f
    ]
  pPrint (Fine f link) = vcat $ fmap text
    [ "- All good:"
    , "  * Symlink: " <> f
    , "  * Target: "  <> link
    ]

data ToolHealthCheck = ToolHealthCheck {
    thHaveInstFile   :: (FilePath, Bool)
  , thHaveSpecFile   :: (FilePath, Bool)
  , thParseSpecFile  :: Bool
  , thInstallDest    :: FilePath
  , thSupposedInstalledFiles :: Maybe Int
  , thMissingFiles   :: Maybe [FilePath]
  , thStrayFiles     :: Maybe [FilePath]
  , thSymlinkState   :: Maybe [SymlinkState]
  , thSetState       :: Maybe [SymlinkState]
  }
  deriving (Show)

instance Pretty ToolHealthCheck where
  pPrint ToolHealthCheck{..} = vcat
    [ let (f, e) = thHaveInstFile in text $ "Installed artifacts file exists at " <> f <> ": " <> show e
    , let (f, e) = thHaveSpecFile in text $ "Installed specification file exists at " <> f <> ": " <> show e
    , text $ "Can parse specification file: " <> show thParseSpecFile
    , text $ "Install destination: " <> show thInstallDest
    , text $ "Number of files that should be installed: " <> maybe "(no information)" show thSupposedInstalledFiles
    , text "Missing files:" $$ nest 4 (ppFiles thMissingFiles)
    , text "Stray files:" $$ nest 4 (ppFiles thStrayFiles)
    , text "Symlink status:" $$ nest 4 (ppFiles thSymlinkState)
    , text "Set symlink status:" $$ nest 4 (ppFiles thSetState)
    ]
   where
    ppFiles Nothing   = text "(no information)"
    ppFiles (Just []) = text "none"
    ppFiles (Just fs) = foldl' ($+$) mempty (map pPrint $ sort fs)

-- TODO
--   -     (_:_:_) -> lift $ do
--            logWarn $ "Looks like you have a corrupted DB/installation\n"
--                    <> "More than one revision installed of: " <> T.pack (prettyShow tool)
--   - detect legacy installations
dbHealthCheck ::
  ( MonadReader env m
  , HasDirs env
  , HasPlatformReq env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> TargetVersion
  -> m ToolHealthCheck
dbHealthCheck tool tver = do
  Dirs{..} <- getDirs

  instFile <- recordedInstallationFile tool tver
  thHaveInstFile <- fmap (instFile,) $ liftIO $ doesFileExist instFile
  toolDest <- toolInstallDestination tool tver
  let thInstallDest = fromGHCupPath toolDest
  specFile <- recordedInstallationSpecFile tool tver

  (thHaveSpecFile, thParseSpecFile) <- fmap (\(a, b) -> ((specFile, a), b)) $ runE (getInstallMetadata tool tver) >>= \case
    VRight _ -> pure (True, True)
    VLeft (V (FileDoesNotExistError{})) -> pure (False, False)
    VLeft (V (ParseError{})) -> pure (True, False)
    VLeft _ -> fail "oops"

  symLSpec <- runE (getSymlinkSpec tool tver)

  thSymlinkState <- case symLSpec of
    VRight spec -> fmap Just $ do
      forMaybe spec $ \(SymlinkSpec target link _ _) -> do
        inspectSymlink (binDir </> link) target binDir toolDest
    VLeft _ -> pure Nothing

  installedFiles <- getInstalledFiles tool tver

  allFiles <- fmap (dropPrefix (fromGHCupPath toolDest <> [pathSeparator])) <$> liftIO (handleIO (\_ -> pure []) $ getFilesDeep toolDest)
  let thStrayFiles :: Maybe [FilePath] = installedFiles <&> \thif -> catMaybes $ allFiles <&> \f -> if f `notElem` thif then Just f else Nothing

  thMissingFiles <- forM installedFiles $ \mf -> forMaybe mf $ \f -> do
    b <- liftIO $ doesFileExist (fromGHCupPath toolDest </> f)
    if b then pure Nothing else pure (Just f)

  let thSupposedInstalledFiles = length <$> installedFiles

  thSetState <- runE (isSet tool tver) >>= \case
    VRight (Just _)
      | VRight spec <- symLSpec
      -> fmap Just $ forMaybe spec $ \case
           (SymlinkSpec _ _ _ Nothing) -> pure Nothing
           (SymlinkSpec target _ _ (Just setName)) -> inspectSymlink (binDir </> setName) target binDir toolDest
    VRight (Just _)
      | VLeft _ <- symLSpec
      -> pure Nothing
    VRight Nothing -> pure Nothing
    VLeft _ -> pure Nothing

  pure ToolHealthCheck{..}
 where
  inspectSymlink f target bindir tooldest = do
    mtfp <- try @_ @SomeException $ liftIO $ getLinkTarget f
    case mtfp of
      Left _ -> pure $ Just $ Missing f
      Right tfp -> do
        dest <- binarySymLinkDestination bindir (fromGHCupPath tooldest </> target)
        broken <- liftIO $ isBrokenSymlink f
        pure $ Just $ if | broken
                         , tfp /= dest
                         -> AllWrong f dest tfp
                         | broken
                         -> DestinationDoesn'tExist f tfp
                         | tfp /= dest
                         -> WrongDestination f dest tfp
                         | otherwise
                         -> Fine f dest