{-# LANGUAGE BangPatterns , LambdaCase , OverloadedStrings , ViewPatterns , NoIncoherentInstances , NoMonomorphismRestriction , NoUndecidableInstances #-} module Vivid.SCServer.Connection ( createSCServerConnection' , defaultConnectConfig , defaultMessageFunction , ignoreMessagesFunction , SCConnectConfig(..) , closeSCServerConnection' , ConnProtocol(..) , getMailboxForSyncId' , getSCServerSocket' , waitForSync_io' , waitForSync_io_noGC' , sendMaybePad -- Exported at least for 'Vivid.GlobalState': , startMailbox' , connectToSCServer' -- for tests: , splitMessagesFromChunk ) where import Vivid.SC.Server.Commands as SCCmd import Vivid.OSC import Vivid.OSC.Bundles (initTreeCommand) import Vivid.SCServer.State import Network.Socket ( SocketType(Datagram , Stream), defaultProtocol, socket , AddrInfo(..), getAddrInfo -- , AddrInfoFlag(..), , defaultHints , Socket, HostName, ServiceName, connect, close -- , listen, bind -- , bindSocket, accept , accept, listen -- We put this everywhere we do socket actions for Windows compatibility: , withSocketsDo ) import Network.Socket.ByteString (sendAll, recv) import Control.Concurrent (forkIO, ThreadId, killThread) import Control.Concurrent.MVar import Control.Concurrent.STM import Control.Monad (forever) import Data.ByteString (ByteString) import qualified Data.ByteString as BS import Data.Int (Int32) import qualified Data.Map as Map import Data.Monoid import Data.Serialize (encode, decode) -- | __You usually don't need to call this function__ -- -- Use this if to connect on a non-default port or to a server not at localhost -- -- Otherwise the connection is created when it's needed. -- You can also use this to explicitly create the connection, so the -- computation is done upfront -- -- The 'HostName' is the ip address or "localhost". The 'ServiceName' is the port createSCServerConnection' :: SCServerState -> SCConnectConfig -> IO (Either String Socket) createSCServerConnection' serverState connConfig = do let !_ = serverState shouldMakeSock serverState >>= \case True -> makeSock serverState connConfig >>= \case Just s -> pure $ Right s Nothing -> pure $ Left "Unable to create socket" False -> pure $ Left "Too late -- connection already established. Disconnect first." -- | Explicitly close Vivid's connection to a SC server. -- -- Day-to-day, you can usually just let your program run without using this. -- -- For example though, if you're running code that uses Vivid in ghci, and -- you ":r", you'll want to disconnect first -- there are processes running -- which can step on the toes of your new instance -- (TODO: this isn't fully true - I ":r" all the time - what do I mean here?) -- -- Also if you want to change the params of your connection (e.g. to connect -- to a different server), you'll want to disconnect from the other -- connection first closeSCServerConnection' :: SCServerState -> IO () closeSCServerConnection' serverState = do let !_ = serverState ish <- atomically $ do writeTVar (_scServerState_socketConnectStarted serverState) False {- (,) <$> swapTVar (_scServerState_socket serverState) Nothing <*> swapTVar (_scServerState_listener serverState) Nothing -} (,) <$> tryTakeTMVar (_scServerState_socket serverState) <*> tryTakeTMVar (_scServerState_listener serverState) case ish of (Just sock, Just listener) -> do killThread listener withSocketsDo $ close sock (Nothing, Nothing) -> pure () _ -> error "well that's weird" data SCConnectConfig = SCConnectConfig { _scConnectConfig_hostName :: HostName , _scConnectConfig_port :: ServiceName , _scConnectConfig_clientId :: Int32 -- ^ To prevent NodeId clashes when multiple clients are connected to -- the same server, each client should have a separate clientId, which -- keeps the nodeId separate. Sclang's default clientId is 0, and ours -- is 1, so you can run both at the same time without config. -- This number must be between 0 and 31 , _scConnectConfig_connProtocol :: ConnProtocol , _scConnectConfig_password :: Maybe ByteString -- TODO: I don't like that e.g. a password could be provided with UDP, which doesn't use them , _scConnectConfig_serverMessageFunction :: OSC -> IO () -- max # of synthdefs -- and clear em out } -- deriving (Show, Read, Eq) -- | The default _scConnectConfig_clientId is 1, and sclang's is 0, so you should -- be able to run vivid side-by-side with the SC IDE out of the box. defaultConnectConfig :: SCConnectConfig defaultConnectConfig = SCConnectConfig { _scConnectConfig_hostName = "127.0.0.1" , _scConnectConfig_port = "57110" , _scConnectConfig_clientId = 1 , _scConnectConfig_connProtocol = ConnProtocol_UDP -- ConnProtocol_TCP -- UDP -- TODO: not for release! , _scConnectConfig_password = Nothing , _scConnectConfig_serverMessageFunction = defaultMessageFunction } -- Internal -- this is what gets called after we check a socket doesn't -- already exist: connectToSCServer' :: SCServerState -> SCConnectConfig -> IO (Socket, ThreadId) connectToSCServer' serverState scConnectConfig = withSocketsDo $ do let !_ = serverState let hostName = _scConnectConfig_hostName scConnectConfig port = _scConnectConfig_port scConnectConfig connType = case _scConnectConfig_connProtocol scConnectConfig of ConnProtocol_UDP -> Datagram ConnProtocol_TCP -> Stream (serverAddr:_) <- getAddrInfo (Just (defaultHints { addrSocketType = connType })) {- (Just (defaultHints {addrFlags = [AI_PASSIVE]})) -} (Just hostName) (Just port) s <- socket (addrFamily serverAddr) connType (addrProtocol serverAddr) -- defaultProtocol setServerClientId serverState (_scConnectConfig_clientId scConnectConfig) connect s (addrAddress serverAddr) atomically $ do writeTVar (_scServerState_serverMessageFunction serverState) $ _scConnectConfig_serverMessageFunction scConnectConfig writeTVar (_scServerState_connProtocol serverState) $ _scConnectConfig_connProtocol scConnectConfig case _scConnectConfig_password scConnectConfig of Nothing -> pure () -- This must be the very first message sent to the server: Just password -> sendAll {- MaybePad (_scConnectConfig_connProtocol scConnectConfig) -} s $ password listener <- forkIO $ startMailbox' serverState s let firstSyncID = toEnum $ numberOfSyncIdsToDrop - 2 _ <- sendMaybePad (_scConnectConfig_connProtocol scConnectConfig) s $ encodeOSCBundle $ OSCBundle (Timestamp 0) [ Right $ SCCmd.dumpOSC DumpOSC_Parsed , Right $ initTreeCommand , Right $ SCCmd.sync (SyncId firstSyncID) ] waitForSync_io' serverState (SyncId firstSyncID) pure (s, listener) -- ALWAYS use this instead of 'send': sendMaybePad :: ConnProtocol -> Socket -> ByteString -> IO () sendMaybePad connProtocol socket msg_noPad = withSocketsDo $ sendAll socket msg -- TODO: send vs sendAll? Makes a difference? where msg :: ByteString msg = case connProtocol of ConnProtocol_UDP -> msg_noPad ConnProtocol_TCP -> (encode (toEnum (BS.length msg_noPad) :: Int32))<>msg_noPad waitForSync_io' :: SCServerState -> SyncId -> IO () waitForSync_io' serverState syncId = do _ <- readMVar =<< getMailboxForSyncId' serverState syncId -- We garbage-collect these so the Map stays small -- but it means you can only wait -- for a sync from one place: atomically $ modifyTVar' (_scServerState_syncIdMailboxes serverState) $ Map.delete syncId waitForSync_io_noGC' :: SCServerState -> SyncId -> IO () waitForSync_io_noGC' serverState syncId = do _ <- readMVar =<< getMailboxForSyncId' serverState syncId pure () -- TODO: what's "mailbox" here? Is it like an Erlang mailbox, to receive and -- dispatch all messages? startMailbox' :: SCServerState -> Socket -> IO () startMailbox' serverState s = do let !_ = serverState connProtocol <- readTVarIO $ _scServerState_connProtocol serverState :: IO ConnProtocol -- note we only read this once at the beginning. If this can change throughout program run, update this code forever $ recv {- From -} s 65536 >>= \(msg_maybePad {- , _ -}) -> do case connProtocol of ConnProtocol_UDP -> handleMsg msg_maybePad ConnProtocol_TCP -> do case splitMessagesFromChunk msg_maybePad of -- From the 'network' docs: "For TCP sockets, a zero length return value means the peer has closed its half side of the connection." Right [] -> errorWithoutStackTrace "Connection to the SC server lost. Probably the server has quit or the network is down. You will need to reconnect." Left err -> putStrLn err Right msgs -> mapM_ handleMsg msgs where handleMsg :: ByteString -> IO () handleMsg msg = do case decodeOSC msg of Right (OSC "/synced" [OSC_I theSyncId]) -> do syncBox <- getMailboxForSyncId' serverState (SyncId theSyncId) tryPutMVar syncBox () >>= \case True -> pure () False -> putStrLn $ "That's weird!: we got the same syncId twice: " ++ show theSyncId Right x -> do otherMessageFunction <- readTVarIO $ _scServerState_serverMessageFunction serverState otherMessageFunction x Left e -> do putStrLn $ "ERROR DECODING OSC: " ++ show (msg, e) -- There can be more than one message that we recieve as one binary blob: splitMessagesFromChunk :: ByteString -> Either String [ByteString] splitMessagesFromChunk b = case BS.splitAt 4 b of ("", "") -> Right [] (sizeBS, rest) -> case decode sizeBS :: Either String Int32 of Left e -> Left $ "ERROR DECODING OSC PAD: "++show b Right (fromEnum -> size) -> let (thisChunk, nextBS) = BS.splitAt size rest in case BS.length thisChunk == size of False -> Left $ "INCORRECT SIZE: "++show b True -> case splitMessagesFromChunk nextBS of Left e -> Left e Right allRest -> Right (thisChunk : allRest) -- | Print all messages other than \"/done\"s defaultMessageFunction :: OSC -> IO () defaultMessageFunction = \case -- Some examples you might want to handle individually: {- OSC "/fail" [OSC_S "/blah", OSC_S "Command not found"] -> pure () OSC "/fail" [OSC_S "/s_new", OSC_S "wrong argument type"] -> pure () OSC "/fail" [OSC_S "/b_allocRead", OSC_S "File 'blah.ogg' could not be opened: Error : flac decoder lost sync.\n",OSC_I 2] -} OSC "/done" [OSC_S _] -> pure () OSC "/done" [OSC_S _, OSC_I _] -> pure () x -> putStrLn $ "Msg from server: " <> show x -- | If you don't want to hear what the server has to say ignoreMessagesFunction :: OSC -> IO () ignoreMessagesFunction _ = pure () -- This is a nice example of when STM can be really helpful - -- It's impossible! (right?) to have 2 threads create mailboxes and have em overwrite each -- other -- so we can make a guarantee about recieving a sync that you register for getMailboxForSyncId' :: SCServerState -> SyncId -> IO (MVar ()) getMailboxForSyncId' serverState syncId = do mvarThatIMightWannaUse <- newEmptyMVar -- TODO: make this a TMVar so it doesn't have to sit outside like this? m <- atomically $ do allMailboxes <- readTVar (_scServerState_syncIdMailboxes serverState) case Map.lookup syncId allMailboxes of Just syncBox -> pure syncBox Nothing -> do writeTVar (_scServerState_syncIdMailboxes serverState) (Map.insert syncId mvarThatIMightWannaUse allMailboxes) pure mvarThatIMightWannaUse pure m getSCServerSocket' :: SCServerState -> IO Socket getSCServerSocket' scServerState' = do let !_ = scServerState' shouldMakeSock scServerState' >>= \case True -> do makeSock scServerState' defaultConnectConfig >>= \case Just x -> pure x Nothing -> error "Unexpected failure creating socket" False -> atomically . readTMVar $ _scServerState_socket scServerState' shouldMakeSock :: SCServerState -> IO Bool shouldMakeSock serverState = atomically $ do let theVar = _scServerState_socketConnectStarted serverState alreadyBeingMade <- readTVar theVar case alreadyBeingMade of True -> pure False False -> do writeTVar theVar True pure True makeSock :: SCServerState -> SCConnectConfig -> IO (Maybe Socket) makeSock serverState connConfig = do (sock, listener) <- connectToSCServer' serverState connConfig atomically $ (do -- writeTVar (_scServerState_socket serverState) $ Just sock -- writeTVar (_scServerState_listener serverState) $ Just listener a <- tryPutTMVar (_scServerState_socket serverState) sock b <- tryPutTMVar (_scServerState_listener serverState) listener check $ a && b pure $ Just sock) `orElse` (pure Nothing)