packages feed

paprika-0.1.0.0: app/WaiCutter.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}

module Main where

import Data.String (fromString)
import System.Environment (getArgs)
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as H
import qualified Network.Wai.Application.Static as Static
import Data.Maybe (fromJust)
import Data.FileEmbed (embedDir)
import WaiAppStatic.Types (toPieces)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import qualified Data.ByteString as BS

import Network.Transport.InMemory (createTransport)
import Control.Distributed.Process.Node (LocalNode,newLocalNode,initRemoteTable,forkProcess,runProcess)
import Control.Distributed.Process (Process,ProcessId,send,receiveWait,match,getSelfPid,terminate)
import Control.Monad (forever)
import Control.Monad.Trans (liftIO)
import Control.Concurrent (threadDelay)
import Control.Exception (catch)
import qualified Data.Set as Set

import Network.Wai.Handler.WebSockets (websocketsOr)
import qualified Network.WebSockets as WS
import Data.Word8 (_question)

import GHC.Generics (Generic)
import Data.Typeable (Typeable)
import Data.Binary (Binary)

import Data.Aeson.Types (ToJSON,toJSON,object,(.=))
import qualified Data.Aeson as AE

import System.Paprika(leftOn,leftOff,rightOn,rightOff)




main :: IO ()
main = do
  node <- createTransport >>= (\t -> newLocalNode t initRemoteTable)
  vmpid <- forkProcess node $ viewerManagerProcess Set.empty
  rmpid <- forkProcess node $ reporterManagerProcess (RMS ([],[]) (Set.empty,Set.empty) 10) vmpid
  _ <- forkProcess node $ timerProcess rmpid
  host:port:_ <- getArgs
  Warp.runSettings (
    Warp.setHost (fromString host) $
    Warp.setPort (read port) $
    Warp.defaultSettings
    ) $ websocketsOr WS.defaultConnectionOptions (wsRouterApp node vmpid rmpid) staticApp


staticApp :: Wai.Application
staticApp = Static.staticApp $ settings { Static.ssIndices = indices }
  where
--    settings = Static.defaultWebAppSettings "static/cutter"
    settings = Static.embeddedSettings $(embedDir "static/cutter")
    indices = fromJust $ toPieces ["viewer.htm"] -- default content


wsRouterApp :: LocalNode -> ProcessId -> ProcessId -> WS.ServerApp
wsRouterApp node vmpid rmpid pconn
  | ("/viewer"   == path) = viewerApp node vmpid rmpid pconn
  | ("/reporter" == path) = reporterApp node rmpid pconn
  | otherwise = WS.rejectRequest pconn "endpoint not found"
  where
    requestPath = WS.requestPath $ WS.pendingRequest pconn
    path = BS.takeWhile (/=_question) requestPath


viewerApp :: LocalNode -> ProcessId -> ProcessId -> WS.ServerApp
viewerApp node vmpid rmpid pconn = do
  conn <- WS.acceptRequest pconn
  WS.forkPingThread conn 30
  
  vpid <- forkProcess node $ viewerProcess conn
  runProcess node $ do
    send vmpid (VMMRegistViewer vpid)
    send rmpid (RMMQueryMembers vpid)
    send rmpid (RMMQueryThreshold vpid)
  loop conn vpid `catch` onError vpid
  where
    loop :: WS.Connection -> ProcessId -> IO ()
    loop conn vpid = do
      msg <- WS.receive conn
      case msg of
        WS.ControlMessage (WS.Close _ _) -> onClose vpid

        WS.DataMessage (WS.Text lbs) -> do
          let thr = read $ T.unpack $ WS.fromLazyByteString lbs
          runProcess node $ send rmpid $ RMMSetThreshold thr
          loop conn vpid
        _ -> loop conn vpid
        
    onError :: ProcessId -> WS.ConnectionException -> IO ()
    onError vpid _ = onClose vpid

    onClose vpid =
      runProcess node $ do
        send vmpid (VMMUnregistViewer vpid)
        send vpid VMClose
      
    requestPath = WS.requestPath $ WS.pendingRequest pconn
    query = BS.drop 1 $ BS.dropWhile (/=_question) requestPath
    name = T.unpack $ decodeUtf8 $ H.urlDecode True query
    


