154 lines
6.2 KiB
Haskell
154 lines
6.2 KiB
Haskell
module Link.Client where
|
|
|
|
import Control.Concurrent.STM
|
|
import Control.Concurrent
|
|
import Control.Exception hiding (handle)
|
|
import Control.Monad (void, forever, when, unless, forM_)
|
|
import Data.Time (getCurrentTime, diffUTCTime)
|
|
import System.IO (hGetLine, Handle)
|
|
import System.Timeout (timeout)
|
|
import Text.Printf (printf, hPrintf)
|
|
|
|
import qualified Data.Map.Strict as Map
|
|
import qualified Data.Set as Set
|
|
|
|
import Link.Protocol
|
|
import Link.Types
|
|
|
|
sendMessage :: Client -> Message -> STM ()
|
|
sendMessage Client {..} = writeTChan clientChan
|
|
|
|
sendMessageIO :: Client -> Message -> IO ()
|
|
sendMessageIO client = atomically . sendMessage client
|
|
|
|
tellMessage :: Channel -> Message -> STM ()
|
|
tellMessage Channel {..} = writeTChan channelChan
|
|
|
|
printToHandle :: Handle -> String -> IO ()
|
|
printToHandle handle = hPrintf handle "%s\n"
|
|
|
|
sendResponse :: Client -> Message -> IO ()
|
|
sendResponse Client {..} = printToHandle clientHandle . formatMessage
|
|
|
|
runClient :: Server -> Client -> IO ()
|
|
runClient Server {..} client@Client {..} = do
|
|
clientAlive <- newMVar True
|
|
pingThread <- forkIO $ ping clientAlive `finally` killClient clientAlive
|
|
commandThread <- forkIO $ readCommands `finally` killClient clientAlive
|
|
run clientAlive `finally` do
|
|
killThread pingThread
|
|
killThread commandThread
|
|
clientChannelMap <- readTVarIO clientChannelChans
|
|
forM_ (Map.keys clientChannelMap) $ \channelName ->
|
|
handleMessage (Leave channelName) clientAlive
|
|
where
|
|
pingDelay = 120
|
|
pingDelayMicros = pingDelay * 1000 * 1000
|
|
|
|
killClient clientAlive = void $ swapMVar clientAlive False
|
|
|
|
ping clientAlive = do
|
|
sendMessageIO client Ping
|
|
threadDelay pingDelayMicros
|
|
now <- getCurrentTime
|
|
pongTime <- readMVar clientPongTime
|
|
if diffUTCTime now pongTime > fromIntegral pingDelay
|
|
then killClient clientAlive
|
|
else ping clientAlive
|
|
|
|
run clientAlive = do
|
|
alive <- readMVar clientAlive
|
|
if not alive
|
|
then printf "Closing connection: %s\n" (userName clientUser)
|
|
else do
|
|
r <- try . timeout pingDelayMicros . atomically $ do
|
|
clientChannelMap <- readTVar clientChannelChans
|
|
foldr (orElse . readTChan) retry $ clientChan : Map.elems clientChannelMap
|
|
case r of
|
|
Left (e :: SomeException) -> printf "Exception: %s\n" (show e)
|
|
Right g -> do
|
|
case g of
|
|
Nothing -> return ()
|
|
Just message -> handleMessage message clientAlive
|
|
run clientAlive
|
|
|
|
readCommands = forever $ do
|
|
command <- hGetLine clientHandle
|
|
printf "<%s>: %s\n" (userName clientUser) command
|
|
case parseCommand command of
|
|
Nothing -> printf "Could not parse command: %s\n" command
|
|
Just c -> sendMessageIO client c
|
|
|
|
handleMessage (Msg user msg) _ =
|
|
withMVar serverUsers $ \clientMap ->
|
|
case Map.lookup user clientMap of
|
|
Nothing -> sendResponse client $ NoSuchUser (userName user)
|
|
Just client' -> sendMessageIO client' $ MsgReply clientUser msg
|
|
|
|
handleMessage Pong _ = do
|
|
now <- getCurrentTime
|
|
void $ swapMVar clientPongTime now
|
|
|
|
handleMessage Quit clientAlive = killClient clientAlive
|
|
|
|
handleMessage (Join channelName) _ = atomically $ do
|
|
-- get user's channels
|
|
clientChannelMap <- readTVar clientChannelChans
|
|
-- if user has not already joined the channel
|
|
unless (Map.member channelName clientChannelMap) $ do
|
|
-- get server channels
|
|
channelMap <- readTVar serverChannels
|
|
channel@Channel {channelChan} <- case Map.lookup channelName channelMap of
|
|
Just (channel@Channel {channelUsers}) -> do
|
|
-- if the channel already exists on the server, add user to it
|
|
modifyTVar' channelUsers $ Set.insert clientUser
|
|
return channel
|
|
Nothing -> do
|
|
-- else create a new channel with this user in it
|
|
channel <- newChannel channelName $ Set.singleton clientUser
|
|
-- and add it to the server
|
|
modifyTVar' serverChannels $ Map.insert channelName channel
|
|
return channel
|
|
-- duplicate channel TChan for this user
|
|
clientChannelChan <- dupTChan channelChan
|
|
-- and add it to the users's channels
|
|
modifyTVar' clientChannelChans $ Map.insert channelName clientChannelChan
|
|
-- send a JOINED message to the channel for this user
|
|
tellMessage channel $ Joined channelName clientUser
|
|
|
|
handleMessage (Leave channelName) _ = atomically $ do
|
|
-- get server channels
|
|
channelMap <- readTVar serverChannels
|
|
case Map.lookup channelName channelMap of
|
|
-- if channel exists on the server
|
|
Just (channel@Channel {channelUsers}) -> do
|
|
-- remove this user from the channel
|
|
modifyTVar' channelUsers $ Set.delete clientUser
|
|
-- get users in the channel
|
|
users <- readTVar channelUsers
|
|
-- if there are no users in the channel
|
|
when (Set.null users) $
|
|
-- remove the channel from the server
|
|
modifyTVar' serverChannels $ Map.delete channelName
|
|
-- remove the channel from the user's channels
|
|
modifyTVar' clientChannelChans $ Map.delete channelName
|
|
-- send a LEFT message to the channel for this user
|
|
tellMessage channel $ Leaved channelName clientUser
|
|
-- nothing to do if the channel does not exist
|
|
Nothing -> return ()
|
|
|
|
handleMessage (Names channelName) _ = atomically $ do
|
|
channelMap <- readTVar serverChannels
|
|
users <- case Map.lookup channelName channelMap of
|
|
Just (Channel {channelUsers}) -> readTVar channelUsers
|
|
Nothing -> return Set.empty
|
|
sendMessage client $ NamesReply channelName users
|
|
|
|
handleMessage (Tell channelName msg) _ = atomically $ do
|
|
channelMap <- readTVar serverChannels
|
|
case Map.lookup channelName channelMap of
|
|
Just channel -> tellMessage channel $ TellReply channelName clientUser msg
|
|
Nothing -> return ()
|
|
|
|
handleMessage message _ = sendResponse client message
|