on master help sign in
Merge pull request #246 from obsidiansystems/improved-error-messages

Improved error messages

commit12903442db92d132db49cadc1719f0db14f2fcd9 authorLuigy Leon <luichi.19@gmail.com> dateThu 18 Oct 2018, 15:23 (8 years ago) parent69830372760991
15 files changed+282−135
lib/cliapp/obelisk-cliapp.cabal+1 lib/cliapp/src/Obelisk/CliApp.hs+5−2 lib/cliapp/src/Obelisk/CliApp/Demo.hs+5−2 lib/cliapp/src/Obelisk/CliApp/Logging.hs+43−16 lib/cliapp/src/Obelisk/CliApp/Process.hs+72−38 lib/cliapp/src/Obelisk/CliApp/Spinner.hs+4−4 lib/cliapp/src/Obelisk/CliApp/Types.hs+38−26 lib/command/src/Obelisk/App.hs+47−7 lib/command/src/Obelisk/Command.hs+16−4 lib/command/src/Obelisk/Command/Nix.hs+33−19 lib/command/src/Obelisk/Command/Run.hs+1−1 lib/command/src/Obelisk/Command/Thunk.hs+8−6 lib/command/src/Obelisk/Command/Upgrade/Hash.hs+4−5 lib/command/src/Obelisk/Command/Utils.hs+3−3 lib/command/src/Obelisk/Command/VmBuilder.hs+2−2
lib/cliapp/obelisk-cliapp.cabal+1
@@ -12,6 +12,7 @@ library
1212 , bytestring
1313 , exceptions
1414 , io-streams
15+ , lens
1516 , logging-effect
1617 , monad-loops
1718 , mtl
lib/cliapp/src/Obelisk/CliApp.hs+5−2
@@ -8,7 +8,6 @@ module Obelisk.CliApp
88 -- .Types
99 CliLog
1010 , CliThrow
11− , Cli
1211 , CliT(..)
1312 , runCli
1413 , CliConfig
@@ -22,18 +21,22 @@ module Obelisk.CliApp
2221 , withSpinner'
2322
2423 -- .Logging
24+ , AsUnstructuredError (..)
2525 , newCliConfig
2626 , getLogLevel
2727 , putLog
2828 , failWith
29+ , errorToWarning
2930 , withExitFailMessage
3031
3132 -- Control.Monad.Log
3233 , Severity (..)
3334
3435 -- .Process
35− , ProcessFailed (..)
36+ , ProcessFailure (..)
37+ , AsProcessFailure (..)
3638 , readProcessAndLogStderr
39+ , readProcessAndLogOutput
3740 , callProcessAndLogOutput
3841 , createProcess_
3942 , callProcess
lib/cliapp/src/Obelisk/CliApp/Demo.hs+5−2
@@ -13,7 +13,10 @@ import Control.Monad.Catch (MonadMask)
1313
1414import Obelisk.CliApp
1515
16−cliDemo :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m) => m ()
16+cliDemo
17+ :: ( MonadIO m, MonadMask m
18+ , CliLog m, HasCliConfig e m, CliThrow e m, AsProcessFailure e, AsUnstructuredError e)
19+ => m ()
1720cliDemo = withSpinner "CLI Demo" $ do
1821 putLog Notice "This demo will showcase the CLI library functionality"
1922 _ <- withSpinner' "Searching long for something" (Just $ \c -> "Discovered " <> T.pack (show c) <> " objects") $ do
@@ -40,7 +43,7 @@ cliDemo = withSpinner "CLI Demo" $ do
4043 withSpinner "Looking around" $ do
4144 delay
4245 output <- readProcessAndLogStderr Notice $ proc "ls" ["-l", "/"]
43− putLog Notice $ "Output was: " <> T.pack output
46+ putLog Notice $ "Output was: " <> output
4447 delay
4548 callProcessAndLogOutput (Notice, Error) $ proc "ls" ["-l", "/does-not-exist"]
4649 delay
lib/cliapp/src/Obelisk/CliApp/Logging.hs+43−16
@@ -4,11 +4,13 @@
44{-# LANGUAGE LambdaCase #-}
55{-# LANGUAGE MultiParamTypeClasses #-}
66{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE RankNTypes #-}
78{-# LANGUAGE ScopedTypeVariables #-}
89-- | Provides a logging handler that facilitates safe ouputting to terminal using MVar based locking.
910-- | Spinner.hs and Process.hs work on this guarantee.
1011module Obelisk.CliApp.Logging
11− ( newCliConfig
12+ ( AsUnstructuredError (..)
13+ , newCliConfig
1214 , runCli
1315 , verboseLogLevel
1416 , isOverwrite
@@ -18,6 +20,7 @@ module Obelisk.CliApp.Logging
1820 , putLog
1921 , putLogRaw
2022 , failWith
23+ , errorToWarning
2124 , withExitFailMessage
2225 , writeLog
2326 , allowUserToMakeLoggingVerbose
@@ -27,9 +30,10 @@ module Obelisk.CliApp.Logging
2730
2831import Control.Concurrent (ThreadId, forkIO, killThread, threadDelay)
2932import Control.Concurrent.MVar (modifyMVar_, newMVar)
33+import Control.Lens (Prism', review)
3034import Control.Monad (unless, void, when)
3135import Control.Monad.Catch (MonadCatch, MonadMask, bracket, catch, throwM)
32−import Control.Monad.Except (MonadError, throwError)
36+import Control.Monad.Except (throwError)
3337import Control.Monad.IO.Class (liftIO)
3438import Control.Monad.Log (Severity (..), WithSeverity (..), logMessage, runLoggingT)
3539import Control.Monad.Loops (iterateUntil)
@@ -48,17 +52,23 @@ import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stder
4852import qualified Obelisk.CliApp.TerminalString as TS
4953import Obelisk.CliApp.Types
5054
51−newCliConfig :: Severity -> Bool -> Bool -> IO CliConfig
52−newCliConfig sev noColor noSpinner = do
55+newCliConfig
56+ :: Severity
57+ -> Bool
58+ -> Bool
59+ -> (e -> (Text, Int))
60+ -> IO (CliConfig e)
61+newCliConfig sev noColor noSpinner errorLogExitCode = do
5362 level <- newIORef sev
5463 lock <- newMVar False
5564 tipDisplayed <- newIORef False
5665 stack <- newIORef ([], [])
57− return $ CliConfig level noColor noSpinner lock tipDisplayed stack
66+ return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode
5867
59−runCli :: MonadIO m => CliConfig -> CliT m a -> m a
68+runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
6069runCli c =
6170 flip runLoggingT (handleLog c)
71+ . flip runReaderT (_cliConfig_errorLogExitCode c)
6272 . unDieT
6373 . flip runReaderT c
6474 . unCliT
@@ -77,18 +87,18 @@ getSeverity = \case
7787 Output_LogRaw (WithSeverity sev _) -> Just sev
7888 _ -> Nothing
7989
80−getLogLevel :: (MonadIO m, HasCliConfig m) => m Severity
90+getLogLevel :: (MonadIO m, HasCliConfig e m) => m Severity
8191getLogLevel = getLogLevel' =<< getCliConfig
8292
83−getLogLevel' :: MonadIO m => CliConfig -> m Severity
93+getLogLevel' :: MonadIO m => CliConfig e -> m Severity
8494getLogLevel' = liftIO . readIORef . _cliConfig_logLevel
8595
86−setLogLevel :: (MonadIO m, HasCliConfig m) => Severity -> m ()
96+setLogLevel :: (MonadIO m, HasCliConfig e m) => Severity -> m ()
8797setLogLevel sev = do
8898 l <- _cliConfig_logLevel <$> getCliConfig
8999 liftIO $ writeIORef l sev
90100
91−handleLog :: MonadIO m => CliConfig -> Output -> m ()
101+handleLog :: MonadIO m => CliConfig e -> Output -> m ()
92102handleLog conf output = do
93103 level <- getLogLevel' conf
94104 liftIO $ modifyMVar_ (_cliConfig_lock conf) $ \wasOverwriting -> do
@@ -126,17 +136,34 @@ handleLog' noColor output = do
126136 return $ isOverwrite output
127137
128138-- | Like `putLog` but without the implicit newline added.
129−putLogRaw :: Cli m => Severity -> Text -> m ()
139+putLogRaw :: CliLog m => Severity -> Text -> m ()
130140putLogRaw sev = logMessage . Output_LogRaw . WithSeverity sev
131141
142+-- | Indicates unstructured errors form one variant (or conceptual projection)
143+-- of the error type.
144+--
145+-- Shouldn't really use this, but who has time to clean up that much!
146+class AsUnstructuredError e where
147+ asUnstructuredError :: Prism' e Text
148+
149+instance AsUnstructuredError Text where
150+ asUnstructuredError = id
151+
132152-- | Like `putLog Alert` but also abrupts the program.
133−failWith :: MonadError Text m => Text -> m a
134−failWith = throwError
153+failWith :: (CliThrow e m, AsUnstructuredError e) => Text -> m a
154+failWith = throwError . review asUnstructuredError
155+
156+errorToWarning
157+ :: (HasCliConfig e m, CliThrow e m, CliLog m)
158+ => e -> m ()
159+errorToWarning e = do
160+ c <- getCliConfig
161+ putLog Warning $ fst $ _cliConfig_errorLogExitCode c e
135162
136163-- | Intercept ExitFailure exceptions and log the given alert before exiting.
137164--
138165-- This is useful when you want to provide contextual information to a deeper failure.
139−withExitFailMessage :: (Cli m, MonadCatch m) => Text -> m a -> m a
166+withExitFailMessage :: (CliLog m, MonadCatch m) => Text -> m a -> m a
140167withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
141168 case e of
142169 ExitFailure _ -> putLog Alert msg
@@ -162,7 +189,7 @@ writeLog withNewLine noColor (WithSeverity severity s)
162189--
163190-- Call this function in a thread, and kill it to turn off keystroke monitoring.
164191allowUserToMakeLoggingVerbose
165− :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
192+ :: (MonadIO m, MonadMask m, CliLog m, HasCliConfig e m)
166193 => String -- ^ The key to press in order to make logging verbose
167194 -> m ()
168195allowUserToMakeLoggingVerbose keyCode = bracket showTip (liftIO . killThread) $ \_ -> do
@@ -195,7 +222,7 @@ getChars = reverse <$> f mempty
195222 True -> f (x:xs)
196223 False -> return (x:xs)
197224
198−fork :: (HasCliConfig m, MonadIO m) => CliT IO () -> m ThreadId
225+fork :: (HasCliConfig e m, MonadIO m) => CliT e IO () -> m ThreadId
199226fork f = do
200227 c <- getCliConfig
201228 liftIO $ forkIO $ runCli c f
lib/cliapp/src/Obelisk/CliApp/Process.hs+72−38
@@ -9,8 +9,10 @@
99-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
1010-- and is thus spinner friendly.
1111module Obelisk.CliApp.Process
12− ( ProcessFailed (..)
12+ ( ProcessFailure (..)
13+ , AsProcessFailure (..)
1314 , readProcessAndLogStderr
15+ , readProcessAndLogOutput
1416 , callProcessAndLogOutput
1517 , createProcess
1618 , createProcess_
@@ -18,16 +20,18 @@ module Obelisk.CliApp.Process
1820 , callCommand
1921 ) where
2022
21−import Control.Applicative (liftA2)
22−import Control.Monad (void, (<=<))
23−import Control.Monad.Catch (Exception(..), MonadMask, throwM)
23+import Control.Monad ((<=<), join, void)
24+import Control.Monad.Except (throwError)
2425import Control.Monad.IO.Class (MonadIO, liftIO)
26+import Control.Lens (Prism', review)
27+import qualified Data.ByteString as BS
2528import qualified Data.ByteString.Char8 as BSC
2629import Data.Function (fix)
2730import Data.Monoid ((<>))
31+import Data.Text (Text)
2832import qualified Data.Text as T
29−import Data.Text.Encoding (decodeUtf8)
30−import qualified Data.Text.IO as T
33+import qualified Data.Text.Encoding as T
34+--import qualified Data.Text.IO as T
3135import System.Exit (ExitCode (..))
3236import System.IO (Handle)
3337import System.IO.Streams (InputStream, handleToInputStream)
@@ -39,60 +43,90 @@ import qualified System.Process as Process
3943
4044import Control.Monad.Log (Severity (..))
4145import Obelisk.CliApp.Logging (putLog, putLogRaw)
42−import Obelisk.CliApp.Types (Cli)
46+import Obelisk.CliApp.Types (CliLog, CliThrow)
4347
44−data ProcessFailed = ProcessFailed Process.CmdSpec Int -- exit code
48+-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
49+data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
4550 deriving Show
4651
47−instance Exception ProcessFailed where
48− displayException (ProcessFailed spec exitCode) =
49− unwords ["ProcessFailed", reconstructCommand spec, show exitCode]
52+-- | Indicates arbitrary process failures form one variant (or conceptual projection) of
53+-- the error type.
54+class AsProcessFailure e where
55+ asProcessFailure :: Prism' e ProcessFailure
56+
57+instance AsProcessFailure ProcessFailure where
58+ asProcessFailure = id
5059
5160readProcessAndLogStderr
52− :: (MonadIO m, MonadMask m, Cli m)
53− => Severity -> CreateProcess -> m String
61+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
62+ => Severity -> CreateProcess -> m Text
5463readProcessAndLogStderr sev process = do
5564 (out, _err) <- withProcess process $ \_out err -> do
5665 streamToLog =<< liftIO (streamHandle sev err)
57− -- TODO: Why are we using Text here?
58− liftIO $ T.unpack . T.strip <$> T.hGetContents out
66+ liftIO $ T.decodeUtf8 <$> BS.hGetContents out
5967
60−-- | Like `System.Process.callProcess` but logs the combined output--stdout and stderr with the corresponding
61−-- severity.
68+-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
69+-- with the corresponding severity.
6270--
63−-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However some processes
64−-- are known to spit out diagnostic or informative messages in stderr, in which case it is advisable to call
65−-- it with a non-Error severity for stderr, like `callProcessAndLogOutput (Debug, Debug)`.
71+-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
72+-- some processes are known to spit out diagnostic or informative messages in stderr, in
73+-- which case it is advisable to call it with a non-Error severity for stderr, like
74+-- `callProcessAndLogOutput (Debug, Debug)`.
75+readProcessAndLogOutput
76+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
77+ => (Severity, Severity) -> CreateProcess -> m Text
78+readProcessAndLogOutput (sev_out, sev_err) process = do
79+ (_, Just out, Just err, p) <- createProcess $ process
80+ { std_out = CreatePipe , std_err = CreatePipe }
81+
82+ -- TODO interleave stdout and stderr in log correctly
83+ streamToLog =<< liftIO (streamHandle sev_err err)
84+ outText <- liftIO $ T.decodeUtf8 <$> BS.hGetContents out
85+ putLogRaw sev_out outText
86+
87+ liftIO (waitForProcess p) >>= \case
88+ ExitSuccess -> pure outText
89+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
90+
91+-- | Like `System.Process.callProcess` but logs the combined output (stdout and stderr)
92+-- with the corresponding severity.
93+--
94+-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
95+-- some processes are known to spit out diagnostic or informative messages in stderr, in
96+-- which case it is advisable to call it with a non-Error severity for stderr, like
97+-- `callProcessAndLogOutput (Debug, Debug)`.
6698callProcessAndLogOutput
67− :: (MonadIO m, MonadMask m, Cli m)
99+
100+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
68101 => (Severity, Severity) -> CreateProcess -> m ()
69−callProcessAndLogOutput (sev_out, sev_err) process = do
102+callProcessAndLogOutput (sev_out, sev_err) process =
70103 void $ withProcess process $ \out err -> do
71− stream <- liftIO $ do
72− uncurry combineStream =<< liftA2 (,) (streamHandle sev_out out) (streamHandle sev_err err)
104+ stream <- liftIO $ join $ combineStream
105+ <$> streamHandle sev_out out
106+ <*> streamHandle sev_err err
73107 streamToLog stream
74108 where
75109 combineStream s1 s2 = concurrentMerge [s1, s2]
76110
77111-- | Like `System.Process.createProcess` but also logs (debug) the process being run
78112createProcess
79− :: (MonadIO m, Cli m)
113+ :: (MonadIO m, CliLog m)
80114 => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
81115createProcess p = do
82− putLog Debug $ "Creating process: " <> T.pack (reconstructCommand (cmdspec p))
116+ putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec p)
83117 liftIO $ Process.createProcess p
84118
85119-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
86120createProcess_
87− :: (MonadIO m, Cli m)
121+ :: (MonadIO m, CliLog m)
88122 => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
89123createProcess_ name p = do
90− putLog Debug $ "Creating process " <> T.pack name <> ": " <> T.pack (reconstructCommand (cmdspec p))
124+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructCommand (cmdspec p)
91125 liftIO $ Process.createProcess p
92126
93127-- | Like `System.Process.callProcess` but also logs (debug) the process being run
94128callProcess
95− :: (MonadIO m, Cli m)
129+ :: (MonadIO m, CliLog m)
96130 => String -> [String] -> m ()
97131callProcess exe args = do
98132 putLog Debug $ "Calling process " <> T.pack exe <> " with args: " <> T.pack (show args)
@@ -100,14 +134,14 @@ callProcess exe args = do
100134
101135-- | Like `System.Process.callCommand` but also logs (debug) the command being run
102136callCommand
103− :: (MonadIO m, Cli m)
137+ :: (MonadIO m, CliLog m)
104138 => String -> m ()
105139callCommand cmd = do
106140 putLog Debug $ "Calling command " <> T.pack cmd
107141 liftIO $ Process.callCommand cmd
108142
109143withProcess
110− :: (MonadIO m, MonadMask m, Cli m)
144+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
111145 => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
112146withProcess process f = do -- TODO: Use bracket.
113147 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
@@ -118,7 +152,7 @@ withProcess process f = do -- TODO: Use bracket.
118152
119153 liftIO (waitForProcess p) >>= \case
120154 ExitSuccess -> return (out, err)
121− ExitFailure code -> throwM $ ProcessFailed (cmdspec process) code
155+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
122156
123157-- Create an input stream from the file handle, associating each item with the given severity.
124158streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -126,16 +160,16 @@ streamHandle sev = Streams.map (sev,) <=< handleToInputStream
126160
127161-- | Read from an input stream and log its contents
128162streamToLog
129− :: (MonadIO m, MonadMask m, Cli m)
163+ :: (MonadIO m, CliLog m)
130164 => InputStream (Severity, BSC.ByteString) -> m ()
131165streamToLog stream = fix $ \loop -> do
132166 liftIO (Streams.read stream) >>= \case
133167 Nothing -> return ()
134− Just (sev, line) -> putLogRaw sev (decodeUtf8 line) >> loop
168+ Just (sev, line) -> putLogRaw sev (T.decodeUtf8 line) >> loop
135169
136−reconstructCommand :: Process.CmdSpec -> String
137−reconstructCommand (Process.ShellCommand str) = str
170+reconstructCommand :: Process.CmdSpec -> Text
171+reconstructCommand (Process.ShellCommand str) = T.pack str
138172reconstructCommand (Process.RawCommand c as) = processToShellString c as
139173 where
140− processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
141− quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
174+ processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
175+ quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
lib/cliapp/src/Obelisk/CliApp/Spinner.hs+4−4
@@ -24,11 +24,11 @@ import System.Console.ANSI (Color (Blue, Cyan, Green, Red))
2424
2525import Obelisk.CliApp.Logging (allowUserToMakeLoggingVerbose, fork, putLog)
2626import Obelisk.CliApp.TerminalString (TerminalString (..), enquiryCode)
27−import Obelisk.CliApp.Types (Cli, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
27+import Obelisk.CliApp.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
2828
2929-- | Run an action with a CLI spinner.
3030withSpinner
31− :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
31+ :: (MonadIO m, MonadMask m, CliLog m, HasCliConfig e m)
3232 => Text -> m a -> m a
3333withSpinner s = withSpinner' s $ Just $ const s
3434
@@ -39,13 +39,13 @@ withSpinner s = withSpinner' s $ Just $ const s
3939-- The 'no trail' property automatically carries over to sub-spinners (in that
4040-- they won't leave a trail either).
4141withSpinnerNoTrail
42− :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
42+ :: (MonadIO m, MonadMask m, CliLog m, HasCliConfig e m)
4343 => Text -> m a -> m a
4444withSpinnerNoTrail s = withSpinner' s Nothing
4545
4646-- | Advanced version that controls the display and content of the trail message.
4747withSpinner'
48− :: (MonadIO m, MonadMask m, Cli m, HasCliConfig m)
48+ :: (MonadIO m, MonadMask m, CliLog m, HasCliConfig e m)
4949 => Text
5050 -> Maybe (a -> Text) -- ^ Leave an optional trail with the given message creator
5151 -> m a
lib/cliapp/src/Obelisk/CliApp/Types.hs+38−26
@@ -1,14 +1,18 @@
11{-# LANGUAGE ConstraintKinds #-}
22{-# LANGUAGE FlexibleContexts #-}
33{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE FunctionalDependencies #-}
5+{-# LANGUAGE LambdaCase #-}
46{-# LANGUAGE GeneralizedNewtypeDeriving #-}
57{-# LANGUAGE MultiParamTypeClasses #-}
8+{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE UndecidableInstances #-}
610module Obelisk.CliApp.Types where
711
812import Control.Concurrent.MVar (MVar)
913import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
1014import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..), logMessage)
11−import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader, ask)
15+import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader (..), ask, mapReaderT)
1216import Control.Monad.Writer (WriterT)
1317import Control.Monad.State (StateT)
1418import Control.Monad.Except (ExceptT, MonadError (..))
@@ -32,40 +36,44 @@ data Output
3236
3337type CliLog m = MonadLog Output m
3438
35−type CliThrow m = MonadError Text m
36−
37−-- TODO could remove for granularity
38−type Cli m = (CliLog m, CliThrow m)
39+type CliThrow e m = MonadError e m
3940
4041-- | Log a message to the console.
4142--
4243-- Logs safely even if there are ongoing spinners.
43−putLog :: Cli m => Severity -> Text -> m ()
44+putLog :: CliLog m => Severity -> Text -> m ()
4445putLog sev = logMessage . Output_Log . WithSeverity sev
4546
46−newtype DieT m a = DieT { unDieT :: LoggingT Output m a }
47+newtype DieT e m a = DieT { unDieT :: ReaderT (e -> (Text, Int)) (LoggingT Output m) a }
4748 deriving
4849 ( Functor, Applicative, Monad, MonadIO
49− , MonadReader r
5050 , MonadThrow, MonadCatch, MonadMask
5151 , MonadLog Output
5252 )
5353
54−instance MonadTrans DieT where
55− lift = DieT . lift
54+instance MonadTrans (DieT e) where
55+ lift = DieT . lift . lift
56+
57+-- | Error printer is private to DieT
58+instance MonadReader r m => MonadReader r (DieT e m) where
59+ ask = DieT $ lift $ ask
60+ local = (\f (DieT a) -> DieT $ f a) . mapReaderT . local
61+ reader = DieT . lift . lift . reader
5662
5763-- 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
64+instance MonadIO m => MonadError e (DieT e m) where
65+ throwError e = do
66+ handler <- DieT ask
67+ let (output, exitCode) = handler e
68+ putLog Alert output
69+ liftIO $ exitWith $ ExitFailure exitCode
6270
6371 -- Cannot catch
6472 catchError m _ = m
6573
6674--------------------------------------------------------------------------------
6775
68−data CliConfig = CliConfig
76+data CliConfig e = CliConfig
6977 { -- | We are capable of changing the log level at runtime
7078 _cliConfig_logLevel :: IORef Severity
7179 , -- | Disallow coloured output
@@ -78,36 +86,40 @@ data CliConfig = CliConfig
7886 _cliConfig_tipDisplayed :: IORef Bool
7987 , -- | Stack of logs from nested spinners
8088 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
89+ , -- | Failure handler. How to log error and what exit status to use.
90+ _cliConfig_errorLogExitCode :: e -> (Text, Int)
8191 }
8292
83−class Monad m => HasCliConfig m where
84− getCliConfig :: m CliConfig
93+class Monad m => HasCliConfig e m | m -> e where
94+ getCliConfig :: m (CliConfig e)
8595
86−instance HasCliConfig m => HasCliConfig (ReaderT r m) where
96+instance HasCliConfig e m => HasCliConfig e (ReaderT r m) where
8797 getCliConfig = lift getCliConfig
8898
89−instance (Monoid w, HasCliConfig m) => HasCliConfig (WriterT w m) where
99+instance (Monoid w, HasCliConfig e m) => HasCliConfig e (WriterT w m) where
90100 getCliConfig = lift getCliConfig
91101
92−instance HasCliConfig m => HasCliConfig (StateT s m) where
102+instance HasCliConfig e m => HasCliConfig e (StateT s m) where
93103 getCliConfig = lift getCliConfig
94104
95−instance HasCliConfig m => HasCliConfig (ExceptT e m) where
105+instance HasCliConfig e m => HasCliConfig e (ExceptT e m) where
96106 getCliConfig = lift getCliConfig
97107
98108--------------------------------------------------------------------------------
99109
100−newtype CliT m a = CliT
101− { unCliT :: ReaderT CliConfig (DieT m) a
110+newtype CliT e m a = CliT
111+ { unCliT :: ReaderT (CliConfig e) (DieT e m) a
102112 }
103113 deriving
104114 ( Functor, Applicative, Monad, MonadIO
105115 , MonadThrow, MonadCatch, MonadMask
106− , MonadLog Output, MonadError Text, MonadReader CliConfig
116+ , MonadLog Output -- CliLog
117+ , MonadError e -- CliThrow
118+ , MonadReader (CliConfig e) -- HasCliConfig
107119 )
108120
109−instance MonadTrans CliT where
121+instance MonadTrans (CliT e) where
110122 lift = CliT . lift . lift
111123
112−instance Monad m => HasCliConfig (CliT m) where
124+instance Monad m => HasCliConfig e (CliT e m)where
113125 getCliConfig = ask
lib/command/src/Obelisk/App.hs+47−7
@@ -2,11 +2,15 @@
22{-# LANGUAGE FlexibleContexts #-}
33{-# LANGUAGE FlexibleInstances #-}
44{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE LambdaCase #-}
56{-# LANGUAGE MultiParamTypeClasses #-}
67{-# LANGUAGE ScopedTypeVariables #-}
78{-# LANGUAGE StandaloneDeriving #-}
9+{-# LANGUAGE TemplateHaskell #-}
10+{-# LANGUAGE TupleSections #-}
811module Obelisk.App where
912
13+import Control.Lens
1014import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
1115import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
1216import Control.Monad.Writer (WriterT)
@@ -17,18 +21,50 @@ import Data.Text (Text)
1721import System.Directory (XdgDirectory (XdgData), getXdgDirectory)
1822import Control.Monad.Log (MonadLog)
1923
20−import Obelisk.CliApp (CliConfig, CliLog, CliThrow, CliT (..), HasCliConfig, runCli, Output)
24+import Obelisk.CliApp
25+ ( CliConfig
26+ , CliLog
27+ , CliThrow
28+ , CliT (..)
29+ , ProcessFailure
30+ , AsProcessFailure (..)
31+ , AsUnstructuredError (..)
32+ , HasCliConfig
33+ , Output
34+ , runCli
35+ )
36+
37+data ObeliskError
38+ = ObeliskError_ProcessError
39+ { obeliskError_err :: ProcessFailure
40+ , obeliskError_mAnn :: Maybe Text
41+ }
42+ | ObeliskError_Unstructured Text
43+
44+makePrisms ''ObeliskError
45+
46+instance AsUnstructuredError ObeliskError where
47+ asUnstructuredError = _ObeliskError_Unstructured
48+
49+-- Only project when the other field is null, otherwise we are not law abiding.
50+instance AsProcessFailure ObeliskError where
51+ asProcessFailure = _ObeliskError_ProcessError . prism (, Nothing) f
52+ where f = \case
53+ (pf, Nothing) -> Right pf
54+ pann@(_, Just _) -> Left pann
2155
2256newtype Obelisk = Obelisk
23− { _obelisk_cliConfig :: CliConfig
57+ { _obelisk_cliConfig :: CliConfig ObeliskError
2458 }
2559
2660newtype ObeliskT m a = ObeliskT
27− { unObeliskT :: ReaderT Obelisk (CliT m) a
61+ { unObeliskT :: ReaderT Obelisk (CliT ObeliskError m) a
2862 }
2963 deriving
3064 ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
31− , MonadLog Output, MonadError Text, HasCliConfig)
65+ , MonadLog Output -- CliLog
66+ , MonadError ObeliskError -- CliThrow ObeliskError
67+ , HasCliConfig ObeliskError)
3268
3369instance MonadTrans ObeliskT where
3470 lift = ObeliskT . lift . lift
@@ -57,14 +93,18 @@ runObelisk c =
5793 . flip runReaderT c
5894 . unObeliskT
5995
60−type MonadObelisk m =
96+type MonadInfallibleObelisk m =
6197 ( CliLog m
62− , CliThrow m
63− , HasCliConfig m
98+ , HasCliConfig ObeliskError m
6499 , HasObelisk m
65100 , MonadIO m
66101 , MonadMask m
67102 )
68103
104+type MonadObelisk m =
105+ ( MonadInfallibleObelisk m
106+ , CliThrow ObeliskError m
107+ )
108+
69109getObeliskUserStateDir :: IO FilePath
70110getObeliskUserStateDir = getXdgDirectory XdgData "obelisk"
lib/command/src/Obelisk/Command.hs+16−4
@@ -8,7 +8,6 @@
88module Obelisk.Command where
99
1010import Control.Monad
11−import Control.Monad.Catch (catch)
1211import Control.Monad.IO.Class (liftIO)
1312import qualified Data.Binary as Binary
1413import Data.Bool (bool)
@@ -270,7 +269,22 @@ mkObeliskConfig = do
270269 -- This function should not use argument parser (full argument parsing happens post handoff)
271270 let logLevel = toLogLevel $ "-v" `elem` cliArgs
272271 notInteractive <- not <$> isInteractiveTerm
273− cliConf <- newCliConfig logLevel notInteractive notInteractive
272+-- instance MonadIO m => MonadError (Either Text ProcessFailed) (DieT m) where
273+-- throwError = \case
274+-- Left s -> do
275+-- putLog Alert s
276+-- liftIO $ exitWith $ ExitFailure 2
277+-- Right (ProcessFailed p code) -> do
278+-- putLog Alert $ "Process exited with code " <> T.pack (show code) <> "; " <> T.pack (show p)
279+-- liftIO $ exitWith $ ExitFailure 2
280+ cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
281+ ObeliskError_ProcessError (ProcessFailure p code) ann ->
282+ ( "Process exited with code " <> T.pack (show code) <> "; " <> T.pack (show p)
283+ <> maybe "" ("\n\n" <>) ann
284+ , 2
285+ )
286+ ObeliskError_Unstructured msg -> (msg, 2)
287+
274288 return $ Obelisk cliConf
275289 where
276290 toLogLevel = bool Notice Debug
@@ -309,8 +323,6 @@ main' argsCfg = do
309323 ]
310324
311325 (mainWithHandOff argsCfg <=< parseHandoff) =<< liftIO getArgs
312− `catch`
313− \(ProcessFailed p code) -> failWith $ "Process exited with code " <> tshow code <> "; " <> tshow p
314326
315327-- Type representing the result of a handoff calculation.
316328data HandOff m
lib/command/src/Obelisk/Command/Nix.hs+33−19
@@ -1,8 +1,12 @@
1+{-# LANGUAGE AllowAmbiguousTypes #-}
2+{-# LANGUAGE ConstraintKinds #-}
13{-# LANGUAGE FlexibleContexts #-}
24{-# LANGUAGE LambdaCase #-}
35{-# LANGUAGE MultiParamTypeClasses #-}
46{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE Rank2Types #-}
58{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TypeApplications #-}
610module Obelisk.Command.Nix
711 ( nixBuild
812 , withNixRemoteCheck
@@ -15,7 +19,9 @@ module Obelisk.Command.Nix
1519 , strArg
1620 ) where
1721
18−import Control.Monad.Catch (catch, throwM)
22+--import Control.Monad.Catch (catch, throwM)
23+import Control.Lens
24+import Control.Monad.Except (ExceptT, runExceptT, throwError)
1925import Control.Monad.IO.Class (liftIO)
2026import Data.Bool (bool)
2127import Data.Default
@@ -26,7 +32,7 @@ import System.Directory
2632import System.Environment
2733import System.Process (proc)
2834
29−import Obelisk.App (MonadObelisk)
35+import Obelisk.App (MonadInfallibleObelisk, MonadObelisk, ObeliskError (..))
3036import Obelisk.CliApp
3137
3238-- | Where to put nix-build output
@@ -81,7 +87,7 @@ instance Default NixBuildConfig where
8187
8288nixBuild :: MonadObelisk m => NixBuildConfig -> m FilePath
8389nixBuild cfg = withSpinner' ("Running nix-build on " <> desc) (Just $ const $ "Built " <> desc) $ do
84− withNixRemoteCheck $ readProcessAndLogStderr Debug $ proc "nix-build" $ mconcat
90+ withNixRemoteCheck $ fmap T.unpack $ readProcessAndLogStderr Debug $ proc "nix-build" $ mconcat
8591 [[path], attrArg, args, outLink, buildersArg]
8692 where
8793 path = _target_path $ _nixBuildConfig_target cfg
@@ -103,19 +109,27 @@ nixBuild cfg = withSpinner' ("Running nix-build on " <> desc) (Just $ const $ "B
103109-- tell the user what to do.
104110--
105111-- Added: June, 2018. Consider removing this eventually.
106−withNixRemoteCheck :: MonadObelisk m => m a -> m a
107−withNixRemoteCheck f = f `catch` \e@(ProcessFailed _ _) -> do
108− liftIO (lookupEnv "NIX_REMOTE") >>= \case
109− Just _ -> throwM e
110− Nothing -> liftIO (writable <$> getPermissions "/nix/var/nix/db") >>= \case
111− True -> throwM e
112− False -> do
113− putLog Error "!!! "
114− putLog Error "!!! A nix command failed to run. You might need to set the NIX_REMOTE environment variable"
115− putLog Error "!!! to `daemon`. To do this, run the following before running obelisk:"
116− putLog Error "!!! "
117− putLog Error "!!! export NIX_REMOTE=daemon"
118− putLog Error "!!! "
119− putLog Error "!!! For details, see https://github.com/NixOS/nixpkgs/issues/5713"
120− putLog Error "!!! "
121− throwM e
112+withNixRemoteCheck
113+ :: MonadObelisk m
114+ => (forall m'. MonadInfallibleObelisk m' => ExceptT ObeliskError m' a)
115+ -> m a
116+withNixRemoteCheck f = do
117+ status <- runExceptT f
118+ case status of
119+ Right a -> pure a
120+ Left e -> throwError =<< case matching asProcessFailure e of
121+ Left _ -> pure e
122+ Right pf -> liftIO (lookupEnv "NIX_REMOTE") >>= \case
123+ Just _ -> pure e
124+ Nothing -> liftIO (writable <$> getPermissions "/nix/var/nix/db") >>= \case
125+ True -> pure e
126+ False -> pure $ ObeliskError_ProcessError pf $ Just $ T.unlines
127+ [ "!!! "
128+ , "!!! A nix command failed to run. You might need to set the NIX_REMOTE environment variable"
129+ , "!!! to `daemon`. To do this, run the following before running obelisk:"
130+ , "!!! "
131+ , "!!! export NIX_REMOTE=daemon"
132+ , "!!! "
133+ , "!!! For details, see https://github.com/NixOS/nixpkgs/issues/5713"
134+ , "!!! "
135+ ]
lib/command/src/Obelisk/Command/Run.hs+1−1
@@ -112,7 +112,7 @@ parseCabalPackage dir = do
112112 mapM_ (putLog Error) $ fmap (T.pack . show) errors
113113 return Nothing
114114
115−withUTF8FileContentsM :: (MonadIO m, HasCliConfig m) => FilePath -> (ByteString -> CliT IO a) -> m a
115+withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
116116withUTF8FileContentsM fp f = do
117117 c <- getCliConfig
118118 liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
lib/command/src/Obelisk/Command/Thunk.hs+8−6
@@ -39,7 +39,6 @@ import qualified Data.Aeson as Aeson
3939import Data.Aeson.Encode.Pretty
4040import qualified Data.Aeson.Types as Aeson
4141import qualified Data.ByteString.Lazy as LBS
42−import qualified Data.ByteString.Lazy.Char8 as LBSC
4342import Data.Default
4443import Data.Either.Combinators (rightToMaybe)
4544import Data.Foldable
@@ -132,10 +131,13 @@ commitNameToRef :: Name Commit -> Ref
132131commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
133132
134133-- TODO: Use spinner here.
135−getNixSha256ForUriUnpacked :: MonadObelisk m => URI -> m NixSha256
134+getNixSha256ForUriUnpacked
135+ :: MonadObelisk m
136+ => URI
137+ -> m NixSha256
136138getNixSha256ForUriUnpacked uri =
137139 withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> T.pack (show uri)) $ do
138− fmap T.pack $ withNixRemoteCheck $ readProcessAndLogStderr Debug $
140+ withNixRemoteCheck $ readProcessAndLogStderr Debug $
139141 proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , show uri]
140142
141143nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
@@ -149,14 +151,14 @@ nixPrefetchGit uri rev fetchSubmodules =
149151 , "--quiet"
150152 ]
151153
152− case parseMaybe (Aeson..: "sha256") =<< Aeson.decode (LBSC.pack out) of
153− Nothing -> failWith $ "nix-prefetch-git: unrecognized output " <> T.pack out
154+ case parseMaybe (Aeson..: "sha256") =<< Aeson.decodeStrict (encodeUtf8 out) of
155+ Nothing -> failWith $ "nix-prefetch-git: unrecognized output " <> out
154156 Just x -> pure x
155157
156158getLatestGitRev :: MonadObelisk m => GitSource -> m ThunkRev
157159getLatestGitRev s = withSystemTempDirectory "git-clone" $ \tmpDir -> do
158160 withExitFailMessage ("Unable to determine latest git revision for branch " <> branch) $ do
159− let git = fmap T.pack . readProcessAndLogStderr Debug . gitProc tmpDir
161+ let git = readProcessAndLogStderr Debug . gitProc tmpDir
160162 out <- liftA2 (<>)
161163 (git ["clone", "--no-checkout", show $ _gitSource_url s]) -- Might be able to use --bare here instead to go faster.
162164 (git ["log", T.unpack branch, "--max-count=1", "--format=%H"])
lib/command/src/Obelisk/Command/Upgrade/Hash.hs+4−5
@@ -102,15 +102,14 @@ tidyUpGitWorkingCopy :: MonadObelisk m => FilePath -> m ()
102102tidyUpGitWorkingCopy dir = withSpinnerNoTrail "Tidying up git working copy" $ do
103103 ignored <- gitLsFiles dir ["--ignored", "--exclude-standard", "--others"]
104104 untracked <- gitLsFiles dir ["--exclude-standard", "--others"]
105− putLog Debug $ T.pack $ "Ignored: " <> show (length ignored) <> " files."
106− putLog Debug $ T.pack $ "Untracked files:\n" <> unlines untracked
105+ putLog Debug $ "Ignored: " <> T.pack (show $ length ignored) <> " files."
106+ putLog Debug $ "Untracked files:\n" <> T.unlines untracked
107107 withSpinnerNoTrail "Removing untracked and ignored files" $ do
108− forM_ (fmap (dir </>) $ ignored <> untracked) $
108+ forM_ (fmap ((dir </>) . T.unpack) $ ignored <> untracked) $
109109 liftIO . removePathForcibly
110110 -- Empty directories won't be included in these lists. Git doesn't track them
111111 -- So we must delete these separately.
112112 runProc $ proc "find" [dir, "-depth", "-empty", "-type", "d", "-delete"]
113113 where
114− gitLsFiles pwd opts = fmap lines $ readProcessAndLogStderr Error $
114+ gitLsFiles pwd opts = fmap T.lines $ readProc $
115115 (proc "git" $ ["ls-files", "."] <> opts) { cwd = Just pwd }
116−
lib/command/src/Obelisk/Command/Utils.hs+3−3
@@ -25,7 +25,7 @@ checkGitCleanStatus repo withIgnored = do
2525 runGit = readProcessAndLogStderr Debug . gitProc repo
2626 gitStatus = runGit $ ["status", "--porcelain"] <> bool [] ["--ignored"] withIgnored
2727 gitDiff = runGit ["diff"]
28− null <$> liftA2 (<>) gitStatus gitDiff
28+ T.null <$> liftA2 (<>) gitStatus gitDiff
2929
3030-- | Ensure that git repo is clean
3131ensureCleanGitRepo :: MonadObelisk m => FilePath -> Bool -> Text -> m ()
@@ -60,7 +60,7 @@ copyDir src dest =
6060 (P.proc "cp" ["-a", ".", dest]) { P.cwd = Just src }
6161
6262readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
63−readGitProcess repo = fmap T.pack . readProcessAndLogStderr Notice . gitProc repo
63+readGitProcess repo = readProcessAndLogStderr Notice . gitProc repo
6464
6565processToShellString :: FilePath -> [String] -> String
6666processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
@@ -76,7 +76,7 @@ runProcSilently = callProcessAndLogOutput (Debug, Debug)
7676
7777-- | A simpler wrapper for CliApp's readProcessAndLogStderr with sensible defaults.
7878readProc :: MonadObelisk m => P.CreateProcess -> m Text
79−readProc = fmap T.pack . readProcessAndLogStderr Error
79+readProc = readProcessAndLogStderr Error
8080
8181tshow :: Show a => a -> Text
8282tshow = T.pack . show
lib/command/src/Obelisk/Command/VmBuilder.hs+2−2
@@ -43,7 +43,7 @@ containerName = "obelisk-docker-nix-builder"
4343-- | Check to see if the Docker container exists. This will exit with a helpful message if Docker is not installed.
4444containerExists :: MonadObelisk m => m Bool
4545containerExists = withExitFailMessage needDockerMsg $ do
46− containerNames <- fmap (map T.strip . T.lines . T.pack) $
46+ containerNames <- fmap (map T.strip . T.lines) $
4747 readProcessAndLogStderr Error $
4848 proc "docker" ["container", "list", "--all", "--format", "{{.Names}}"]
4949 pure $ containerName `elem` containerNames
@@ -81,7 +81,7 @@ setupNixDocker stateDir = withSpinner ("Creating Docker container named " <> con
8181 , "--restart", "always"
8282 , "--detach"
8383 , "--publish", show containerSshPort <> ":22"
84− , "--name", containerName, containerId
84+ , "--name", T.unpack containerName, T.unpack containerId
8585 ]
8686 exists <- containerExists
8787 unless exists $