packages feed

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