on master help sign in
cliapp: Make `failWith` impose a MonadError rather than directly kill the app

so it can be caught with an `ExceptT`.

commit685dd1f66f4fe55bfa1ec4153349ff1f265ae05c authorJohn Ericson <John.Ericson@Obsidian.Systems> dateTue 16 Oct 2018, 16:12 (8 years ago) parent8187d90
4 files changed+104−47
lib/cliapp/src/Obelisk/CliApp.hs+3−1 lib/cliapp/src/Obelisk/CliApp/Logging.hs+28−18 lib/cliapp/src/Obelisk/CliApp/Types.hs+65−20 lib/command/src/Obelisk/App.hs+8−8
lib/cliapp/src/Obelisk/CliApp.hs+3−1
@@ -6,7 +6,9 @@
66module Obelisk.CliApp
77 (
88 -- .Types
9− Cli
9+ CliLog
10+ , CliThrow
11+ , Cli
1012 , CliT(..)
1113 , runCli
1214 , CliConfig
lib/cliapp/src/Obelisk/CliApp/Logging.hs+28−18
@@ -7,12 +7,29 @@
77{-# LANGUAGE ScopedTypeVariables #-}
88-- | Provides a logging handler that facilitates safe ouputting to terminal using MVar based locking.
99-- | Spinner.hs and Process.hs work on this guarantee.
10−module Obelisk.CliApp.Logging where
10+module Obelisk.CliApp.Logging
11+ ( newCliConfig
12+ , runCli
13+ , verboseLogLevel
14+ , isOverwrite
15+ , getSeverity
16+ , getLogLevel
17+ , setLogLevel
18+ , putLog
19+ , putLogRaw
20+ , failWith
21+ , withExitFailMessage
22+ , writeLog
23+ , allowUserToMakeLoggingVerbose
24+ , getChars
25+ , fork
26+ ) where
1127
1228import Control.Concurrent (ThreadId, forkIO, killThread, threadDelay)
1329import Control.Concurrent.MVar (modifyMVar_, newMVar)
1430import Control.Monad (unless, void, when)
1531import Control.Monad.Catch (MonadCatch, MonadMask, bracket, catch, throwM)
32+import Control.Monad.Except (MonadError, throwError)
1633import Control.Monad.IO.Class (liftIO)
1734import Control.Monad.Log (Severity (..), WithSeverity (..), logMessage, runLoggingT)
1835import Control.Monad.Loops (iterateUntil)
@@ -25,7 +42,7 @@ import qualified Data.Text.IO as T
2542import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
2643 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
2744 SGR (SetColor, SetConsoleIntensity), clearLine)
28−import System.Exit (ExitCode (..), exitWith)
45+import System.Exit (ExitCode (..))
2946import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stderr, stdin, stdout)
3047
3148import qualified Obelisk.CliApp.TerminalString as TS
@@ -42,6 +59,7 @@ newCliConfig sev noColor noSpinner = do
4259runCli :: MonadIO m => CliConfig -> CliT m a -> m a
4360runCli c =
4461 flip runLoggingT (handleLog c)
62+ . unDieT
4563 . flip runReaderT c
4664 . unCliT
4765
@@ -59,17 +77,17 @@ getSeverity = \case
5977 Output_LogRaw (WithSeverity sev _) -> Just sev
6078 _ -> Nothing
6179
62−setLogLevel :: (MonadIO m, HasCliConfig m) => Severity -> m ()
63−setLogLevel sev = do
64− l <- _cliConfig_logLevel <$> getCliConfig
65− liftIO $ writeIORef l sev
66−
6780getLogLevel :: (MonadIO m, HasCliConfig m) => m Severity
6881getLogLevel = getLogLevel' =<< getCliConfig
6982
7083getLogLevel' :: MonadIO m => CliConfig -> m Severity
7184getLogLevel' = liftIO . readIORef . _cliConfig_logLevel
7285
86+setLogLevel :: (MonadIO m, HasCliConfig m) => Severity -> m ()
87+setLogLevel sev = do
88+ l <- _cliConfig_logLevel <$> getCliConfig
89+ liftIO $ writeIORef l sev
90+
7391handleLog :: MonadIO m => CliConfig -> Output -> m ()
7492handleLog conf output = do
7593 level <- getLogLevel' conf
@@ -107,21 +125,13 @@ handleLog' noColor output = do
107125 hFlush stdout
108126 return $ isOverwrite output
109127
110−-- | Log a message to the console.
111−--
112−-- Logs safely even if there are ongoing spinners.
113−putLog :: Cli m => Severity -> Text -> m ()
114−putLog sev = logMessage . Output_Log . WithSeverity sev
115−
116128-- | Like `putLog` but without the implicit newline added.
117129putLogRaw :: Cli m => Severity -> Text -> m ()
118130putLogRaw sev = logMessage . Output_LogRaw . WithSeverity sev
119131
120132-- | Like `putLog Alert` but also abrupts the program.
121−failWith :: (MonadIO m, Cli m) => Text -> m a
122−failWith s = do
123− putLog Alert s
124− liftIO $ exitWith $ ExitFailure 2
133+failWith :: MonadError Text m => Text -> m a
134+failWith = throwError
125135
126136-- | Intercept ExitFailure exceptions and log the given alert before exiting.
127137--
@@ -134,7 +144,7 @@ withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
134144 throwM e
135145
136146-- | Write log to stdout, with colors (unless `noColor`)
137−writeLog:: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
147+writeLog :: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
138148writeLog withNewLine noColor (WithSeverity severity s)
139149 | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
140150 | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
lib/cliapp/src/Obelisk/CliApp/Types.hs+65−20
@@ -7,25 +7,20 @@ module Obelisk.CliApp.Types where
77
88import Control.Concurrent.MVar (MVar)
99import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
10−import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..))
11−import Control.Monad.Reader (MonadIO, ReaderT (..), ask)
10+import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..), logMessage)
11+import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader, ask)
1212import Control.Monad.Writer (WriterT)
1313import Control.Monad.State (StateT)
14−import Control.Monad.Except (ExceptT)
14+import Control.Monad.Except (ExceptT, MonadError (..))
15+import Control.Monad.IO.Class (liftIO)
1516import Control.Monad.Trans (MonadTrans, lift)
1617import Data.IORef (IORef)
1718import Data.Text (Text)
19+import System.Exit (ExitCode (..), exitWith)
1820
1921import Obelisk.CliApp.TerminalString (TerminalString)
2022
21−data CliConfig = CliConfig
22− { _cliConfig_logLevel :: IORef Severity -- We are capable of changing the log level at runtime
23− , _cliConfig_noColor :: Bool -- Disallow coloured output
24− , _cliConfig_noSpinner :: Bool -- Disallow spinners
25− , _cliConfig_lock :: MVar Bool -- Whether the last message was an Overwrite output
26− , _cliConfig_tipDisplayed :: IORef Bool -- Whether the user tip (to make verbose) was already displayed
27− , _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString]) -- Stack of logs from nested spinners
28− }
23+--------------------------------------------------------------------------------
2924
3025data Output
3126 = Output_Log (WithSeverity Text) -- Regular logging message (with colors and newlines)
@@ -35,26 +30,59 @@ data Output
3530 | Output_ClearLine -- Clear the line
3631 deriving (Eq, Show, Ord)
3732
38−type Cli m = MonadLog Output m
33+type CliLog m = MonadLog Output m
3934
40−newtype CliT m a = CliT
41− { unCliT :: ReaderT CliConfig (LoggingT Output m) a
42− }
35+type CliThrow m = MonadError Text m
36+
37+-- TODO could remove for granularity
38+type Cli m = (CliLog m, CliThrow m)
39+
40+-- | Log a message to the console.
41+--
42+-- Logs safely even if there are ongoing spinners.
43+putLog :: Cli m => Severity -> Text -> m ()
44+putLog sev = logMessage . Output_Log . WithSeverity sev
45+
46+newtype DieT m a = DieT { unDieT :: LoggingT Output m a }
4347 deriving
4448 ( Functor, Applicative, Monad, MonadIO
49+ , MonadReader r
4550 , MonadThrow, MonadCatch, MonadMask
4651 , MonadLog Output
4752 )
4853
49−instance MonadTrans CliT where
50− lift = CliT . lift . lift
54+instance MonadTrans DieT where
55+ lift = DieT . lift
56+
57+-- TODO generalize to bigger error types
58+instance MonadIO m => MonadError Text (DieT m) where
59+ throwError s = do
60+ putLog Alert s
61+ liftIO $ exitWith $ ExitFailure 2
62+
63+ -- Cannot catch
64+ catchError m _ = m
65+
66+--------------------------------------------------------------------------------
67+
68+data CliConfig = CliConfig
69+ { -- | We are capable of changing the log level at runtime
70+ _cliConfig_logLevel :: IORef Severity
71+ , -- | Disallow coloured output
72+ _cliConfig_noColor :: Bool
73+ , -- | Disallow spinners
74+ _cliConfig_noSpinner :: Bool
75+ , -- | Whether the last message was an Overwrite output
76+ _cliConfig_lock :: MVar Bool
77+ , -- | Whether the user tip (to make verbose) was already displayed
78+ _cliConfig_tipDisplayed :: IORef Bool
79+ , -- | Stack of logs from nested spinners
80+ _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
81+ }
5182
5283class Monad m => HasCliConfig m where
5384 getCliConfig :: m CliConfig
5485
55−instance Monad m => HasCliConfig (CliT m) where
56− getCliConfig = CliT ask
57−
5886instance HasCliConfig m => HasCliConfig (ReaderT r m) where
5987 getCliConfig = lift getCliConfig
6088
@@ -66,3 +94,20 @@ instance HasCliConfig m => HasCliConfig (StateT s m) where
6694
6795instance HasCliConfig m => HasCliConfig (ExceptT e m) where
6896 getCliConfig = lift getCliConfig
97+
98+--------------------------------------------------------------------------------
99+
100+newtype CliT m a = CliT
101+ { unCliT :: ReaderT CliConfig (DieT m) a
102+ }
103+ deriving
104+ ( Functor, Applicative, Monad, MonadIO
105+ , MonadThrow, MonadCatch, MonadMask
106+ , MonadLog Output, MonadError Text, MonadReader CliConfig
107+ )
108+
109+instance MonadTrans CliT where
110+ lift = CliT . lift . lift
111+
112+instance Monad m => HasCliConfig (CliT m) where
113+ getCliConfig = ask
lib/command/src/Obelisk/App.hs+8−8
@@ -11,12 +11,13 @@ import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
1111import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
1212import Control.Monad.Writer (WriterT)
1313import Control.Monad.State (StateT)
14−import Control.Monad.Except (ExceptT)
14+import Control.Monad.Except (ExceptT, MonadError)
1515import Control.Monad.Trans.Class (MonadTrans, lift)
16+import Data.Text (Text)
1617import System.Directory (XdgDirectory (XdgData), getXdgDirectory)
1718import Control.Monad.Log (MonadLog)
1819
19−import Obelisk.CliApp (CliConfig, CliT (..), HasCliConfig, runCli, Output)
20+import Obelisk.CliApp (CliConfig, CliLog, CliThrow, CliT (..), HasCliConfig, runCli, Output)
2021
2122newtype Obelisk = Obelisk
2223 { _obelisk_cliConfig :: CliConfig
@@ -27,16 +28,14 @@ newtype ObeliskT m a = ObeliskT
2728 }
2829 deriving
2930 ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
30− , HasCliConfig)
31+ , MonadLog Output, MonadError Text, HasCliConfig)
3132
32−deriving instance Monad m => MonadLog Output (ObeliskT m)
33+instance MonadTrans ObeliskT where
34+ lift = ObeliskT . lift . lift
3335
3436class Monad m => HasObelisk m where
3537 getObelisk :: m Obelisk
3638
37−instance MonadTrans ObeliskT where
38− lift = ObeliskT . lift . lift
39−
4039instance Monad m => HasObelisk (ObeliskT m) where
4140 getObelisk = ObeliskT ask
4241
@@ -59,7 +58,8 @@ runObelisk c =
5958 . unObeliskT
6059
6160type MonadObelisk m =
62− ( MonadLog Output m
61+ ( CliLog m
62+ , CliThrow m
6363 , HasCliConfig m
6464 , HasObelisk m
6565 , MonadIO m