vivid-0.5.0.0: Vivid/SCServer/Connection.hs
{-# 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)