-- | This module handles all display of output to the console when -- propellor is ensuring Properties. -- -- When two threads both try to display a message concurrently, -- the messages will be displayed sequentially. module Propellor.Message ( getMessageHandle, isConsole, forceConsole, actionMessage, actionMessageOn, warningMessage, infoMessage, errorMessage, processChainOutput, messagesDone, createProcessConcurrent, ) where import System.Console.ANSI import System.IO import Control.Monad import Control.Monad.IO.Class (liftIO, MonadIO) import Control.Applicative import Control.Monad.IfElse import System.IO.Unsafe (unsafePerformIO) import Control.Concurrent import Propellor.Types import Utility.ConcurrentOutput import Utility.PartialPrelude import Utility.Monad import Utility.Exception data MessageHandle = MessageHandle { isConsole :: Bool } -- | A shared global variable for the MessageHandle. {-# NOINLINE globalMessageHandle #-} globalMessageHandle :: MVar MessageHandle globalMessageHandle = unsafePerformIO $ newMVar =<< MessageHandle <$> hIsTerminalDevice stdout -- | Gets the global MessageHandle. getMessageHandle :: IO MessageHandle getMessageHandle = readMVar globalMessageHandle -- | Force console output. This can be used when stdout is not directly -- connected to a console, but is eventually going to be displayed at a -- console. forceConsole :: IO () forceConsole = modifyMVar_ globalMessageHandle $ \mh -> pure (mh { isConsole = True }) -- | Only performs the action when at the console, or when console -- output has been forced. whenConsole :: IO () -> IO () whenConsole a = whenM (isConsole <$> getMessageHandle) a -- | Shows a message while performing an action, with a colored status -- display. actionMessage :: (MonadIO m, MonadMask m, ActionResult r) => Desc -> m r -> m r actionMessage = actionMessage' Nothing -- | Shows a message while performing an action on a specified host, -- with a colored status display. actionMessageOn :: (MonadIO m, MonadMask m, ActionResult r) => HostName -> Desc -> m r -> m r actionMessageOn = actionMessage' . Just actionMessage' :: (MonadIO m, MonadMask m, ActionResult r) => Maybe HostName -> Desc -> m r -> m r actionMessage' mhn desc a = do liftIO $ whenConsole $ lockOutput $ do setTitle $ "propellor: " ++ desc hFlush stdout r <- a liftIO $ lockOutput $ do whenConsole $ setTitle "propellor: running" showhn mhn putStr $ desc ++ " ... " let (msg, intensity, color) = getActionResult r colorLine intensity color msg hFlush stdout return r where showhn Nothing = return () showhn (Just hn) = do whenConsole $ setSGR [SetColor Foreground Dull Cyan] putStr (hn ++ " ") whenConsole $ setSGR [] warningMessage :: MonadIO m => String -> m () warningMessage s = liftIO $ lockOutput $ colorLine Vivid Magenta $ "** warning: " ++ s infoMessage :: MonadIO m => [String] -> m () infoMessage ls = liftIO $ lockOutput $ mapM_ putStrLn ls errorMessage :: MonadIO m => String -> m a errorMessage s = liftIO $ lockOutput $ do colorLine Vivid Red $ "** error: " ++ s error "Cannot continue!" colorLine :: ColorIntensity -> Color -> String -> IO () colorLine intensity color msg = do whenConsole $ setSGR [SetColor Foreground intensity color] putStr msg whenConsole $ setSGR [] -- Note this comes after the color is reset, so that -- the color set and reset happen in the same line. putStrLn "" hFlush stdout -- | Reads and displays each line from the Handle, except for the last line -- which is a Result. processChainOutput :: Handle -> IO Result processChainOutput h = go Nothing where go lastline = do v <- catchMaybeIO (hGetLine h) case v of Nothing -> case lastline of Nothing -> do return FailedChange Just l -> case readish l of Just r -> pure r Nothing -> do lockOutput $ do putStrLn l hFlush stdout return FailedChange Just s -> do lockOutput $ do maybe noop (\l -> unless (null l) (putStrLn l)) lastline hFlush stdout go (Just s) -- | Called when all messages about properties have been printed. messagesDone :: IO () messagesDone = lockOutput $ do whenConsole $ setTitle "propellor: done" hFlush stdout