-- TODO: Put current client's ThreadID into mvar and kill it once new one comes {- A little snap server that lives at localhost that - communicates over websockets at "localhost:PORT/ws" - answers HTTP requests on any other localhost URL combination -} {-# LANGUAGE OverloadedStrings #-} module QuickPlot.IPC.Server ( runServer, sendMessage ) where import Network.WebSockets.Snap import Network.WebSockets hiding (runServer) import Data.ByteString.Lazy.Internal import Snap import Snap.Util.FileServe import Control.Concurrent import System.IO.Unsafe import Control.Exception import Control.Monad import QuickPlot.IPC.Protocol import Data.IORef import Paths_QuickPlot -- | Get the filepath to the static HTML/CSS/JS getFrontendDir :: IO FilePath getFrontendDir = getDataFileName "src/frontend" -- | Run snap server in background as a new process -- Run only once (even after reloading in ghci) runServer :: FilePath -- ^ Path to directory that contains user scripts -> Int -- ^ Port of the server -> IO () runServer userDir port = do noClients <- isEmptyMVar clientThread if noClients then do atomicWriteIORef serverRunning (True, port) frontendDir <- getFrontendDir void $ forkIO $ httpServe config (service frontendDir) putStrLn $ "Find QuickPlot at \"localhost:" ++ show port ++ "\" in your browser" else putStrLn "Start QuickPlot server only once per ghci session" where config = setErrorLog ConfigNoLog $ setAccessLog ConfigNoLog $ setVerbose False $ setPort port defaultConfig -- | Declare the routing and the services of the server service :: FilePath -- ^ Path to directory of files to serve -> Snap () service staticDir = route [ ("/", serveDirectory staticDir) , ("/ws", runWebSocketsSnap websocketHandler) , ("", serveFile "src/404.html") ] -- | Handle new websocket connections -- Send content of channel to browser and clear channel afterwards -- If the channel contains "stop" the next websocketHandler who reads it will stop websocketHandler :: PendingConnection -- ^ About to be a websocket connection -> IO () websocketHandler pending = do connection <- acceptRequest pending login forkPingThread connection 20 -- keep alive (every 20 seconds) handle close $ handleUntilNewClient connection -- | Handle websocket connection until new client connects to server -- Will halt if it reads "stop" in the channel handleUntilNewClient :: Connection -- ^ Websocket connection -> IO () -- ^ Message sent to browser handleUntilNewClient connection = do msg <- takeMVar channel sendTextData connection msg handleUntilNewClient connection -- | Handle connection exceptions of the websocket -- TODO: Clean up instead of just messaging close :: ConnectionException -- ^ Websocket connection -> IO () -- ^ Message that exception happened in stdout close (CloseRequest _ _) = print "Exception: CloseRequest" close ConnectionClosed = print "Exception: ConnectionClosed" close exception = print $ "Exception: " ++ show exception -- | Login for client threads -- It will kill other clients if there are any login :: IO () login = do noClients <- isEmptyMVar clientThread if noClients then myThreadId >>= putMVar clientThread else do takeMVar clientThread >>= killThread myThreadId >>= putMVar clientThread -- | Contains the newest message for the browser channel :: MVar ByteString -- ^ Message for the browser {-# NOINLINE channel #-} channel = unsafePerformIO newEmptyMVar -- | Tell if client connected and on which thread clientThread :: MVar ThreadId -- ^ Thread of the current client if there is one {-# NOINLINE clientThread #-} clientThread = unsafePerformIO newEmptyMVar -- | Tell if server was started and on which port serverRunning :: IORef (Bool, Int) -- ^ (if running, port) {-# NOINLINE serverRunning #-} serverRunning = unsafePerformIO (newIORef (False, 0)) -- | Send a raw message to the browser -- By setting the channel which the server reads from for new messages sendRawMessage :: ByteString -- ^ Message for the browser -> IO () -- ^ Either message sent to browser or reminder to start server in stdout sendRawMessage message = do noClients <- isEmptyMVar clientThread (running, port) <- readIORef serverRunning if running then if noClients then do threadDelay 1000000 -- In case browser will reconnect itself putStrLn $ "You need to go to: \"localhost:" ++ show port ++ "\" in your browser before plotting" else putMVar channel message else putStrLn "You need to start QuickPlot with \"runQuickPlot\" before plotting" -- | Send a message to the browser sendMessage :: QPMessage -- ^ Message for the browser -> IO () -- ^ Either message sent to server or rminder to start server in stdout sendMessage = sendRawMessage . encode