Start implementing line-prefix stuff

readProcess wrapper still missing from CabalHelper
This commit is contained in:
Daniel Gröber 2015-08-13 06:47:12 +02:00
parent 443650705c
commit 2806f702d9
12 changed files with 218 additions and 40 deletions

View File

@ -56,6 +56,11 @@ module Language.Haskell.GhcMod (
-- * SymbolDb -- * SymbolDb
, loadSymbolDb , loadSymbolDb
, isOutdated , isOutdated
-- * Output
, gmPutStr
, gmErrStr
, gmPutStrLn
, gmErrStrLn
) where ) where
import Language.Haskell.GhcMod.Boot import Language.Haskell.GhcMod.Boot
@ -76,3 +81,4 @@ import Language.Haskell.GhcMod.Monad
import Language.Haskell.GhcMod.PkgDoc import Language.Haskell.GhcMod.PkgDoc
import Language.Haskell.GhcMod.Types import Language.Haskell.GhcMod.Types
import Language.Haskell.GhcMod.Target import Language.Haskell.GhcMod.Target
import Language.Haskell.GhcMod.Output

View File

@ -39,6 +39,7 @@ import Language.Haskell.GhcMod.Monad.Types
import Language.Haskell.GhcMod.Utils import Language.Haskell.GhcMod.Utils
import Language.Haskell.GhcMod.PathsAndFiles import Language.Haskell.GhcMod.PathsAndFiles
import Language.Haskell.GhcMod.Logging import Language.Haskell.GhcMod.Logging
import Language.Haskell.GhcMod.Stderr
import System.FilePath import System.FilePath
import Prelude hiding ((.)) import Prelude hiding ((.))

View File

@ -22,7 +22,7 @@ import Language.Haskell.GhcMod.Caching.Types
import Language.Haskell.GhcMod.Logging import Language.Haskell.GhcMod.Logging
-- | Cache a MonadIO action with proper invalidation. -- | Cache a MonadIO action with proper invalidation.
cached :: forall m a d. (MonadIO m, GmLog m, GmState m, Serialize a, Eq d, Serialize d, Show d) cached :: forall m a d. (Gm m, MonadIO m, Serialize a, Eq d, Serialize d, Show d)
=> FilePath -- ^ Directory to prepend to 'cacheFile' => FilePath -- ^ Directory to prepend to 'cacheFile'
-> Cached m GhcModState d a -- ^ Cache descriptor -> Cached m GhcModState d a -- ^ Cache descriptor
-> d -> d

View File

@ -39,6 +39,7 @@ import Prelude
import Language.Haskell.GhcMod.Monad.Types import Language.Haskell.GhcMod.Monad.Types
import Language.Haskell.GhcMod.Types import Language.Haskell.GhcMod.Types
import Language.Haskell.GhcMod.Pretty import Language.Haskell.GhcMod.Pretty
import Language.Haskell.GhcMod.Output
gmSetLogLevel :: GmLog m => GmLogLevel -> m () gmSetLogLevel :: GmLog m => GmLogLevel -> m ()
gmSetLogLevel level = gmSetLogLevel level =
@ -64,7 +65,7 @@ decreaseLogLevel l = pred l
-- True -- True
-- >>> Just GmDebug <= Just GmException -- >>> Just GmDebug <= Just GmException
-- False -- False
gmLog :: (MonadIO m, GmLog m) => GmLogLevel -> String -> Doc -> m () gmLog :: (MonadIO m, GmLog m, GmEnv m) => GmLogLevel -> String -> Doc -> m ()
gmLog level loc' doc = do gmLog level loc' doc = do
GhcModLog { gmLogLevel = Just level' } <- gmlHistory GhcModLog { gmLogLevel = Just level' } <- gmlHistory
@ -73,7 +74,7 @@ gmLog level loc' doc = do
msgDoc = gmLogLevelDoc level <+>: sep [loc, doc] msgDoc = gmLogLevelDoc level <+>: sep [loc, doc]
msg = dropWhileEnd isSpace $ gmRenderDoc msgDoc msg = dropWhileEnd isSpace $ gmRenderDoc msgDoc
when (level <= level') $ liftIO $ hPutStrLn stderr msg when (level <= level') $ gmErrStrLn msg
gmlJournal (GhcModLog Nothing (Last Nothing) [(level, loc', msgDoc)]) gmlJournal (GhcModLog Nothing (Last Nothing) [(level, loc', msgDoc)])

View File

