reanimate-1.1.3.0: src/Reanimate/Driver/Server.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Reanimate.Driver.Server
( daemon
) where
import Control.Concurrent
import Control.Exception (finally)
import qualified Control.Exception as E
import Control.Monad (forM_, forever, unless, void, when)
import qualified Data.ByteString.Char8 as BS
import qualified Data.Foldable as F
import qualified Data.Map as Map
import qualified Data.Text as T
import Network.Socket (AddrInfo (..), AddrInfoFlag (..), SocketOption (..),
SocketType (Stream), accept, bind, close, defaultHints,
getAddrInfo, gracefulClose, listen, socket,
setCloseOnExecIfNeeded, setSocketOption, withFdSocket,
withSocketsDo)
import Network.Socket.ByteString (recv)
import Network.WebSockets
import Paths_reanimate (getDataFileName)
import System.IO (hPutStrLn, stderr)
import Web.Browser (openBrowser)
opts :: ConnectionOptions
opts = defaultConnectionOptions
{ connectionCompressionOptions = PermessageDeflateCompression defaultPermessageDeflate }
daemon :: IO ()
daemon = do
state <- newMVar (0, Map.empty)
connsRef <- newMVar Map.empty
self <- myThreadId
dTid <- daemonReceive self $ \msg ->
case msg of
WebStatus _status -> return ()
WebError _err -> return ()
WebFrameCount count -> do
void $ swapMVar state (count, Map.empty)
conns <- readMVar connsRef
F.forM_ conns $ \(conn) -> do
sendWebMessage conn (WebFrameCount count)
when (Map.null conns) openViewer
WebFrame nth path -> do
modifyMVar_ state $ \(count, frames) ->
pure (count, Map.insert nth path frames)
conns <- readMVar connsRef
F.forM_ conns $ \conn -> do
sendWebMessage conn (WebFrame nth path)
openViewer
let options = ServerOptions
{ serverHost = "127.0.0.1"
, serverPort = 9161
, serverConnectionOptions = opts
, serverRequirePong = Nothing }
runServerWithOptions options (\pending -> do
tid <- myThreadId
conn <- acceptRequest pending
modifyMVar_ connsRef $ pure . Map.insert tid conn
(count, frames) <- readMVar state
when (count > 0) $ do
sendWebMessage conn (WebFrameCount count)
forM_ (Map.toList frames) $ \(nth, path) ->
sendWebMessage conn (WebFrame nth path)
let loop = do
-- FIXME: We don't use msg here.
_msg <- receiveData conn :: IO T.Text
loop
cleanup = do
modifyMVar_ connsRef $ pure . Map.delete tid
nConns <- Map.size <$> readMVar connsRef
when (nConns == 0) $ do
threadDelay (second * 5)
nConns' <- Map.size <$> readMVar connsRef
when (nConns'==0) $ killThread self
loop `finally` cleanup)
`finally` (killThread dTid)
second :: Int
second = 10^(6::Int)
daemonReceive :: ThreadId -> (WebMessage -> IO ()) -> IO ThreadId
daemonReceive parent cb = withSocketsDo $ do
addr <- resolve
sock <- open addr
forkIO $ handler sock `finally` close sock
where
handler sock = forever $ E.bracketOnError (accept sock) (close . fst) $ \(conn, _peer) -> do
inp <- BS.unpack <$> recv conn 4096
case words inp of
["frame_count", n] -> cb $ WebFrameCount (read n)
["frame", nth, path] -> cb $ WebFrame (read nth) path
["stop"] -> killThread parent
[] -> return ()
_ -> error $ "Bad message: " ++ inp
gracefulClose conn 5000
resolve = do
let hints = defaultHints {
addrFlags = [AI_PASSIVE]
, addrSocketType = Stream
}
head <$> getAddrInfo (Just hints) (Just "127.0.0.1") (Just "9162")
oSocket addr = socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
open addr = E.bracketOnError (oSocket addr) close $ \sock -> do
setSocketOption sock ReuseAddr 1
withFdSocket sock setCloseOnExecIfNeeded
bind sock $ addrAddress addr
listen sock 1024
return sock
openViewer :: IO ()
openViewer = do
url <- getDataFileName "viewer-elm/dist/index.html"
bSucc <- openBrowser url
unless bSucc $
hPutStrLn stderr $ "Failed to open browser. Manually visit: " ++ url
-------------------------------------------------------------------------------
-- Websocket API
data WebMessage
= WebStatus String
| WebError String
| WebFrameCount Int
| WebFrame Int FilePath
sendWebMessage :: Connection -> WebMessage -> IO ()
sendWebMessage conn msg = sendTextData conn $
case msg of
WebStatus txt -> T.pack "status\n" <> T.pack txt
WebError txt -> T.pack "error\n" <> T.pack txt
WebFrameCount n -> T.pack $ "frame_count\n" ++ show n
WebFrame n path -> T.pack $ "frame\n" ++ show n ++ "\n" ++ path