penrose-0.1.1.1: src/Penrose/Server.hs
-- | "Server" contains functions that serves the Penrose runtime over
-- websockets connection.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_HADDOCK prune #-}
module Penrose.Server
( serveEditor
, serveRenderer
) where
import Control.Concurrent (MVar, forkIOWithUnmask, modifyMVar_,
newMVar, readMVar)
import Control.Exception
import Control.Monad (forever, void)
import Data.Aeson
import Data.Tuple.Extra (fst3)
import Data.UUID
import GHC.Generics
import qualified Network.Socket as S
import qualified Network.WebSockets as WS
import Penrose.Env (VarEnv)
import Penrose.GenOptProblem as GenOptProblem
import Penrose.API
import Penrose.Style
import System.Console.Pretty (Color (..), Style (..), bgColor)
import qualified System.Console.Pretty as Console
import System.IO (Handle, stderr)
import System.Log.Formatter
import System.Log.Handler (setFormatter)
import System.Log.Handler.Simple (GenericHandler (..), fileHandler,
streamHandler)
import System.Log.Logger (Priority (..), debugM, errorM,
infoM, rootLoggerName, setHandlers,
setLevel, updateGlobalLogger,
warningM)
import System.Random
--------------------------------------------------------------------------------
-- Types
data Packet a = Packet
{ typ :: String
, contents :: a
} deriving (Generic)
instance (ToJSON a) => ToJSON (Packet a) where
toJSON Packet {typ = t, contents = c} = object ["type" .= t, "contents" .= c]
-- | TODO
type ClientID = UUID
-- | TODO
type ServerState = [Client]
-- | TODO
type Client = (ClientID, WS.Connection, ClientState)
-- | TODO
data ClientState
= Editor VarEnv
StyProg
(Maybe BackendState) -- TODO: no longer used, remove
| Renderer BackendState -- TODO: no longer used, remove
| Stateless
-- | TODO
type BackendState = GenOptProblem.State
--------------------------------------------------------------------------------
-- RESTful server
data Request
= Step Int
State
| Resample Int
State
| StepUntilConvergence State
| CompileTrio String
String
String
| GetEnv String
String
deriving (Generic)
instance FromJSON Request
instance ToJSON Request
processRequests :: Client -> IO ()
processRequests client@(_, conn, _) = do
logDebug client "Waiting for Commands"
msg_json <- WS.receiveData conn
logDebug client $ "Message received from frontend: \n" ++ show msg_json
case decode msg_json of
Just e ->
case e of
Step steps s -> sendSafe "state" $ step s steps
Resample samples s -> sendSafe "state" $ resample s samples
StepUntilConvergence s -> sendSafe "state" $ stepUntilConvergence s
CompileTrio sub sty elm ->
sendSafe "compilerOutput" $ compileTrio sub sty elm
GetEnv sub elm -> sendSafe "varEnv" $ getEnv sub elm
Nothing -> do
logError client "Error reading JSON"
processRequests client
logDebug client "Messege received and decoded successfully."
processRequests client
where
sendSafe :: (ToJSON a, ToJSON b) => String -> Either a b -> IO ()
sendSafe flag res =
case res of
Right state -> wsSendPacket conn Packet {typ = flag, contents = state}
Left error -> wsSendPacket conn Packet {typ = "error", contents = error}
--------------------------------------------------------------------------------
-- Server-level functions
newServerState :: ServerState
newServerState = []
numClients :: ServerState -> Int
numClients = length
addClient :: Client -> ServerState -> ServerState
addClient client clients = client : clients
removeClient :: Client -> ServerState -> ServerState
removeClient client = filter ((/= fst3 client) . fst3)
newUUID :: IO UUID
newUUID = randomIO
idString :: Client -> String
idString = toString . fst3
prettyAddress :: String -> Int -> String
prettyAddress domain port = "ws://" ++ domain ++ ":" ++ show port ++ "/"
-- | 'serveEditor' starts the Penrose server in the editor mode and compile user's input programs dynamically
serveEditor ::
String -- ^ the domain of the server
-> Int -- ^ port number of the server
-> Bool -- ^ verbosity option
-> IO ()
serveEditor domain port isVerbose = do
initState <- newMVar newServerState
putStrLn $ "Penrose editor server started on " ++ prettyAddress domain port
putStrLn "Waiting for clients to connect..."
catch (runServer domain port $ handleClient initState isVerbose) handler
where
handler :: ErrorCall -> IO ()
handler e = putStrLn "Internal server Error"
handleClient ::
MVar ServerState -- ^ current list of clients
-> Bool -- ^ verbosity option
-> WS.ServerApp
handleClient state isVerbose pending = do
conn <- WS.acceptRequest pending
WS.forkPingThread conn 30 -- To keep the connection alive
clients <- readMVar state
clientID <- newUUID
let client = (clientID, conn, Stateless)
let logLevel =
if isVerbose
then DEBUG
else INFO
myStreamHandler <- fmap withColoredFormatter $ streamHandler stderr logLevel
updateGlobalLogger rootLoggerName (setHandlers [myStreamHandler])
updateGlobalLogger rootLoggerName (setLevel DEBUG)
flip finally (disconnect client) $ do
modifyMVar_ state $ \s -> do
let s' = addClient client s
return s'
logInfo client $ "Client connected " ++ toString clientID
-- start an editor session
wsSendPacket conn Packet {typ = "connection", contents = String "connected"}
processRequests client
where
disconnect client
-- Remove client
= do
modifyMVar_ state $ \s -> return $ removeClient client s
logInfo client (idString client ++ " disconnected")
-- let logPath = "/var/log/penrose-" ++ idString client ++ ".log"
-- myFileHandler <- fmap withFormatter $ fileHandler logPath logLevel
-- updateGlobalLogger
-- rootLoggerName
-- (setHandlers [myStreamHandler, myFileHandler])
--------------------------------------------------------------------------------
-- Client-level functions
-- | 'serveRenderer' is the top-level function that "Main" uses to start serving
-- the Penrose Runtime.
serveRenderer ::
String -- the domain of the server
-> Int -- port number of the server
-> BackendState -- initial state of Penrose Runtime
-> IO ()
serveRenderer domain port initState = do
putStrLn $ "Penrose renderer server started on " ++ prettyAddress domain port
putStrLn "Waiting for the frontend UI to connect..."
let s = Renderer initState
catch (runServer domain port $ renderer s) handler
where
handler :: ErrorCall -> IO ()
handler _ = putStrLn "Server Error"
renderer :: ClientState -> WS.ServerApp
renderer (Renderer s) pending = do
conn <- WS.acceptRequest pending
WS.forkPingThread conn 30 -- To keep the connection alive
clientID <- newUUID
let client = (clientID, conn, Stateless)
-- send the initial state to the frontend renderer first
wsSendPacket conn Packet {typ = "state", contents = s}
processRequests client
--------------------------------------------------------------------------------
-- Logger
withColoredFormatter, withFormatter ::
GenericHandler Handle -> GenericHandler Handle
withFormatter handler = setFormatter handler formatter
where
formatter = simpleLogFormatter ("[$time $loggername $prio]" ++ "\n$msg")
withColoredFormatter handler = setFormatter handler coloredLogFormatter
coloredLogFormatter :: LogFormatter a
coloredLogFormatter _h (prio, msg) loggername
| prio == DEBUG = formatter Blue
| prio == INFO = formatter Green
| prio == ERROR = formatter Red
where
formatter color =
simpleLogFormatter
(bgColor color $
Console.style Bold "[$time $loggername $prio]" ++ "\n$msg")
_h
(prio, msg)
loggername
logDebug, logInfo, logError :: Client -> String -> IO ()
logDebug client = debugM (idString client)
logInfo client = infoM (idString client)
logError client = errorM (idString client)
--------------------------------------------------------------------------------
-- WebSocket utils
wsSendPacket :: ToJSON a => WS.Connection -> Packet a -> IO ()
wsSendPacket conn packet = WS.sendTextData conn $ encode packet
-- | This 'runServer' is exactly the same as the one in "Network.WebSocket". Duplicated for calling a customized version of 'runServerWith' with error messages enabled.
runServer ::
String -- ^ Address to bind
-> Int -- ^ Port to listen on
-> WS.ServerApp -- ^ Application
-> IO () -- ^ Never returns
runServer host port app =
runServerWith host port options app
where
options = WS.defaultConnectionOptions {WS.connectionCompressionOptions = WS.PermessageDeflateCompression WS.defaultPermessageDeflate}
-- | A version of 'runServer' which allows you to customize some options.
runServerWith :: String -> Int -> WS.ConnectionOptions -> WS.ServerApp -> IO ()
runServerWith host port opts app =
S.withSocketsDo $
bracket
(WS.makeListenSocket host port)
S.close
(\sock ->
mask_ $
forever $ do
allowInterrupt
(conn, _) <- S.accept sock
void $
forkIOWithUnmask $ \unmask ->
finally (unmask $ runApp conn opts app) (S.close conn))
runApp :: S.Socket -> WS.ConnectionOptions -> WS.ServerApp -> IO ()
runApp socket opts app = do
sock <- WS.makePendingConnection socket opts
app sock
--------------------------------------------------------------------------------
-- Old server code for reference
-- -- TODO use the more generic wsSendJSON?
-- wsSendShapes :: WS.Connection -> [Shape Double] -> IO ()
-- wsSendShapes conn shapes = WS.sendTextData conn $ encode packet
-- where packet = Packet { typ = "shapes", contents = shapes }
-- wsSendFrame :: WS.Connection -> Frame -> IO ()
-- wsSendFrame conn frame = WS.sendTextData conn $ encode packet
-- where packet = Packet { typ = "shapes", contents = frame }
-- updateState :: ClientState -> BackendState -> ClientState
-- updateState (Renderer s) s' = Renderer s'
-- updateState (Editor e sty s) s' = Editor e sty $ Just s'
-- getBackendState :: ClientState -> BackendState
-- getBackendState (Renderer s) = s
-- getBackendState (Editor _ _ (Just s)) = s
-- getBackendState (Editor _ _ Nothing) = error "Server error: Backend state has not been initialized yet."
-- data Feedback
-- = Cmd Command
-- | Drag DragEvent
-- | Update UpdateShapes
-- | Edit SubstanceEdit
-- | Recompile RecompileDomain
-- deriving (Generic)
-- data Command = Command { command :: String }
-- deriving (Show, Generic)
-- data DragEvent = DragEvent { name :: String,
-- xm :: Float,
-- ym :: Float }
-- deriving (Show, Generic)
-- data SubstanceEdit = SubstanceEdit { program :: String, enableAutostep :: Bool }
-- deriving (Show, Generic)
-- data RecompileDomain = RecompileDomain { element :: String, style :: String }
-- deriving (Show, Generic)
-- data UpdateShapes = UpdateShapes { shapes :: [Shape Double] }
-- deriving (Show, Generic)
-- data Frame = Frame { flag :: String,
-- shapes :: [Shape Double],
-- ordering :: [String]
-- } deriving (Show, Generic)
-- instance FromJSON Feedback
-- instance FromJSON Command
-- instance FromJSON DragEvent
-- instance FromJSON UpdateShapes
-- instance FromJSON SubstanceEdit
-- instance FromJSON RecompileDomain
-- instance ToJSON Frame
-- loop :: Client -> IO ()
-- loop client@(clientID, conn, clientState)
-- | optStatus (paramsr s) == EPConverged = do
-- logInfo client "Optimization completed."
-- logInfo client ("Current weight: " ++ show (weight (paramsr s)))
-- wsSendFrame conn
-- Frame {
-- flag = "final",
-- ordering = shapeOrdering s,
-- shapes = shapesr s :: [Shape Double]
-- }
-- processCommand client
-- | autostep s = stepAndSend client
-- | otherwise = processCommand client
-- where s = getBackendState clientState
-- -- | In editor mode, the server first waits for a well-formed Substance program
-- -- before accepting any other kinds of commands. The default action on other
-- -- commands is to continue waiting without crashing
-- waitSubstance :: Client -> IO ()
-- waitSubstance client@(clientID, conn, clientState) = do
-- infoM (toString clientID) "Waiting for Substance program..."
-- msg_json <- WS.receiveData conn
-- case decode msg_json of
-- Just e -> case e of
-- Edit (SubstanceEdit subProg auto) -> substanceEdit subProg auto client
-- Recompile (RecompileDomain element style) -> recompileDomain element style client
-- _ -> continue
-- Nothing -> continue
-- where continue = do
-- warningM (toString clientID) "Invalid command. Returning to wait for Substance program"
-- waitSubstance client
-- -- } COMBAK: abstract this logic out to `wait`
-- waitUpdate :: Client -> IO ()
-- waitUpdate client@(clientID, conn, clientState) = do
-- logInfo client "Waiting for image/label dimension update"
-- msg_json <- WS.receiveData conn
-- case decode msg_json of
-- Just e -> case e of
-- Update (UpdateShapes shapes) -> updateShapes shapes client
-- _ -> continue
-- Nothing -> continue
-- where continue = do
-- warningM (toString clientID) "Invalid command. Returning to wait for image/label update."
-- waitUpdate client
-- substanceError, elementError, styleError :: Client -> CompilerError -> IO ()
-- substanceError client@(_, conn, _) e = do
-- logError client $ "Substance compiler error: " ++ show e
-- wsSendPacket conn Packet { typ = "error", contents = e}
-- waitSubstance client
-- elementError client@(_, conn, _) e = do
-- logError client $ "Element parser error: " ++ show e
-- wsSendPacket conn Packet { typ = "error", contents = e}
-- waitSubstance client
-- styleError client@(_, conn, _) e = do
-- logError client $ "Style parser error: " ++ show e
-- wsSendPacket conn Packet { typ = "error", contents = e}
-- waitSubstance client
-- styleRuntimeError :: Client -> ErrorCall -> IO ()
-- styleRuntimeError client@(_, conn, _) e = do
-- logError client $ "Style runtime error: " ++ show e
-- wsSendPacket conn Packet { typ = "error", contents = show e}
-- waitSubstance client
-- -- -- TODO: this match might be redundant, but not sure why the linter warns that.
-- -- substanceError client s _ = do
-- -- putStrLn "Substance compiler error: Unknown error."
-- -- waitSubstance c s
-- -- COMBAK: this function should be updated to remove
-- processCommand :: Client -> IO ()
-- processCommand client@(clientID, conn, s) = do
-- logInfo client "Waiting for Commands"
-- msg_json <- WS.receiveData conn
-- logInfo client $ "Messege received from frontend: \n" ++ show msg_json
-- case decode msg_json of
-- Just e -> case e of
-- Cmd (Command cmd) -> executeCommand cmd client
-- Drag (DragEvent name xm ym) -> dragUpdate name xm ym client
-- Edit (SubstanceEdit subProg auto) -> substanceEdit subProg auto client
-- Update (UpdateShapes shapes) -> updateShapes shapes client
-- Recompile (RecompileDomain element style) -> recompileDomain element style client
-- Nothing -> logError client "Error reading JSON"
-- -- TODO: might need to return to `loop`
-- recompileDomain :: String -> String -> Client -> IO ()
-- recompileDomain _ _ client@(_, conn, Renderer s) = do
-- logError client "Cannot change domains in Renderer mode."
-- loop client
-- recompileDomain element style client@(clientID, conn, Editor {}) = do
-- logInfo client "Switching to another domain..."
-- logInfo client $ "Element program received: " ++ element
-- logInfo client $ "Style program received: " ++ style
-- let elementRes = parseElement "" element
-- case elementRes of
-- Right elementEnv -> do
-- -- Send Env to the frontend for language services (for now, bag-of-word autocompletion)
-- wsSendPacket conn $ Packet { typ = "env", contents = elementEnv}
-- let styRes = parseStyle "" style elementEnv
-- case styRes of
-- Right styProg -> do
-- logDebug client ("Style AST:\n" ++ ppShow styProg)
-- let client = (clientID, conn, Editor elementEnv styProg Nothing)
-- waitSubstance client
-- Left err -> styleError client err
-- Left err -> elementError client err
-- substanceEdit :: String -> Bool -> Client -> IO ()
-- substanceEdit subIn _ client@(_, _, Renderer _) =
-- logError client "Server Error: the Substance program cannot be updated when the server is in Renderer mode."
-- substanceEdit subIn auto client@(clientID, conn, Editor env styProg s) = do
-- logInfo client $ "Substance program received: " ++ subIn
-- let subRes = Substance.parseSubstance "" (Sugarer.sugarStmts subIn env) env
-- case subRes of
-- Right subOut@(Substance.SubOut _ (subEnv, _) _) -> do
-- logInfo client $ show subOut
-- -- TODO: store the Style values to reuse on Substance edit
-- -- TODO: pass in any new optimization config values here?
-- wsSendPacket conn $ Packet { typ = "env", contents = subEnv }
-- let styVals = []
-- let optConfig = case s of
-- Nothing -> G.defaultOptConfig
-- Just currState -> oConfig currState
-- styRes <- try (compileStyle styProg subOut styVals optConfig)
-- case styRes of
-- Right newState -> do
-- wsSendFrame conn Frame {
-- flag = "initial",
-- ordering = shapeOrdering newState,
-- shapes = shapesr newState :: [Shape Double]
-- }
-- waitUpdate (clientID, conn, Editor env styProg $ Just newState { G.autostep = auto })
-- Left styError -> styleRuntimeError client styError
-- Left subError -> substanceError client subError
-- updateShapes :: [Shape Double] -> Client -> IO ()
-- updateShapes newShapes client@(clientID, conn, clientState) =
-- let polyShapes = toPolymorphics newShapes
-- uninitVals = map G.toTagExpr $ G.shapes2vals polyShapes $ G.uninitializedPaths s
-- trans' = G.insertPaths (G.uninitializedPaths s) uninitVals (G.transr s)
-- -- -- Respect the optimization policy
-- -- TODO: rewrite this such that it works with the new overallObjFn
-- -- policyFns = currFns $ policyParams s
-- -- newObjFn = G.genObjfn (castTranslation trans') (filter isObjFn policyFns) (filter isConstr policyFns) (G.varyingPaths s)
-- varyMapNew = G.mkVaryMap (G.varyingPaths s) (G.varyingState s)
-- news = s {
-- G.shapesr = polyShapes,
-- G.varyingState = G.shapes2floats polyShapes varyMapNew $ G.varyingPaths s,
-- G.transr = trans',
-- -- G.paramsr = (G.paramsr s) { G.weight = G.initWeight, G.optStatus = G.NewIter, G.overallObjFn = newObjFn, G.bfgsInfo = G.defaultBfgsParams }}
-- G.paramsr = (G.paramsr s) { G.weight = G.initWeight, G.optStatus = G.NewIter, G.bfgsInfo = G.defaultBfgsParams }}
-- nextClientS = updateState clientState news
-- client' = (clientID, conn, nextClientS)
-- in if autostep s
-- then stepAndSend client'
-- else loop client'
-- where s = getBackendState clientState
-- dragUpdate :: String -> Float -> Float -> Client -> IO ()
-- dragUpdate name xm ym client@(clientID, conn, clientState) =
-- let (xm', ym') = (r2f xm, r2f ym)
-- newShapes = map (\shape ->
-- if getName shape == name
-- then dragShape shape xm' ym'
-- else shape)
-- (G.shapesr s)
-- varyMapNew = G.mkVaryMap (G.varyingPaths s) (G.varyingState s)
-- news = s { G.shapesr = newShapes,
-- G.varyingState = G.shapes2floats newShapes varyMapNew $ G.varyingPaths s,
-- G.paramsr = (G.paramsr s) { G.weight = G.initWeight, G.optStatus = G.NewIter, G.bfgsInfo = G.defaultBfgsParams }}
-- nextClientS = updateState clientState news
-- client' = (clientID, conn, nextClientS)
-- in if autostep s
-- then stepAndSend client'
-- else loop client'
-- where s = getBackendState clientState
-- dragShape :: Autofloat a => Shape a -> a -> a -> Shape a
-- dragShape shape dx dy
-- | shape `is` "Line" =
-- trRaw "here" $ move "startX" dx $
-- move "startY" dy $
-- move "endX" dx $
-- move "endY" dy shape
-- | shape `is` "Image" =
-- move "centerX" dx $
-- move "centerY" dy shape
-- | shape `is` "Arrow" =
-- move "startX" dx $
-- move "startY" dy $
-- move "endX" dx $
-- move "endY" dy shape
-- | shape `is` "Curve" =
-- movePath (dx, dy) shape
-- | otherwise = setX (FloatV (dx + getX shape)) $ setY (FloatV (dy + getY shape)) shape
-- move :: Autofloat a => PropID -> a -> Shape a -> Shape a
-- move prop dd s = set s prop (FloatV (dd + getNum s prop))
-- executeCommand :: String -> Client -> IO ()
-- executeCommand cmd client@(clientID, conn, clientState)
-- | cmd == "resample" = resampleAndSend client
-- | cmd == "step" = stepAndSend client
-- | cmd == "autostep" =
-- let os = getBackendState clientState
-- os' = os { autostep = not $ autostep os }
-- in loop (clientID, conn, updateState clientState os')
-- | otherwise = logError client ("Can't recognize command " ++ cmd)
-- resampleAndSend, stepAndSend :: Client -> IO ()
-- resampleAndSend client@(clientID, conn, clientState) = do
-- -- Sample several states and choose the one with lowest energy
-- let news = G.resampleBest G.numStateSamples s
-- let (newShapes, _, _) = evalTranslation news
-- wsSendFrame conn
-- Frame {
-- flag = "initial",
-- ordering = shapeOrdering news,
-- shapes = newShapes
-- }
-- let nextClientS = updateState clientState news
-- let client' = (clientID, conn, nextClientS)
-- -- NOTE: could have called `loop` here, but this would result in a race condition between autostep and updateShapes somehow. Therefore, we explicitly transition to waiting for an update on image/label sizes whenever resampled.
-- waitUpdate client'
-- where s = getBackendState clientState
-- stepAndSend client@(clientID, conn, clientState) = do
-- --------------------------------------------------------------------------------
-- -- DEBUG: performance test for JSON encode/decode speed
-- -- let s' = getBackendState clientState
-- -- let s = unsafePerformIO $ do
-- -- B.writeFile "state.json" (encode s')
-- -- stateStr <- B.readFile "state.json"
-- -- return (fromMaybe (error "json decode error") $ decode stateStr)
-- -- let nexts = O.step s
-- --------------------------------------------------------------------------------
-- -- COMBAK: revert
-- let s = getBackendState clientState
-- let nexts = O.step s
-- wsSendFrame conn
-- Frame {
-- flag = "running",
-- ordering = shapeOrdering s,
-- shapes = shapesr s :: [Shape Double]
-- }
-- -- loop conn (trRaw "state:" nexts)
-- loop (clientID, conn, updateState clientState nexts)