packages feed

ide-backend-0.10.0.1: IdeSession/ExeCabalServer.hs

-- | Run Cabal functions upon receiving RPC requests, log output and provide
-- Progress updates.
module IdeSession.ExeCabalServer (
    exeCabalEngine
  ) where

import Control.Concurrent.Async (async, wait)
import System.Exit (ExitCode)
import System.IO (IOMode(..))
import System.IO.Error (isEOFError)
import qualified Control.Exception  as Ex
import qualified Data.ByteString    as BSS
import qualified Data.Text.Encoding as E
import qualified System.IO          as IO

import IdeSession.Cabal
import IdeSession.RPC.Server
import IdeSession.Types.Progress
import IdeSession.Util
import IdeSession.Util.PortableIO

-- | Handle RPC requests by calling Cabal functions, keeping track
-- of progress and passing the results.
exeCabalEngine :: FilePath -> RpcConversation -> IO ()
exeCabalEngine _errorLog conv@RpcConversation{..} = do
    -- Start handling RPC calls
    let go = do
          req <- get
          exitCode <- runExeCabal conv req
          put $ ExeCabalDone exitCode
          go  -- not long-running, but we can't exit or RPC would crash

    go

-- | Run the cabal functions inside the executable.
runExeCabal :: RpcConversation -> ExeCabalRequest -> IO ExitCode
runExeCabal conv req = do
   -- Create pipe
  (stdOutputRd, stdOutputWr) <- createPipe

  -- Backup stdout, then replace stdout with the pipe's write end
  (exitCode, stdoutThread) <- swizzleStdout stdOutputWr $ do
    -- Convert the read end to a handle
    stdOutputRdHandle <- fdToHandle stdOutputRd ReadMode
    IO.hSetBuffering stdOutputRdHandle IO.LineBuffering

    let stdoutLog = case req of
          ReqExeBuild buildExeArgs _ -> beStdoutLog buildExeArgs
          ReqExeDoc   buildExeArgs   -> beStdoutLog buildExeArgs
          ReqExeCc    runCcArgs      -> rcStdoutLog runCcArgs
          ReqExeLic   licenseArgs    -> liStdoutLog licenseArgs
    stdoutThread <- async $ readStdout conv stdOutputRdHandle stdoutLog

    exitCode <- case req of
      ReqExeBuild buildExeArgs modArgs ->
        configureAndBuild buildExeArgs modArgs
      ReqExeDoc buildExeArgs ->
        configureAndHaddock buildExeArgs
      ReqExeCc runCcArgs ->
        runComponentCc runCcArgs
      ReqExeLic licenseArgs ->
        buildLicsFromPkgs True licenseArgs

    return (exitCode, stdoutThread)

  -- Closing the write end of the stdout pipe will cause the stdout
  -- thread to terminate after it processed all remaining output;
  -- wait for this to happen
  closeFd stdOutputWr
  wait stdoutThread

  return exitCode

readStdout :: RpcConversation -> IO.Handle -> FilePath -> IO ()
readStdout RpcConversation{..} stdOutputRdHandle stdoutLog = do
  logHandle <- IO.openFile stdoutLog IO.WriteMode
  let go = do
        res <- Ex.try $ BSS.hGetLine stdOutputRdHandle
        case res of
          Left ex -> if isEOFError ex then return () else Ex.throw ex
          Right bs -> do
            progressCallback bs
            BSS.hPut logHandle bs
            BSS.hPut logHandle (BSS.singleton (0x0a))  -- \n
            go
  go
  IO.hClose logHandle
 where
    progressCallback :: BSS.ByteString -> IO ()
    progressCallback ghcMsg = do
      let ghcMsg' = E.decodeUtf8 ghcMsg
      case parseProgressMessage ghcMsg' of
        Right (step, numSteps, msg) ->
          put $ ExeCabalProgress $ Progress {
               progressStep      = step
             , progressNumSteps  = numSteps
             , progressParsedMsg = Just msg
             , progressOrigMsg   = Just ghcMsg'
             }
        _ ->
          -- Ignore messages we cannot parse
          return ()