-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdevel.hs
118 lines (103 loc) · 4.11 KB
/
devel.hs
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
{-# LANGUAGE OverloadedStrings, PackageImports, TemplateHaskell #-}
import "websocktest" Application (getApplicationDev)
import Network.Wai.Handler.Warp
(runSettings, defaultSettings, settingsPort, settingsIntercept)
import Control.Concurrent (forkIO)
import System.Directory (doesFileExist, removeFile)
import System.Exit (exitSuccess)
import Control.Concurrent (threadDelay)
import Data.Char (isPunctuation, isSpace)
import Data.Monoid (mappend)
import Data.Text (Text)
import Control.Exception (fromException)
import Control.Monad (forM_)
import Control.Concurrent (MVar, newMVar, modifyMVar_, readMVar)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Network.WebSockets as WS
import qualified Network.Wai
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai.Handler.WebSockets as WaiWS
import qualified Network.Wai.Application.Static as Static
import Data.FileEmbed (embedDir)
type Client = (Text, WS.Sink WS.Hybi10)
type ServerState = [Client]
newServerState :: ServerState
newServerState = []
numClients :: ServerState -> Int
numClients = length
clientExists :: Client -> ServerState -> Bool
clientExists client = any ((== fst client) . fst)
addClient :: Client -> ServerState -> ServerState
addClient client clients = client : clients
removeClient :: Client -> ServerState -> ServerState
removeClient client = filter ((/= fst client) . fst)
broadcast :: Text -> ServerState -> IO ()
broadcast message clients = do
T.putStrLn message
forM_ clients $ \(_, sink) -> WS.sendSink sink $ WS.textData message
staticApp :: Network.Wai.Application
staticApp = Static.staticApp Static.defaultFileServerSettings
{ Static.ssFolder = Static.embeddedLookup $ Static.toEmbedded $(embedDir "static")
}
application :: MVar ServerState -> WS.Request -> WS.WebSockets WS.Hybi10 ()
application state rq = do
WS.acceptRequest rq
WS.getVersion >>= liftIO . putStrLn . ("Client version: " ++)
sink <- WS.getSink
msg <- WS.receiveData
liftIO $ print msg
clients <- liftIO $ readMVar state
case msg of
_ | not (prefix `T.isPrefixOf` msg) ->
WS.sendTextData ("Wrong announcement" :: Text)
| any ($ fst client)
[T.null, T.any isPunctuation, T.any isSpace] ->
WS.sendTextData ("Name cannot " `mappend`
"contain punctuation or whitespace, and " `mappend`
"cannot be empty" :: Text)
| clientExists client clients ->
WS.sendTextData ("User already exists" :: Text)
| otherwise -> do
liftIO $ modifyMVar_ state $ \s -> do
let s' = addClient client s
WS.sendSink sink $ WS.textData $
"Welcome! Users: " `mappend`
T.intercalate ", " (map fst s)
broadcast (fst client `mappend` " joined") s'
return s'
talk state client
where
prefix = "Hi! I am "
client = (T.drop (T.length prefix) msg, sink)
talk :: WS.Protocol p => MVar ServerState -> Client -> WS.WebSockets p ()
talk state client@(user, _) = flip WS.catchWsError catchDisconnect $ do
msg <- WS.receiveData
liftIO $ readMVar state >>= broadcast
(user `mappend` ": " `mappend` msg)
talk state client
where
catchDisconnect e = case fromException e of
Just WS.ConnectionClosed -> liftIO $ modifyMVar_ state $ \s -> do
let s' = removeClient client s
broadcast (user `mappend` " disconnected") s'
return s'
_ -> return ()
main :: IO ()
main = do
putStrLn "Starting devel application"
(port, app) <- getApplicationDev
state <- newMVar newServerState
forkIO $ runSettings defaultSettings
{ settingsPort = port
, settingsIntercept = WaiWS.intercept (application state)
} app
loop
loop :: IO ()
loop = do
threadDelay 100000
e <- doesFileExist "dist/devel-terminate"
if e then terminateDevel else loop
terminateDevel :: IO ()
terminateDevel = exitSuccess