network-transport-quic-0.1.2: src/Network/Transport/QUIC/Internal/Client.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Network.Transport.QUIC.Internal.Client (
streamToEndpoint,
)
where
import Control.Concurrent (forkIOWithUnmask, newEmptyMVar)
import Control.Concurrent.Async (withAsync)
import Control.Concurrent.MVar (MVar, putMVar, takeMVar, tryPutMVar)
import Control.Exception (SomeException, bracket, catch, finally, mask, mask_, throwIO)
import Data.List.NonEmpty (NonEmpty)
import Network.QUIC qualified as QUIC
import Network.QUIC.Client qualified as QUIC.Client
import Network.Transport (ConnectErrorCode (ConnectNotFound), EndPointAddress, TransportError (..))
import Network.Transport.QUIC.Internal.Configuration (Credential, mkClientConfig)
import Network.Transport.QUIC.Internal.Messaging (MessageReceived (..), handshake, receiveMessage)
import Network.Transport.QUIC.Internal.QUICAddr (QUICAddr (QUICAddr), decodeQUICAddr)
streamToEndpoint ::
NonEmpty Credential ->
-- | Validate credentials
Bool ->
-- | Our address
EndPointAddress ->
-- | Their address
EndPointAddress ->
-- | Called when the QUIC connection or stream ends without us having initiated the
-- close. Must be idempotent (the caller typically gates on remote endpoint state so
-- that repeated invocations are safe) — this handler is invoked from multiple sites
-- (peer-initiated close signal, QUIC.Client.run exception, thread finally) to cover
-- every termination path.
IO () ->
IO
( Either
(TransportError ConnectErrorCode)
( MVar ()
, -- \^ put '()' to close the stream
QUIC.Stream
)
)
streamToEndpoint creds validateCreds ourAddress theirAddress onConnLoss =
case decodeQUICAddr theirAddress of
Left errmsg -> pure $ Left (TransportError ConnectNotFound errmsg)
Right (QUICAddr hostname servicename _) -> do
clientConfig <- mkClientConfig hostname servicename creds validateCreds
streamMVar <- newEmptyMVar
doneMVar <- newEmptyMVar
let runClient :: QUIC.Connection -> IO ()
runClient conn = mask $ \restore -> do
QUIC.waitEstablished conn
restore $
bracket (QUIC.stream conn) QUIC.closeStream $ \stream -> do
handshake (ourAddress, theirAddress) stream
>>= either
(\_ -> putMVar streamMVar (Left $ TransportError ConnectNotFound "handshake failed"))
(\_ -> putMVar streamMVar (Right stream))
withAsync (listenForClose stream doneMVar) $ \_ ->
takeMVar doneMVar
_ <- mask_ $
forkIOWithUnmask $
\unmask ->
catch
( unmask $
QUIC.Client.run
clientConfig
( \conn ->
catch
(runClient conn)
(throwIO @SomeException)
)
)
(\(_ :: SomeException) -> pure ())
`finally` onConnLoss
streamOrError <- takeMVar streamMVar
pure $ (doneMVar,) <$> streamOrError
where
listenForClose :: QUIC.Stream -> MVar () -> IO ()
listenForClose stream doneMVar =
receiveMessage stream
>>= \case
-- Any message from the peer on this stream means we're done listening.
-- Peer-initiated closes (StreamClosed/CloseEndPoint) additionally call
-- onConnLoss; the idempotent gate in the handler dedupes with the finally
-- that also fires on QUIC.Client.run exit.
--
-- Mask signalling+onConnLoss as an atomic pair: tryPutMVar unblocks
-- runClient's takeMVar, which causes withAsync to cancel this thread.
-- Without mask, the async ThreadKilled can fire partway through
-- onConnLoss, dropping the ErrorEvent. The finally in the parent thread
-- is a backup but cannot recover if surfaceConnectionLost already
-- transitioned the remote state to Closed.
Right StreamClosed -> mask_ $ do
_ <- tryPutMVar doneMVar ()
onConnLoss
Right CloseConnection ->
-- Peer closed the logical connection cleanly; no ErrorEvent.
() <$ tryPutMVar doneMVar ()
Right CloseEndPoint -> mask_ $ do
_ <- tryPutMVar doneMVar ()
onConnLoss
other -> throwIO . userError $ "Unexpected incoming message to client: " <> show other