CurryInfo: cpns-3.0.0 / Network.CPNS

classes:

              
documentation:
------------------------------------------------------------------------------
--- Implementation of a Curry Port Name Server based on raw sockets.
--- It is used to implement the library `Network.NamedPorts` and
--- the library `Ports` for distributed programming with ports.
---
--- @author Michael Hanus
--- @version March 2021
------------------------------------------------------------------------------
name:
Network.CPNS
operations:
cpnsAlive cpnsStart cpnsStatus cpnsStop getPortInfo registerPort unregisterPort
sourcecode:
module Network.CPNS
  ( registerPort, getPortInfo, unregisterPort
  , cpnsStart, cpnsStop, cpnsStatus, cpnsAlive
  )
 where

import Control.Monad      ( unless )
import Data.List          ( delete )
import System.Environment ( getHostname )
import System.IO

import Data.Time          ( calendarTimeToString, getLocalTime )
import System.Process     ( getPID, system )

import Debug.Profile      ( getProcessInfos, showMemInfo )
import Network.Socket     ( Socket, accept, connectToSocket, listenOn )

import Network.CPNS.Config

--- Type of messages to be processed by the Curry Port Name Server.
---
--- @cons Register name pid sn pn
---       -  assign the values pid, sn, and pn to name
---          (pid is the process number of the registered process
---           (should be 0 if it is unknown); the server returns True
---           if registration had no problems, otherwise False)
--- @cons GetRegister name - request for a registered port name;
---         the server returns the values (sn,pn) that are assigned to the
---         port name
--- @cons Unregister name - request to remove a registered port name
--- @cons ShowRegistry - show status and current port registrations
--- @cons Ping         - ping the CPNS demon for liveness check
--- @cons Terminate    - terminate the CPNS demon
data CPNSMessage = Terminate
                 | Ping
                 | Register String Int Int Int
                 | GetRegister String
                 | Unregister String
                 | ShowRegistry
  deriving (Read, Show)

--- Starts the "Curry Port Name Server" (CPNS) running on the local machine.
--- The CPNS is responsible to resolve symbolic names for ports
--- into physical socket numbers so that a port can be reached
--- under its symbolic name from any machine in the world.

cpnsStart :: IO ()
cpnsStart = catch startup
                  (\_ -> addLogLn "FAILURE occurred during startup!" >>
                         deleteStartupLockfile >>
                         return Nothing) >>=
             maybe (return ()) (cpnsServer [])
 where
   deleteStartupLockfile = do
     lockfile <- getStartupLockFile
     addLogLn $ "Removing startup lock file \"" ++ lockfile ++ "\"..."
     system $ "/bin/rm -f " ++ lockfile
     return ()

   startup = do
     addLogLn $ "Starting Curry Port Name Server on port " ++
                show cpnsSocket ++ "..."
     socket <- listenOn cpnsSocket
     deleteStartupLockfile
     pid <- getPID
     addLogLn $ "Curry Port Name Server is ready (PID: "++show pid++")."
     return (Just socket)

--- The main loop of the CPNS demon
cpnsServer :: [(String,Int,Int,Int)] -> Socket -> IO ()
cpnsServer regs socket = do
  (chost,stream) <- accept socket
  --addLogLn $ "Connection from "++chost
  serveRequest chost stream
 where
   doIfLocalHost rhost action = do
     hostname <- getHostname
     if rhost `elem` ["localhost","localhost.localdomain",hostname]
        || take 8 rhost == "127.0.0."
      then action
      else do addLogLn $ "Illegal request received from host: " ++ rhost
              cpnsServer regs socket

   serveRequest rhost h = do
     msg <- readMsgLine h
     either
      (\line -> do addLogLn $ "ERROR: Illegal message:\n" ++ line
                   cpnsServer regs socket )
      (\m -> case m of
        Terminate -> doIfLocalHost rhost $ do
          hClose h
          addLogLn "CPNS demon terminated."
        Ping -> do
          hPutStrLn h (show True)
          hClose h
          cpnsServer regs socket
        Register pname pid sn pn -> doIfLocalHost rhost $ do
          (ack, newregs) <- tryRegisterPortName regs pname pid sn pn
          hPutStrLn h (show ack)
          hClose h
          cpnsServer (id $!! newregs) socket
        GetRegister pname -> do
          --addLogLn $ "Request for port name: " ++ pname
          (newregs,pinfo) <- getRegisteredPortName regs pname
          hPutStrLn h (show pinfo)
          hClose h
          cpnsServer (id $!! newregs) socket
        Unregister pname -> doIfLocalHost rhost $ do
          newregs <- unregisterPortName regs pname
          hClose h
          cpnsServer (id $!! newregs) socket
        ShowRegistry -> doIfLocalHost rhost $ do
          newregs <- getAndCleanRegs regs
          meminfo <- getMemInfo regs
          pid     <- getPID
          hPutStrLn h (show (pid,meminfo,newregs))
          hClose h
          cpnsServer (id $!! newregs) socket )
      msg

