packages feed

ghc-debugger-0.1.0.0: ghc-debug-adapter/Development/Debug/Adapter/Handles.hs

{-# LANGUAGE OverloadedStrings, OverloadedRecordDot, CPP, DeriveAnyClass, DeriveGeneric, DerivingVia, LambdaCase, RecordWildCards #-}
module Development.Debug.Adapter.Handles
  ( handleLogger
  , withInterceptedStdout
  , withInterceptedStderr
  , withInterceptedStdoutForwarding
  , withInterceptedStderrForwarding
  ) where

import DAP


import System.IO ()
import DAP.Log
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Control.Concurrent.MVar
import GHC.IO.Handle.FD
import GHC.IO.Handle
import System.Process
import Control.Exception
import Control.Monad
import Control.Concurrent.Async

{- Note [Debugger Handles]
~~~~~~~~~~~~~~~~~~~~~~~~~~

The debugger executes arbritary user programs which will output to stdout and stderr.
This output needs to be sent to the debug client. Any output we wish to put in a
log or elsewhere needs to be directed to another handle.


-}

handleLogger :: Handle -> IO (LogAction IO T.Text)
handleLogger out_handle = do
  handleLock               <- newMVar ()
  return $ LogAction $ \msg -> do
    withLock handleLock $ do
      T.hPutStrLn out_handle msg

-- | Redirect stdout globally, run the continuation with a bypass handle for real stdout.
withInterceptedStdout :: (Handle -- ^ realStdout, write to this handle sends output to stdout
                          -> Handle -- ^ interceptedStdout, stdout captured from attempting to write to stdout handle.
                          -> IO ()) -> IO ()
withInterceptedStdout k = do
  withPipe $ \readOutHandle writeOutHandle -> do
    hSetBuffering readOutHandle NoBuffering
    hSetBuffering writeOutHandle LineBuffering
    withStdoutBypass writeOutHandle $ \realStdout -> do
      k realStdout readOutHandle

-- | Redirect stderr globally, run the continuation with a bypass handle for real stderr.
withInterceptedStderr :: (Handle -- ^ realStderr, write to this handle sends output to stderr
                          -> Handle -- ^ interceptedStderr, stdout captured from attempting to write to stderr handle.
                          -> IO ()) -> IO ()
withInterceptedStderr k = do
  withPipe $ \readErrHandle writeErrHandle -> do
    hSetBuffering readErrHandle NoBuffering
    hSetBuffering writeErrHandle LineBuffering
    withStderrBypass writeErrHandle $ \realStderr -> do
      k realStderr readErrHandle

-- | Intercept stderr, and spawn a thread which forwards the input
-- onwards using the supplied IO action.
withInterceptedStderrForwarding :: (T.Text -> IO ())
                                -- ^ All stderr input that is intercepted is forwarded to this thread
                                -> (Handle -> IO ())
                                -- ^ The continuation receives the REAL STDERR
                                -> IO ()
withInterceptedStderrForwarding write_stderr k = do
  withInterceptedStderr $ \realStderr interceptedStderr -> do
      withAsync (forwardingThread write_stderr interceptedStderr) $ \_ -> do
        k realStderr

-- | Intercept stdout, and spawn a thread which forwards the input
-- onwards using the supplied IO action.
withInterceptedStdoutForwarding :: (T.Text -> IO ())
                                -- ^ All stdout input that is intercepted is forwarded to this thread
                                -> (Handle -> IO ())
                                -- ^ The continuation receives the REAL STDOUT
                                -> IO ()
withInterceptedStdoutForwarding write_stdout k = do
  withInterceptedStdout $ \realStdout interceptedStdout -> do
    withAsync (forwardingThread write_stdout interceptedStdout) $ \_ ->
        k realStdout

--------------------------------------------------------------------------------
-- Auxiliary
--------------------------------------------------------------------------------

-- | Temporarily bypass the intercepted stdout/stderr to write directly to the original stdout/stderr.
-- This is useful for debugging or for sending output that should not be intercepted.
withStdoutBypass, withStderrBypass
  :: Handle
  -> (Handle -> IO r)
  -> IO r
withStdoutBypass interceptH = withHandleBypass stdout interceptH
withStderrBypass interceptH = withHandleBypass stderr interceptH

-- | Capture all output written to a given handle and redirect it to the other
-- one; the continuation can use the "real" copy of the first handle to write
-- to it while bypassing the redirection.
--
-- This is useful for debugging or for sending output that should not be intercepted.
withHandleBypass :: Handle
                 -- ^ Text written to this handle...
                 -> Handle
                 -- ^ ...will be redirected to this handle
                 -> (Handle -> IO r)
                 -- ^ Continuation receives as an argument a "real" copy of the handle that is now being redirected.
                 -- If you write to this Handle, it will write to the original one and *bypass* the redirection (ie it will not be redirected)
                 -> IO r
withHandleBypass originalHandle interceptWriteHandle action =
  bracket setup clean action
  where
    setup = do
      realHandle <- hDuplicate originalHandle
      hFlush originalHandle
      hDuplicateTo interceptWriteHandle originalHandle
      hSetBuffering originalHandle LineBuffering
      return realHandle

    clean realHandle = do
      hFlush originalHandle
      hDuplicateTo realHandle originalHandle
      hClose realHandle

-- | Thread to read from the intercepted stdout pipe and forward onwards
forwardingThread :: (T.Text -> IO ()) -> Handle -> IO ()
forwardingThread write_action fromPipe = forever $ do
  line <- T.hGetLine fromPipe
  write_action line


withPipe :: (Handle -> Handle -> IO r) -> IO r
withPipe action = bracket createPipe closeBoth (uncurry action)
  where
    closeBoth (readH, writeH) = do
      hClose readH
      hClose writeH