CurryInfo: cass-4.1.0 / CASS.Server

classes:

              
documentation:
--------------------------------------------------------------------------
--- This is the main module of the analysis server.
--- It provides operations to initialize the server system,
--- start the server on a socket, or use the analysis server
--- by other Curry applications.
---
--- @author Heiko Hoffmann, Michael Hanus
--- @version January 2025
--------------------------------------------------------------------------
name:
CASS.Server
operations:
analyzeFunctionForBrowser analyzeGeneric analyzeGenericWithDebug analyzeGenericWithOptions analyzeInterface analyzeModuleAndPrint analyzeModuleAsText analyzeModuleForBrowser analyzePublic initializeAnalysisSystem mainServer
sourcecode:
module CASS.Server
  ( mainServer, initializeAnalysisSystem
  , analyzeModuleAndPrint, analyzeModuleAsText
  , analyzeModuleForBrowser, analyzeFunctionForBrowser
  , analyzeGeneric, analyzeGenericWithDebug, analyzeGenericWithOptions
  , analyzePublic, analyzeInterface
  ) where

import Numeric            ( readNat )
import Data.Char          ( isSpace, toLower )
import Control.Monad      ( unless )
import RW.Base
import System.CurryPath   ( runModuleAction )
import System.Directory
import System.FilePath
import System.IO
import System.Process     ( system, sleep )
import System.Environment

import Analysis.Logging   ( DLevel, debugMessage )
import Analysis.ProgInfo
import Analysis.Types     ( Analysis, AOutFormat(..) )
import FlatCurry.Types    ( QName )
import Network.Socket     ( Socket(..), listenOn, listenOnFresh
                          , close, waitForSocketAccept )

import CASS.Configuration
import CASS.RCFile          ( readRCFile )
import CASS.Registry
import CASS.ServerFormats
import CASS.ServerFunctions ( WorkerMessage(..) )

-- Messages to communicate with the analysis server from external programs.
data AnalysisServerMessage =
    GetAnalysis
  | AnalyzeModule  String OutputFormat String Bool
  | AnalyzeEntity  String OutputFormat String String
  | StopServer
  | SetCurryPath String
  | ParseError

--- Initializations to be done when the system is started.
initializeAnalysisSystem :: IO ()
initializeAnalysisSystem = readRCFile >> return ()

--- Start the analysis server on a socket.
mainServer :: CConfig -> Maybe Int -> IO ()
mainServer cconfig mbport = do
  putStrLn "Start Server"
  (port1,socket1) <- maybe listenOnFresh
                           (\p -> listenOn p >>= \s -> return (p,s))
                           mbport
  putStrLn ("Server Port: "++show port1)
  storeServerPortNumber port1
  getDefaultPath cconfig >>= setEnv "CURRYPATH"
  let numworkers = numberOfWorkers cconfig
  if numworkers > 0
   then do
    serveraddress <- getServerAddress
    (workerport,workersocket) <- listenOnFresh
    debugMessage dl 2 ("SERVER: port to workers: "++show workerport)
    handles <- startWorkers cconfig numworkers workersocket serveraddress
                            workerport []
    serverLoop cconfig socket1 handles
    close workersocket
   else
    serverLoop cconfig socket1 []
 where dl = debugLevel cconfig

--- Run the analysis system and print the analysis results in standard textual
--- representation.
--- If the fourth argument is true, all operations are shown,
--- otherwise only the interface operations.
--- If the fifth argument is false, generated operations (e.g.,
--- operations of derived class instances) are not shown.
--- The sixth argument is a flag indicating whether the
--- (re-)analysis should be enforced.
analyzeModuleAndPrint :: CConfig -> String -> String -> Bool -> OutputFormat
                      -> Bool -> Bool -> IO ()
analyzeModuleAndPrint cconfig ananame mname optall format optgenerated enforce =
  analyzeProgram cconfig ananame enforce format (format2AOut format) mname >>=
  putStr . formatResult mname format Nothing (not optall) optgenerated

format2AOut :: OutputFormat -> AOutFormat
format2AOut format = if format == FormatShort then ANote else AText

--- Run the analysis system and show the analysis results in standard textual
--- representation.
--- If the fourth argument is true, all operations are shown,
--- otherwise only the interface operations.
--- If the fifth argument is false, generated operations (e.g.,
--- operations of derived class instances) are not shown.
--- The sixth argument is a flag indicating whether the
--- (re-)analysis should be enforced.
--- Note that, before its first use, the analysis system must be initialized
--- by 'initializeAnalysisSystem'.
analyzeModuleAsText :: CConfig -> String -> String -> Bool -> Bool -> Bool
                    -> IO String
