packages feed

haskell-debugger-0.9.0.0: hdb/Development/Debug/Adapter/Proxy.hs

{-# LANGUAGE OverloadedStrings, DerivingStrategies #-}
-- | Run the proxy mode, which forwards stdin/stdout to/from the DAP server and
-- is displayed in a terminal in the DAP client using 'runInTerminal'
module Development.Debug.Adapter.Proxy
  ( serverSideHdbProxy
  , runInTerminalHdbProxy
  , ProxyLog(..)
  ) where

import DAP

import System.IO
import System.Exit (exitSuccess)
import System.Environment
import System.FilePath
import Control.Exception.Base
import Control.Monad
import Control.Monad.IO.Class
import Control.Concurrent
import qualified Data.List.NonEmpty as NE

import qualified Data.Text as T
import Network.Socket hiding (Debug)
import Network.Run.TCP
import qualified Network.Socket.ByteString as NBS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.HashMap.Strict as H

import GHC.Debugger.Logger
import Development.Debug.Adapter

newtype ProxyLog = ProxyLog T.Text
  deriving newtype Pretty

-- | Connect to a running @hdb proxy@ process on the given port
-- connectToHdbProxy :: Recorder (WithVerbosity x) -> Int -> DebugAdaptor ()
-- connectToHdbProxy = _

-- | Fork a new thread to run the server-side of the proxy.
--
-- 1. To setup:
-- Ask the DAP client to launch a process running @hdb proxy --port <port>@
-- by sending a 'runInTerminal' DAP reverse request. This is done outside of
-- this function by signaling the given MVar (this is the case because we cannot use `network` with `DebugAdaptor`
--
-- 2. In a loop,
-- 2.1 Read stdin from the socket and push it to a Chan
-- 2.1 Read from a stdout Chan and write to the socket
serverSideHdbProxy :: Recorder (WithSeverity ProxyLog)
                   -> MVar ()
                   -> DebugAdaptor ()
serverSideHdbProxy l client_conn_signal = do
  DAS { syncProxyIn = dbIn
      , syncProxyOut = dbOut
      , syncProxyErr = dbErr } <- getDebugSession

  sock <- liftIO $ do
    let hints = defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV], addrSocketType = Stream }
    addr <- NE.head <$> getAddrInfo (Just hints) (Just "127.0.0.1") (Just "0")
    -- Bind on "0" to let the OS pick a free port
    openTCPServerSocket addr

  port <- liftIO $ socketPort sock

  _ <- liftIO $ forkIO $ ignoreIOException $ do
    runTCPServerWithSocket sock $ \scket -> do

      logWith l Info $ ProxyLog $ T.pack $ "Connected to client on port " ++ show port ++ "...!"
      putMVar client_conn_signal () -- signal ready (see #95)

      -- -- Read stdout from chan and write to socket
      _ <- forkIO $ ignoreIOException $ do
        forever $ do
          bs <- readChan dbOut
          logWith l Debug $ ProxyLog $ T.pack $ "Writing to socket: " ++ BS8.unpack bs
          NBS.sendAll scket bs

      -- Read stderr from chan and write to socket
      _ <- forkIO $ ignoreIOException $ do
        forever $ do
          bs <- readChan dbErr
          logWith l Debug $ ProxyLog $ T.pack $ "Writing to socket (from stderr): " ++ BS8.unpack bs
          NBS.sendAll scket bs

      -- Read stdin from socket and write to chan
      let loop = do
            bs <- NBS.recv scket 4096
            if BS8.null bs
              then do
                logWith l Debug $ ProxyLog $ T.pack "Connection to client was closed."
                close scket
              else do
                logWith l Debug $ ProxyLog $ T.pack $ "Read from socket: " ++ BS8.unpack bs
                writeChan dbIn bs >> loop
       in ignoreIOException loop

  sendRunProxyInTerminal port

  where
    ignoreIOException a = catch a $ \(e::IOException) ->
      logWith l Info $ ProxyLog $ T.pack $ "Ignoring connection broken to proxy client: " ++ show e

-- | The proxy code running on the terminal in which the @hdb proxy@ process is launched.
--
-- This client-side proxy is responsible for
-- 1. Connecting to the given proxy-server port
-- 2. Forwarding stdin to the port it is connected to
-- 3. Read from the network the output and write it to stdout
runInTerminalHdbProxy :: Recorder (WithSeverity ProxyLog) -> Int -> IO ()
runInTerminalHdbProxy l port = do
  logWith l Info $ ProxyLog $ T.pack $ "Running in terminal on port " ++ show port ++ "...!"
  hSetBuffering stdin LineBuffering

  dbg_inv <- lookupEnv "DEBUGGEE_INVOCATION"
  case dbg_inv of
    Nothing  -> pure ()
    Just inv ->
      putStrLn $ "Running the debugger input/output proxy for the following debuggee execution:\n\n\n    " ++ inv ++ "\n\n"

  catch (
    runTCPClient "127.0.0.1" (show port) $ \sock -> do
      -- Forward stdin to sock
      _ <- forkIO $
        catch (forever $ do
          str <- BS8.hGetLine stdin
          NBS.sendAll sock (str <> BS8.pack "\n")
          ) $ \(e::IOException) -> return () -- connection dropped, just exit.

      -- Forward stdout from sock
      catch (forever $ do
        msg <- NBS.recv sock 4096
        if BS8.null msg
          then do
            logWith l Info $ ProxyLog $ T.pack "Exiting..."
            close sock
            exitSuccess
          else BS8.hPut stdout msg >> hFlush stdout
        ) $ \(e::IOException) -> return () -- connection dropped, just exit.

    ) $ \(e::IOException) -> do
      hPutStrLn stderr "Failed to connect to debugger server proxy -- did the debuggee compile and start running successfully?"

-- | Send a 'runInTerminal' reverse request to the DAP client
-- with the @hdb proxy@ invocation
sendRunProxyInTerminal :: PortNumber -> DebugAdaptor ()
sendRunProxyInTerminal port = do
  DAS { entryFile
      , entryPoint
      , entryArgs
      , projectRoot } <- getDebugSession
  let debuggee_inv = T.pack $ makeRelative projectRoot entryFile ++ ":" ++ entryPoint ++
                              (if null entryArgs then "" else " ") ++ unwords entryArgs
  sendRunInTerminalReverseRequest
    RunInTerminalRequestArguments
      { runInTerminalRequestArgumentsKind = Just RunInTerminalRequestArgumentsKindIntegrated
      , runInTerminalRequestArgumentsTitle = Just debuggee_inv
      , runInTerminalRequestArgumentsCwd = ""
      , runInTerminalRequestArgumentsArgs = ["hdb", "proxy", "--port", T.pack (show port)]
      , runInTerminalRequestArgumentsEnv = Just (H.singleton "DEBUGGEE_INVOCATION" debuggee_inv)
      , runInTerminalRequestArgumentsArgsCanBeInterpretedByShell = False
      }