% Copyright (C) 2009 John Millikin % % 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 . \documentclass[12pt]{article} \usepackage{color} \usepackage{hyperref} \usepackage{noweb} % Smaller margins \usepackage[left=1.5cm,top=2cm,right=1.5cm,nohead,nofoot]{geometry} % Remove boxes from hyperlinks \hypersetup{ colorlinks, linkcolor=blue, } \makeindex \begin{document} \addcontentsline{toc}{section}{Contents} \tableofcontents @ \section{Introduction} This library provides a simplified, high-level interface for use by D-Bus clients. It implements async operations, remote object proxies, and local object exporting. All source code is licensed under the terms of the GNU GPL v3 or later. <>= {- Copyright (C) 2009 John Millikin 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 . -} @ \section{Clients} <>= <> {-# LANGUAGE OverloadedStrings #-} module DBus.Client ( <> ) 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 <>= 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 <>= Client , clientConnection <>= 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 <> return client <>= , mkClient <>= 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 @ \section{Name reservation} @ A client can request a ``well-known'' name from the bus. This allows messages sent to that name to be received by the client, without senders being aware of which application is actually handling requests. <>= data RequestNameFlag = AllowReplacement | ReplaceExisting | DoNotQueue deriving (Show) <>= data RequestNameReply = PrimaryOwner | InQueue | Exists | AlreadyOwner deriving (Show) <>= data ReleaseNameReply = Released | NonExistent | NotOwner deriving (Show) <>= , RequestNameFlag (..) , RequestNameReply (..) , ReleaseNameReply (..) @ All name requests are handled by the main bus service, which has names provided by the {\tt DBus.Constants} module. <>= dbus :: Proxy dbus = Proxy (RemoteObject Const.dbusName Const.dbusPath) Const.dbusInterface @ There are only two methods of interest here, {\tt RequestName} and {\tt ReleaseName}. <>= request, release :: T.MemberName request = T.mkMemberName' "RequestName" release = T.mkMemberName' "ReleaseName" @ A name may be requested for any client, using the given flags. The bus's reply will be returned, or an exception raised if the reply was invalid. <>= 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 <>= , requestName @ Releasing a name is similar, except there's no flags for releasing a name. <>= 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 <>= , releaseName <>= import Data.Word (Word32) import Data.Bits ((.|.)) <>= 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 @ \section{Receiving messages} Each client runs a separate thread for receiving messages, and every callback is called in a separate thread. This allows callbacks to perform long computations without blocking receipt of other messages. <>= import Control.Concurrent (forkIO) import Control.Monad (forever) import qualified Data.Set as Set <>= forkIO $ forever (receiveMessages client) <>= 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]) [] @ Method returns and errors both have a serial attached, which is used to find the proper callback. If the callback cannot be found, no action will be taken. <>= import Data.Maybe (isJust, fromJust, isNothing) <>= 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)) @ \subsection{Signals} <>= 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) ] <>= , onSignal , signalFilter @ \section{Remote object proxies} <>= , RemoteObject (..) , Proxy (..) , call , callBlocking , onSignalFrom <>= data RemoteObject = RemoteObject T.BusName T.ObjectPath data Proxy = Proxy RemoteObject T.InterfaceName @ \subsection{Method calls} <>= 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 @ \subsection{Signals} <>= 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 @ \section{Exporting local objects} <>= import qualified DBus.Introspection as I <>= export client (T.mkObjectPath' "/") rootObject <>= 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] <>= , LocalObject (..) , Member (..) , Interface (..) , MethodCall (..) , export <>= import qualified Data.Text.Lazy as TL <>= 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 @ \subsection{Responding to method calls} <>= data MethodCall = MethodCall { methodCallObject :: LocalObject , methodCallClient :: Client , methodCallMethod :: Member , methodCallSerial :: M.Serial , methodCallSender :: Maybe T.BusName , methodCallFlags :: Set.Set M.Flag , methodCallBody :: [T.Variant] } @ Technically method calls don't have to specify an interface if there's only one available in the destination object, but that'll never be the case here, so treat an unspecified interface as unknown. <>= 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 <>= , replyReturn , replyError @ \end{document}