@ -35,10 +35,13 @@ import Language.Haskell.GhcMod.Error
import Language.Haskell.GhcMod.Logging import Language.Haskell.GhcMod.Logging
import Language.Haskell.GhcMod.Cradle import Language.Haskell.GhcMod.Cradle
import Language.Haskell.GhcMod.Target import Language.Haskell.GhcMod.Target
import Language.Haskell.GhcMod.Stderr
import Control.Arrow (first) import Control.Arrow (first)
import Control.Applicative import Control.Applicative
import Control.Concurrent
import Control.Monad.Reader (runReaderT) import Control.Monad.Reader (runReaderT)
import Control.Monad.State.Strict (runStateT) import Control.Monad.State.Strict (runStateT)
import Control.Monad.Trans.Journal (runJournalT) import Control.Monad.Trans.Journal (runJournalT)
@ -58,11 +61,21 @@ withGhcModEnv dir opt f = withCradle dir (withGhcModEnv' opt f)
withGhcModEnv' :: IOish m => Options -> (GhcModEnv -> m a) -> Cradle -> m a withGhcModEnv' :: IOish m => Options -> (GhcModEnv -> m a) -> Cradle -> m a
withGhcModEnv' opt f crdl = do withGhcModEnv' opt f crdl = do
olddir <- liftIO getCurrentDirectory olddir <- liftIO getCurrentDirectory
gbracket_ (liftIO $ setCurrentDirectory $ cradleRootDir crdl) c <- liftIO newChan
(liftIO $ setCurrentDirectory olddir) let outp = case linePrefix opt of
(f $ GhcModEnv opt crdl) Just _ -> GmOutputChan c
Nothing -> GmOutputStdio
gbracket_ (setup c) (teardown olddir) (f $ GhcModEnv opt crdl outp)
where where
gbracket_ ma mb mc = gbracket ma (const mb) (const mc) setup c = liftIO $ do
setCurrentDirectory $ cradleRootDir crdl
forkIO $ stdoutGateway c
teardown olddir tid = liftIO $ do
setCurrentDirectory olddir
killThread tid
gbracket_ ma mb mc = gbracket ma mb (const mc)
-- | Run a @GhcModT m@ computation. -- | Run a @GhcModT m@ computation.
runGhcModT :: IOish m runGhcModT :: IOish m

View File

@ -39,6 +39,7 @@ module Language.Haskell.GhcMod.Monad.Types (
, GmLogLevel(..) , GmLogLevel(..)
, GhcModLog(..) , GhcModLog(..)
, GhcModError(..) , GhcModError(..)
, Gm
, GmEnv(..) , GmEnv(..)
, GmState(..) , GmState(..)
, GmLog(..) , GmLog(..)
@ -198,6 +199,8 @@ class Monad m => GmEnv m where
gmeLocal :: (GhcModEnv -> GhcModEnv) -> m a -> m a gmeLocal :: (GhcModEnv -> GhcModEnv) -> m a -> m a
{-# MINIMAL (gmeAsk | gmeReader), gmeLocal #-} {-# MINIMAL (gmeAsk | gmeReader), gmeLocal #-}
type Gm m = (GmEnv m, GmState m, GmLog m)
instance Monad m => GmEnv (GhcModT m) where instance Monad m => GmEnv (GhcModT m) where
gmeAsk = GhcModT ask gmeAsk = GhcModT ask
gmeReader = GhcModT . reader gmeReader = GhcModT . reader

View File

@ -0,0 +1,54 @@
module Language.Haskell.GhcMod.Output (
gmPutStr
, gmErrStr
, gmPutStrLn
, gmErrStrLn
) where
import Data.Char
import System.IO
import Control.Monad
import Control.Concurrent
import Language.Haskell.GhcMod.Types hiding (LineSeparator)
import Language.Haskell.GhcMod.Monad.Types
withLines :: (String -> String) -> String -> String
withLines f s = let
res = unlines $ map f $ lines s
in
case s of
[] -> res
_ | generalCategory (last s) /= LineSeparator ->
reverse $ drop 1 $ reverse res
_ -> res
outputFns :: (GmEnv m, MonadIO m') => m (String -> m' (), String -> m' ())
outputFns = do
GhcModEnv {..} <- gmeAsk
let Options {..} = gmOptions
let pfx f = withLines f
let (outPfx, errPfx) = case linePrefix of
Nothing -> ( id, id )
Just (op, ep) -> ( pfx (op++), pfx (ep++) )
return $ case gmOutput of
GmOutputStdio ->
(liftIO . putStr . outPfx , liftIO . hPutStr stderr . errPfx)
GmOutputChan c ->
(liftIO . writeChan c . outPfx, liftIO . writeChan c . errPfx)
gmPutStr, gmPutStrLn, gmErrStr, gmErrStrLn
:: (MonadIO m, GmEnv m) => String -> m ()
gmPutStr str = do
putOut <- fst `liftM` outputFns
putOut str
gmPutStrLn = gmPutStr . (++"\n")
gmErrStrLn = gmErrStr . (++"\n")
gmErrStr str = do
putErr <- snd `liftM` outputFns
putErr str

View File

@ -0,0 +1,88 @@
-- ghc-mod: Making Haskell development *more* fun
-- Copyright (C) 2015 Daniel Gröber <dxld ÄT darkboxed DOT org>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
-- Derived from process:System.Process
-- Copyright (c) The University of Glasgow 2004-2008
module Language.Haskell.GhcMod.Stderr where
import Data.List
import System.IO
import System.Exit
import System.Process
import Control.Monad
import Control.DeepSeq
import Control.Exception
import Control.Concurrent
stdoutGateway :: Chan String -> IO ()
stdoutGateway chan = do
l <- readChan chan
putStrLn l
stdoutGateway chan
readProcessStderrChan ::
Chan String -> FilePath -> [String] -> String -> IO String
readProcessStderrChan cout exe args input = do
let cp = (proc exe args) {
std_out = CreatePipe
, std_err = CreatePipe
, std_in = CreatePipe
}
(Just i, Just o, Just e, h) <- createProcess cp
_ <- forkIO $ reader e
output <- hGetContents o
withForkWait (evaluate $ rnf output) $ \waitOut -> do
-- now write any input
unless (null input) $
ignoreSEx $ hPutStr i input
-- hClose performs implicit hFlush, and thus may trigger a SIGPIPE
ignoreSEx $ hClose i
-- wait on the output
waitOut
hClose o
res <- waitForProcess h
case res of
ExitFailure rv ->
processFailedException "readProcessStderrChan" exe args rv
ExitSuccess ->
return output
where
ignoreSEx = handle (\(SomeException _) -> return ())
reader h = ignoreSEx $ do
l <- hGetLine h
writeChan cout l
reader h
withForkWait :: IO () -> (IO () -> IO a) -> IO a
withForkWait async body = do
waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ()))
mask $ \restore -> do
tid <- forkIO $ try (restore async) >>= putMVar waitVar
let wait = takeMVar waitVar >>= either throwIO return
restore (body wait) `onException` killThread tid
processFailedException :: String -> String -> [String] -> Int -> IO a
processFailedException fn exe args rv =
error $ concat [fn, ": ", exe, " "
, intercalate " " (map show args)
, " (exit " ++ show rv ++ ")"]

View File

@ -334,7 +334,7 @@ resolveGmComponent mums c@GmComponent {..} = do
[ "-optP-include", "-optP" ++ macrosHeaderPath ] [ "-optP-include", "-optP" ++ macrosHeaderPath ]
] ]
resolveEntrypoint :: (IOish m, GmLog m) resolveEntrypoint :: (IOish m, GmEnv m, GmLog m)
=> Cradle => Cradle
-> GmComponent 'GMCRaw ChEntrypoint -> GmComponent 'GMCRaw ChEntrypoint
-> m (GmComponent 'GMCRaw (Set ModulePath)) -> m (GmComponent 'GMCRaw (Set ModulePath))
@ -366,7 +366,7 @@ resolveChEntrypoints srcDir ChSetupEntrypoint = do
chModToMod :: ChModuleName -> ModuleName chModToMod :: ChModuleName -> ModuleName
chModToMod (ChModuleName mn) = mkModuleName mn chModToMod (ChModuleName mn) = mkModuleName mn
resolveModule :: (MonadIO m, GmLog m) => resolveModule :: (MonadIO m, GmEnv m, GmLog m) =>
HscEnv -> [FilePath] -> CompilationUnit -> m (Maybe ModulePath) HscEnv -> [FilePath] -> CompilationUnit -> m (Maybe ModulePath)
resolveModule env _srcDirs (Right mn) = resolveModule env _srcDirs (Right mn) =
liftIO $ traverse canonicalizeModulePath =<< findModulePath env mn liftIO $ traverse canonicalizeModulePath =<< findModulePath env mn

