-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathMain.hs
More file actions
156 lines (133 loc) · 5.01 KB
/
Main.hs
File metadata and controls
156 lines (133 loc) · 5.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
{-# LANGUAGE RecordWildCards #-}
import Prelude hiding (id)
import Control.Applicative
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception (bracket_, finally)
import Control.Monad (forM_, forever, join)
import Data.Int (Int64)
import Data.Map (Map)
import Network
import System.IO
import qualified Data.Foldable as F
import qualified Data.Map as Map
type ClientId = Int64
type ClientName = String
data Message = Notice String
| MessageFrom ClientName String
data Server = Server
{ serverClients :: TVar (Map ClientId Client)
, serverClientsByName :: TVar (Map ClientName Client)
}
initServer :: IO Server
initServer =
Server <$> newTVarIO Map.empty
<*> newTVarIO Map.empty
data Client = Client
{ clientId :: ClientId
, clientName :: ClientName
, clientHandle :: Handle
, clientSendChan :: TChan Message
, clientKicked :: TVar (Maybe String)
}
instance Eq Client where
a == b = clientId a == clientId b
initClient :: ClientId -> ClientName -> Handle -> IO Client
initClient id name handle =
Client <$> return id
<*> return name
<*> return handle
<*> newTChanIO
<*> newTVarIO Nothing
broadcast :: Server -> Message -> STM ()
broadcast Server{..} msg =
readTVar serverClients >>= F.mapM_ (\client -> sendMessage client msg)
sendMessage :: Client -> Message -> STM ()
sendMessage Client{..} msg =
writeTChan clientSendChan msg
kickClient :: Client -> String -> STM ()
kickClient Client{..} reason =
writeTVar clientKicked $ Just reason
serve :: Server -> ClientId -> Handle -> IO ()
serve server@Server{..} id handle = do
hSetNewlineMode handle universalNewlineMode
-- Swallow carriage returns sent by telnet clients
hSetBuffering handle LineBuffering
hPutStrLn handle "What is your name?"
name <- hGetLine handle
if null name
then hPutStrLn handle "Bye, anonymous coward"
else do
client <- initClient id name handle
bracket_ (atomically $ insertClient server client)
(atomically $ deleteClient server client)
(serveLoop server client)
-- | Register the client with the server. If another client with the same name
-- is connected already, kick it.
insertClient :: Server -> Client -> STM ()
insertClient server@Server{..}
client@Client{..} = do
modifyTVar' serverClients $ Map.insert clientId client
m <- readTVar serverClientsByName
writeTVar serverClientsByName $! Map.insert clientName client m
case Map.lookup clientName m of
Nothing ->
broadcast server $ Notice $
clientName ++ " has connected"
Just victim -> do
broadcast server $ Notice $
clientName ++ " has connected (kicking previous client)"
kickClient victim $
"Another client by the name of " ++ clientName ++ " has connected"
-- | Unregister the client.
deleteClient :: Server -> Client -> STM ()
deleteClient server@Server{..}
client@Client{..} = do
modifyTVar' serverClients $ Map.delete clientId
m <- readTVar serverClientsByName
case Map.lookup clientName m of
-- Make sure the client in the map is actually me, and not another
-- client who took my name.
Just c | c == client -> do
broadcast server $ Notice $ clientName ++ " has disconnected"
writeTVar serverClientsByName $! Map.delete clientName m
_ ->
return ()
-- | Handle client I/O.
serveLoop :: Server -> Client -> IO ()
serveLoop server@Server{..}
client@Client{..} = do
done <- newEmptyMVar
let spawnWorker io = forkIO (io `finally` tryPutMVar done ())
recv_tid <- spawnWorker $ forever $ do
msg <- hGetLine clientHandle
atomically $ broadcast server $ MessageFrom clientName msg
send_tid <- spawnWorker $
let loop = join $ atomically $ do
k <- readTVar clientKicked
case k of
Just reason -> return $
hPutStrLn clientHandle $ "You have been kicked: " ++ reason
Nothing -> do
msg <- readTChan clientSendChan
return $ do
handleMessage client msg
loop
in loop
takeMVar done
mapM_ killThread [recv_tid, send_tid]
handleMessage :: Client -> Message -> IO ()
handleMessage Client{..} message =
hPutStrLn clientHandle $
case message of
Notice msg -> "* " ++ msg
MessageFrom name msg -> "<" ++ name ++ ">: " ++ msg
main :: IO ()
main = do
server <- initServer
sock <- listenOn $ PortNumber 1234
putStrLn "Listening on port 1234"
forM_ [1..] $ \id -> do
(handle, host, port) <- accept sock
putStrLn $ "Accepted connection from " ++ host ++ ":" ++ show port
forkIO $ serve server id handle `finally` hClose handle