analyzeModuleAsText cconfig ananame mname optall optgenerated enforce =
  analyzeProgram cconfig ananame enforce FormatText AText mname >>=
  return . formatResult mname FormatText Nothing (not optall) optgenerated

--- Run the analysis system to show the analysis results in the BrowserGUI.
--- The options are read from the rc file.
analyzeModuleForBrowser :: String -> String -> AOutFormat -> IO [(QName,String)]
analyzeModuleForBrowser ananame mname aoutformat = do
  cconfig <- readRCFile
  analyzeProgram cconfig ananame False FormatText aoutformat mname >>=
    return . either pinfo2list (const [])
 where
   pinfo2list pinfo = let (pubinfo,privinfo) = progInfo2Lists pinfo
                      in pubinfo ++ privinfo

--- Run the analysis system to show the analysis result of a single function
--- in the BrowserGUI. The options are read from the rc file.
analyzeFunctionForBrowser :: String -> QName -> AOutFormat -> IO String
analyzeFunctionForBrowser ananame qn@(mname,_) aoutformat = do
  cconfig <- readRCFile
  analyzeProgram cconfig ananame False FormatText aoutformat mname >>=
    return . either (maybe "" id . lookupProgInfo qn) (const "")

--- Analyze a given program (i.e., a module possibly prefixed with a
--- directory name) for a given analysis result format.
--- The third argument is a flag indicating whether the
--- (re-)analysis should be enforced.
analyzeProgram :: CConfig -> String -> Bool -> OutputFormat -> AOutFormat
               -> String -> IO (Either (ProgInfo String) String)
analyzeProgram cconfig ananame enforce outformat aoutformat progname =
  runModuleAction (analyzeModule cconfig ananame enforce outformat aoutformat) progname

--- Analyze a complete module for a given analysis result format.
--- The third argument is a flag indicating whether the
--- (re-)analysis should be enforced.
analyzeModule :: CConfig -> String -> Bool -> OutputFormat -> AOutFormat
              -> String -> IO (Either (ProgInfo String) String)
analyzeModule cconfig ananame enforce outformat aoutformat modname = do
  getDefaultPath cconfig >>= setEnv "CURRYPATH"
  let numworkers = numberOfWorkers cconfig
  if numworkers > 0
    then do
     serveraddress <- getServerAddress
     (port,socket) <- listenOnFresh
     handles <- startWorkers cconfig numworkers socket serveraddress port []
     result <- runAnalysisWithWorkers cconfig ananame outformat aoutformat
                                      enforce handles modname
     stopWorkers handles
     close socket
     return result
    else runAnalysisWithWorkers cconfig ananame outformat aoutformat enforce
                                [] modname

--- Start the analysis system with a particular analysis.
--- The analysis must be a registered one if workers are used.
--- If it is a combined analysis, the base analysis must be also
--- a registered one. The options are read from the rc file.
--- Returns either the analysis information or an error message.
analyzeGeneric :: (Eq a, Read a, Show a, ReadWrite a)
               => Analysis a -> String -> IO (Either (ProgInfo a) String)
analyzeGeneric = analyzeGenericWithDebug Nothing

--- Start the analysis system with a particular analysis and
--- an optional debug level (first argument).
--- The analysis must be a registered one if workers are used.
--- If it is a combined analysis, the base analysis must be also
--- a registered one. The options are read from the rc file.
--- Returns either the analysis information or an error message.
analyzeGenericWithDebug :: (Eq a, Read a, Show a, ReadWrite a) =>
  Maybe Int -> Analysis a -> String -> IO (Either (ProgInfo a) String)
analyzeGenericWithDebug debuglevel analysis moduleName = do
  configrc <- readRCFile
  let cconfig = maybe configrc
                      (\dl -> setDebugLevel dl configrc)
                      debuglevel
  analyzeGenericWithOptions cconfig analysis moduleName

--- Start the analysis system with a particular analysis and
--- some options of CASS (first argument).
--- The analysis must be a registered one if workers are used.
--- If it is a combined analysis, the base analysis must be also
--- a registered one. The options are read from the rc file.
--- Returns either the analysis information or an error message.
analyzeGenericWithOptions :: (Eq a, Read a, Show a, ReadWrite a) =>
  CConfig -> Analysis a -> String -> IO (Either (ProgInfo a) String)