tryRegisterPortName :: [(String,Int,Int,Int)] -> String -> Int -> Int -> Int
                    -> IO (Bool, [(String, Int, Int, Int)])
tryRegisterPortName regs name pid sn pn = do
  let nameregs = filter (\(n,_,_,_) -> name==n) regs
  ack <- if null nameregs
         then return True
         else let (_,pid',_,_) = head nameregs in
              if pid'>0 && pid'/=pid
              -- we allow registration from the same process
              then doesProcessExist pid' >>= \pex -> return (not pex)
              else return True
  ctime <- getLocalTime
  addLogLn $ "Register port \""++name++"\": pid "++show pid++
             " / socket "++show sn++
             " / number "++show pn ++ " at " ++ calendarTimeToString ctime
  let newregs = (name,pid,sn,pn) : filter (\ (n,_,_,_) -> name/=n) regs
  getMemInfo newregs >>= addLogLn
  hFlush stdout
  return (ack, newregs)

-- Delete all registrations for a given port name:
unregisterPortName :: [(String,Int,Int,Int)] -> String
                   -> IO [(String,Int,Int,Int)]
unregisterPortName regs name = do
  ctime <- getLocalTime
  addLogLn $ "Unregister port \""++name++"\" at "++calendarTimeToString ctime
  let newregs = filter (\ (n,_,_,_) -> name/=n) regs
  getMemInfo newregs >>= addLogLn
  hFlush stdout
  return newregs

-- Get the socket number for a registered port name
-- (or (0,0) if not registered).
-- In addition, a new registration list is returned where a registration
-- is deleted if the corresponding server process does not exist.
getRegisteredPortName :: [(String,Int,Int,Int)] -> String
                      -> IO ([(String,Int,Int,Int)],(Int,Int))
getRegisteredPortName regs pname =
  let nameregs = filter (\(n,_,_,_)->pname==n) regs in
  if null nameregs
  then return (regs,(0,0))
  else let (_,pid,sn,pn) = head nameregs in
       if pid>0
       then doesProcessExist pid >>= \pex ->
            if pex
            then return (regs,(sn,pn))
            else --addLogLn ("WARNING: Process "++show pid++" not running!") >>
                 return (delete (head nameregs) regs, (0,0))
       else return (regs,(sn,pn))

-- Returns the registration list but delete a registration
-- if the corresponding server process does not exist.
getAndCleanRegs :: [(String,Int,Int,Int)] -> IO [(String,Int,Int,Int)]
getAndCleanRegs regs = do
  newreglist <- mapM checkAndShow regs
  return (concat newreglist)
 where
  checkAndShow reg@(_,pid,_,_) = do
    pidexist <- doesProcessExist pid
    return $ if pidexist then [reg] else []

-- Returns memory status information of current CPNS demon process as string.
getMemInfo :: [(String,Int,Int,Int)] -> IO String
getMemInfo regs = do
  pinfos <- getProcessInfos
  return $ "NumRegs: " ++ show (length regs) ++ ", " ++ showMemInfo pinfos

-- Tests whether a process with a given pid is running.
doesProcessExist :: Int -> IO Bool
doesProcessExist pid = do
  status <- system("test -z \"`ps -p "++show pid++" | fgrep "++show pid++"`\"")
  return (status>0)

-- Read a line from a stream and check syntactical correctness of message:
readMsgLine :: Read a => Handle -> IO (Either String a)
readMsgLine handle = do
  line <- hGetLine handle
  case reads line of
     [(msg,rem)] -> return (if all isSpace rem then Right msg else Left line)
     _ -> return (Left line)

-- Perform an action if the CPNS demon at a given host is alive:
doIfAlive :: String -> IO a -> IO a
doIfAlive host action = do
  alive <- cpnsAlive host
  if not alive
   then error $ timeOutMessage host
   else action

timeOutMessage :: String -> String
timeOutMessage host =
  "Curry port name server at host \"" ++ host ++
  "\" is not reachable (timeout after " ++ show cpnsTimeOut ++ " ms)!"

sendToLocalCPNS :: CPNSMessage -> IO ()
sendToLocalCPNS msg = doIfAlive "localhost" $ do
  h <- connectToSocket "localhost" cpnsSocket
  hPutStrLn h (show msg)
  hClose h

--- Shows all registered ports at the local CPNS demon (in its logfile).
cpnsStatus :: IO ()
cpnsStatus = do
  (pid,meminfo,regs) <- cpnsTryGetAnswer "localhost" ShowRegistry
  putStrLn $ "CPNSD PID: " ++ show (pid :: Int)
  putStrLn $ "Memory information:\n" ++ meminfo
  putStrLn "Currently registered port names:"
  putStrLn $ unlines $ map showReg regs

showReg :: (String,Int,Int,Int) -> String
showReg (n,pid,sn,pn) = n ++ ": pid " ++ show pid ++
                        " / socket " ++ show sn ++ " / number " ++ show pn

--- Terminates the local CPNS demon
cpnsStop :: IO ()
cpnsStop = sendToLocalCPNS Terminate

--- Gets an answer from a Curry port name server on a host,
--- or reports an error.
cpnsTryGetAnswer :: Read a => String -> CPNSMessage -> IO a
cpnsTryGetAnswer host msg = catch tryGetAnswer connectError
 where
  tryGetAnswer = do
    h <- connectToSocket host cpnsSocket
    hPutStrLn h (show msg)
    hFlush h
    ready <- hWaitForInput h cpnsTimeOut
    if ready
     then do
       answer <- readMsgLine h
       hClose h
       either (\line -> error $ "cpnsTryGetAnswer: Illegal answer: " ++ line)
              return
              answer
     else error $ timeOutMessage host

  connectError _ = error $ "Curry port name server at host \""++host++
                           "\" is not reachable!"

--- Registers a symbolic port at the local host.
--- The symbolic name, the socket number, and the port number are passed
--- as arguments.
registerPort :: String -> Int -> Int -> IO ()
registerPort pname sn pn = do
  startCPNSDIfNecessary
  pid <- getPID
  ack <- cpnsTryGetAnswer "localhost" (Register pname pid sn pn)
  if ack then return ()
         else addLogLn ("WARNING: Port name '"++pname++"' already registered!")

--- Gets the information about a symbolic port (first argument)
--- at some host (second argument).
--- If there is no registration, `(0,0)` is returned, otherwise a pair
--- consisting of a socket and port number.
getPortInfo :: String -> String -> IO (Int,Int)
getPortInfo pname host = cpnsTryGetAnswer host (GetRegister pname)

--- Unregisters a symbolic port at the local host.
unregisterPort :: String -> IO ()
unregisterPort pname = sendToLocalCPNS (Unregister pname)

--- Tests whether the CPNS demon at a host is alive, i.e.,
--- reacts on `Ping` message.
cpnsAlive :: String -> IO Bool
cpnsAlive host = catch tryPingCPNS (\_ -> return False)
 where
  tryPingCPNS = do
    h <- connectToSocket host cpnsSocket
    hPutStrLn h (show Ping)
    hFlush h
    answer <- hWaitForInput h cpnsTimeOut
    hClose h
    return answer

--- Starts the CPNS demon at localhost if it is not already running:
startCPNSDIfNecessary :: IO ()
startCPNSDIfNecessary = do
  alive <- cpnsAlive "localhost"
  unless alive $ do
    cpnsbin <- getCPNSD
    system $ cpnsbin ++ " start"
    return ()

{-
Testing:

Network.CPNS> registerPort "xxx" 42 2
Network.CPNS> getPortInfo "xxx" "localhost"
(42,2)

-}
types:

              
unsafe:
safe