ghci4luatex-0.1.2: src/System/Process/Ghci.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
--------------------------------------------------------------------------------
-- |
--
-- Module : System.Process.Ghci
-- Description : GHCi as a process
-- Copyright : (c) Alice Rixte 2024
-- License : BSD 3
-- Maintainer : alice.rixte@u-bordeaux.fr
-- Stability : unstable
-- Portability : non-portable (GHC extensions)
--
-- Run GHCi as a process, and execute commands.
--
-- = Usage
--
-- >>> ghci <- startGhci Quiet "ghci" []
-- >>> execGhciCmd ghci Quiet "1+2"
-- GhciResult {ghciErr = "", ghciOut = "3"}
--
--------------------------------------------------------------------------------
module System.Process.Ghci
( Ghci (..)
, startGhci
, GhciResult (..)
, execGhciCmd
)
where
import Control.Exception
import Control.Monad
import Control.Concurrent
import Control.Concurrent.STM
import GHC.Generics
import System.IO
import System.IO.Error
import System.Process
import Data.String
import Data.Text
import System.Console.CmdArgs.Verbosity
import Data.Aeson
-- | A GHCi process
--
data Ghci = Ghci
{ ghciIn :: Handle -- ^ Standard input
, ghciErrVar :: TMVar String -- ^ Current line on stderr
, ghciOutVar :: TMVar String -- ^ Current line on stdout
, ghciProcess :: ProcessHandle -- ^ Process handle
, ghciErrId :: ThreadId -- ^ Stderr listener thread
, ghciOutId :: ThreadId -- ^ Stdout listener thread
}
-- | Start a GHCi process.
--
-- >>> ghci = startGhci Normal "ghci" []
--
-- >>> ghci = startGhci Loud "stack" ["ghci"]
--
startGhci ::
Verbosity -- ^ When @verbosity >= 'Normal'@, the entire output
-- of GHCi is printed
-> String -- ^ The command to run (e.g. "ghci", "cabal" or "stack")
-> [String] -- ^ The list of the command's arguments
-- (e.g. "repl" for "cabal" or "ghci" for "stack")
-> IO Ghci
startGhci v cmd args = do
chans <- createProcess (proc cmd args) { std_in = CreatePipe
, std_err = CreatePipe
, std_out = CreatePipe
}
case chans of
(Just hin, Just hout , Just herr , hp) -> do
verr <- newEmptyTMVarIO
vout <- newEmptyTMVarIO
errId <- forkIO $ listenHandle verr herr
outId <- forkIO $ listenHandle vout hout
let g = Ghci hin verr vout hp errId outId
-- the following is very hacky : Sometimes, "stack ghci" may ask some file
-- to the user. For this reason we send ghci a confirmation for whatever
-- default values it proposes, then wait for 200 ms to make sur ghci did
-- receive that answer.
flushGhciCmd (ghciIn g) "\n"
threadDelay 2000000 -- wait 200 ms to make sure GHCi is ready to listen
_ <- waitGhciResult g v
return g
_ -> error "Error : Ghci command failed."
-- | Listen to a handle by putting lines into a mutable variable.
--
listenHandle :: TMVar String -> Handle -> IO ()
listenHandle v h =
catch (forever $ do
s <- hGetLine h
atomically $ putTMVar v s) handler
where
handler :: IOError -> IO ()
handler err =
unless (isEOFError err) $ throw err
-- | Merge stderr and stdout streams.
--
mergeErrOut :: TMVar String -> TMVar String -> IO (Either String String)
mergeErrOut verr vout=
atomically $ (Left <$> takeTMVar verr) `orElse`
( Right . cleanResultString <$> takeTMVar vout)
-- | The result printed by GHCi.
--
data GhciResult = GhciResult
{ ghciErr :: Text -- Errors and warnings of GHCi
, ghciOut :: Text -- Output of GHCi
}
deriving (Show, Generic)
deriving Semigroup via Generically GhciResult
deriving Monoid via Generically GhciResult
instance ToJSON GhciResult where
toEncoding = genericToEncoding defaultOptions
instance FromJSON GhciResult
-- | Sends a command to ghci and wait for its result.
--
-- >>> execGhciCmd ghci Normal "1+2"
-- GhciResult {ghciErr = "", ghciOut = "3"}
--
execGhciCmd
:: Ghci -- ^ A GHCi process
-> Verbosity -- ^ When @verbosity >= Normal@, the entire output
-- of GHCi is printed
-> String -- ^ The command to execute
-> IO GhciResult -- ^ The result of the execution
execGhciCmd g v cmd = do
flushGhciCmd (ghciIn g) cmd
waitGhciResult g v
-- | Wait for GHCi to complete its computation
--
waitGhciResult :: Ghci -> Verbosity -> IO GhciResult
waitGhciResult g v = do
-- this is a hack : we send a "putStrLn" command to ghci in order to be able
-- to wait for the result of the command.
flushGhciCmd (ghciIn g) $ "putStrLn\"" ++ readyString ++ "\"\n"
loop mempty
where
loop acc = do
s <- mergeErrOut (ghciErrVar g) (ghciOutVar g)
if s == Right readyString then
return acc
else do
when (v >= Normal) $
case s of
Left s' -> do
hPutStr stderr s'
hPutChar stderr '\n'
hFlush stderr
Right s' -> do
putStrLn s'
hFlush stdout
loop (appendGhciResult acc s)
-- | A very unlikely string that we make ghci print in order to know when ghci
-- is finished.
--
-- This is a hack but it works well. The same hack is used by lhs2tex.
--
readyString :: String
readyString = "`}$/*^`a`('))}{h}"
-- | Flush a string to the standard input of of ghci.
--
-- Multiple line strings are
-- accepted and will be surrounded by ":{" and ":}"
--
flushGhciCmd :: Handle -> String -> IO ()
flushGhciCmd hin cmd = do
hPutStr hin (":{\n" ++ cmd ++ "\n:}\n") -- TODO optimization when no newline ?
hFlush hin
-- | Remove the @"ghci> "@ and @"ghci| "@ prefixes from the output stream of
-- ghci.
--
cleanResultString :: String -> String
cleanResultString ('g' : 'h' : 'c' : 'i' : '>': ' ' : s) =
cleanResultString s
cleanResultString ('g' : 'h' : 'c' : 'i' : '|': ' ' : s) =
cleanResultString s
cleanResultString s = s
-- | Utility function that merges the stderr and the stdout streams of
-- ghci.
--
appendGhciResult :: GhciResult -> Either String String -> GhciResult
appendGhciResult acc (Left s) =
acc { ghciErr = ghciErr acc <> fromString s }
appendGhciResult acc (Right s) =
acc { ghciOut = ghciOut acc <> fromString s }