roshask-0.2: src/Ros/Graph/Slave.hs
{-# LANGUAGE CPP, OverloadedStrings #-}
module Ros.Graph.Slave (RosSlave(..), runSlave, requestTopicClient,
cleanupNode) where
import Control.Applicative
import Control.Concurrent (killThread, forkIO, threadDelay, MVar, putMVar,
isEmptyMVar, readMVar, modifyMVar_)
import Control.Concurrent.SSem (SSem)
import qualified Control.Concurrent.SSem as Sem
import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteString.UTF8 ()
import qualified Data.ByteString.Lazy.UTF8 as BLU
import Snap.Http.Server (simpleHttpServe)
import Snap.Http.Server.Config (defaultConfig, setPort, Config, ConfigLog(..),
setVerbose, setAccessLog, setErrorLog)
-- import Snap.Types (Snap, getRequestBody, writeLBS,
-- getResponse, putResponse, setContentLength)
import Snap.Core (Snap, readRequestBody, writeLBS, getResponse, putResponse,
setContentLength)
import Network.Socket hiding (Stream)
import qualified Network.Socket as Net
import Network.XmlRpc.Internals (Value)
import Network.XmlRpc.Server (handleCall, methods, fun)
import Network.XmlRpc.Client (remote)
#ifndef mingw32_HOST_OS
import System.Posix.Process (getProcessID)
#endif
import System.Process (readProcess)
import Ros.Internal.RosTypes
import Ros.Topic.Stats (PubStats(PubStats), SubStats(SubStats))
import Ros.Graph.Master
class RosSlave a where
getMaster :: a -> URI
getNodeName :: a -> String
getNodeURI :: a -> MVar URI
getSubscriptions :: a -> IO [(TopicName, TopicType, [(URI, SubStats)])]
getPublications :: a -> IO [(TopicName, TopicType, [(URI, PubStats)])]
publisherUpdate :: a -> TopicName -> [URI] -> IO ()
getTopicPortTCP :: a -> TopicName -> Maybe Int
setShutdownAction :: a -> IO () -> IO ()
stopNode :: a -> IO ()
#ifdef mingw32_HOST_OS
getProcessID :: IO Int
getProcessID = return 42
#endif
-- |Unregister all of a node's publishers and subscribers, then stop
-- the node's servers.
cleanupNode :: RosSlave n => n -> IO ()
cleanupNode n = do pubs <- getPublications n
subs <- getSubscriptions n
nuri <- readMVar (getNodeURI n)
let nname = getNodeName n
master = getMaster n
stop f (tname,_,_) = f nname tname nuri
mapM_ (stop (unregisterPublisher master)) pubs
mapM_ (stop (unregisterSubscriber master)) subs
stopNode n
type RpcResult a = IO (Int, String, a)
mkPublishStats :: (TopicName, a, [(URI, PubStats)]) ->
(TopicName, Int, [(Int, Int, Int, Bool)])
mkPublishStats (n, _, pstats) = (n, 0, map formatStats pstats)
where formatStats (_, (PubStats bytesSent numSent conn)) =
(0, bytesSent, numSent, conn)
mkSubStats :: (TopicName, a, [(URI, SubStats)]) ->
(String, Int, [(Int, Int, Int, Bool)])
mkSubStats (n, _, sstats) = (n, 0, map formatStats sstats)
where formatStats (_, (SubStats bytesReceived conn)) =
(0, bytesReceived, -1, conn)
getBusStats :: (RosSlave a) => a -> CallerID ->
RpcResult ([(String,Int,[(Int,Int,Int,Bool)])],
[(String,Int,[(Int,Int,Int,Bool)])],
(Int,Int,Int))
getBusStats n _ = do
publishStats <- map (mkPublishStats) <$> getPublications n
subscribeStats <- map (mkSubStats) <$> getSubscriptions n
let serviceStats = (0,0,0)
return (1, "", (publishStats, subscribeStats, serviceStats))
getBusInfo :: (RosSlave a) => a -> CallerID ->
RpcResult [(Int,String,String,String,String)]
getBusInfo n _ = do
pubs <- concatMap formatPubs <$> getPublications n
subs <- concatMap formatSubs <$> getSubscriptions n
return (1, "", pubs ++ subs)
where formatPubs (tname, _, stats) =
map (\(uri,_) -> (0, uri, "o", "TCPROS", tname)) stats
formatSubs (tname, _, stats) =
map (\(uri,_) -> (0, uri, "i", "TCPROS", tname)) stats
getMaster' :: RosSlave a => a -> CallerID -> IO (Int, String, URI)
getMaster' n _ = return (1, "", getMaster n)
shutdown' :: RosSlave a => a -> SSem -> CallerID -> IO (Int, String, Bool)
shutdown' n q _ = stopNode n >> Sem.signal q >> return (1, "", True)
-- This requires a dependency on the unix package and so is not cross
-- platform.
getPid' :: CallerID -> RpcResult Int
getPid' _ = do pid <- getProcessID
return (1, "", fromEnum pid)
getSubscriptions' :: RosSlave a => a -> CallerID -> RpcResult [(String, String)]
getSubscriptions' node _ = do
subs <- map (\(n,t,_) -> (n,t)) <$> getSubscriptions node
return (1, "", subs)
getPublications' :: RosSlave a => a -> CallerID -> RpcResult [(String, String)]
getPublications' node _ = do
pubs <- map (\(n,t,_) -> (n,t)) <$> getPublications node
return (1, "", pubs)
paramUpdate' :: RosSlave a => a -> CallerID -> String -> Value -> RpcResult Bool
paramUpdate' _n _ _paramKey _paramVal = do
putStrLn "paramUpdate not implemented!"
return (1, "", True)
pubUpdate :: RosSlave a => a -> CallerID -> TopicName -> [URI] -> RpcResult Int
pubUpdate n _ topic publishers = do publisherUpdate n topic publishers
return (1, "", 1)
-- Extract just the hostname or IP part of a Node's URI.
myName :: RosSlave a => a -> IO String
myName n = extractName `fmap` readMVar (getNodeURI n)
where extractName uri = takeWhile (/=':') $ drop 7 uri
requestTopic :: RosSlave a => a -> CallerID -> TopicName -> [[Value]] ->
RpcResult (String,String,Int)
requestTopic n _ topic _protocols =
case getTopicPortTCP n topic of
Just p -> do --putStrLn $ topic++" requested "++show p
host <- myName n
return (1, "", ("TCPROS",host,p))
Nothing -> return (0, "Unknown topic", ("TCPROS", "", 0))
requestTopicClient :: URI -> CallerID -> TopicName -> [[String]] ->
RpcResult (String,String,Int)
requestTopicClient = flip remote "requestTopic"
-- Dispatch an XML-RPC request body and return the response. The first
-- parameter is a value that provides the necessary reflective API as
-- to ROS Node state. The second parameter is a semaphore indicating
-- that the node should terminate.
slaveRPC :: (RosSlave a) => a -> SSem -> String -> IO BLU.ByteString
slaveRPC n = -- \q s -> putStrLn ("Slave call "++s)>>(handleCall (dispatch q) s)
handleCall . dispatch
where dispatch q = methods [ ("getBusStats", fun (getBusStats n))
, ("getBusInfo", fun (getBusInfo n))
, ("getMasterUri", fun (getMaster' n))
, ("shutdown", fun (shutdown' n q))
, ("getPid", fun getPid')
, ("getSubscriptions", fun (getSubscriptions' n))
, ("getPublications", fun (getPublications' n))
, ("paramUpdate", fun (paramUpdate' n))
, ("publisherUpdate", fun (pubUpdate n))
, ("requestTopic", fun (requestTopic n)) ]
-- Start a Snap webserver on the specified port with the specified
-- handler.
simpleServe :: Int -> Snap () -> IO ()
simpleServe port handler = simpleHttpServe conf handler
where conf :: Config Snap ()
conf = setAccessLog ConfigNoLog .
setErrorLog ConfigNoLog .
setVerbose False .
setPort port $
defaultConfig
-- Find a free port by opening a socket, getting its port, then
-- closing it.
findFreePort :: IO Int
findFreePort = do s <- socket AF_INET Net.Stream defaultProtocol
bindSocket s (SockAddrInet aNY_PORT iNADDR_ANY)
port <- fromInteger . toInteger <$> socketPort s
sClose s
return port
-- |Run a ROS slave node. Returns an action that will wait for the
-- node to shutdown along with the port the server is running on.
runSlave :: RosSlave a => a -> IO (IO (), Int)
runSlave n = do quitNow <- Sem.new 0
port <- findFreePort
let myUri = getNodeURI n
myPort = ":" ++ show port
myURIEmpty <- isEmptyMVar myUri
if myURIEmpty
then do myIP <- init <$> readProcess "hostname" [] ""
putMVar myUri $! "http://"++myIP++myPort
else modifyMVar_ myUri ((return $!) . (++myPort))
t <- forkIO $ simpleServe port (rpc (slaveRPC n quitNow))
let wait = do Sem.wait quitNow
-- Wait a second for the response to flush
threadDelay 1000000
stopNode n
killThread t
return (wait, port)
where rpc f = do body <- BLU.toString <$> readRequestBody 4096
response <- liftIO $ f body
writeLBS response
let len = fromIntegral $ BLU.length response
putResponse . setContentLength len =<< getResponse