reporterApp :: LocalNode -> ProcessId -> WS.ServerApp
reporterApp node rmpid pconn = do
  conn <- WS.acceptRequest pconn
  WS.forkPingThread conn 30
  
  rpid <- forkProcess node $ reporterProcess conn
  runProcess node $ send rmpid (RMMRegistReporter rpid)
  loop conn rpid `catch` onError rpid
  where
    loop :: WS.Connection -> ProcessId -> IO ()
    loop conn rpid = do
      msg <- WS.receive conn
      case msg of
        WS.ControlMessage (WS.Close _ _) -> onClose rpid
            
        WS.DataMessage (WS.Text _) -> do
          runProcess node $ send rmpid $ RMMCountUp rpid
          loop conn rpid
        _ -> loop conn rpid
        
    onError :: ProcessId -> WS.ConnectionException -> IO ()
    onError rpid _ = onClose rpid
      
    onClose rpid = 
      runProcess node $ do
        send rmpid (RMMUnregistReporter rpid)
        send rpid RMClose
      
    requestPath = WS.requestPath $ WS.pendingRequest pconn
    query = BS.drop 1 $ BS.dropWhile (/=_question) requestPath
    name = T.unpack $ decodeUtf8 $ H.urlDecode True query  




type ViewerManagerState = Set.Set ProcessId

data ViewerManagerMsg = VMMRegistViewer ProcessId
                      | VMMUnregistViewer ProcessId
                      | VMMFreq Int Int
                      | VMMMembers Int Int
                      | VMMThreshold Int
                      deriving (Show,Generic,Typeable)
                        
instance Binary ViewerManagerMsg

viewerManagerProcess :: ViewerManagerState -> Process ()
viewerManagerProcess state = do
  state' <- receiveWait [match (p state)]
  viewerManagerProcess state'
    where
      p :: ViewerManagerState -> ViewerManagerMsg -> Process ViewerManagerState
      
      p state (VMMRegistViewer pid)   = return $ Set.insert pid state
      
      p state (VMMUnregistViewer pid) = return $ Set.delete pid state
      
      p state (VMMFreq lf rf) = do
        mapM_ (\pid -> send pid $ VMFreq lf rf) $ Set.toList state
        return state
        
      p state (VMMMembers lm rm) = do
        mapM_ (\pid -> send pid $ VMMembers lm rm) $ Set.toList state
        return state

      p state (VMMThreshold thr) = do
        mapM_ (\pid -> send pid $ VMThreshold thr) $ Set.toList state
        return state



data ViewerMsg = VMFreq Int Int
               | VMMembers Int Int
               | VMThreshold Int
               | VMClose
               deriving (Show,Generic,Typeable)
                        
instance Binary ViewerMsg

instance ToJSON ViewerMsg where
  toJSON (VMFreq lf rf) =
    object ["type" .= ("freq" :: String)
           ,"content" .= object ["left"  .= lf
                                ,"right" .= rf
                                ]
           ]
    
  toJSON (VMMembers lm rm) =
    object ["type" .= ("members" :: String)
           ,"content" .= object ["left"  .= lm
                                ,"right" .= rm
                                ]
           ]

  toJSON (VMThreshold thr) =
    object ["type" .= ("threshold" :: String)
           ,"content" .= thr
           ]

viewerProcess :: WS.Connection -> Process ()    
viewerProcess conn = forever $ receiveWait [match (p conn)]
  where
    p :: WS.Connection -> ViewerMsg -> Process ()
    
    p conn msg@(VMFreq _ _) = liftIO $ WS.sendTextData conn $ AE.encode msg
       
    p conn msg@(VMMembers _ _) = liftIO $ WS.sendTextData conn $ AE.encode msg

    p conn msg@(VMThreshold _ ) = liftIO $ WS.sendTextData conn $ AE.encode msg

    p conn VMClose = terminate 



data ReporterManagerState = RMS ([Int],[Int]) (Set.Set ProcessId, Set.Set ProcessId) Int

data ReporterManagerMsg = RMMRegistReporter ProcessId
                        | RMMUnregistReporter ProcessId
                        | RMMCountUp ProcessId
                        | RMMQueryMembers ProcessId
                        | RMMSetThreshold Int
                        | RMMQueryThreshold ProcessId
                        | RMMRequestAggregate
                        | RMMRequestFreq
                        deriving (Show,Generic,Typeable)
                                 
