diff --git a/quasar-wayland.cabal b/quasar-wayland.cabal index fa2fb27c90c32f390ed7f48c9dde629c3aa66e42..0553b541fe2c3d2fdb3c260c228d040a01c5233f 100644 --- a/quasar-wayland.cabal +++ b/quasar-wayland.cabal @@ -84,6 +84,7 @@ library import: shared-properties exposed-modules: Quasar.Wayland.Client + Quasar.Wayland.Connection Quasar.Wayland.Core Quasar.Wayland.Protocol Quasar.Wayland.TH diff --git a/src/Quasar/Wayland/Client.hs b/src/Quasar/Wayland/Client.hs index 2851e28ff60c5caa2dfa783a1379c127800c1e56..9e0b4101712fda43d36a882b4f14b22d3327352b 100644 --- a/src/Quasar/Wayland/Client.hs +++ b/src/Quasar/Wayland/Client.hs @@ -5,12 +5,16 @@ module Quasar.Wayland.Client ( import Control.Concurrent.STM import Control.Monad.Catch +import Control.Monad.State (StateT, lift, runStateT, execStateT) +import Data.ByteString qualified as BS +import Data.ByteString.Lazy qualified as BSL import Network.Socket (Socket) import Network.Socket qualified as Socket import Network.Socket.ByteString qualified as Socket import Network.Socket.ByteString.Lazy qualified as SocketL import Quasar import Quasar.Prelude +import Quasar.Wayland.Connection import Quasar.Wayland.Core import Quasar.Wayland.Protocol import System.Environment (getEnv, lookupEnv) @@ -18,67 +22,21 @@ import System.FilePath ((</>), isRelative) import Text.Read (readEither) -data WaylandClient = WaylandClient { - protocolStateVar :: TVar ClientProtocolState, - socket :: Socket, - resourceManager :: ResourceManager -} +data WaylandClient = WaylandClient (WaylandConnection 'Client) instance IsResourceManager WaylandClient where - toResourceManager client = client.resourceManager + toResourceManager (WaylandClient connection) = toResourceManager connection instance IsDisposable WaylandClient where - toDisposable client = toDisposable client.resourceManager + toDisposable (WaylandClient connection) = toDisposable connection newWaylandClient :: MonadResourceManager m => Socket -> m WaylandClient -newWaylandClient socket = do - protocolStateVar <- liftIO $ newTVarIO initialClientProtocolState - resourceManager <- newResourceManager - - onResourceManager resourceManager do - let client = WaylandClient { - protocolStateVar, - socket, - resourceManager - } - - registerDisposeAction $ closeWaylandClient client - - runUnlimitedAsync do - async $ liftIO $ waylandClientSendThread client `catchAll` \ex -> traceIO (displayException ex) >> void (dispose resourceManager) - async $ liftIO $ waylandClientReceiveThread client `catchAll` \ex -> traceIO (displayException ex) >> void (dispose resourceManager) - - pure client - -waylandClientSendThread :: WaylandClient -> IO () -waylandClientSendThread client = forever do - bytes <- atomically do - outbox <- stateTVar client.protocolStateVar takeOutbox - case outbox of - Just bytes -> pure bytes - Nothing -> retry - - traceIO $ "Sending data" - SocketL.sendAll client.socket bytes - - -waylandClientReceiveThread :: WaylandClient -> IO () -waylandClientReceiveThread client = forever do - bytes <- Socket.recv client.socket 4096 - traceIO $ "Received data" - events <- atomically $ stateTVar client.protocolStateVar $ feedInput bytes - - traceIO $ "Received " <> show (length events) <> " events" - mapM_ (traceIO . show) events - - state <- atomically $ readTVar client.protocolStateVar - traceIO $ show state.bytesReceived - -closeWaylandClient :: WaylandClient -> IO (Awaitable ()) -closeWaylandClient client = isDisposed <$> forkTask do - -- gracefulClose may fail but guarantees that the socket is deallocated - Socket.gracefulClose client.socket 2000 `catch` \(_ :: SomeException) -> pure () +newWaylandClient socket = WaylandClient <$> newWaylandConnection wlDisplayCallback socket +wlDisplayCallback :: ClientCallback STM I_wl_display +wlDisplayCallback = Callback { + messageCallback = \_ _ -> lift $ traceM "Callback called" +} connectWaylandClient :: MonadResourceManager m => m WaylandClient connectWaylandClient = mask_ do diff --git a/src/Quasar/Wayland/Connection.hs b/src/Quasar/Wayland/Connection.hs new file mode 100644 index 0000000000000000000000000000000000000000..2ba630effeca6c3d794709b925e61bb12c98fec8 --- /dev/null +++ b/src/Quasar/Wayland/Connection.hs @@ -0,0 +1,95 @@ +module Quasar.Wayland.Connection ( + WaylandConnection, + newWaylandConnection, +) where + +import Control.Concurrent.STM +import Control.Monad.Catch +import Data.ByteString qualified as BS +import Data.ByteString.Lazy qualified as BSL +import Network.Socket (Socket) +import Network.Socket qualified as Socket +import Network.Socket.ByteString qualified as Socket +import Network.Socket.ByteString.Lazy qualified as SocketL +import Quasar +import Quasar.Prelude +import Quasar.Wayland.Core +import Quasar.Wayland.Protocol + + +data WaylandConnection s = WaylandConnection { + protocolStateVar :: TVar (ProtocolState s STM), + outboxVar :: TMVar BSL.ByteString, + socket :: Socket, + resourceManager :: ResourceManager +} + +instance IsResourceManager (WaylandConnection s) where + toResourceManager connection = connection.resourceManager + +instance IsDisposable (WaylandConnection s) where + toDisposable connection = toDisposable connection.resourceManager + +newWaylandConnection :: forall s m. MonadResourceManager m => Callback s STM I_wl_display -> Socket -> m (WaylandConnection s) +newWaylandConnection wlDisplayCallback socket = do + protocolStateVar <- liftIO $ newTVarIO $ initialProtocolState wlDisplayCallback + outboxVar <- liftIO newEmptyTMVarIO + + resourceManager <- newResourceManager + + onResourceManager resourceManager do + let connection = WaylandConnection { + protocolStateVar, + outboxVar, + socket, + resourceManager + } + + registerDisposeAction $ closeConnection connection + + runUnlimitedAsync do + async $ liftIO $ waylandConnectionSendThread connection `catchAll` \ex -> traceIO (displayException ex) >> void (dispose resourceManager) + async $ liftIO $ waylandConnectionReceiveThread connection `catchAll` \ex -> traceIO (displayException ex) >> void (dispose resourceManager) + + -- HACK to send first message (queued internally) + stepProtocol connection $ feedInput "" + + pure connection + +stepProtocol :: forall s m a. MonadIO m => WaylandConnection s -> ProtocolStep s STM a -> m a +stepProtocol connection step = liftIO do + result <- atomically do + oldState <- readTVar connection.protocolStateVar + (result, outBytes, newState) <- step oldState + writeTVar connection.protocolStateVar newState + mapM_ (putTMVar connection.outboxVar) outBytes + pure result + case result of + Left ex -> throwM (ex :: SomeException) + Right result -> pure result + + +waylandConnectionSendThread :: WaylandConnection s -> IO () +waylandConnectionSendThread connection = forever do + bytes <- atomically $ takeTMVar connection.outboxVar + + traceIO $ "Sending data: " <> show (BSL.length bytes) <> " bytes" + SocketL.sendAll connection.socket bytes + + +waylandConnectionReceiveThread :: WaylandConnection s -> IO () +waylandConnectionReceiveThread connection = forever do + bytes <- Socket.recv connection.socket 4096 + + when (BS.length bytes == 0) do + fail "Socket is closed" + + traceIO $ "Received " <> show (BS.length bytes) <> " bytes" + + stepProtocol connection $ feedInput bytes + +closeConnection :: WaylandConnection s -> IO (Awaitable ()) +closeConnection connection = do + -- gracefulClose may fail but guarantees that the socket is deallocated + Socket.close connection.socket `catch` \(_ :: SomeException) -> pure () + pure $ pure ()