analyzeGenericWithOptions cconfig analysis moduleName = do
  let (mdir,mname) = splitFileName moduleName
  getDefaultPath cconfig >>= setEnv "CURRYPATH"
  curdir <- getCurrentDirectory
  unless (mdir==".") $ setCurrentDirectory mdir
  let numworkers = numberOfWorkers cconfig
  aresult <-
    if numworkers > 0
     then do
      serveraddress <- getServerAddress
      (port,socket) <- listenOnFresh
      handles <- startWorkers cconfig numworkers socket serveraddress port []
      result <- analyzeMain cconfig analysis mname handles False True
      stopWorkers handles
      close socket
      return result
     else
      analyzeMain cconfig analysis mname [] False True
  setCurrentDirectory curdir
  return aresult

--- Start the analysis system with a given analysis to compute properties
--- of a module interface.
--- The analysis must be a registered one if workers are used.
--- If it is a combined analysis, the base analysis must be also
--- a registered one.
--- Returns either the analysis information or an error message.
analyzePublic :: (Eq a, Read a, Show a, ReadWrite a)
              => Analysis a -> String -> IO (Either (ProgInfo a) String)
analyzePublic analysis moduleName =
  analyzeGeneric analysis moduleName
  >>= return . either (Left . publicProgInfo) Right

--- Start the analysis system with a given analysis to compute properties
--- of a module interface.
--- The analysis must be a registered one if workers are used.
--- If it is a combined analysis, the base analysis must be also
--- a registered one.
analyzeInterface :: (Eq a, Read a, Show a, ReadWrite a)
                 => Analysis a -> String -> IO (Either [(QName,a)] String)
analyzeInterface analysis moduleName =
  analyzeGeneric analysis moduleName
  >>= return . either (Left . publicListFromProgInfo) Right

--------------------------------------------------------------------------
-- start a number of workers at server start
startWorkers:: CConfig -> Int -> Socket -> String -> Int -> [Handle]
            -> IO [Handle]
startWorkers cconfig number workersocket serveraddress workerport handles = do
  if number > 0
    then do
      debugMessage dl 4 ("Number:"++(show number))
      let command = unwords [ executableName, " --worker "
                            , serveraddress, show workerport, "&" ]
      debugMessage dl 4 ("system command: " ++ command)
      system command
      debugMessage dl 4 ("Wait for socket accept for client "++show number)
      connection <- waitForSocketAccept workersocket waitTime
      debugMessage dl 4 ("Socket accept for client "++show number)
      case connection of
        Just (_,handle) -> do
          startWorkers cconfig (number-1) workersocket serveraddress workerport
                       (handle:handles)
        Nothing -> do
          putStrLn ("startWorkers: connection error worker "++(show number))
          startWorkers cconfig (number-1) workersocket serveraddress workerport
                       handles
    else return handles
 where dl = debugLevel cconfig

-- stop all workers at server stop
stopWorkers :: [Handle] -> IO ()
stopWorkers [] = return ()
stopWorkers (handle:whandles) = do
  hPutStrLn handle (show StopWorker)
  hClose handle
  stopWorkers whandles

--------------------------------------------------------------------------
-- server loop to answer analysis requests over network
serverLoop :: CConfig -> Socket -> [Handle] -> IO ()
serverLoop cconfig socket1 whandles = do
  --debugMessage 3 "SERVER: serverLoop"
  connection <- waitForSocketAccept socket1 waitTime
  case connection of
    Just (_,handle) -> serverLoopOnHandle cconfig socket1 whandles handle
    Nothing -> do
      putStrLn "serverLoop: connection error: time out in waitForSocketAccept"
      sleep 1
      serverLoop cconfig socket1 whandles

--- Reads a line from an input handle and returns it.
hGetLineUntilEOF  :: Handle -> IO String
hGetLineUntilEOF h = do
  eof <- hIsEOF h
  if eof
   then return ""
   else do c <- hGetChar h
           if c=='\n' then return ""
                      else do cs <- hGetLineUntilEOF h
                              return (c:cs)

