haskell-debugger-0.14.0.0: hdb-dap/Development/Debug/Adapter/Init.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.Init
( module Development.Debug.Adapter.Init
, DAPSessionLog(..)
)
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 qualified Data.Text as T
import qualified System.Process as P
import Control.Exception (displayExceptionWithInfo, ExceptionWithContext (ExceptionWithContext), AsyncException (..))
import Control.Monad (when)
import Control.Monad.Except
import Control.Monad.Trans
import Data.Function
import Data.Maybe
import Data.UUID.V4 qualified as UUID
import System.IO
import GHC.IO.Encoding
import Control.Monad.Catch
import Control.Concurrent
import Data.Aeson as Aeson
import GHC.Generics
import System.Directory
import Data.Functor.Contravariant
import Development.Debug.Adapter
import Colog.Core as Logger
import qualified Development.Debug.Adapter.Output as Output
import GHC (Ghc)
import GHC.Utils.Logger (defaultLogActionWithHandles)
import GHC.Debugger.Utils (forwardHandleToLogger, IsLine (text))
import qualified GHC.Debugger as Debugger
import qualified GHC.Debugger.Monad as Debugger
import qualified GHC.Debugger.Interface.Messages as D (Command, Response)
import GHC.Debugger.Interface.Messages hiding (Command, Response)
import DAP
import Development.Debug.Adapter.Handles
import Development.Debug.Session.Setup
import GHC.Debugger.Debuggee as Debugger
import Development.Debug.Adapter.DAPDebuggee
--------------------------------------------------------------------------------
-- * Client
--------------------------------------------------------------------------------
-- | Client arguments are custom for launch
data LaunchArgs
= LaunchArgs
{ __sessionId :: Maybe String
-- ^ SessionID, set by VSCode client
, projectRoot :: Maybe FilePath
-- ^ Absolute path to the project root
, entryFile :: Maybe FilePath
-- ^ The file with the entry point e.g. @app/Main.hs@
, entryPoint :: Maybe String
-- ^ Either @main@ or a function name
, entryArgs :: Maybe [String]
-- ^ The arguments to either set as environment arguments when @entryPoint = "main"@
-- or function arguments otherwise.
, extraGhcArgs :: Maybe [String]
-- ^ Additional arguments to pass to the GHC invocation inferred by hie-bios for this project
, cradleFile :: Maybe FilePath
-- ^ specify cradle file rather than let it be inferred from @entryFile@, relative to @projectRoot@.
} deriving stock (Show, Eq, Generic)
deriving anyclass FromJSON
--------------------------------------------------------------------------------
-- * Launch Debugger
--------------------------------------------------------------------------------
data DAPServerConf = DAPServerConf
{ hdbProgram :: FilePath
-- ^ invoked with `external-interpreter` to serve as the external interpreter
, getDebugRunner :: DebugRunnerProvider ()
, dapServerConfig :: ServerConfig
}
data InterpreterChoice = InterpreterChoice { runInTerminal :: Bool, internal :: Bool }
-- | Initialize debugger
--
-- Returns @()@ if successful, throws @InitFailed@ otherwise
initDebugger :: LogAction IO (T.Text,DAPSessionLog) -> DAPServerConf -> InterpreterChoice
-> LaunchArgs -> DebugAdaptor ()
initDebugger l0 servConf interpChoice
LaunchArgs{ __sessionId
, projectRoot = givenRoot
, entryFile = entryFileMaybe
, entryPoint = fromMaybe "main" -> entryPoint
, entryArgs = fromMaybe [] -> entryArgs
, extraGhcArgs = fromMaybe [] -> extraGhcArgs
, cradleFile
} = do
syncRequests <- liftIO newEmptyMVar
syncResponses <- liftIO newEmptyMVar
entryFile <- case entryFileMaybe of
Nothing -> throwError ("Missing \"entryFile\" key in debugger configuration", Nothing)
Just ef -> pure ef
projectRoot <- liftIO $ mkAbsolute <$> maybe getCurrentDirectory makeAbsolute givenRoot
sessionId <- liftIO $ maybe (T.show <$> UUID.nextRandom) (pure . T.pack) __sessionId
let l = contramap (sessionId,) l0
-- Create a pipe to which messages to send to the DAP console are written and read.
-- todo: This could just be a Haskell channel now...
(readDAPOutput, writeDAPOutput) <- liftIO P.createPipe
liftIO $ do
hSetBuffering readDAPOutput LineBuffering
hSetBuffering writeDAPOutput NoBuffering
-- GHC output uses utf8
hSetEncoding readDAPOutput utf8
hSetEncoding writeDAPOutput utf8
setLocaleEncoding utf8
dapLogger <- liftIO $ handleLogger writeDAPOutput
let hieBiosLogger = contramap DAPSessionSetupLog l <> logHieBiosToDAP
logHieBiosToDAP = LogAction $ \case
WithSeverity msg sev
| sev >= Info -> dapLogger <& renderSessionSetupLog msg
| otherwise -> mempty
let debugRunnerConf = DebugRunnerConf (unAbs projectRoot) entryFile extraGhcArgs cradleFile
liftIO (getDebugRunner servConf hieBiosLogger debugRunnerConf) >>= \case
Left e -> throwError (ErrorMessage (T.pack e), Nothing)
Right (ghcInvocation, debugRunner) -> do
let
nextFreshId = 0
breakpointMap = mempty
stackFrameMap = mempty
variablesMap = mempty
dbgLog <- liftIO $ createDebuggerLogger l dapLogger writeDAPOutput
dapd <- initDAPDebuggee l (hdbProgram servConf) interpChoice
let
defaultRunConf = Debugger.RunDebuggerSettings
{ supportsANSIStyling = True -- TODO: Initialize Request sends supportsANSIStyling; this is False for nvim-dap
, supportsANSIHyperlinks = False -- VSCode does not support this
, interpreterSettings = dapdInterpreterSettings dapd
}
absEntryFile = projectRoot /> entryFile
daState = DAS{entryFile=absEntryFile,waitForDebuggee = dapdWaitForDebuggee dapd,..}
registerNewDebugSession sessionId daState $ map (destroyDebugSessionOnException l) $
[ \withAdaptor -> do
-- The info here is already taken into account in debugRunner.
let GhcInvocation libdir units args = ghcInvocation
withAdaptor $
Output.console $ T.pack $ unlines $
[ "libdir: " <> libdir
, "units: " <> unwords units
, "args: " <> unwords args
]
debuggerThread dbgLog debugRunner defaultRunConf syncRequests syncResponses
, \withAdaptor -> forwardHandleToLogger readDAPOutput $
LogAction (\msg -> withAdaptor (Output.neutral msg))
]
++
dapdThreads dapd
dapdAfterRegister dapd
destroyDebugSessionOnException :: LogAction IO DAPSessionLog
-> ((Adaptor a r () -> IO ()) -> IO ())
-> (Adaptor a r () -> IO ())
-> IO ()
destroyDebugSessionOnException l k withAdaptor = do
k withAdaptor
`catchNoPropagate` \ x@(ExceptionWithContext _ctx e) -> do
l <& DAPDebuggerLog (DebuggerSessionLog Debug $ (T.pack $ displayExceptionWithInfo (toException x)))
case fromException e of
-- TODO: would be better if destroyDebugSession from dap sent a custom exception, so we are sure we don't have to propagate it to the other threads.
Just ThreadKilled -> return ()
_ -> do
withAdaptor $ do
sendTerminatedEvent (TerminatedEvent False)
safeDestroyDebugSession
initDAPDebuggee
:: LogAction IO DAPSessionLog
-> FilePath
-> InterpreterChoice
-> DebugAdaptor DAPDebuggee
initDAPDebuggee _ _ InterpreterChoice{runInTerminal = False, internal = True}
= internalNoInTerminalDAPD
initDAPDebuggee _ hdbProg InterpreterChoice{runInTerminal = False, internal = False}
= externalNoInTerminalDAPD hdbProg
initDAPDebuggee _ hdbProg InterpreterChoice{internal = False, runInTerminal = True}
= externalInTerminalDAPD hdbProg
initDAPDebuggee l hdbProg InterpreterChoice{runInTerminal = True, internal = True}
= internalInTerminalDAPD l hdbProg
-- | The main debugger thread launches a GHC.Debugger session.
--
-- Then, forever:
-- 1. Reads commands from the given 'D.Command' 'MVar'
-- 2. Executes the command with `execute`
-- 3. Writes responses to the given 'D.Response' 'MVar'
--
-- Concurrently, it reads from the process's stderr forever and outputs it through OutputEvents.
--
debuggerThread :: LogAction IO Debugger.DebuggerLog
-> Debugger.DebugRunner Ghc ()
-> Debugger.RunDebuggerSettings -- ^ Settings for running the debugger
-> MVar D.Command -- ^ Read commands
-> MVar D.Response -- ^ Write reponses
-> IO ()
debuggerThread l debugRunner runConf requests replies = do
liftIO $ do
tid <- myThreadId
labelThread tid "Main Debugger Thread (before runDebugger)"
Debugger.runDebugger l debugRunner runConf $ do
liftIO $ do
tid <- myThreadId
labelThread tid "Main Debugger Thread"
let loop = do
req <- takeMVar requests & liftIO
resp <- try (Debugger.execute req)
case resp of
Right x -> do
liftIO (putMVar replies x)
loop
Left e | Just (Debugger.NonFatalException {Debugger.userMessage = userm,
Debugger.debugMessage = dbgm}) <- fromException e -> do
Debugger.logSDoc Logger.Error (text userm)
Debugger.logSDoc Logger.Debug (text dbgm)
liftIO $ putMVar replies (NonFatalError userm)
Left e ->
-- don't loop in this case! just exit.
liftIO $ putMVar replies (Aborted ("Aborted debugger thread: " ++ displayExceptionWithInfo e))
loop
--------------------------------------------------------------------------------
-- * Logging
--------------------------------------------------------------------------------
{-
Note [Debugger, debuggee, and DAP logs]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Specification for the logger given to `Debugger`:
1. All -v3 DebuggerLog and GHCLog messages go to the normal stdout/stderr (this shows up in
the OUTPUT console in VSCode, without having to send special messages)
2. All -v1 DebuggerLog, GHCLog, and all LogDebuggeeOut and LogDebuggeeErr output
goes to the DAP console (this is DEBUG CONSOLE in VSCode)
3. All LogDebuggeeOut and LogDebuggeeErr output are forwarded to the proxy if
the proxy is enabled.
-}
-- See Note [Debugger, debuggee, and DAP logs]
createDebuggerLogger
:: LogAction IO DAPSessionLog
-> LogAction IO T.Text -- ^ Logger that writes to to DAP output
-> Handle -- ^ Handle to DAP output
-> IO (LogAction IO Debugger.DebuggerLog)
createDebuggerLogger l dapLogger writeDAPOutput = do
return $
-- (1) (all output is logged to normal logger)
contramap DAPDebuggerLog l <>
-- (2) and (3) (log relevant output to DAP handle)
LogAction (\case
Debugger.DebuggerLog sev msg ->
when (sev >= Info) $
dapLogger <& T.pack (show msg)
Debugger.GHCLog logflags msg_class srcSpan msg ->
defaultLogActionWithHandles writeDAPOutput writeDAPOutput logflags msg_class srcSpan msg
-- don't log other messages, already logged to (1)
Debugger.DebuggerSessionLog{} -> pure ()
)