dap-0.1.0.0: src/DAP/Server.hs
-----------------------------------------------------------------------------
-- |
-- Module : DAP.Server
-- Copyright : (C) 2023 David M. Johnson
-- License : BSD3-style (see the file LICENSE)
-- Stability : experimental
-- Portability : non-portable
----------------------------------------------------------------------------
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
----------------------------------------------------------------------------
module DAP.Server
( runDAPServer
, readPayload
) where
----------------------------------------------------------------------------
import Control.Concurrent.MVar ( MVar )
import Control.Monad ( when )
import Control.Concurrent.MVar ( newMVar, newEmptyMVar, modifyMVar_
, putMVar, readMVar )
import Control.Concurrent.STM ( newTVarIO )
import Control.Exception ( SomeException
, IOException
, catch
, fromException
, throwIO )
import Control.Monad ( void )
import Control.Monad.State ( gets )
import Data.Aeson ( decodeStrict, eitherDecode, Value, FromJSON )
import Data.Aeson.Encode.Pretty ( encodePretty )
import Data.ByteString ( ByteString )
import Data.Char ( isDigit )
import Network.Simple.TCP ( serve, HostPreference(Host) )
import Network.Socket ( socketToHandle, withSocketsDo, SockAddr )
import System.IO ( hClose, hSetNewlineMode, Handle, Newline(CRLF)
, NewlineMode(NewlineMode, outputNL, inputNL)
, IOMode(ReadWriteMode) )
import System.IO.Error ( isEOFError )
import Text.Read ( readMaybe )
import qualified Data.ByteString.Lazy.Char8 as BL8
import qualified Data.ByteString.Char8 as BS
----------------------------------------------------------------------------
import DAP.Types
import DAP.Internal
import DAP.Utils
import DAP.Adaptor
----------------------------------------------------------------------------
runDAPServer
:: ServerConfig
-- ^ Top-level Server configuration, global across all debug sessions
-> (Command -> Adaptor app ())
-- ^ A function to facilitate communication between DAP clients, debug adaptors and debuggers
-> IO ()
runDAPServer serverConfig@ServerConfig {..} communicate = withSocketsDo $ do
when debugLogging $ putStrLn ("Running DAP server on " <> show port <> "...")
appStore <- newTVarIO mempty
serve (Host host) (show port) $ \(socket, address) -> do
when debugLogging $ do
withGlobalLock $ do
putStrLn $ "TCP connection established from " ++ show address
handle <- socketToHandle socket ReadWriteMode
hSetNewlineMode handle NewlineMode { inputNL = CRLF, outputNL = CRLF }
request <- getRequest handle address serverConfig
adaptorStateMVar <- initAdaptorState handle address appStore serverConfig request
serviceClient communicate adaptorStateMVar `catch` exceptionHandler handle address debugLogging
-- | Initializes the Adaptor
--
initAdaptorState
:: Handle
-> SockAddr
-> AppStore app
-> ServerConfig
-> Request
-> IO (MVar (AdaptorState app))
initAdaptorState handle address appStore serverConfig request = do
handleLock <- newMVar ()
sessionId <- pure Nothing
adaptorStateMVar <- newEmptyMVar
putMVar adaptorStateMVar AdaptorState
{ messageType = MessageTypeResponse
, payload = []
, ..
}
pure adaptorStateMVar
----------------------------------------------------------------------------
-- | Communication loop between editor and adaptor
-- Evaluates the current 'Request' located in the 'AdaptorState'
-- Fetches, updates and recurses on the next 'Request'
--
serviceClient
:: (Command -> Adaptor app ())
-> MVar (AdaptorState app)
-> IO ()
serviceClient communicate adaptorStateMVar = do
runAdaptorWith adaptorStateMVar $ do
request <- gets request
communicate (command request)
-- HINT: getRequest is a blocking action so we use readMVar to leave MVar available
AdaptorState { address, handle, serverConfig } <- readMVar adaptorStateMVar
nextRequest <- getRequest handle address serverConfig
modifyMVar_ adaptorStateMVar $ \s -> pure s { request = nextRequest }
-- loop: serve the next request
serviceClient communicate adaptorStateMVar
----------------------------------------------------------------------------
-- | Handle exceptions from client threads, parse and log accordingly
exceptionHandler :: Handle -> SockAddr -> Bool -> SomeException -> IO ()
exceptionHandler handle address shouldLog (e :: SomeException) = do
let
dumpError
| Just (ParseException msg) <- fromException e
= logger ERROR address Nothing
$ withBraces
$ BL8.pack ("Parse Exception encountered: " <> msg)
| Just (err :: IOException) <- fromException e, isEOFError err
= logger INFO address (Just SENT)
$ withBraces "Client has ended its connection"
| otherwise
= logger ERROR address Nothing
$ withBraces
$ BL8.pack ("Unknown Exception: " <> show e)
when shouldLog $ do
dumpError
logger INFO address (Just SENT) (withBraces "Closing Connection")
hClose handle
----------------------------------------------------------------------------
-- | Internal function for parsing a 'ProtocolMessage' header
-- This function also dispatches on 'talk'
--
-- 'parseHeader' Attempts to parse 'Content-Length: <byte-count>'
-- Helper function for parsing message headers
-- e.g. ("Content-Length: 11\r\n")
getRequest :: Handle -> SockAddr -> ServerConfig -> IO Request
getRequest handle addr ServerConfig {..} = do
headerBytes <- BS.hGetLine handle
void (BS.hGetLine handle)
parseHeader headerBytes >>= \case
Left errorMessage -> do
logger ERROR addr Nothing (BL8.pack errorMessage)
throwIO (ParseException errorMessage)
Right count -> do
body <- BS.hGet handle count
when debugLogging $ do
logger DEBUG addr (Just RECEIVED)
("\n" <> encodePretty (decodeStrict body :: Maybe Value))
case eitherDecode (BL8.fromStrict body) of
Left couldn'tDecodeBody -> do
logger ERROR addr Nothing (BL8.pack couldn'tDecodeBody)
throwIO (ParseException couldn'tDecodeBody)
Right request ->
pure request
----------------------------------------------------------------------------
-- | Parses the HeaderPart of all ProtocolMessages
parseHeader :: ByteString -> IO (Either String PayloadSize)
parseHeader bytes = do
let byteSize = BS.takeWhile isDigit (BS.drop (BS.length "Content-Length: ") bytes)
case readMaybe (BS.unpack byteSize) of
Just contentLength ->
pure (Right contentLength)
Nothing ->
pure $ Left ("Invalid payload: " <> BS.unpack bytes)
-- | Helper function to parse a 'ProtocolMessage', extracting it's body.
-- used for testing.
--
readPayload :: FromJSON json => Handle -> IO (Either String json)
readPayload handle = do
headerBytes <- BS.hGetLine handle
void (BS.hGetLine handle)
parseHeader headerBytes >>= \case
Left e -> pure (Left e)
Right count -> do
body <- BS.hGet handle count
pure $ eitherDecode (BL8.fromStrict body)