serverLoopOnHandle :: CConfig -> Socket -> [Handle] -> Handle -> IO ()
serverLoopOnHandle cconfig socket1 whandles handle = do
  eof <- hIsEOF handle
  if eof
   then do hClose handle
           debugMessage dl 2 "SERVER connection: eof"
           serverLoop cconfig socket1 whandles
   else do
     string <- hGetLineUntilEOF handle
     debugMessage dl 2 ("SERVER got message: "++string)
     let force = False
     case parseServerMessage string of
       ParseError -> do
         sendServerError dl handle ("Illegal message received: "++string)
         serverLoopOnHandle cconfig socket1 whandles handle
       GetAnalysis -> do
         sendServerResult handle showAnalysisNamesAndFormats
         serverLoopOnHandle cconfig socket1 whandles handle
       AnalyzeModule ananame outform modname public ->
         catch (runAnalysisWithWorkers cconfig ananame outform
                 (format2AOut outform) force whandles modname >>=
                return . formatResult modname outform Nothing public True >>=
                sendResult)
               sendAnalysisError
       AnalyzeEntity ananame outform modname functionName ->
         catch (runAnalysisWithWorkers cconfig ananame outform
                 (format2AOut outform) force whandles modname >>=
                return . formatResult modname outform (Just functionName)
                                      False True >>= sendResult)
               sendAnalysisError
       SetCurryPath path -> do
         setEnv "CURRYPATH" path
         changeWorkerPath path whandles
         sendServerResult handle ""
         serverLoopOnHandle cconfig socket1 whandles handle
       StopServer -> do
         stopWorkers whandles
         sendServerResult handle ""
         hClose handle
         close socket1
         putStrLn "Stop Server"
         removeServerPortNumber
 where
  dl = debugLevel cconfig

  sendResult resultstring = do
    debugMessage dl 4 ("formatted result:\n"++resultstring)
    sendServerResult handle resultstring
    serverLoopOnHandle cconfig socket1 whandles handle

  sendAnalysisError err = do
    sendServerError dl handle ("ERROR in analysis server: "++ show err)
    serverLoopOnHandle cconfig socket1 whandles handle

-- Send a server result in the format "ok <n>\n<result text>" where <n>
-- is the number of lines of the <result text>.
sendServerResult :: Handle -> String -> IO ()
sendServerResult handle resultstring = do
  let resultlines = lines resultstring
  hPutStrLn handle ("ok " ++ show (length resultlines))
  hPutStr handle (unlines resultlines)
  hFlush handle

-- Send a server error in the format "error <error message>\n".
sendServerError :: DLevel -> Handle -> String -> IO ()
sendServerError dl handle errstring = do
  debugMessage dl 1 errstring
  hPutStrLn handle ("error "++errstring)
  hFlush handle

-- Inform the worker threads about a given changed library search path
changeWorkerPath :: String -> [Handle] -> IO ()
changeWorkerPath _ [] = return ()
changeWorkerPath path (handle:whandles) = do
  hPutStrLn handle (show (ChangePath path))
  changeWorkerPath path whandles

-- parse incoming message for type of request
parseServerMessage :: String -> AnalysisServerMessage
parseServerMessage message = case words message of
  [] -> ParseError
  w:ws -> case w of
    "GetAnalysis" -> GetAnalysis
    "AnalyzeModule" -> case ws of
      s1:s2:s3:[] -> checkFormat s2 $ \f -> AnalyzeModule s1 f s3 False
      _ -> ParseError
    "AnalyzeInterface" -> case ws of
      s1:s2:s3:[] -> checkFormat s2 $ \f -> AnalyzeModule s1 f s3 True
      _ -> ParseError
    "AnalyzeFunction" -> case ws of
      s1:s2:s3:s4:[] -> checkFormat s2 $ \f -> AnalyzeEntity s1 f s3 s4
      _ -> ParseError
    "AnalyzeTypeConstructor" -> case ws of
      s1:s2:s3:s4:[] -> checkFormat s2 $ \f -> AnalyzeEntity s1 f s3 s4
      _ -> ParseError
    "AnalyzeDataConstructor" -> case ws of
      s1:s2:s3:s4:[] -> checkFormat s2 $ \f -> AnalyzeEntity s1 f s3 s4
      _ -> ParseError
    "SetCurryPath" -> case ws of
      s:[] -> SetCurryPath s
      _ -> ParseError
    "StopServer" -> StopServer
    _ -> ParseError
 where
  checkFormat fmt fmsg =
    maybe ParseError fmsg (lookup (map toLower fmt) serverFormatNames)

--- Show all analysis names and formats.
showAnalysisNamesAndFormats :: String
showAnalysisNamesAndFormats =
  unlines (concatMap (\an -> map ((an++" ") ++) (map fst serverFormatNames))
                     registeredAnalysisNames)
types:

              
unsafe:
unsafe due to modules CASS.Registry Analysis.NondetOps System.IO.Unsafe Analysis.UnsafeModule