:# Copyright (C) 2009-2011 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 . \clearpage \section{D-Bus clients} \subsection{Client types} :d DBus.Client data Client = Client { clientConnection :: Connection , clientCallbacks :: MVar (Map Serial Callback) , clientSignalHandlers :: MVar [Callback] , clientObjects :: MVar (Map ObjectPath ObjectInfo) , clientThreadID :: ThreadId , clientMessageProcessor :: IORef (ReceivedMessage -> IO Bool) } type Callback = (ReceivedMessage -> IO ()) data Reply = ReplyReturn [Variant] | ReplyError ErrorName [Variant] data Method = Method InterfaceName MemberName Signature Signature ([Variant] -> IO Reply) type ObjectInfo = Map InterfaceName InterfaceInfo type InterfaceInfo = Map MemberName MemberInfo data MemberInfo = MemberMethod Signature Signature Callback | MemberSignal Signature : \clearpage \subsection{Connecting and disconnecting clients} :d DBus.Client attach :: Connection -> IO Client attach connection = do callbacks <- newMVar Data.Map.empty signalHandlers <- newMVar [] objects <- newMVar Data.Map.empty processor <- newIORef (\_ -> return False) clientMVar <- newEmptyMVar threadID <- forkIO $ do client <- readMVar clientMVar mainLoop client let client = Client { clientConnection = connection , clientCallbacks = callbacks , clientSignalHandlers = signalHandlers , clientObjects = objects , clientThreadID = threadID , clientMessageProcessor = processor } putMVar clientMVar client export client "/" [introspectRoot client] void (call_ client (MethodCall { methodCallDestination = Just "org.freedesktop.DBus" , methodCallMember = "Hello" , methodCallInterface = Just "org.freedesktop.DBus" , methodCallPath = "/org/freedesktop/DBus" , methodCallFlags = Data.Set.empty , methodCallBody = [] })) return client : :d DBus.Client connect :: Address -> IO Client connect addr = do connection <- DBus.Connection.connect [unix, tcp] [external] addr attach connection |apidoc DBus.Client.disconnect| disconnect :: Client -> IO () disconnect client = do killThread (clientThreadID client) disconnect' client disconnect' :: Client -> IO () disconnect' client = do let connection = clientConnection client modifyMVar_ (clientCallbacks client) (\_ -> return Data.Map.empty) modifyMVar_ (clientSignalHandlers client) (\_ -> return []) modifyMVar_ (clientObjects client) (\_ -> return Data.Map.empty) DBus.Connection.disconnect connection : \clearpage \subsection{Main message dispatch loop} :d DBus.Client setMessageProcessor :: Client -> (ReceivedMessage -> IO Bool) -> IO () setMessageProcessor client io = atomicModifyIORef (clientMessageProcessor client) (\_ -> (io, ())) : :d DBus.Client mainLoop :: Client -> IO () mainLoop client = forever $ do let connection = clientConnection client received <- DBus.Connection.receive connection msg <- case received of Left err -> do disconnect' client connectionError ("Received invalid message: " ++ show err) Right msg -> return msg dispatch client msg dispatch :: Client -> ReceivedMessage -> IO () dispatch client received = void . forkIO $ do process <- readIORef (clientMessageProcessor client) handled <- process received let onReply serial = do let mvar = clientCallbacks client maybeCB <- modifyMVar mvar $ \callbacks -> let x = Data.Map.lookup serial callbacks callbacks' = if isJust x then Data.Map.delete serial callbacks else callbacks in return (callbacks', x) case maybeCB of Just cb -> void (cb received) Nothing -> return () unless handled $ case received of (ReceivedMethodReturn _ _ msg) -> onReply (methodReturnSerial msg) (ReceivedError _ _ msg) -> onReply (errorSerial msg) (ReceivedSignal _ _ _) -> do handlers <- readMVar (clientSignalHandlers client) mapM_ ($ received) handlers (ReceivedMethodCall serial sender msg) -> do objects <- readMVar (clientObjects client) case findMethod objects msg of Just io -> io received Nothing -> send_ client (Error errorUnknownMethod serial sender []) (\_ -> return ()) _ -> return () : \clearpage \subsection{Making method calls} :d DBus.Client send_ :: Message msg => Client -> msg -> (Serial -> IO a) -> IO a send_ client msg io = do result <- DBus.Connection.send (clientConnection client) msg io case result of Right serial -> return serial Left err -> connectionError ("Error sending message: " ++ show err) call :: Client -> MethodCall -> IO (Either Error MethodReturn) call client msg = do mvar <- newEmptyMVar let callback (ReceivedError _ _ err) = putMVar mvar (Left err) callback (ReceivedMethodReturn _ _ reply) = putMVar mvar (Right reply) callback _ = return () send_ client msg (\serial -> modifyMVar_ (clientCallbacks client) (\callbacks -> return (Data.Map.insert serial callback callbacks))) tried <- Control.Exception.try (takeMVar mvar) case tried of #if MIN_VERSION_base(4,2,0) Left Control.Exception.BlockedIndefinitelyOnMVar -> #else Left Control.Exception.BlockedOnDeadMVar -> #endif connectionError "DBus.Client.call: connection closed during method call" Right ret -> return ret call_ :: Client -> MethodCall -> IO MethodReturn call_ client msg = do result <- call client msg case result of Left err -> connectionError ("Call failed: " ++ Data.Text.unpack (errorMessage err)) Right ret -> return ret : \clearpage \subsection{Emitting signals} :d DBus.Client emit :: Client -> Signal -> IO () emit client msg = send_ client msg (\_ -> return ()) : \clearpage \subsection{Receiving signals} :d DBus.Client data MatchRule = MatchRule { matchSender :: Maybe BusName , matchDestination :: Maybe BusName , matchPath :: Maybe ObjectPath , matchInterface :: Maybe InterfaceName , matchMember :: Maybe MemberName } deriving (Show) listen :: Client -> MatchRule -> (BusName -> Signal -> IO ()) -> IO () listen client rule io = do let handler (ReceivedSignal _ (Just sender) msg) | checkMatchRule rule sender msg = io sender msg handler _ = return () modifyMVar_ (clientSignalHandlers client) (\hs -> return (handler : hs)) void (call_ client (MethodCall { methodCallPath = DBus.Constants.dbusPath , methodCallMember = "AddMatch" , methodCallInterface = Just DBus.Constants.dbusInterface , methodCallDestination = Just DBus.Constants.dbusName , methodCallFlags = Data.Set.empty , methodCallBody = [toVariant (formatMatchRule rule)] })) formatMatchRule :: MatchRule -> Text formatMatchRule rule = Data.Text.intercalate "," predicates where predicates = catMaybes [ f "sender" matchSender busNameText , f "destination" matchDestination busNameText , f "path" matchPath objectPathText , f "interface" matchInterface interfaceNameText , f "member" matchMember memberNameText ] f :: Text -> (MatchRule -> Maybe a) -> (a -> Text) -> Maybe Text f key get text = do val <- fmap text (get rule) return (Data.Text.concat [key, "='", val, "'"]) checkMatchRule :: MatchRule -> BusName -> Signal -> Bool checkMatchRule rule sender msg = and [ maybe True (== sender) (matchSender rule) , maybe True (\x -> signalDestination msg == Just x) (matchDestination rule) , maybe True (== signalPath msg) (matchPath rule) , maybe True (== signalInterface msg) (matchInterface rule) , maybe True (== signalMember msg) (matchMember rule) ] : \clearpage \subsection{Exporting objects and methods} :d DBus.Client data MethodError = MethodError ErrorName [Variant] deriving (Show, Eq, Typeable) instance Control.Exception.Exception MethodError |apidoc DBus.Client.throwError| throwError :: ErrorName -> Text -> [Variant] -> IO a throwError name message extra = Control.Exception.throwIO (MethodError name (toVariant message : extra)) : :d DBus.Client method :: InterfaceName -> MemberName -> Signature -> Signature -> ([Variant] -> IO Reply) -> Method method iface name inSig outSig io = Method iface name inSig outSig (\vs -> Control.Exception.catch (Control.Exception.catch (io vs) (\(MethodError name' vs') -> return (ReplyError name' vs'))) (\exc -> return (ReplyError errorFailed [toVariant (Data.Text.pack (show (exc :: SomeException)))]))) export :: Client -> ObjectPath -> [Method] -> IO () export client path methods = modifyMVar_ (clientObjects client) addObject where addObject objs = return (Data.Map.insert path info objs) info = foldl' addMethod Data.Map.empty (defaultIntrospect : methods) addMethod m (Method iface name inSig outSig cb) = Data.Map.insertWith' Data.Map.union iface (Data.Map.fromList [(name, MemberMethod inSig outSig (wrapCB cb))]) m wrapCB cb (ReceivedMethodCall serial sender msg) = do reply <- cb (methodCallBody msg) case reply of ReplyReturn vs -> send_ client (MethodReturn serial sender vs) (\_ -> return ()) ReplyError name vs -> send_ client (Error name serial sender vs) (\_ -> return ()) wrapCB _ _ = return () defaultIntrospect = methodIntrospect $ do objects <- readMVar (clientObjects client) let Just obj = Data.Map.lookup path objects return (introspect path obj) : :d DBus.Client findMethod :: Map ObjectPath ObjectInfo -> MethodCall -> Maybe Callback findMethod objects msg = do ifaceName <- methodCallInterface msg obj <- Data.Map.lookup (methodCallPath msg) objects iface <- Data.Map.lookup ifaceName obj member <- Data.Map.lookup (methodCallMember msg) iface case member of MemberMethod _ _ io -> return io _ -> Nothing : \clearpage \subsubsection{Automatic introspection} :d DBus.Client introspectRoot :: Client -> Method introspectRoot client = methodIntrospect $ do objects <- readMVar (clientObjects client) let paths = filter (/= "/") (Data.Map.keys objects) let iface = "org.freedesktop.DBus.Introspectable" let name = "Introspect" return (DBus.Introspection.Object "/" [DBus.Introspection.Interface iface [DBus.Introspection.Method name [] [DBus.Introspection.Parameter "" "s"]] [] []] [DBus.Introspection.Object p [] [] | p <- paths]) : :d DBus.Client methodIntrospect :: IO DBus.Introspection.Object -> Method methodIntrospect get = method iface name "" "s" impl where iface = "org.freedesktop.DBus.Introspectable" name = "Introspect" impl [] = do obj <- get let Just xml = DBus.Introspection.toXML obj return (ReplyReturn [toVariant xml]) impl _ = return (ReplyError errorInvalidParameters []) introspect :: ObjectPath -> ObjectInfo -> DBus.Introspection.Object introspect path obj = DBus.Introspection.Object path interfaces [] where interfaces = map introspectIface (Data.Map.toList obj) introspectIface (name, iface) = let members = Data.Map.toList iface methods = concatMap introspectMethod members signals = concatMap introspectSignal members in DBus.Introspection.Interface name methods signals [] introspectMethod (name, (MemberMethod inSig outSig _)) = [DBus.Introspection.Method name (map introspectParam (signatureTypes inSig)) (map introspectParam (signatureTypes outSig))] introspectMethod _ = [] introspectSignal (name, (MemberSignal sig)) = [DBus.Introspection.Signal name (map introspectParam (signatureTypes sig))] introspectSignal _ = [] introspectParam t = DBus.Introspection.Parameter "" (Signature [t]) : \clearpage \subsection{Simple clients} :d DBus.Client.Simple connectFirst :: [Address] -> IO Client connectFirst addrs = loop addrs where loop [] = connectionError (concat [ "connectFirst: no usable" , " addresses in " , show addrs]) loop (a:as) = Control.Exception.catch (DBus.Client.connect a) (\(ConnectionError _) -> loop as) |apidoc DBus.Client.Simple.connectSession| connectSession :: IO Client connectSession = do env <- DBus.Address.getSession case env of Nothing -> connectionError (concat [ "connectSession: DBUS_SESSION_BUS_ADDRESS is" , " missing or invalid." ]) Just addrs -> connectFirst addrs |apidoc DBus.Client.Simple.connectSystem| connectSystem :: IO Client connectSystem = do env <- DBus.Address.getSystem case env of Nothing -> connectionError (concat [ "connectSession: DBUS_SYSTEM_BUS_ADDRESS is" , " invalid." ]) Just addrs -> connectFirst addrs |apidoc DBus.Client.Simple.connectStarter| connectStarter :: IO Client connectStarter = do env <- DBus.Address.getStarter case env of Nothing -> connectionError (concat [ "connectSession: DBUS_STARTER_BUS_ADDRESS is" , " missing or invalid." ]) Just addrs -> connectFirst addrs : \clearpage \subsubsection{Remote object proxies} :d DBus.Client.Simple data Proxy = Proxy Client BusName ObjectPath : :d DBus.Client.Simple proxy :: Client -> BusName -> ObjectPath -> IO Proxy proxy client dest path = return (Proxy client dest path) : :d DBus.Client.Simple call :: Proxy -> InterfaceName -> MemberName -> [Variant] -> IO [Variant] call (Proxy client dest path) iface member body = do reply <- DBus.Client.call_ client $ MethodCall { methodCallDestination = Just dest , methodCallMember = member , methodCallInterface = Just iface , methodCallPath = path , methodCallFlags = Data.Set.empty , methodCallBody = body } return (methodReturnBody reply) : :d DBus.Client.Simple emit :: Client -> ObjectPath -> InterfaceName -> MemberName -> [Variant] -> IO () emit client path iface member body = DBus.Client.emit client $ Signal { signalDestination = Nothing , signalPath = path , signalInterface = iface , signalMember = member , signalBody = body } : :d DBus.Client.Simple listen :: Proxy -> InterfaceName -> MemberName -> (BusName -> Signal -> IO ()) -> IO () listen (Proxy client dest path) iface member = DBus.Client.listen client (MatchRule { matchSender = Just dest , matchInterface = Just iface , matchMember = Just member , matchPath = Just path , matchDestination = Nothing }) : \clearpage \subsubsection{Name reservation} :d DBus.Client.Simple data RequestNameFlag = AllowReplacement | ReplaceExisting | DoNotQueue deriving (Show) data RequestNameReply = PrimaryOwner | InQueue | Exists | AlreadyOwner deriving (Show) data ReleaseNameReply = Released | NonExistent | NotOwner deriving (Show) : :d DBus.Client.Simple encodeFlags :: [RequestNameFlag] -> Word32 encodeFlags = foldr (.|.) 0 . map flagValue where flagValue AllowReplacement = 0x1 flagValue ReplaceExisting = 0x2 flagValue DoNotQueue = 0x4 : :d DBus.Client.Simple requestName :: Client -> BusName -> [RequestNameFlag] -> IO RequestNameReply requestName client name flags = do bus <- proxy client "org.freedesktop.DBus" "/org/freedesktop/DBus" reply <- call bus "org.freedesktop.DBus" "RequestName" [ toVariant name , toVariant (encodeFlags flags) ] case (maybeIndex reply 0 >>= fromVariant :: Maybe Word32) of Just 1 -> return PrimaryOwner Just 2 -> return InQueue Just 3 -> return Exists Just 4 -> return AlreadyOwner _ -> connectionError "Call failed: received invalid reply" releaseName :: Client -> BusName -> IO ReleaseNameReply releaseName client name = do bus <- proxy client "org.freedesktop.DBus" "/org/freedesktop/DBus" reply <- call bus "org.freedesktop.DBus" "ReleaseName" [ toVariant name ] case (maybeIndex reply 0 >>= fromVariant :: Maybe Word32) of Just 1 -> return Released Just 2 -> return NonExistent Just 3 -> return NotOwner _ -> connectionError "Call failed: received invalid reply" : \clearpage \subsubsection{Simplified exports} :d DBus.Client.Simple |apidoc DBus.Client.Simple.AutoSignature| class AutoSignature a where funTypes :: a -> ([Type], [Type]) instance AutoSignature (IO ()) where funTypes _ = ([], []) instance IsValue a => AutoSignature (IO a) where funTypes io = ([], case ioT io undefined of (_, t) -> case t of TypeStructure ts -> ts _ -> [t]) ioT :: IsValue a => IO a -> a -> (a, Type) ioT _ a = (a, typeOf a) instance (IsValue a, AutoSignature fun) => AutoSignature (a -> fun) where funTypes fn = case valueT undefined of (a, t) -> case funTypes (fn a) of (ts, ts') -> (t : ts, ts') valueT :: IsValue a => a -> (a, Type) valueT a = (a, typeOf a) : :d DBus.Client.Simple |apidoc DBus.Client.Simple.AutoReply| class AutoReply fun where apply :: fun -> [Variant] -> Maybe (IO [Variant]) instance AutoReply (IO ()) where apply io [] = Just (io >> return []) apply _ _ = Nothing instance IsVariant a => AutoReply (IO a) where apply io [] = Just (do var <- fmap toVariant io case fromVariant var of Just struct -> return (structureItems struct) Nothing -> return [var]) apply _ _ = Nothing instance (IsVariant a, AutoReply fun) => AutoReply (a -> fun) where apply _ [] = Nothing apply fn (v:vs) = case fromVariant v of Just v' -> apply (fn v') vs Nothing -> Nothing : :d DBus.Client.Simple |apidoc DBus.Client.Simple.method| method :: (AutoSignature fun, AutoReply fun) => InterfaceName -> MemberName -> fun -> Method method iface name fun = DBus.Client.method iface name inSig outSig io where (typesIn, typesOut) = funTypes fun inSig = case checkSignature typesIn of Just sig -> sig Nothing -> invalid "input" outSig = case checkSignature typesOut of Just sig -> sig Nothing -> invalid "output" io vs = case apply fun vs of Nothing -> return (ReplyError errorInvalidParameters []) Just io' -> fmap ReplyReturn io' invalid label = error (concat [ "Method " , Data.Text.unpack (interfaceNameText iface) , "." , Data.Text.unpack (memberNameText name) , " has an invalid " , label , " signature."]) |apidoc DBus.Client.Simple.export| export :: Client -> ObjectPath -> [Method] -> IO () export = DBus.Client.export :