View File

@ -13,6 +13,7 @@ import Control.Monad.Error (Error(..))
import qualified Control.Monad.IO.Class as MTL import qualified Control.Monad.IO.Class as MTL
import Control.Exception (Exception) import Control.Exception (Exception)
import Control.Applicative import Control.Applicative
import Control.Concurrent
import Control.Monad import Control.Monad
import Data.Serialize import Data.Serialize
import Data.Version import Data.Version
@ -72,6 +73,9 @@ data Options = Options {
outputStyle :: OutputStyle outputStyle :: OutputStyle
-- | Line separator string. -- | Line separator string.
, lineSeparator :: LineSeparator , lineSeparator :: LineSeparator
-- | Stdout/err line multiplexing using prefix encoding. @fst@ is stdout,
-- @snd@ is stderr prefix.
, linePrefix :: Maybe (String, String)
-- | Verbosity -- | Verbosity
, logLevel :: GmLogLevel , logLevel :: GmLogLevel
-- | @ghc@ program name. -- | @ghc@ program name.
@ -96,6 +100,7 @@ defaultOptions :: Options
defaultOptions = Options { defaultOptions = Options {
outputStyle = PlainStyle outputStyle = PlainStyle
, lineSeparator = LineSeparator "\0" , lineSeparator = LineSeparator "\0"
, linePrefix = Nothing
, logLevel = GmWarning , logLevel = GmWarning
, ghcProgram = "ghc" , ghcProgram = "ghc"
, ghcPkgProgram = "ghc-pkg" , ghcPkgProgram = "ghc-pkg"
@ -125,9 +130,13 @@ data Cradle = Cradle {
, cradleCabalFile :: Maybe FilePath , cradleCabalFile :: Maybe FilePath
} deriving (Eq, Show) } deriving (Eq, Show)
data GmOutput = GmOutputStdio
| GmOutputChan (Chan String)
data GhcModEnv = GhcModEnv { data GhcModEnv = GhcModEnv {
gmOptions :: Options gmOptions :: Options
, gmCradle :: Cradle , gmCradle :: Cradle
, gmOutput :: GmOutput
} }
data GhcModLog = GhcModLog { data GhcModLog = GhcModLog {

View File

@ -119,11 +119,13 @@ Library
Language.Haskell.GhcMod.Modules Language.Haskell.GhcMod.Modules
Language.Haskell.GhcMod.Monad Language.Haskell.GhcMod.Monad
Language.Haskell.GhcMod.Monad.Types Language.Haskell.GhcMod.Monad.Types
Language.Haskell.GhcMod.Output
Language.Haskell.GhcMod.PathsAndFiles Language.Haskell.GhcMod.PathsAndFiles
Language.Haskell.GhcMod.PkgDoc Language.Haskell.GhcMod.PkgDoc
Language.Haskell.GhcMod.Pretty Language.Haskell.GhcMod.Pretty
Language.Haskell.GhcMod.Read Language.Haskell.GhcMod.Read
Language.Haskell.GhcMod.SrcUtils Language.Haskell.GhcMod.SrcUtils
Language.Haskell.GhcMod.Stderr
Language.Haskell.GhcMod.Target Language.Haskell.GhcMod.Target
Language.Haskell.GhcMod.Types Language.Haskell.GhcMod.Types
Language.Haskell.GhcMod.Utils Language.Haskell.GhcMod.Utils

View File

@ -74,10 +74,10 @@ usage =
\\n" \\n"
++ (unlines $ indent <$> optionUsage indent globalArgSpec) ++ ++ (unlines $ indent <$> optionUsage indent globalArgSpec) ++
"*Commands*\n\ "*Commands*\n\
\ - version | --version\n\ \ - version\n\
\ Print the version of the program.\n\ \ Print the version of the program.\n\
\\n\ \\n\
\ - help | --help\n\ \ - help\n\
\ Print this help message.\n\ \ Print this help message.\n\
\\n\ \\n\
\ - list [FLAGS...] | modules [FLAGS...]\n\ \ - list [FLAGS...] | modules [FLAGS...]\n\
@ -259,8 +259,12 @@ globalArgSpec =
, option "l" ["tolisp"] "Format output as an S-Expression" $ , option "l" ["tolisp"] "Format output as an S-Expression" $
NoArg $ \o -> Right $ o { outputStyle = LispStyle } NoArg $ \o -> Right $ o { outputStyle = LispStyle }
, option "b" ["boundary"] "Output line separator"$ , option "b" ["boundary", "line-seperator"] "Output line separator"$
reqArg "SEP" $ \s o -> Right $ o { lineSeparator = LineSeparator s } reqArg "SEP" $ \s o -> Right $ o { lineSeparator = LineSeparator s }
, option "" ["line-prefix"] "Output line separator"$
reqArg "OUT,ERR" $ \s o -> let
[out, err] = splitOn "," s
in Right $ o { linePrefix = Just (out, err) }
, option "g" ["ghcOpt", "ghc-option"] "Option to be passed to GHC" $ , option "g" ["ghcOpt", "ghc-option"] "Option to be passed to GHC" $
reqArg "OPT" $ \g o -> Right $ reqArg "OPT" $ \g o -> Right $
@ -339,25 +343,29 @@ main :: IO ()
main = handler $ do main = handler $ do
hSetEncoding stdout utf8 hSetEncoding stdout utf8
args <- getArgs args <- getArgs
-- This doesn't handle --help and --version being given after any global
-- options. To do that we'd have to fiddle with getOpt.
case parseGlobalArgs args of case parseGlobalArgs args of
Left e -> case globalCommands args of Left e -> throw e
Just s -> putStr s Right res -> progMain res
Nothing -> throw e
Right res@(_,cmdArgs) ->
case globalCommands cmdArgs of
Just s -> putStr s
Nothing -> progMain res
progMain :: (Options,[String]) -> IO () progMain :: (Options,[String]) -> IO ()
progMain (globalOptions,cmdArgs) = do progMain (globalOptions,cmdArgs) = hndle $ runGhcModT globalOptions $ do
(res,_) <- runGhcModT globalOptions $ ghcCommands cmdArgs case globalCommands cmdArgs of
case res of Just s -> gmPutStr s
Right () -> return () Nothing -> ghcCommands cmdArgs
Left e -> exitError $ renderStyle ghcModStyle (gmeDoc e) where
hndle action = do
(e, _l) <- action
case e of
Right _ ->
return ()
Left ed ->
exitError $ renderStyle ghcModStyle (gmeDoc ed)
globalCommands :: [String] -> Maybe String
globalCommands (cmd:_)
| cmd == "help" = Just usage
| cmd == "version" = Just ghcModVersion
globalCommands _ = Nothing
-- ghc-modi -- ghc-modi
legacyInteractive :: IOish m => GhcModT m () legacyInteractive :: IOish m => GhcModT m ()
@ -367,10 +375,10 @@ legacyInteractive = do
world <- getCurrentWorld world <- getCurrentWorld
legacyInteractiveLoop symdbreq world legacyInteractiveLoop symdbreq world
bug :: String -> IO () bug :: IOish m => String -> GhcModT m ()
bug msg = do bug msg = do
putStrLn $ notGood $ "BUG: " ++ msg gmPutStrLn $ notGood $ "BUG: " ++ msg
exitFailure liftIO exitFailure
notGood :: String -> String notGood :: String -> String
notGood msg = "NG " ++ escapeNewlines msg notGood msg = "NG " ++ escapeNewlines msg
@ -422,20 +430,13 @@ legacyInteractiveLoop symdbreq world = do
"" -> liftIO $ exitSuccess "" -> liftIO $ exitSuccess
_ -> fatalError $ "unknown command: `" ++ cmd ++ "'" _ -> fatalError $ "unknown command: `" ++ cmd ++ "'"
liftIO $ putStr res >> putStrLn "OK" >> hFlush stdout gmPutStr res >> gmPutStrLn "OK" >> liftIO (hFlush stdout)
legacyInteractiveLoop symdbreq world legacyInteractiveLoop symdbreq world
globalCommands :: [String] -> Maybe String
globalCommands [] = Nothing
globalCommands (cmd:_) = case cmd of
_ | cmd == "help" -> Just usage
_ | cmd == "version" -> Just ghcModVersion
_ -> Nothing
ghcCommands :: IOish m => [String] -> GhcModT m () ghcCommands :: IOish m => [String] -> GhcModT m ()
ghcCommands [] = fatalError "No command given (try --help)" ghcCommands [] = fatalError "No command given (try --help)"
ghcCommands (cmd:args) = do ghcCommands (cmd:args) = do
liftIO . putStr =<< action args gmPutStr =<< action args
where where
action = case cmd of action = case cmd of
_ | cmd == "list" || cmd == "modules" -> modulesCmd _ | cmd == "list" || cmd == "modules" -> modulesCmd