module Ganeti.UDSServer
( ConnectConfig(..)
, Client
, Server
, RecvResult(..)
, MsgKeys(..)
, strOfKey
, openClientSocket
, closeClientSocket
, openServerSocket
, closeServerSocket
, acceptSocket
, connectClient
, connectServer
, acceptClient
, closeClient
, closeServer
, buildResponse
, parseCall
, recvMsg
, recvMsgExt
, sendMsg
, Handler(..)
, HandlerResult
, listener
) where
import Control.Applicative
import Control.Concurrent (forkIO)
import Control.Exception (catch)
import Data.IORef
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.ByteString.Lazy.UTF8 as UTF8L
import Data.Word (Word8)
import Control.Monad
import qualified Network.Socket as S
import System.Directory (removeFile)
import System.IO (hClose, hFlush, hWaitForInput, Handle, IOMode(..))
import System.IO.Error (isEOFError)
import System.Timeout
import Text.JSON (encodeStrict, decodeStrict)
import qualified Text.JSON as J
import Text.JSON.Types
import Ganeti.BasicTypes
import Ganeti.Errors (GanetiException)
import Ganeti.JSON
import Ganeti.Logging
import Ganeti.Runtime (GanetiDaemon(..), MiscGroup(..), GanetiGroup(..))
import Ganeti.THH
import Ganeti.Utils
withTimeout :: Int -> String -> IO a -> IO a
withTimeout secs descr action = do
result <- timeout (secs * 1000000) action
case result of
Nothing -> fail $ "Timeout in " ++ descr
Just v -> return v
data RecvResult = RecvConnClosed
| RecvError String
| RecvOk String
deriving (Show, Eq)
eOM :: Word8
eOM = 3
bEOM :: B.ByteString
bEOM = B.singleton eOM
data MsgKeys = Method
| Args
| Success
| Result
$(genStrOfKey ''MsgKeys "strOfKey")
data ConnectConfig = ConnectConfig
{ connDaemon :: GanetiDaemon
, recvTmo :: Int
, sendTmo :: Int
}
data Client = Client { socket :: Handle
, rbuf :: IORef B.ByteString
, clientConfig :: ConnectConfig
}
data Server = Server { sSocket :: S.Socket
, sPath :: FilePath
, serverConfig :: ConnectConfig
}
openClientSocket
:: Int
-> FilePath
-> IO Handle
openClientSocket tmo path = do
sock <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
withTimeout tmo "creating a connection" $
S.connect sock (S.SockAddrUnix path)
S.socketToHandle sock ReadWriteMode
closeClientSocket :: Handle -> IO ()
closeClientSocket = hClose
openServerSocket :: FilePath -> IO S.Socket
openServerSocket path = do
sock <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
S.bindSocket sock (S.SockAddrUnix path)
return sock
closeServerSocket :: S.Socket -> FilePath -> IO ()
closeServerSocket sock path = do
S.sClose sock
removeFile path
acceptSocket :: S.Socket -> IO Handle
acceptSocket sock = do
(clientSock, _) <- S.accept sock
S.socketToHandle clientSock ReadWriteMode
connectClient
:: ConnectConfig
-> Int
-> FilePath
-> IO Client
connectClient conf tmo path = do
h <- openClientSocket tmo path
rf <- newIORef B.empty
return Client { socket=h, rbuf=rf, clientConfig=conf }
connectServer :: ConnectConfig -> Bool -> FilePath -> IO Server
connectServer conf setOwner path = do
s <- openServerSocket path
when setOwner . setOwnerAndGroupFromNames path (connDaemon conf) $
ExtraGroup DaemonsGroup
S.listen s 5
return Server { sSocket=s, sPath=path, serverConfig=conf }
closeServer :: Server -> IO ()
closeServer server =
closeServerSocket (sSocket server) (sPath server)
acceptClient :: Server -> IO Client
acceptClient s = do
handle <- acceptSocket (sSocket s)
new_buffer <- newIORef B.empty
return Client { socket=handle
, rbuf=new_buffer
, clientConfig=serverConfig s
}
closeClient :: Client -> IO ()
closeClient = closeClientSocket . socket
sendMsg :: Client -> String -> IO ()
sendMsg s buf = withTimeout (sendTmo $ clientConfig s) "sending a message" $ do
let encoded = UTF8L.fromString buf
handle = socket s
BL.hPut handle encoded
B.hPut handle bEOM
hFlush handle
recvUpdate :: ConnectConfig -> Handle -> B.ByteString
-> IO (B.ByteString, B.ByteString)
recvUpdate conf handle obuf = do
nbuf <- withTimeout (recvTmo conf) "reading a response" $ do
_ <- hWaitForInput handle (1)
B.hGetNonBlocking handle 4096
let (msg, remaining) = B.break (eOM ==) nbuf
newbuf = B.append obuf msg
if B.null remaining
then recvUpdate conf handle newbuf
else return (newbuf, B.tail remaining)
recvMsg :: Client -> IO String
recvMsg s = do
cbuf <- readIORef $ rbuf s
let (imsg, ibuf) = B.break (eOM ==) cbuf
(msg, nbuf) <-
if B.null ibuf
then recvUpdate (clientConfig s) (socket s) cbuf
else return (imsg, B.tail ibuf)
writeIORef (rbuf s) nbuf
return $ UTF8.toString msg
recvMsgExt :: Client -> IO RecvResult
recvMsgExt s =
Control.Exception.catch (liftM RecvOk (recvMsg s)) $ \e ->
return $ if isEOFError e
then RecvConnClosed
else RecvError (show e)
parseCall :: (J.JSON mth, J.JSON args) => String -> Result (mth, args)
parseCall s = do
arr <- fromJResult "parsing top-level JSON message" $
decodeStrict s :: Result (JSObject JSValue)
let keyFromObj :: (J.JSON a) => MsgKeys -> Result a
keyFromObj = fromObj (fromJSObject arr) . strOfKey
(,) <$> keyFromObj Method <*> keyFromObj Args
buildResponse :: Bool
-> JSValue
-> String
buildResponse success args =
let ja = [ (strOfKey Success, JSBool success)
, (strOfKey Result, args)]
jo = toJSObject ja
in encodeStrict jo
logMsg
:: (Show e, J.JSON e, MonadLog m)
=> Handler i o
-> i
-> GenericResult e J.JSValue
-> m ()
logMsg handler req (Bad err) =
logWarning $ "Failed to execute request " ++ hInputLogLong handler req ++ ": "
++ show err
logMsg handler req (Ok result) = do
logDebug $ "Result (truncated): " ++ take 2000 (J.encode result)
logDebug $ "Successfully handled " ++ hInputLogShort handler req
prepareMsg
:: (J.JSON e)
=> GenericResult e J.JSValue
-> (Bool, J.JSValue)
prepareMsg (Bad err) = (False, J.showJSON err)
prepareMsg (Ok result) = (True, result)
type HandlerResult o = IO (Bool, GenericResult GanetiException o)
data Handler i o = Handler
{ hParse :: J.JSValue -> J.JSValue -> Result i
, hInputLogShort :: i -> String
, hInputLogLong :: i -> String
, hExec :: i -> HandlerResult o
}
handleJsonMessage
:: (J.JSON o)
=> Handler i o
-> i
-> HandlerResult J.JSValue
handleJsonMessage handler req = do
(close, call_result) <- hExec handler req
return (close, fmap J.showJSON call_result)
handleRawMessage
:: (J.JSON o)
=> Handler i o
-> String
-> IO (Bool, String)
handleRawMessage handler payload =
case parseCall payload >>= uncurry (hParse handler) of
Bad err -> do
let errmsg = "Failed to parse request: " ++ err
logWarning errmsg
return (False, buildResponse False (J.showJSON errmsg))
Ok req -> do
logDebug $ "Request: " ++ hInputLogLong handler req
(close, call_result_json) <- handleJsonMessage handler req
logMsg handler req call_result_json
let (status, response) = prepareMsg call_result_json
return (close, buildResponse status response)
handleClient
:: (J.JSON o)
=> Handler i o
-> Client
-> IO Bool
handleClient handler client = do
msg <- recvMsgExt client
logDebug $ "Received message: " ++ show msg
case msg of
RecvConnClosed -> logDebug "Connection closed" >>
return False
RecvError err -> logWarning ("Error during message receiving: " ++ err) >>
return False
RecvOk payload -> do
(close, outMsg) <- handleRawMessage handler payload
sendMsg client outMsg
return close
clientLoop
:: (J.JSON o)
=> Handler i o
-> Client
-> IO ()
clientLoop handler client = do
result <- handleClient handler client
if result
then clientLoop handler client
else closeClient client
listener
:: (J.JSON o)
=> Handler i o
-> Server
-> IO ()
listener handler server = do
client <- acceptClient server
_ <- forkIO $ clientLoop handler client
return ()