instance Binary ReporterManagerMsg
         
reporterManagerProcess :: ReporterManagerState -> ProcessId -> Process ()
reporterManagerProcess state vmpid = do
  state' <- receiveWait [match (p state)]
  reporterManagerProcess state' vmpid
    where
      p :: ReporterManagerState -> ReporterManagerMsg -> Process ReporterManagerState
      
      p (RMS cs (lms,rms) thr) (RMMRegistReporter rpid) =
        if Set.size lms < Set.size rms
        then do
          let lms' = Set.insert rpid lms
          send rpid $ RMIsLeft True
          send vmpid $ VMMMembers (Set.size lms') (Set.size rms)
          return $ RMS cs (lms',rms) thr
        else do
          let rms' = Set.insert rpid rms
          send rpid $ RMIsLeft False
          send vmpid $ VMMMembers (Set.size lms) (Set.size rms')
          return $ RMS cs (lms,rms') thr
                     
      p (RMS cs (lms,rms) thr) (RMMUnregistReporter rpid) =
        if Set.member rpid lms
        then do
          let lms' = Set.delete rpid lms
          send vmpid $ VMMMembers (Set.size lms') (Set.size rms)
          return $ RMS cs (lms',rms) thr
        else do
          let rms' = Set.delete rpid rms
          send vmpid $ VMMMembers (Set.size lms) (Set.size rms')
          return $ RMS cs (lms,rms') thr
                     
      p (RMS (lcs,rcs) ms@(lms,_) thr) (RMMCountUp rpid) =
        if Set.member rpid lms
        then return $ RMS (head lcs +1 : tail lcs, rcs) ms thr
        else return $ RMS (lcs, head rcs +1 : tail rcs) ms thr
             
      p state@(RMS _ (lms,rms) _) (RMMQueryMembers vpid) = do
        send vpid $ VMMembers (Set.size lms) (Set.size rms)
        return state
        
      p (RMS cs ms _) (RMMSetThreshold thr) = do
        send vmpid $ VMMThreshold thr
        return $ RMS cs ms thr

      p state@(RMS _ _ thr) (RMMQueryThreshold vpid) = do
        send vpid $ VMThreshold thr
        return state
        
      p (RMS (lcs,rcs) ms thr) RMMRequestAggregate = do
        -- [TODO] calc correct interbal
        return $ RMS (0 : take 10 lcs,0: take 10 rcs) ms thr
        
      p state@(RMS (lcs,rcs) _ thr) RMMRequestFreq = do
        -- [TODO] calc correct interbal
        let lf = sum lcs
        let rf = sum rcs
        if thr <= lf
          then liftIO leftOn
          else liftIO leftOff
        if thr <= rf
          then liftIO rightOn
          else liftIO rightOff
        send vmpid $ VMMFreq lf rf
        return state

        



data ReporterMsg = RMIsLeft Bool
                 | RMClose
                 deriving (Show,Generic,Typeable)
                        
instance Binary ReporterMsg

instance ToJSON ReporterMsg where
  toJSON (RMIsLeft True) =
    object ["type" .= ("side" :: String), "content" .= ("left"  :: String)]
    
  toJSON (RMIsLeft False) =
    object ["type" .= ("side" :: String), "content" .= ("right" :: String)]

reporterProcess :: WS.Connection -> Process ()    
reporterProcess conn = forever $ receiveWait [match (p conn)]
  where
    p :: WS.Connection -> ReporterMsg -> Process ()
    
    p conn msg@(RMIsLeft _) = liftIO $ WS.sendTextData conn $ AE.encode msg

    p conn RMClose = terminate -- [TODO] RMMUnregistReporter



timerProcess :: ProcessId -> Process ()
timerProcess rmpid = loop 0
  where
    loop :: Int -> Process ()
    loop i = do
      send rmpid $ RMMRequestAggregate
      if i `mod` 5 == 0
        then send rmpid $ RMMRequestFreq
        else return ()
      liftIO $ threadDelay $ 100 * 1000
      loop (i+1)