packages feed

dbus-client-0.1: hs/DBus/Client.hs

{-
  Copyright (C) 2009 John Millikin <jmillikin@gmail.com>
  
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  any later version.
  
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  
  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-}


{-# LANGUAGE OverloadedStrings #-}
module DBus.Client
        (   Client
          , clientConnection

          , mkClient

          , RequestNameFlag (..)
          , RequestNameReply (..)
          , ReleaseNameReply (..)

          , requestName

          , releaseName

          , onSignal
          , signalFilter

          , RemoteObject (..)
          , Proxy (..)
          , call
          , callBlocking
          , onSignalFrom

          , LocalObject (..)
          , Member (..)
          , Interface (..)
          , MethodCall (..)
          , export

          , replyReturn
          , replyError

        ) where
import qualified Control.Concurrent.MVar as MV
import qualified Data.Map as Map
import qualified DBus.Connection as C
import qualified DBus.Constants as Const
import qualified DBus.Message as M
import qualified DBus.Types as T

import Data.Word (Word32)
import Data.Bits ((.|.))

import Control.Concurrent (forkIO)
import Control.Monad (forever)
import qualified Data.Set as Set

import Data.Maybe (isJust, fromJust, isNothing)

import qualified DBus.Introspection as I

import qualified Data.Text.Lazy as TL


type Callback = (M.ReceivedMessage -> IO ())

data Client = Client C.Connection
        (MV.MVar (Map.Map M.Serial Callback))
        (MV.MVar (Map.Map T.ObjectPath Callback))
        (MV.MVar [Callback])

clientConnection :: Client -> C.Connection
clientConnection (Client x _ _ _) = x

mkClient :: IO (C.Connection, T.BusName) -> IO Client
mkClient getBus = do
        (c, _) <- getBus
        replies <- MV.newMVar Map.empty
        exports <- MV.newMVar Map.empty
        signals <- MV.newMVar []
        let client = Client c replies exports signals
        forkIO $ forever (receiveMessages client)

        export client (T.mkObjectPath' "/") rootObject

        return client

send' :: M.Message a => C.Connection -> (M.Serial -> IO b) -> a
      -> IO b
send' c f msg = C.send c f msg >>= \result -> case result of
        Left x -> error $ show x
        Right x -> return x

data RequestNameFlag
        = AllowReplacement
        | ReplaceExisting
        | DoNotQueue
        deriving (Show)

data RequestNameReply
        = PrimaryOwner
        | InQueue
        | Exists
        | AlreadyOwner
        deriving (Show)

data ReleaseNameReply
        = Released
        | NonExistent
        | NotOwner
        deriving (Show)

dbus :: Proxy
dbus = Proxy (RemoteObject Const.dbusName Const.dbusPath) Const.dbusInterface

request, release :: T.MemberName
request = T.mkMemberName' "RequestName"
release = T.mkMemberName' "ReleaseName"

requestName :: Client -> T.BusName -> [RequestNameFlag] -> IO RequestNameReply
requestName client name flags = do
        returned <- callBlocking client dbus request []
                [ T.toVariant name
                , T.toVariant . encodeFlags $ flags]
        
        let reply = M.messageBody returned !! 0
        case decodeRequestReply =<< T.fromVariant reply of
                Just x  -> return x
                Nothing -> error $ "Unknown request name reply: " ++
                                   show reply

releaseName :: Client -> T.BusName -> IO ReleaseNameReply
releaseName client name = do
        returned <- callBlocking client dbus release [] [T.toVariant name]
        
        let reply = M.messageBody returned !! 0
        case decodeReleaseReply =<< T.fromVariant reply of
                Just x  -> return x
                Nothing -> error $ "Unknown release name reply: " ++
                                   show reply

encodeFlags :: [RequestNameFlag] -> Word32
encodeFlags = foldr (.|.) 0 . map flagValue where
        flagValue AllowReplacement = 0x1
        flagValue ReplaceExisting  = 0x2
        flagValue DoNotQueue       = 0x4

decodeRequestReply :: Word32 -> Maybe RequestNameReply
decodeRequestReply 1 = Just PrimaryOwner
decodeRequestReply 2 = Just InQueue
decodeRequestReply 3 = Just Exists
decodeRequestReply 4 = Just AlreadyOwner
decodeRequestReply _ = Nothing

decodeReleaseReply :: Word32 -> Maybe ReleaseNameReply
decodeReleaseReply 1 = Just Released
decodeReleaseReply 2 = Just NonExistent
decodeReleaseReply 3 = Just NotOwner
decodeReleaseReply _ = Nothing

receiveMessages :: Client -> IO ()
receiveMessages client@(Client c _ _ _) = do
        received <- C.receive c
        msg <- case received of
                Left x -> error $ show x
                Right x -> return x
        case msg of
                (M.ReceivedMethodCall   _ _ _   ) -> gotMethodCall client msg
                (M.ReceivedMethodReturn _ _ msg') -> gotReply client (M.methodReturnSerial msg') msg
                (M.ReceivedError        _ _ msg') -> gotReply client (M.errorSerial msg') msg
                (M.ReceivedSignal       _ _ _   ) -> gotSignal client msg
                _                                 -> return ()

gotMethodCall :: Client -> M.ReceivedMessage -> IO ()
gotMethodCall client@(Client _ _ mvar _) msg = do
        objects <- MV.readMVar mvar
        let M.ReceivedMethodCall _ _ call' = msg
        case Map.lookup (M.methodCallPath call') objects of
                Just x  -> x msg
                Nothing -> unknownMethod client msg

unknownMethod :: Client -> M.ReceivedMessage -> IO ()
unknownMethod client msg = send' c (const $ return ()) errorMsg where
        M.ReceivedMethodCall serial sender _ = msg
        c = clientConnection client
        errorMsg = M.Error
                Const.errorUnknownMethod
                serial sender
                (Set.fromList [M.NoReplyExpected, M.NoAutoStart])
                []

gotReply :: Client -> M.Serial -> M.ReceivedMessage -> IO ()
gotReply (Client _ mvar _ _) serial msg = do
        callback <- MV.modifyMVar mvar $ \callbacks -> let
                x = Map.lookup serial callbacks
                callbacks' = if isJust x
                        then Map.delete serial callbacks
                        else callbacks
                in return (callbacks', x)
        case callback of
                Just x -> forkIO (x msg) >> return ()
                Nothing -> return ()

gotSignal :: Client -> M.ReceivedMessage -> IO ()
gotSignal (Client _ _ _ mvar) msg = MV.withMVar mvar $
        mapM_ (\cb -> forkIO (cb msg))

onSignal :: Client
            -> (T.BusName -> M.Signal -> Bool)
            -> (T.BusName -> M.Signal -> IO ())
            -> IO ()
onSignal (Client _ _ _ mvar) test callback = MV.modifyMVar_ mvar addCallback where
        addCallback callbacks = return $ callback' : callbacks
        callback' (M.ReceivedSignal _ sender msg) =
                if test (fromJust sender) msg
                        then callback (fromJust sender) msg
                        else return ()
        callback' _ = undefined

signalFilter :: Maybe T.BusName -> Maybe T.ObjectPath -> Maybe T.InterfaceName
             -> Maybe T.MemberName -> T.BusName -> M.Signal -> Bool
signalFilter sender path iface member sender' msg = all id
        [ isNothing sender || sender == Just sender'
        , isNothing path   || path == Just (M.signalPath msg)
        , isNothing iface  || iface == Just (M.signalInterface msg)
        , isNothing member || member == Just (M.signalMember msg)
        ]

data RemoteObject = RemoteObject T.BusName T.ObjectPath
data Proxy = Proxy RemoteObject T.InterfaceName

call :: Client -> Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]
     -> (M.Error -> IO ())
     -> (M.MethodReturn -> IO ())
     -> IO ()
call client proxy name flags body onError onReturn = let
        Proxy (RemoteObject dest path) iface = proxy
        Client bus mvar _ _ = client
        msg = M.MethodCall path name (Just iface) (Just dest)
                (Set.fromList flags)
                body
        
        callback (M.ReceivedError _ _ z) = onError z
        callback (M.ReceivedMethodReturn _ _ z) = onReturn z
        callback _ = undefined
        
        addCallback s = MV.modifyMVar_ mvar $ \callbacks ->
                return $ Map.insert s callback callbacks
        in send' bus addCallback msg

callBlocking :: Client -> Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]
             -> IO M.MethodReturn
callBlocking client proxy name flags body = do
        mvar <- MV.newEmptyMVar
        call client proxy name flags body
                (MV.putMVar mvar . Left)
                (MV.putMVar mvar . Right)
        reply <- MV.takeMVar mvar
        case reply of
                Left x  -> error (show x)
                Right x -> return x

onSignalFrom :: Client -> Proxy -> T.MemberName -> (M.Signal -> IO ())
             -> IO ()
onSignalFrom client proxy member io = onSignal client test io' where
        Proxy (RemoteObject dest path) iface = proxy
        test = signalFilter (Just dest) (Just path) (Just iface)
                            (Just member)
        io' _ msg = io msg

rootObject :: LocalObject
rootObject = LocalObject $ Map.fromList [(ifaceName, interface)] where
        ifaceName = T.mkInterfaceName' "org.freedesktop.DBus.Introspectable"
        memberName = T.mkMemberName' "Introspect"
        inSig = T.mkSignature' ""
        outSig = T.mkSignature' "s"
        interface = Interface $ Map.fromList [(memberName, impl)]
        
        method = I.Method memberName [] [I.Parameter "xml" outSig]
        iface = I.Interface ifaceName [method] [] []
        path = T.mkObjectPath' "/"
        
        impl = Method inSig outSig $ \call' -> do
                let Client _ _ mvar _ = methodCallClient call'
                paths <- fmap Map.keys $ MV.readMVar mvar
                
                let paths' = filter (/= path) paths
                let Just xml = I.toXML $ I.Object path [iface]
                        [I.Object p [] [] | p <- paths']
                replyReturn call' [T.toVariant xml]

newtype LocalObject = LocalObject (Map.Map T.InterfaceName Interface)
newtype Interface = Interface (Map.Map T.MemberName Member)
data Member
        = Method T.Signature T.Signature (MethodCall -> IO ())
        | Signal T.Signature

export :: Client -> T.ObjectPath -> LocalObject -> IO ()
export client@(Client _ _ mvar _) path obj = MV.modifyMVar_ mvar $
        return . Map.insert path (onMethodCall client (addIntrospectable path obj))

addIntrospectable :: T.ObjectPath -> LocalObject -> LocalObject
addIntrospectable path (LocalObject ifaces) = LocalObject ifaces' where
        ifaces' = Map.insertWith (\_ x -> x) name iface ifaces
        name = T.mkInterfaceName' "org.freedesktop.DBus.Introspectable"
        iface = Interface $ Map.fromList [(T.mkMemberName' "Introspect", impl)]
        impl = Method (T.mkSignature' "") (T.mkSignature' "s") $ \call' -> do
                let Just xml = I.toXML . introspect path . methodCallObject $ call'
                replyReturn call' [T.toVariant xml]

introspect :: T.ObjectPath -> LocalObject -> I.Object
introspect path obj = I.Object path interfaces [] where
        LocalObject ifaceMap = obj
        interfaces = map introspectIface (Map.toList ifaceMap)
        
        introspectIface :: (T.InterfaceName, Interface) -> I.Interface
        introspectIface (name, iface) = I.Interface name methods signals [] where
                Interface memberMap = iface
                members = Map.toList memberMap
                methods = concatMap introspectMethod members
                signals = concatMap introspectSignal members
        
        introspectMethod :: (T.MemberName, Member) -> [I.Method]
        introspectMethod (name, (Method inSig outSig _)) = [I.Method name
                (map introspectParam (T.signatureTypes inSig))
                (map introspectParam (T.signatureTypes outSig))]
        introspectMethod _ = []
        
        introspectSignal :: (T.MemberName, Member) -> [I.Signal]
        introspectSignal (name, (Signal sig)) = [I.Signal name
                (map introspectParam (T.signatureTypes sig))]
        introspectSignal _ = []
        
        introspectParam = I.Parameter "" . T.mkSignature' . T.typeCode

data MethodCall = MethodCall
        { methodCallObject :: LocalObject
        , methodCallClient :: Client
        , methodCallMethod :: Member
        , methodCallSerial :: M.Serial
        , methodCallSender :: Maybe T.BusName
        , methodCallFlags  :: Set.Set M.Flag
        , methodCallBody   :: [T.Variant]
        }

findMember :: M.MethodCall -> LocalObject -> Maybe Member
findMember call' (LocalObject ifaces) = do
        iface <- M.methodCallInterface call'
        Interface members <- Map.lookup iface ifaces
        Map.lookup (M.methodCallMember call') members

onMethodCall :: Client -> LocalObject -> M.ReceivedMessage -> IO ()
onMethodCall client obj msg = do
        let M.ReceivedMethodCall serial sender call' = msg
            sigStr = TL.concat . map (T.typeCode . T.variantType)
                               . M.methodCallBody $ call'
            sig = T.mkSignature' sigStr
        
        case findMember call' obj of
                Just method@(Method inSig _ x) -> let
                        call'' = MethodCall obj client method
                                serial sender
                                (M.methodCallFlags call')
                                (M.methodCallBody call')
                        invalidArgs = replyError call'' Const.errorInvalidArgs []
                        in if inSig == sig
                                then x call''
                                else invalidArgs
                _               -> unknownMethod client msg
                

replyReturn :: MethodCall -> [T.Variant] -> IO ()
replyReturn call' body = reply where
        c = clientConnection . methodCallClient $ call'
        replyInvalid = M.Error
                Const.errorFailed
                (methodCallSerial call')
                (methodCallSender call')
                (Set.fromList [M.NoReplyExpected, M.NoAutoStart])
                [T.toVariant $ TL.pack "Method return didn't match signature."]
        
        replyValid = M.MethodReturn
                (methodCallSerial call')
                (methodCallSender call')
                (Set.fromList [M.NoReplyExpected, M.NoAutoStart])
                body
        
        sendReply :: M.Message a => a -> IO ()
        sendReply = send' c (const $ return ())
        
        sigStr = TL.concat . map (T.typeCode . T.variantType) $ body
        (Method _ outSig _) = methodCallMethod call'
        reply = if T.mkSignature sigStr == Just outSig
                then sendReply replyValid
                else sendReply replyInvalid

replyError :: MethodCall -> T.ErrorName -> [T.Variant] -> IO ()
replyError call' name body = send' c (const $ return ()) reply where
        c = clientConnection . methodCallClient $ call'
        reply = M.Error
                name
                (methodCallSerial call')
                (methodCallSender call')
                (Set.fromList [M.NoReplyExpected, M.NoAutoStart])
                body