packages feed

haskell-debugger-0.14.0.0: hdb/Main.hs

{-# LANGUAGE OverloadedStrings, OverloadedRecordDot, CPP, DeriveAnyClass,
   DeriveGeneric, DerivingVia, LambdaCase, RecordWildCards, ViewPatterns,
   DataKinds #-}
module Main where

import System.Process
import System.Environment
import Control.Exception (bracket, uninterruptibleMask, bracketOnError)
import Control.Exception.Backtrace

import DAP

import Development.Debug.Adapter.Init
import Development.Debug.Adapter.Handles
import Development.Debug.Adapter.Server
import Colog.Core

import System.IO
  ( hFlush
  , hClose
  , hPutStrLn
  , hSetBuffering
  , BufferMode(..)
  , Handle

  , IOMode(ReadWriteMode)
  )
import qualified Data.Text as T
import qualified Data.Text.IO as T
import GHC.IO.Handle.FD
import Data.Functor.Contravariant
import Network.Socket hiding (Debug)

import qualified GHCi.Server as GHCi
import qualified GHCi.Signals as GHCi
import qualified GHCi.Utils as GHCi
import qualified GHCi.Message as GHCi

import GHC.Debugger.Monad (RunDebuggerSettings(..))
import Development.Debug.Options (HdbOptions(..))
import Development.Debug.Options.Parser (parseHdbOptions)
import Development.Debug.Adapter.Proxy
import Development.Debug.Interactive
import GHC.Stack.Annotation (annotateCallStackIO)
import GHC.Utils.Logger (defaultLogActionWithHandles)
import Development.Debug.Session.Setup (hieDebugRunner)
import GHC.Debugger.Debuggee (mkCliInterpreterSettings)
import GHC.Debugger.Session (initUniqSupplyIO)

#if MIN_VERSION_ghc(9,15,0)
import GHC.Debugger.Runtime.Interpreter.Custom (dbgInterpCmdHandler)
#endif

--------------------------------------------------------------------------------

main :: IO ()
main = do
  setBacktraceMechanismState CostCentreBacktrace False
  setBacktraceMechanismState HasCallStackBacktrace True

  allArgs <- getArgs
  hdbOpts <- case allArgs of
    [writeFd, readFd, "--external-interpreter"] ->
         -- Special case to detect --external-interpreter in the third
         -- position. If we could specify -opti options to put *before* the
         -- descriptors we could get rid of this.
         pure (HdbExternalInterpreter (read writeFd) (read readFd) Error)
    _ -> parseHdbOptions
  case hdbOpts of
    HdbDAPServer{port, internalInterpreter, disableIpeBacktraces} -> do
      setBacktraceMechanismState IPEBacktrace (not disableIpeBacktraces)
      config <- getConfig port
      -- the same program invoked with `external-interpreter` serves as the external interpreter
      hdbProgram <- getExecutablePath

      -- See Note [UniqueSupply is process global]
      initUniqSupplyIO

      let servConf = DAPServerConf
            { getDebugRunner = hieDebugRunner
            , hdbProgram
            , dapServerConfig = config
            }
      redirectRealStdout internalInterpreter $ \realStdout -> do
        hSetBuffering realStdout LineBuffering
        l <- contramap DAPLog <$> mainLogger hdbOpts.verbosity realStdout
        runDAPServerWithLogger (contramap DAPLibraryLog l) config
          (talk l servConf internalInterpreter)
          (ack l )
    HdbCLI{..} -> do
        setBacktraceMechanismState IPEBacktrace (not disableIpeBacktraces)

        -- See Note [UniqueSupply is process global]
        initUniqSupplyIO

        l <- mainLogger hdbOpts.verbosity stdout
        cliInterpSettings <- mkCliInterpreterSettings internalInterpreter debuggeeStdin
        let runConf = RunDebuggerSettings
              { supportsANSIStyling = True -- todo: check!!
              , supportsANSIHyperlinks = False
              , interpreterSettings = cliInterpSettings }
        runIDM (contramap InteractiveLog l) entryPoint entryFile entryArgs extraGhcArgs cradleFile
          runConf debugInteractive
    HdbProxy{port} -> do
        setBacktraceMechanismState IPEBacktrace True
        l <- mainLogger hdbOpts.verbosity stdout
        runInTerminalHdbProxy (contramap RunProxyClientLog l) port
    HdbExternalInterpreter{writeFd, readFd} -> do
      inh  <- GHCi.readGhcHandle (show readFd)
      outh <- GHCi.readGhcHandle (show writeFd)
      runExternalInterpreterServer inh outh hdbOpts.verbosity
    HdbExternalInterpreterPort{port} -> do
      pid <- getCurrentPid
      withExternalInterpreterPort (fromIntegral port) $ \h -> do
        hPutStrLn h (show pid)
        hFlush h
        runExternalInterpreterServer h h hdbOpts.verbosity
  where
    runExternalInterpreterServer inh outh verbosity = do
      GHCi.installSignalHandlers
      pipe <- GHCi.mkPipeFromHandles inh outh
      let verbose = verbosity <= Info -- Debug || Info
#if MIN_VERSION_ghc(9,14,2)
      uninterruptibleMask $ \restore -> do
        GHCi.servWithCustom verbose hook pipe restore dbgInterpCmdHandler
#else
      uninterruptibleMask $ GHCi.serv verbose hook pipe
#endif
      where hook = return -- empty hook
        -- we cannot allow any async exceptions while communicating, because
        -- we will lose sync in the protocol, hence uninterruptibleMask.

    withExternalInterpreterPort :: PortNumber -> (Handle -> IO a) -> IO a
    withExternalInterpreterPort port k = do
      bracket (mkHandleFromPortSock "127.0.0.1" port) hClose $ \ h -> do
        annotateCallStackIO $ k h

    mkHandleFromPortSock :: HostName -> PortNumber -> IO Handle
    mkHandleFromPortSock host port = do
      let hints = defaultHints { addrSocketType = Stream }
      addr:_ <- getAddrInfo (Just hints) (Just host) (Just (show port))

      Control.Exception.bracketOnError
        (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
        close
        (\sock -> do
            -- Don't delay, avoids batching
            setSocketOption sock NoDelay 1
            connect sock (addrAddress addr)
            h <- socketToHandle sock ReadWriteMode
            hSetBuffering h NoBuffering
            return h)

    -- When using the internal interpreter in DAP mode, we can't write to
    -- stdout directly because there will also be a thread forwarding the
    -- debuggee stdout by capturing it from stdout (and we'd get into a loop
    -- trying to forward what we're writing).
    --
    -- The redirection we use requires hDuplicateTo which isn't supported on
    -- Windows (ghc#22146), so using the internal interpreter on Windows
    -- currently unsupported.
    --
    -- When using the external interpreter, the debuggee output is read from
    -- its process handle directly, so this is unnecessary.
    redirectRealStdout internalInterpreter k
      | internalInterpreter =
        withInterceptedStdoutForwarding
          (\interceptedOut -> T.hPutStrLn stderr ("[INTERCEPTED STDOUT] " <> interceptedOut))
          (\realStdout -> k realStdout)
      | otherwise = k stdout



--------------------------------------------------------------------------------
-- * Logging
--------------------------------------------------------------------------------

data MainLog
  = InteractiveLog InteractiveLog
  | RunProxyClientLog (WithSeverity T.Text)
  | DAPLog DAPLog

-- | Given the severity threshold from which we start logging, create a base
-- logger for consuming the top-level debugger logs ('MainLog').
-- Outputs to given handle.
mainLogger :: Severity -> Handle -> IO (LogAction IO MainLog)
mainLogger threshold h = do
  l <- handleLogger h
  let logGhcLog = defaultLogActionWithHandles h h
  pure $ LogAction $ \case
    InteractiveLog (ISessionSetupLog sessionLog) -> logSessionLog l threshold sessionLog
    InteractiveLog (IDebuggerLog debuggerLog)    -> logDebuggerLog logGhcLog l threshold debuggerLog
    RunProxyClientLog sev_msg -> defaultLog l threshold sev_msg
    DAPLog dapLog -> logDAPLog logGhcLog l threshold <& dapLog