haskell-debugger-0.14.0.0: hdb-dap/Development/Debug/Adapter/DAPDebuggee.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NondecreasingIndentation #-}
-- | TODO: This module should be called Launch.
module Development.Debug.Adapter.DAPDebuggee where
#if !MIN_VERSION_ghc(9,15,0)
-- no longer needs to be imported from here in 9.15
import GHC.Conc.Sync (labelThread)
#endif
import GHC.IO.Handle
import GHC.Stack.Annotation (annotateStackStringIO)
import System.Process
import qualified Data.ByteString as BS
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Text.Encoding as T
import Control.Monad.Trans
import System.IO
import Control.Monad.Catch
import Control.Exception (throwIO, IOException)
import Control.Concurrent
import Control.Concurrent.Async (concurrently_)
import Control.Monad
import Data.Functor.Contravariant
import Development.Debug.Adapter
import Colog.Core as Logger
import qualified Development.Debug.Adapter.Output as Output
import DAP
import Development.Debug.Adapter.Handles
import Development.Debug.Session.Setup
import Development.Debug.Adapter.Proxy
import Network.Socket (socketPort, close)
import GHC.Debugger.Debuggee as Debugger
import GHC.Debugger.Utils (forwardHandleToLogger)
data DAPDebuggee = DAPDebuggee
{ dapdInterpreterSettings :: InterpreterSettings
, dapdWaitForDebuggee :: IO ()
, dapdThreads :: [(DebugAdaptorCont () -> IO ()) -> IO ()]
-- ^ Additional threads to register for this session depending on the process
-- we're running through `runInTerminal` (see 'interpreterInit').
, dapdAfterRegister :: DebugAdaptor ()
-- ^ additional commands to run after registering the session.
}
internalNoInTerminalDAPD :: Applicative f => f DAPDebuggee
internalNoInTerminalDAPD
-- Not using the terminal proxy, but we still want to output our own
-- stdout/err (from the internal interpreter) as console events.
= do
let interpSettings = InterpreterSettings
{ interpreterFlags = mkInternalInterpreterFlags
, interpreterSetup = mkInternalInterpreterSetup
}
pure $
DAPDebuggee
interpSettings
(pure ())
[ stdoutCaptureThread Nothing, stderrCaptureThread Nothing ]
(pure ())
externalNoInTerminalDAPD :: MonadIO f => FilePath -> f DAPDebuggee
externalNoInTerminalDAPD hdbProg = do
iserv_handles <- liftIO newEmptyMVar
let interpSettings = InterpreterSettings
{ interpreterFlags = mkExternalInterpreterFlags hdbProg
, interpreterSetup = mkExternalInterpreterSubProcessSetup CreatePipe CreatePipe CreatePipe (putMVar iserv_handles)
}
pure $
DAPDebuggee
interpSettings
(pure ())
[\ withAdaptor -> fwdThread iserv_handles (LogAction $ withAdaptor . Output.stdout) (LogAction $ withAdaptor . Output.stderr)
]
(pure ())
where
fwdThread iserv_handles logOut logErr = annotateStackStringIO "External interpreter forwarding parent thread" $ do
(_, Just serv_out, Just serv_err, _) <- takeMVar iserv_handles
concurrently_
(annotateStackStringIO "External interpreter stderr forwarding" $ forwardHandleToLogger serv_err logErr)
(annotateStackStringIO "External interpreter stdout forwarding" $ forwardHandleToLogger serv_out logOut)
externalInTerminalDAPD :: MonadIO m => FilePath -> m DAPDebuggee
externalInTerminalDAPD hdbProg
-- No additional bookkeeping is needed in this case because GHC will
-- naturally have to wait for the external interpreter in order to start execution
= liftIO $ do
-- We keep the socket open so we claim the port.
bracketOnError openSocketAvailablePort Network.Socket.close $ \ sock -> do
let
interpSettings = InterpreterSettings
{ interpreterFlags = mkExternalInterpreterFlags hdbProg
, interpreterSetup = mkExternalInterpreterFromIOSetup
$ annotateStackStringIO "Waiting for an external interpreter run-in-terminal process"
$ extInterpFromListeningSocket sock
}
extInterpPort <- liftIO $ socketPort sock
pure $
DAPDebuggee
interpSettings
(pure ())
-- When session is killed the socket is closed too.
[\ _ -> forever (threadDelay 100_000_000) `finally` Network.Socket.close sock]
(sendRunInTerminalReverseRequest
RunInTerminalRequestArguments
{ runInTerminalRequestArgumentsKind = Just RunInTerminalRequestArgumentsKindIntegrated
, runInTerminalRequestArgumentsTitle = Nothing
, runInTerminalRequestArgumentsCwd = ""
, runInTerminalRequestArgumentsArgs =
[T.pack hdbProg, "external-interpreter", "--port", T.pack (show extInterpPort)]
, runInTerminalRequestArgumentsEnv = Nothing
, runInTerminalRequestArgumentsArgsCanBeInterpretedByShell = False
})
internalInTerminalDAPD :: LogAction IO DAPSessionLog -> FilePath -> Adaptor DebugAdaptorState r DAPDebuggee
internalInTerminalDAPD l hdbProg = do
(syncProxyIn, syncProxyOut, syncProxyErr)
<- liftIO $ (,,) <$> newChan <*> newChan <*> newChan
proxyClientReady <- liftIO $ newEmptyMVar
(serverPort, serverProxyThread) <- liftIO $
mkServerSideHdbProxy (contramap RunProxyServerLog l)
syncProxyIn syncProxyOut syncProxyErr proxyClientReady
let interpSettings = InterpreterSettings
{ interpreterFlags = mkInternalInterpreterFlags
, interpreterSetup = mkInternalInterpreterSetup
}
waitForDebuggee =
-- Only start executing after proxy client connects succesfully (#95)
takeMVar proxyClientReady
pure $ DAPDebuggee
interpSettings
waitForDebuggee
[ const serverProxyThread
-- Setup capturing of the process' own stdout and forwarding of the process' own stdin,
-- but only because we're using the internal interpreter!
, stdinForwardThread syncProxyIn
, stdoutCaptureThread (Just syncProxyOut)
, stderrCaptureThread (Just syncProxyErr)
]
-- When using the internal interpreter and 'runInTerminal' is supported
-- (the 'RunProxyInTerminal' case), we ask the DAP client to launch the
-- `hdb proxy` attached to the user's terminal. The proxy forwards
-- input/output from the user terminal to the debugger+debuggee shared process
(sendRunProxyInTerminal hdbProg serverPort)
--------------------------------------------------------------------------------
-- * Logging
--------------------------------------------------------------------------------
type SessionId = T.Text
data DAPSessionLog
= DAPSessionSetupLog (WithSeverity SessionSetupLog)
| DAPDebuggerLog Debugger.DebuggerLog
| RunProxyServerLog (WithSeverity T.Text)
--------------------------------------------------------------------------------
-- * Capturing stdout, stderr, and writing to self stdin
--------------------------------------------------------------------------------
-- | Hijack the current process stdin and forward to it the messages from the given channel
stdinForwardThread :: Chan BS.ByteString -> (DebugAdaptorCont () -> IO ()) -> IO ()
stdinForwardThread syncIn _withAdaptor = do
tid <- myThreadId
labelThread tid "Stdin Forward Thread"
-- We need to hijack stdin to write to it
-- 1. Create a new pipe from writeEnd->readEnd
(readEnd, writeEnd) <- createPipe
-- 2. Substitute the read-end of the pipe by stdin
_ <- hDuplicateTo readEnd stdin
hClose readEnd -- we'll never need to read from readEnd
forever $ do
i <- readChan syncIn
-- 3. Write to write-end of the pipe
BS.hPut writeEnd i >> hFlush writeEnd
-- | This thread captures stdout from the debuggee and sends it to the client.
-- NOTE, redirecting the stdout handle is a process-global operation. So this thread
-- will capture ANY stdout the debuggee emits. Therefore you should never directly
-- write to stdout, but always write to the appropiate handle.
stdoutCaptureThread :: Maybe (Chan BS.ByteString) -> (DebugAdaptorCont () -> IO ()) -> IO ()
stdoutCaptureThread msyncOut withAdaptor = do
tid <- myThreadId
labelThread tid "Stdout Capture Thread"
withInterceptedStdout $ \_ interceptedStdout -> do
forever $ do
line <- liftIO $ T.hGetLine interceptedStdout
case msyncOut of
Nothing -> pure ()
Just syncOut -> writeChan syncOut $ T.encodeUtf8 (line <> T.pack "\n")
-- Always output to Debug Console
catch
(withAdaptor $ Output.stdout line)
(\(_ :: IOException) ->
throwIO (FailedToWriteToAdaptor line))
-- | Like 'stdoutCaptureThread' but for stderr
stderrCaptureThread :: Maybe (Chan BS.ByteString) -> (DebugAdaptorCont () -> IO ()) -> IO ()
stderrCaptureThread msyncErr withAdaptor = do
tid <- myThreadId
labelThread tid "Stderr Capture Thread"
withInterceptedStderr $ \_ interceptedStderr -> do
forever $ do
line <- liftIO $ T.hGetLine interceptedStderr
case msyncErr of
Nothing -> pure ()
Just syncErr -> writeChan syncErr $ T.encodeUtf8 (line <> "\n")
-- Always output to Debug Console
catch
(withAdaptor $ Output.stderr line)
(\(_ :: IOException) ->
throwIO (FailedToWriteToAdaptor line))
newtype FailedToWriteToAdaptor = FailedToWriteToAdaptor T.Text
instance Show FailedToWriteToAdaptor where
show (FailedToWriteToAdaptor t) = "Failed to write to debug adapter: " ++ T.unpack t
instance Exception FailedToWriteToAdaptor