packages feed

ide-backend-server-0.10.0.1: RequestRunning.hs

{-# LANGUAGE CPP, TemplateHaskell, ScopedTypeVariables #-}

-- A module that handles request running. This is taken out of Server to allow
-- for conditional compilation on non-Unix platforms.
module RequestRunning (runCommand) where

import IdeSession.RPC.API
import IdeSession.GHC.Requests
import GhcMonad(Ghc(..))

#ifdef VERSION_unix

-- Unix specific imports
import System.Posix (createSession)
import System.Posix.Process (forkProcess, getProcessStatus)
import System.Posix.Terminal (openPseudoTerminal)
#ifdef PTY_SUPPORT
import qualified Posix
#endif

-- General imports
import Network
import Control.Concurrent (ThreadId, throwTo, forkIO, myThreadId)
import Control.Concurrent.Async (async, withAsync)
import Control.Concurrent.MVar (MVar, newEmptyMVar)
import Control.Monad (void, unless, forever)
import Data.Function (fix)
import Foreign.C.Types (CFile)
import Foreign.Ptr (Ptr, nullPtr)
import GHC.IO.Exception (IOException(..), IOErrorType(..))
import System.Environment (withArgs, getEnvironment, setEnv)
import System.IO (Handle, hFlush, hClose, IOMode(..))
import System.IO.Temp (openTempFile)
import System.Mem (performGC)
import qualified Control.Exception as Ex
import qualified Data.ByteString   as BSS
import qualified System.Directory  as Dir

import qualified GHC

import IdeSession.RPC.Server
import IdeSession.GHC.API
import IdeSession.RPC.Sockets
import IdeSession.Types.Private
import IdeSession.Util
import IdeSession.Util.PortableProcess
import IdeSession.Util.PortableIO
import IdeSession.Util.BlockingOps
import Run
import Auxiliary
import Debug

foreign import ccall "fflush" fflush :: Ptr CFile -> IO ()

#else

import Control.Monad.Trans (liftIO)
import IdeSession.Util

#endif


runCommand :: RpcConversation -> [String] -> FilePath -> RunCmd -> Ghc [String]


#ifndef VERSION_unix
runCommand RpcConversation{..} args _ _ = do
  liftIO $ put $ ReqRunUnsupported "Cannot run commands on non-Unix platforms"
  return args
#else
runCommand conv args sessionDir runCmd
  | runCmdPty runCmd = do
#if PTY_SUPPORT
        fds <- liftIO openPseudoTerminal
        conversationTuple <- startConcurrentConversation $ \_ _ _ ->
          ghcWithArgs args $ ghcHandleRunPtySlave fds runCmd
        liftIO $ runPtyMaster fds conversationTuple
        rpcConversationTuple conv conversationTuple
        return args
#else
        --TODO: fail more gracefully than this?
        fail "ide-backend-server not build with -DPTY_SUPPORT / pty-support cabal flag"
#endif
  | otherwise = do
        conversationTuple <- startConcurrentConversation $
          ghcConcurrentConversation $ \_errorLog' conv' ->
            ghcWithArgs args $ ghcHandleRun conv' runCmd
        rpcConversationTuple conv conversationTuple
        return args

rpcConversationTuple :: RpcConversation -> (Pid, Socket, Socket, FilePath) -> Ghc ()
rpcConversationTuple RpcConversation{..} (pid, stdin, stdout, errorLogPath) = do
  [stdinPort, stdoutPort] <- liftIO $ mapM socketPort [stdin, stdout]
  liftIO $ put $ ReqRunConversation pid (WriteChannel stdinPort) (ReadChannel stdoutPort) errorLogPath

-- | Handle a run request
ghcHandleRun :: RpcConversation -> RunCmd -> Ghc ()
ghcHandleRun RpcConversation{..} runCmd = do
    (stdOutputRd, stdOutputBackup, stdErrorBackup) <- redirectStdout
    (stdInputWr,  stdInputBackup)                  <- redirectStdin

    -- We don't need to keep a reference to the reqThread: when the snippet
    -- terminates, the whole server process terminates with it and hence
    -- so does the reqThread. If we wanted to reuse this server process we
    -- would need to have some sort of handshake so make sure that the client
    -- and the server both agree that further requests are no longer accepted
    -- (we used to do that when we ran snippets inside the main server process).
    ghcThread    <- liftIO newEmptyMVar :: Ghc (MVar (Maybe ThreadId))
    _reqThread   <- liftIO . async $ readRunRequests ghcThread stdInputWr
    stdoutThread <- liftIO . async $ readStdout stdOutputRd

    -- This is a little tricky. We only want to deliver the UserInterrupt
    -- exceptions when we are running 'runInGhc'. If the UserInterrupt arrives
    -- before we even get a chance to call 'runInGhc' the exception should not
    -- be delivered until we are in a position to catch it; after 'runInGhc'
    -- completes we should just ignore any further 'GhcRunInterrupt' requests.
    --
    -- We achieve this by
    --
    -- 1. The thread ID is stored in an MVar ('ghcThread'). Initially this
    --    MVar is empty, so if a 'GhcRunInterrupt' arrives before we are ready
    --    to deal with it the 'reqThread' will block
    -- 2. We install an exception handler before putting the thread ID into
    --    the MVar
    -- 3. We override the MVar with Nothing before leaving the exception handler
    -- 4. In the 'reqThread' we ignore GhcRunInterrupts once the 'MVar' is
    --    'Nothing'

    runOutcome <- ghandle ghcException . ghandleJust isUserInterrupt return $
      GHC.gbracket
        (liftIO (myThreadId >>= $putMVar ghcThread . Just))
        (\() -> liftIO $ $modifyMVar_ ghcThread (\_ -> return Nothing))
        (\() -> runInGhc runCmd)

    liftIO $ do
      -- Make sure the C buffers are also flushed before swapping the handles
      fflush nullPtr

      -- Restore stdin and stdout
      dupTo stdOutputBackup stdOutput >> closeFd stdOutputBackup
      dupTo stdErrorBackup  stdError  >> closeFd stdErrorBackup
      dupTo stdInputBackup  stdInput  >> closeFd stdInputBackup

      -- 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
      $wait stdoutThread

      -- Report the final result
      liftIO $ debug dVerbosity $ "returned from ghcHandleRun with "
                                  ++ show runOutcome
      put $ GhcRunDone runOutcome
  where
    -- Wait for and execute run requests from the client
    readRunRequests :: MVar (Maybe ThreadId) -> Handle -> IO ()
    readRunRequests ghcThread stdInputWr =
      let go = do request <- get
                  case request of
                    GhcRunInterrupt -> do
                      $withMVar ghcThread $ \mTid -> do
                        case mTid of
                          Just tid -> throwTo tid Ex.UserInterrupt
                          Nothing  -> return () -- See above
                      go
                    GhcRunInput bs -> do
                      BSS.hPut stdInputWr bs
                      hFlush stdInputWr
                      go
      in go

    -- Wait for the process to output something or terminate
    readStdout :: Handle -> IO ()
    readStdout stdOutputRd =
      let go = do
            mbs <- Ex.try $ BSS.hGetSome stdOutputRd blockSize
            case mbs of
              Right bs -> unless (BSS.null bs) $ put (GhcRunOutp bs) >> go
              -- hGetSome might throw some very unpleasant exceptions in case
              -- someone force cancels the operation
              -- (which triggered some weird race conditions in test220 in Issues.hs)
              -- Swallowing everything, since an exception here implies that there
              -- is no more output to read
              Left (_ :: Ex.SomeException) -> return ()
      in go

    -- Turn an asynchronous exception into a RunResult
    isUserInterrupt :: Ex.AsyncException -> Maybe RunResult
    isUserInterrupt ex@Ex.UserInterrupt =
      Just . RunProgException . showExWithClass . Ex.toException $ ex
    isUserInterrupt _ =
      Nothing

    -- Turn a GHC exception into a RunResult
    ghcException :: GhcException -> Ghc RunResult
    ghcException = return . RunGhcException . show

    -- TODO: What is a good value here?
    blockSize :: Int
    blockSize = 4096

    -- Setup loopback pipe so we can capture runStmt's stdout/stderr
    redirectStdout :: Ghc (Handle, FileDescriptor, FileDescriptor)
    redirectStdout = liftIO $ do
      -- Create pipe
      (stdOutputRd, stdOutputWr) <- liftIO createPipe

      -- Backup stdout, then replace stdout and stderr with the pipe's write end
      stdOutputBackup <- liftIO $ dup stdOutput
      stdErrorBackup  <- liftIO $ dup stdError
      _ <- dupTo stdOutputWr stdOutput
      _ <- dupTo stdOutputWr stdError
      closeFd stdOutputWr

      -- Convert to the read end to a handle and return
      stdOutputRd' <- fdToHandle stdOutputRd ReadMode
      return (stdOutputRd', stdOutputBackup, stdErrorBackup)

    -- Setup loopback pipe so we can write to runStmt's stdin
    redirectStdin :: Ghc (Handle, FileDescriptor)
    redirectStdin = liftIO $ do
      -- Create pipe
      (stdInputRd, stdInputWr) <- liftIO createPipe

      -- Swizzle stdin
      stdInputBackup <- liftIO $ dup stdInput
      _ <- dupTo stdInputRd stdInput
      closeFd stdInputRd

      -- Convert the write end to a handle and return
      stdInputWr' <- fdToHandle stdInputWr WriteMode
      return (stdInputWr', stdInputBackup)

#if PTY_SUPPORT

runPtyMaster :: (FileDescriptor, FileDescriptor) -> (Pid, Socket, Socket, FilePath) -> IO ()
runPtyMaster (masterFd, slaveFd) (processId, stdin, stdout, errorLog) = do
  -- Since we're in the master process, close the slave FD.
  closeFd slaveFd

  let readOutput :: RpcConversation -> IO RunResult
      readOutput conv = fix $ \loop -> do
        bs <- Posix.readChunk masterFd `Ex.catch` \ex ->
          -- Ignore HardwareFaults as they seem to always happen when
          -- process exits..
          if ioe_type ex == HardwareFault
            then return BSS.empty
            else Ex.throwIO ex
        if BSS.null bs
          then return RunOk
          else do
            put conv (GhcRunOutp bs)
            loop
      handleRequests :: RpcConversation -> IO ()
      handleRequests conv = forever $ do
        request <- get conv
        case request of
          GhcRunInput bs -> Posix.write masterFd bs
          -- Fork a new thread because this could throw exceptions.
          GhcRunInterrupt -> void $ forkIO $ sigKillProcess processId
      -- Turn a GHC exception into a RunResult
      ghcException :: GhcException -> IO RunResult
      ghcException = return . RunGhcException . show


  void $ forkIO $
    concurrentConversation stdin stdout errorLog $ \_ conv -> do
      result <- Ex.handle ghcException $
        withAsync (handleRequests conv) $ \_ ->
        readOutput conv
      put conv (GhcRunDone result)

ghcHandleRunPtySlave :: (FileDescriptor, FileDescriptor) -> RunCmd -> Ghc ()
ghcHandleRunPtySlave (masterFd, slaveFd) runCmd = do
  liftIO $ do
    -- Since we're in the slave process, close the master FD.
    closeFd masterFd
    -- Create a new session with a controlling terminal.
    void createSession
    Posix.setControllingTerminal slaveFd
    -- Redirect standard IO to the terminal FD.
    void $ dupTo slaveFd stdInput
    void $ dupTo slaveFd stdOutput
    void $ dupTo slaveFd stdError
    closeFd slaveFd
    -- Set TERM env variable
    setEnv "TERM" "xterm-256color"
  --FIXME: Properly pass the run result to the client as a GhcRunDone
  --value. Instead, we write it to standard output, which gets sent to
  --the terminal.
  result <- runInGhc runCmd
  case result of
    -- A successful result will be sent by runPtyMaster - only send
    -- failures.
    RunOk -> return ()
    _ -> liftIO $ putStrLn $ "\r\nProcess done: " ++ show result ++ "\r\n"

#endif

startConcurrentConversation
  :: (Socket -> Socket -> FilePath -> Ghc ())
  -> Ghc (Pid, Socket, Socket, FilePath)
startConcurrentConversation inner = do
  -- Ideally, we'd have the child process create the temp directory and
  -- communicate the name back to us, so that the child process can remove the
  -- directories again when it's done with it. However, this means we need some
  -- interprocess communication, which is awkward. So we create the temp
  -- directory here; I suppose we could still delegate the responsibility of
  -- deleting the directory to the child, but instead we'll just remove the
  -- directory along with the rest of the session temp dirs on session exit.
  (stdin, stdout, errorLog) <- liftIO $ do
    stdin <- makeSocket
    stdout <- makeSocket

    tmpDir <- Dir.getTemporaryDirectory
    (errorLogPath, errorLogHandle) <- openTempFile tmpDir "rpc-snippet-.log"
    hClose errorLogHandle

    return (stdin, stdout, errorLogPath)

  -- Start the concurrent conversion. We use forkGhcProcess rather than forkGhc
  -- because we need to change global state in the child process; in particular,
  -- we need to redirect stdin, stdout, and stderr (as well as some other global
  -- state, including withArgs).
  liftIO performGC
  processId <- forkGhcProcess $ inner stdin stdout errorLog

  -- We wait for the process to finish in a separate thread so that we do not
  -- accumulate zombies
  --
  -- FIXME(mgs): I didn't write this, and I'm not sure I see the point
  -- of doing this.
  liftIO $ void $ forkIO $
    void $ getProcessStatus True False processId

  return (processId, stdin, stdout, errorLog)
#endif