packages feed

dbus-client 0.3.0.1 → 0.4

raw patch · 8 files changed

+1617/−1120 lines, 8 filesdep +monads-tfdep +transformersdep ~containersdep ~text

Dependencies added: monads-tf, transformers

Dependency ranges changed: containers, text

Files

+ Examples/exporting.hs view
@@ -0,0 +1,65 @@+-- Copyright (C) 2009-2010 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 Main (main) where+import Control.Monad.IO.Class (liftIO)+import DBus.Client+import DBus.Constants+import qualified Data.Map as Map++a :: String -> Object+a x = object+	[ ("test.iface_1", interface+	  [ ("Foo", onFoo "a" x)+	  , ("Bar", onBar "a" x)+	  ])+	]++b :: String -> Object+b x = object+	[ ("test.iface_1", interface+	  [ ("Foo", onFoo "b" x)+	  , ("Bar", onBar "b" x)+	  ])+	]++onFoo :: String -> String -> Member+onFoo x y = method "" "s" $ \call -> do+	liftIO $ putStrLn $ "Foo " ++ x ++ " " ++ y+	replyReturn call [toVariant $ "Foo " ++ x ++ " " ++ y]++onBar :: String -> String -> Member+onBar x y = method "" "s" $ \call -> do+	liftIO $ putStrLn $ "Bar " ++ x ++ " " ++ y+	replyError call errorFailed [toVariant $ "Bar " ++ x ++ " " ++ y]++main :: IO ()+main = do+	-- Connect to the bus+	client <- newClient =<< getSessionBus+	runDBus client $ do+		-- Request a unique name on the bus. If the name is already+		-- in use, continue without it.+		requestName "org.test.exporting" []+			(\e -> liftIO $ putStrLn "Error requesting unique name")+			(\response -> return ())+		+		-- Export two example objects+		export "/a" (a "hello")+		export "/b" (b "world")+		+		-- Wait forever for method calls+		mainLoop
+ Examples/introspect.hs view
@@ -0,0 +1,107 @@+-- Copyright (C) 2009-2010 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 Main (main) where+import Prelude hiding (replicate)+import qualified Prelude as Prelude+import qualified Data.Text.Lazy as TL+import Control.Monad.IO.Class (liftIO)+import Control.Concurrent (forkIO)+import Data.Maybe++import DBus.Client+import qualified DBus.Introspection as I+import System (getArgs)++main :: IO ()+main = do+	[service, path] <- getArgs+	c <- newClient =<< getSessionBus+	let introspect' p = runDBus c $ introspect (mkBusName_ $ TL.pack service) p+	forkIO $ runDBus c mainLoop+	printObj introspect' 0 (mkObjectPath_ $ TL.pack path)++introspect :: BusName -> ObjectPath -> DBus I.Object+introspect service path = do+	let proxy = Proxy service path "org.freedesktop.DBus.Introspectable"+	+	reply <- callProxyBlocking_ proxy "Introspect" [] []+	let text = fromJust $ fromVariant (methodReturnBody reply !! 0)+	return . fromJust $ I.fromXML path text++-- most of this stuff is just boring text formatting++printObj :: (ObjectPath -> IO I.Object) -> Integer -> ObjectPath -> IO ()+printObj get depth path = do+	let strPath = strObjectPath path+	putStr $ replicate (depth * 4) ' '+	putStrLn $ TL.unpack strPath+	+	(I.Object _ interfaces children) <- get path+	+	putStr $ replicate (depth * 4) ' '+	putStrLn $ replicate (fromIntegral $ TL.length strPath) '='+	+	mapM_ (printIface (depth + 1)) interfaces+	+	mapM_ (printObj get (depth + 1)) [x | (I.Object x _ _) <- children]++printIface :: Integer -> I.Interface -> IO ()+printIface depth (I.Interface name methods signals properties) = do+	let strName = strInterfaceName name+	putStr $ replicate (depth * 4) ' '+	putStrLn . TL.unpack $ strName+	putStr $ replicate (depth * 4) ' '+	putStrLn $ replicate (fromIntegral $ TL.length strName) '-'+	+	mapM_ (printMethod (depth + 1)) methods+	mapM_ (printSignal (depth + 1)) signals+	mapM_ (printProperty (depth + 1)) properties++printMethod :: Integer -> I.Method -> IO ()+printMethod depth (I.Method name inParams outParams) = do+	putStr $ replicate (depth * 4) ' '+	putStr "M "+	putStrLn . TL.unpack . strMemberName $ name+	+	mapM_ (printParam "IN" (depth + 1)) inParams+	mapM_ (printParam "OUT" (depth + 1)) outParams++printSignal :: Integer -> I.Signal -> IO ()+printSignal depth (I.Signal name params) = do+	putStr $ replicate (depth * 4) ' '+	putStr "S "+	putStrLn . TL.unpack . strMemberName $ name+	+	mapM_ (printParam "OUT" (depth + 1)) params++printProperty :: Integer -> I.Property -> IO ()+printProperty depth (I.Property name sig access) = do+	putStr $ replicate (depth * 4) ' '+	putStr $ "P " ++ show (strSignature sig) ++ " "+	putStrLn $ TL.unpack name+	putStr $ replicate ((depth + 1) * 4) ' '+	putStrLn $ show access+	+printParam :: String -> Integer -> I.Parameter -> IO ()+printParam label depth (I.Parameter name sig) = do+	putStr $ replicate (depth * 4) ' '+	putStr $ "[" ++ label ++ " "+	putStr $ show (strSignature sig) ++ " ] "+	putStrLn $ TL.unpack name++replicate :: Integer -> a -> [a]+replicate = Prelude.replicate . fromInteger
Examples/simple.hs view
@@ -1,4 +1,4 @@--- Copyright (C) 2009 John Millikin <jmillikin@gmail.com>+-- Copyright (C) 2009-2010 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@@ -16,27 +16,31 @@ {-# LANGUAGE OverloadedStrings #-} module Main (main) where -import DBus.Bus import DBus.Client-import DBus.Message-import DBus.Types+import Control.Concurrent (forkIO)+import Control.Monad (forever) import Data.List (sort)  dbus :: Proxy-dbus = Proxy (RemoteObject "org.freedesktop.DBus" "/org/freedesktop/DBus") "org.freedesktop.DBus"+dbus = Proxy "org.freedesktop.DBus" "/org/freedesktop/DBus" "org.freedesktop.DBus"  main :: IO () main = do 	-- Connect to the bus, and print which name was assigned to this 	-- connection.-	client <- mkClient =<< getSessionBus+	client <- newClient =<< getSessionBus 	putStrLn $ "Connected as: " ++ show (clientName client) 	-	-- Request a list of connected clients from the bus-	reply <- callProxyBlocking_ client dbus "ListNames" [] []+	-- Start message handling in another thread+	forkIO $ runDBus client mainLoop 	-	-- Pull out the body, and convert it to [String]-	let Just names = fromArray =<< fromVariant (messageBody reply !! 0)+	-- Request a list of connected clients from the bus+	names <- runDBus client $ do+		reply <- callProxyBlocking_ dbus "ListNames" [] []+		+		-- Pull out the body, and convert it to [String]+		let Just names = fromArray =<< fromVariant (messageBody reply !! 0)+		return names 	 	-- Print each name on a line, sorted so reserved names are below 	-- temporary names.
− Makefile
@@ -1,18 +0,0 @@-HS_SOURCES=hs/DBus/Client.hs--all: $(HS_SOURCES)--%.tex: %.nw-	noweave -delay "$<" | cpif "$@"--hs/%.hs: dbus-client.nw hs/DBus-	notangle -R"$*.hs" "$<" | cpif "$@"--hs/DBus:-	mkdir -p hs/DBus--dbus-client.pdf: dbus-client.tex-	xelatex dbus-client.tex-	xelatex dbus-client.tex--pdf: dbus-client.pdf
+ dbus-client.anansi view
@@ -0,0 +1,966 @@+:# 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/>.++\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.++The {\tt DBus.Client} module provides the public interface to this library.++:f DBus/Client.hs+|copyright|+|language extensions|+module DBus.Client (+	|exports|+	) where+|imports|+:++{\tt DBus.Client} also re-exports some modules from the {\tt dbus-core}+package.++:d exports+  module DBus.Bus+, module DBus.Types+, module DBus.Message+:++:d imports+import DBus.Bus+import DBus.Types+import DBus.Message++import qualified DBus.Connection as C+import qualified DBus.Constants as Const+import qualified DBus.Introspection as I+import qualified DBus.MatchRule as MR+import qualified DBus.Message as M+import qualified DBus.NameReservation as NR+import qualified DBus.Types as T+import qualified DBus.Wire as W+:++All source code is licensed under the terms of the GNU GPL v3 or later.++:d copyright+-- 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/>.+:++:d language extensions+{-# LANGUAGE OverloadedStrings #-}+:++\section{DBus Clients}++The {\tt Client} type provides an opaque handle to internal client state,+including callback registration and the open connection.++:d imports+import qualified Control.Concurrent.MVar as MV+import qualified Data.Map as Map+:++:f DBus/Client.hs+|apidoc Client|+data Client = Client+	{ clientConnection :: C.Connection+	, clientName :: T.BusName+	, clientCallbacks :: MV.MVar (Map.Map M.Serial MessageHandler)+	, clientObjects :: MV.MVar (Map.Map T.ObjectPath Object)+	, clientSignalHandlers :: MV.MVar [MessageHandler]+	}+type MessageHandler = (M.ReceivedMessage -> DBus ())+:++:d exports+  -- * Clients+, Client+, C.Connection+, clientName+:++The signature of {\tt newClient} is a bit weird so it can be called with+results from the {\tt DBus.Bus} family of computations, without having to+unwrap or curry.++:f DBus/Client.hs+|apidoc newClient|+newClient :: (C.Connection, T.BusName) -> IO Client+newClient (c, name) = do+	callbacks <- MV.newMVar Map.empty+	objects <- MV.newMVar Map.empty+	signals <- MV.newMVar []+	let client = Client c name callbacks objects signals+	|initialize client|+	return client+:++:d exports+, newClient+:++\section{Monadic interface}++Most of this module uses the {\tt DBus} monad, which wraps up the DBus+connection into a more abstract form.++:d imports+import Control.Monad (liftM, ap, forever)+import Control.Monad.IO.Class (liftIO)+import qualified Control.Monad.IO.Class as MIO+import qualified Control.Monad.Reader as R+import qualified Control.Applicative as A+:++:f DBus/Client.hs+newtype DBus a = DBus { unDBus :: R.ReaderT Client IO a }++instance Monad DBus where+	return = DBus . return+	(>>=) (DBus m) f = DBus $ m >>= unDBus . f++instance MIO.MonadIO DBus where+	liftIO = DBus . MIO.liftIO++instance Functor DBus where+	fmap = liftM++instance A.Applicative DBus where+	pure = return+	(<*>) = ap+:++The low-level DBus module prefers to return errors in the standard+{\tt Either} type, to let clients explicitly handle errors. Most of the+time, there's no reasonable way to handle such an error, so any errors+encountered when running a DBus client are thrown as exceptions.++:d language extensions+{-# LANGUAGE DeriveDataTypeable #-}+:++:d imports+import Data.Typeable (Typeable)+import qualified Control.Exception as Exc+:++Errors marshaling and unmarshaling are supported. Additionally, uncaught+errors from blocking method calls may be thrown from some computations.++:f DBus/Client.hs+data DBusException+	= MarshalFailed W.MarshalError+	| UnmarshalFailed W.UnmarshalError+	| MethodCallFailed M.Error+	| InvalidRequestNameReply M.MethodReturn+	| InvalidReleaseNameReply M.MethodReturn+	deriving (Show, Eq, Typeable)+instance Exc.Exception DBusException+:++Having to run {\tt liftIO} everywhere is annoying, so {\tt MonadError}+is instanced to let clients throw/catch more easily.++:d language extensions+{-# LANGUAGE TypeFamilies #-}+:++:d imports+import qualified Control.Monad.Error as E+:++:f DBus/Client.hs+instance E.MonadError DBus where+	type E.ErrorType DBus = DBusException+	throwError = MIO.liftIO . Exc.throwIO+	catchError dbus h = do+		c <- getClient+		liftIO $ Exc.catch+			(runDBus c dbus)+			(runDBus c . h)+:++Typical monad unwrapper, and some access computations.++:f DBus/Client.hs+|apidoc runDBus|+runDBus :: Client -> DBus a -> IO a+runDBus c (DBus m) = R.runReaderT m c++getClient :: DBus Client+getClient = DBus R.ask++getConnection :: DBus C.Connection+getConnection = fmap clientConnection getClient+:+++:d exports+, DBus+, DBusException+, runDBus+, getClient+:++\subsection{Dispatching messages}++Messages are read sequentially from the connection (typically in a central+loop), and then processed according to the client's message handler maps.+Unknown message types are ignored.++:f DBus/Client.hs+|apidoc processMessage|+processMessage :: M.ReceivedMessage -> DBus ()+processMessage received = p received where+	p (M.ReceivedUnknown _ _ _) = return ()+	|process messages|+	reply s = onReply s received+:++:d exports+, processMessage+:++\subsection{Useful wrappers}++Sending and receiving messages is common enough to use some small wrapper+functions.++:f DBus/Client.hs+|apidoc send|+send :: M.Message msg => (M.Serial -> DBus a) -> msg -> DBus a+send onSerial msg = do+	c <- getConnection+	client <- getClient+	sent <- liftIO $ C.send c (runDBus client . onSerial) msg+	case sent of+		Left err -> E.throwError $ MarshalFailed err+		Right a -> return a++|apidoc send_|+send_ :: M.Message msg => msg -> DBus ()+send_ = send (const $ return ())+:++:f DBus/Client.hs+|apidoc receive|+receive :: DBus M.ReceivedMessage+receive = do+	c <- getConnection+	parsed <- liftIO $ C.receive c+	case parsed of+		Left err -> E.throwError $ UnmarshalFailed err+		Right msg -> return msg+:++:f DBus/Client.hs+|apidoc mainLoop|+mainLoop :: DBus ()+mainLoop = forever $ receive >>= processMessage+:++:d exports+, send+, send_+, receive+, mainLoop+:++\section{Method calls}++DBus is inherently asynchronous. Method calls are sent over the bus, and+at some future point a response might be received.++Because all messages are sent and received over a single connection, waiting+for replies should be performed from a single thread. However, it's safe to+send messages from any thread.++\subsection{Asynchronous}++:f DBus/Client.hs+|apidoc call|+call :: M.MethodCall+     -> (M.Error -> DBus ())+     -> (M.MethodReturn -> DBus ())+     -> DBus ()+call msg onError onReturn = send addCallback msg where+	cb (M.ReceivedError _ _ msg') = onError msg'+	cb (M.ReceivedMethodReturn _ _ msg') = onReturn msg'+	cb _ = return ()+	+	addCallback s = do+		mvar <- fmap clientCallbacks getClient+		liftIO $ MV.modifyMVar_ mvar $ return . Map.insert s cb+:++:d exports+, call+:++:d process messages+p (M.ReceivedMethodReturn _ _ msg) = reply $ M.methodReturnSerial msg+p (M.ReceivedError _ _ msg) = reply $ M.errorSerial msg+:++When a {\tt MethodReturn} or {\tt Error} message is received, DBus expects+that its stored serial refers to a registered callback. Sometimes this isn't+true -- for example, a client might have used {\tt send} to call a method and+ignore its return value. In these cases, the return message should be+ignored.++:d imports+import Data.Maybe (isJust)+:++:f DBus/Client.hs+onReply :: M.Serial -> M.ReceivedMessage -> DBus ()+onReply serial msg = do+	mvar <- fmap clientCallbacks getClient+	maybeCB <- liftIO $ 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 maybeCB of+		Just cb -> cb msg+		Nothing -> return ()+:++\subsection{Synchronous}++Synchronous (or ``blocking'') operation is emulated using an {\tt MVar}.++:f DBus/Client.hs+|apidoc callBlocking|+callBlocking :: M.MethodCall -> DBus (Either M.Error M.MethodReturn)+callBlocking msg = do+	mvar <- liftIO $ MV.newEmptyMVar+	call msg+		(liftIO . MV.putMVar mvar . Left)+		(liftIO . MV.putMVar mvar . Right)+	liftIO $ MV.takeMVar mvar++|apidoc callBlocking_|+callBlocking_ :: M.MethodCall -> DBus M.MethodReturn+callBlocking_ msg = do+	reply <- callBlocking msg+	case reply of+		Left err -> E.throwError $ MethodCallFailed err+		Right x -> return x+:++:d exports+, callBlocking+, callBlocking_+:++\section{Handling signals}++Before the bus forwards any signals to this client, the client must send a+match rule to the bus. The rule is kept around so the correct callback can+be found when the signal is received.++:f DBus/Client.hs+|apidoc onSignal|+onSignal :: MR.MatchRule+	 -> (T.BusName -> M.Signal -> DBus ())+	 -> DBus ()+onSignal rule h = addHandler where+	rule' = rule { MR.matchType = Just MR.Signal }+	+	handler msg@(M.ReceivedSignal _ (Just sender) signal)+		| MR.matches rule' msg = h sender signal+	handler _ = return ()+	+	addHandler = do+		callBlocking_ $ MR.addMatch rule'+		mvar <- fmap clientSignalHandlers getClient+		liftIO $ MV.modifyMVar_ mvar $ return . (handler :)+:++:d process messages+p (M.ReceivedSignal _ _ _) = do+	mvar <- fmap clientSignalHandlers getClient+	handlers <- liftIO $ MV.readMVar mvar+	mapM_ ($ received) handlers+:++:d exports+-- * Handling signals+, onSignal+:++\section{Name reservation}++:d exports+-- * Name reservation+, NR.RequestNameFlag (..)+, NR.RequestNameReply (..)+, NR.ReleaseNameReply (..)+, requestName+, releaseName+, requestName_+, releaseName_+:++:f DBus/Client.hs+requestName :: T.BusName+            -> [NR.RequestNameFlag]+            -> (M.Error -> DBus ())+            -> (NR.RequestNameReply -> DBus ())+            -> DBus ()+requestName name flags onError callback =+	call (NR.requestName name flags) onError $ \reply -> +	case NR.mkRequestNameReply reply of+		Nothing -> E.throwError $ InvalidRequestNameReply reply+		Just x -> callback x+:++:f DBus/Client.hs+releaseName :: T.BusName+            -> (M.Error -> DBus ())+            -> (NR.ReleaseNameReply -> DBus ())+            -> DBus ()+releaseName name onError callback =+	call (NR.releaseName name) onError $ \reply ->+	case NR.mkReleaseNameReply reply of+		Nothing -> E.throwError $ InvalidReleaseNameReply reply+		Just x -> callback x+:++:f DBus/Client.hs+requestName_ :: T.BusName -> [NR.RequestNameFlag] -> DBus NR.RequestNameReply+requestName_ name flags = do+	reply <- callBlocking_ $ NR.requestName name flags+	case NR.mkRequestNameReply reply of+		Nothing -> E.throwError $ InvalidRequestNameReply reply+		Just x -> return x+:++:f DBus/Client.hs+releaseName_ :: T.BusName -> DBus NR.ReleaseNameReply+releaseName_ name = do+	reply <- callBlocking_ $ NR.releaseName name+	case NR.mkReleaseNameReply reply of+		Nothing -> E.throwError $ InvalidReleaseNameReply reply+		Just x -> return x+:++\section{Exporting local objects}++DBus is an object-oriented design, and most languages with DBus libraries+conform at least vaguely to object-oriented principles. Since Haskell is+functional, it's somewhat difficult to make exporting local behavior as+simple as it is in other languages.++In DBus, the basic unit of behavior is a ``member''. Members may be either+methods, which are called, or signals, which are emitted. A collection of+members indexed by name is an ``interface''. A collection interfaces (also+indexed by name) is an ``object''.++A method has two signatures, one for inputs and another for outputs. DBus+does not support in-out (aka ``reference'') parameters. Signals have only+one signature, which is of their output.++:f DBus/Client.hs+newtype Object = Object (Map.Map T.InterfaceName Interface)+newtype Interface = Interface (Map.Map T.MemberName Member)+data Member+	= MemberMethod Method+	| MemberSignal T.Signature+data Method = Method T.Signature T.Signature (MethodCtx -> DBus ())+:++:d exports+  -- * Exporting local objects+, Object (..)+, Interface (..)+, Member (..)+, Method (..)+:++Exporting is mostly just a matter of adding the object and its path to the+client's lookup map. However, note the call to {\tt addIntrospectable} --+DBus supports remote object introspection, and it's useful to generate a+basic schema by default.++:f DBus/Client.hs+|apidoc export|+export :: T.ObjectPath -> Object -> DBus ()+export path obj = do+	let obj' = addIntrospectable path obj+	mvar <- fmap clientObjects getClient+	liftIO $ MV.modifyMVar_ mvar $ return . Map.insert path obj'+:++:d exports+, export+:++To simplify building objects from existing functions, some helper functions+are defined.++:f DBus/Client.hs+object :: [(T.InterfaceName, Interface)] -> Object+object = Object . Map.fromList++interface :: [(T.MemberName, Member)] -> Interface+interface = Interface . Map.fromList++method :: T.Signature -- ^ Input signature+       -> T.Signature -- ^ Output signature+       -> (MethodCtx -> DBus ()) -- ^ Implementation+       -> Member+method inSig outSig cb = MemberMethod $ Method inSig outSig cb+:++:d exports+, object+, interface+, method+:++\subsection{Responding to method calls}++When a method call message is received for a local object, relevant+information for replying to the call is placed in a {\tt MethodCtx} value.++:d imports+import qualified Data.Set as Set+:++:f DBus/Client.hs+data MethodCtx = MethodCtx+	{ methodCtxObject :: Object+	, methodCtxMethod :: Method+	, methodCtxSerial :: M.Serial+	, methodCtxSender :: Maybe T.BusName+	, methodCtxFlags  :: Set.Set M.Flag+	, methodCtxBody   :: [T.Variant]+	}+:++{\tt replyReturn} and {\tt replyError} can be used by client code to+send a response message to a method call. Using these is easier than+constructing the reply manually.++:f DBus/Client.hs+|apidoc replyReturn|+replyReturn :: MethodCtx -> [T.Variant] -> DBus ()+replyReturn call' body = if valid then sendReply else sendError where+	sendError = replyError call' Const.errorFailed+		[T.toVariant ("Method return didn't match signature." :: String)]+	+	sendReply = send_ $ M.MethodReturn+		(methodCtxSerial call')+		(methodCtxSender call')+		body+	+	(Method _ outSig _) = methodCtxMethod call'+	valid = listSig body == Just outSig+:++:f DBus/Client.hs+replyError :: MethodCtx -> T.ErrorName -> [T.Variant] -> DBus ()+replyError call' name body = send_ $ M.Error+	name+	(methodCtxSerial call')+	(methodCtxSender call')+	body+:++:d exports+  -- ** Responding to method calls+, MethodCtx (..)+, replyReturn+, replyError+:++\subsection{Dispatching method calls}++Method calls are dispatched using the client's object map. If the+specified method is not found, an error will be returned.++:d process messages+p (M.ReceivedMethodCall _ _ msg) = do+	mvar <- fmap clientObjects getClient+	objects <- liftIO $ MV.readMVar mvar+	case findMethod objects msg of+		Just (obj, m) -> onMethodCall obj m received+		Nothing -> unknownMethod received+:++:f DBus/Client.hs+unknownMethod :: M.ReceivedMessage -> DBus ()+unknownMethod msg = send_ errorMsg where+	M.ReceivedMethodCall serial sender _ = msg+	errorMsg = M.Error+		Const.errorUnknownMethod+		serial sender+		[]+:++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.++:f DBus/Client.hs+findMethod :: Map.Map T.ObjectPath Object -> M.MethodCall -> Maybe (Object, Method)+findMethod objects call' = do+	Object obj <- Map.lookup (M.methodCallPath call') objects+	ifaceName <- M.methodCallInterface call'+	Interface iface <- Map.lookup ifaceName obj+	member <- Map.lookup (M.methodCallMember call') iface+	case member of+		MemberMethod m -> return (Object obj, m)+		_ -> Nothing+:++If the method was found, it needs to have its parameter list validated+before being sent on to the inner callback. This prevents DBus's dynamic+typing from causing trouble in Haskell code.++:f DBus/Client.hs+onMethodCall :: Object -> Method -> M.ReceivedMessage -> DBus ()+onMethodCall obj method' received = runCall where+	M.ReceivedMethodCall serial sender msg = received+	sig = listSig $ M.methodCallBody msg+	Method inSig _ cb = method'+	+	call' = MethodCtx obj method' serial sender+		(M.methodCallFlags msg)+		(M.methodCallBody msg)+	+	runCall = if sig == Just inSig+		then cb call'+		else replyError call' Const.errorInvalidArgs []+:++\subsection{Automatic introspection}++Some basic introspection can be performed automatically, based on the+contents of an {\tt Object}. This is only added to objects which do not+already define the {\tt Introspectable} interface -- that way, clients can+provide their own implementations if needed.++:f DBus/Client.hs+addIntrospectable :: T.ObjectPath -> Object -> Object+addIntrospectable path (Object ifaces) = Object ifaces' where+	ifaces' = Map.insertWith (\_ x -> x) name iface ifaces+	name = Const.interfaceIntrospectable+	iface = interface [("Introspect", impl)]+	impl = method "" "s" $ \call' -> do+		let Just xml = I.toXML . introspect path . methodCtxObject $ call'+		replyReturn call' [T.toVariant xml]+:++:f DBus/Client.hs+introspect :: T.ObjectPath -> Object -> I.Object+introspect path obj = I.Object path interfaces [] where+	Object 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, (MemberMethod (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, (MemberSignal sig)) = [I.Signal name+		(map introspectParam (T.signatureTypes sig))]+	introspectSignal _ = []+	+	introspectParam = I.Parameter "" . T.mkSignature_ . T.typeCode+:++\subsection{The root object}++Every client exports a ``root'' object, which provides introspection for+all exported objects. Although the DBus export list is hierarchical, this+module models it as a flat map.++:f DBus/Client.hs+rootObject :: Object+rootObject = object [(ifaceName, interface [(memberName, impl)])] where+	ifaceName = Const.interfaceIntrospectable+	memberName =  "Introspect"+	+	methodXML = I.Method memberName [] [I.Parameter "xml" "s"]+	ifaceXML = I.Interface ifaceName [methodXML] [] []+	+	impl = method "" "s" $ \call' -> do+		mvar <- fmap clientObjects getClient+		paths <- liftIO $ fmap Map.keys $ MV.readMVar mvar+		+		let paths' = filter (/= "/") paths+		let Just xml = I.toXML $ I.Object "/" [ifaceXML]+			[I.Object p [] [] | p <- paths']+		replyReturn call' [T.toVariant xml]+:++:d initialize client+liftIO $ MV.modifyMVar_ objects $ return . Map.insert "/" rootObject+:++\section{Remote object proxies}++Most DBus libraries support the concept of an ``object proxy'', which+behaves like a native object but runs its operations in another process.+A {\tt Proxy} is an approximation of this model for Haskell.++:f DBus/Client.hs+data Proxy = Proxy+	{ proxyName :: T.BusName+	, proxyObjectPath :: T.ObjectPath+	, proxyInterface :: T.InterfaceName+	}+	deriving (Show, Eq)+:++:f DBus/Client.hs+|apidoc callProxy|+callProxy :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]+          -> (M.Error -> DBus ())+          -> (M.MethodReturn -> DBus ())+          -> DBus ()+callProxy proxy name flags body onError onReturn = let+	msg = buildMethodCall proxy name flags body+	in call msg onError onReturn+:++:f DBus/Client.hs+|apidoc callProxyBlocking|+callProxyBlocking :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]+                  -> DBus (Either M.Error M.MethodReturn)+callProxyBlocking proxy name flags body =+	callBlocking $ buildMethodCall proxy name flags body+:++:f DBus/Client.hs+|apidoc callProxyBlocking_|+callProxyBlocking_ :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]+                   -> DBus M.MethodReturn+callProxyBlocking_ proxy name flags body =+	callBlocking_ $ buildMethodCall proxy name flags body+:++:f DBus/Client.hs+|apidoc onProxySignal|+onProxySignal :: Proxy -> T.MemberName -> (M.Signal -> DBus ())+             -> DBus ()+onProxySignal proxy member handler = onSignal rule handler' where+	Proxy dest path iface = proxy+	rule = MR.MatchRule+		{ MR.matchType = Nothing+		, MR.matchSender = Just dest+		, MR.matchInterface = Just iface+		, MR.matchMember = Just member+		, MR.matchPath = Just path+		, MR.matchDestination = Nothing+		, MR.matchParameters = []+		}+	handler' _ msg = handler msg+:++:d exports+, Proxy (..)+, callProxy+, callProxyBlocking+, callProxyBlocking_+, onProxySignal+:++:f DBus/Client.hs+buildMethodCall :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]+                -> M.MethodCall+buildMethodCall proxy name flags body = msg where+	Proxy dest path iface = proxy+	msg = M.MethodCall path name (Just iface) (Just dest)+		(Set.fromList flags) body+:++\section{Utility functions}++:d imports+import Data.Monoid (mconcat)+:++:f DBus/Client.hs+listSig :: [T.Variant] -> Maybe T.Signature+listSig = T.mkSignature . mconcat . map (T.typeCode . T.variantType)+:++\section{Haddock API documentation}++:d apidoc Client+-- | 'Client's are opaque handles to an open connection and other internal+-- state.+:++:d apidoc newClient+-- | Create a new 'Client' from an open connection and bus name. The weird+-- signature allows @newClient@ to use the computations in "DBus.Bus"+-- directly, without unpacking:+--+-- @+-- client <- newClient =<< 'getSessionBus'+-- @+--+-- Only one client should be created for any given connection. Otherwise,+-- they will compete to receive messages.+:++:d apidoc runDBus+-- | Run a DBus computation with the given client callbacks. Errors+-- encountered while running will be thrown as exceptions, using the+-- 'DBusException' type.+--+-- Use the 'E.MonadError' instance for 'DBus' to handle errors inside+-- the computation.+:++:d apidoc processMessage+-- | Run message handlers with the received message. If any method reply+-- callbacks or signal handlers are found, they will be run in the current+-- thread.+:++:d apidoc send+-- | A wrapper around 'C.send'.+:++:d apidoc send_+-- | A wrapper around 'C.send', which does not allow the message serial+-- to be recorded. This is a useful shortcut when sending messages which+-- are not expected to receive a reply.+:++:d apidoc receive+-- | A wrapper around 'C.receive'.+:++:d apidoc mainLoop+-- | Run in a loop forever, processing messages.+--+-- This is commonly run in a separate thread, ie+--+-- > client <- newClient =<< getSessionBus+-- > forkIO $ runDBus client mainLoop+:++:d apidoc call+-- | Perform an asynchronous method call. One of the provided computations+-- will be performed depending on what message type the destination sends+-- back.+:++:d apidoc callBlocking+-- | Sends a method call, and then blocks until a reply is received. Use+-- this when the receive/process loop is running in a separate thread.+:++:d apidoc callBlocking_+-- | A variant of 'callBlocking', which throws an exception if the+-- remote client returns 'M.Error'.+:++:d apidoc onSignal+-- | Perform some computation every time this client receives a matching+-- signal.+:++:d apidoc export+-- | Export a set of interfaces on the bus. Whenever a method call is+-- received which matches the object's path, interface, and member name,+-- one of its members will be called.+--+-- Exported objects automatically implement the+-- @org.freedesktop.DBus.Introspectable@ interface.+:++:d apidoc replyReturn+-- | Send a successful return reply for a method call.+:++:d apidoc replyError+-- | Send an error reply for a method call.+:++:d apidoc callProxy+-- | As 'call', except that the proxy's information is used to+-- build the message.+:++:d apidoc callProxyBlocking+-- | As 'callBlocking', except that the proxy's information is used+-- to build the message.+:++:d apidoc callProxyBlocking_+-- | As 'callBlocking_', except that the proxy's information is used+-- to build the message.+:++:d apidoc onProxySignal+-- | As 'onSIgnal', except that the proxy's information is used+-- to build the match rule.+:
dbus-client.cabal view
@@ -1,6 +1,6 @@ name: dbus-client-version: 0.3.0.1-synopsis: D-Bus client libraries+version: 0.4+synopsis: Monadic and object-oriented interfaces to DBus license: GPL-3 license-file: License.txt author: John Millikin@@ -11,26 +11,27 @@ stability: experimental bug-reports: mailto:jmillikin@gmail.com homepage: http://ianen.org/haskell/dbus/-tested-with: GHC==6.10.4+tested-with: GHC==6.12.1  extra-source-files:-  dbus-client.nw+  dbus-client.anansi   Examples/*.hs-  Makefile  source-repository head   type: darcs   location: http://ianen.org/haskell/dbus/client/  library-  ghc-options: -Wall+  ghc-options: -Wall -fno-warn-unused-do-bind   hs-source-dirs: hs    build-depends:       base >=3 && < 5     , dbus-core >= 0.8 && < 0.9-    , containers-    , text+    , containers >= 0.1 && < 0.4+    , text >= 0.7 && < 0.8+    , transformers >= 0.2 && < 0.3+    , monads-tf >= 0.1 && < 0.2    exposed-modules:     DBus.Client
− dbus-client.nw
@@ -1,662 +0,0 @@--% 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/>.--\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.--The {\tt DBus.Client} module provides the public interface to this library.--<<DBus/Client.hs>>=-<<copyright>>-{-# LANGUAGE OverloadedStrings #-}-module DBus.Client-	( <<exports>>-	) where-<<imports>>--@ All source code is licensed under the terms of the GNU GPL v3 or later.--<<copyright>>=-{--  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/>.--}--@-\section{Clients}--The {\tt Client} type provides an opaque handle to internal client state,-including callback registration and the open connection.--<<imports>>=-import qualified Control.Concurrent.MVar as MV-import qualified Data.Map as Map-import qualified DBus.Connection as C-import qualified DBus.Message as M-import qualified DBus.Types as T--<<DBus/Client.hs>>=-type Callback = (M.ReceivedMessage -> IO ())---- | 'Client's are opaque handles to an open connection and other internal--- state.--- -data Client = Client C.Connection T.BusName-	(MV.MVar (Map.Map M.Serial Callback))-	(MV.MVar (Map.Map T.ObjectPath Callback))-	(MV.MVar [Callback])--@ Two accessor functions are defined for the {\tt Client}'s {\tt Connection}-and unique bus name. The {\tt Connection} is used internally by this module,-but is not otherwise useful -- sharing a {\tt Connection} between two-{\tt Client}s is a bad idea.--The {\tt Client}'s bus name might be useful, and there's no harm in exposing-it.--<<DBus/Client.hs>>=-clientConnection :: Client -> C.Connection-clientConnection (Client x _ _ _ _) = x--clientName :: Client -> T.BusName-clientName (Client _ x _ _ _) = x--<<exports>>=-  -- * Clients-  Client-, clientName--<<DBus/Client.hs>>=--- | Create a new 'Client' from an open connection and bus name. The weird---- signature allows 'mkClient' to use the computations in "DBus.Bus"--- directly, without unpacking:--- --- @--- client <- mkClient =<< getSessionBus--- @--- --- Only one client should be created for any given connection. Otherwise,--- they will compete to receive messages.--- -mkClient :: (C.Connection, T.BusName) -> IO Client-mkClient (c, name) = do-	replies <- MV.newMVar Map.empty-	exports <- MV.newMVar Map.empty-	signals <- MV.newMVar []-	let client = Client c name replies exports signals-	<<initialize client>>-	return client--<<exports>>=-, mkClient--@ \subsection{Sending messages}--To simplify error conditions, errors returned from the {\tt DBus.Connection}-computations are converted to exceptions via {\tt error}. These errors only-occur if the message is malformed, so it's not worth the additional API-complexity to handle them.--<<DBus/Client.hs>>=-send_ :: M.Message a => Client -> (M.Serial -> IO b) -> a -> IO b-send_ c f msg = do-	result <- C.send (clientConnection c) f msg-	case result of-		Left x -> error $ show x-		Right x -> return x--@ And since most uses of {\tt send\_} don't care about the message's-serial, it can be reduced further.--<<DBus/Client.hs>>=-sendOnly_ :: M.Message a => Client -> a -> IO ()-sendOnly_ c = send_ c (const $ return ())--@ Additional helper computations are useful for sending method calls, to-keep track of which pending calls are currently expecting replies. The-{\tt call} computation is asynchronous; it will return immediately, and-one of the provided computations will be invoked depending on whether a-{\tt MethodReturn} or {\tt Error} were received.--TODO: pending method calls should be removed periodically, after a-decently long timeout.--TODO: if method calls with the {\tt NoReplyExpected} flag are sent, a-callback will be added but never removed. This could cause a space leak.--<<DBus/Client.hs>>=--- | Perform an asynchronous method call. One of the provided computations--- will be performed depending on what message type the destination sends--- back.--- -call :: Client -> M.MethodCall-     -> (M.Error -> IO ())-     -> (M.MethodReturn -> IO ())-     -> IO ()-call client msg onError onReturn = send_ client addCallback msg where-	Client _ _ mvar _ _ = client-	addCallback s = MV.modifyMVar_ mvar $ return . Map.insert s callback-	-	callback (M.ReceivedError        _ _ msg') = onError msg'-	callback (M.ReceivedMethodReturn _ _ msg') = onReturn msg'-	callback _ = return ()--<<DBus/Client.hs>>=--- | Similar to 'call', except that it waits for the reply and returns it--- in the current 'IO' thread.--- -callBlocking :: Client -> M.MethodCall -> IO (Either M.Error M.MethodReturn)-callBlocking client msg = do-	mvar <- MV.newEmptyMVar-	call client msg-		(MV.putMVar mvar . Left)-		(MV.putMVar mvar . Right)-	MV.takeMVar mvar--<<DBus/Client.hs>>=--- | Similar to 'callBlocking', except that an exception is raised if the--- destination sends back an error.--- -callBlocking_ :: Client -> M.MethodCall -> IO M.MethodReturn-callBlocking_ client msg = do-	reply <- callBlocking client msg-	case reply of-		Left x -> error . TL.unpack . M.errorMessage $ x-		Right x -> return x--<<exports>>=-  -- ** Sending messages-, call-, callBlocking-, callBlocking_--@ \subsubsection{Emitting signals}--TODO: this should be written in terms of locally exported objects; having-to construct a {\tt Signal} is a pain.--<<DBus/Client.hs>>=-emitSignal :: Client -> M.Signal -> IO ()-emitSignal = sendOnly_--<<exports>>=-  -- *** Emitting signals-, emitSignal--@ \subsection{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.--<<imports>>=-import Control.Concurrent (forkIO)-import Control.Monad (forever)-import qualified Data.Set as Set-import qualified DBus.Constants as Const--<<initialize client>>=-forkIO $ forever (receiveMessages client)--@ FIXME: does raising an exception here cause the thread to terminate?--<<DBus/Client.hs>>=-receiveMessages :: Client -> IO ()-receiveMessages client = do-	received <- C.receive $ clientConnection client-	case received of-		Left x -> error $ show x-		Right x -> handleMessage client x--<<DBus/Client.hs>>=-handleMessage :: Client -> M.ReceivedMessage -> IO ()-<<message handlers>>-handleMessage _ _ = return ()--@ \subsubsection{Method calls}--Method calls are dispatched to the client's list of exported objects. If-no object is available at the requested path, an error will be returned.--<<message handlers>>=-handleMessage client msg@(M.ReceivedMethodCall _ _ call') = do-	let Client _ _ _ mvar _ = client-	objects <- MV.readMVar mvar-	case Map.lookup (M.methodCallPath call') objects of-		Just x  -> x msg-		Nothing -> unknownMethod client msg--<<DBus/Client.hs>>=-unknownMethod :: Client -> M.ReceivedMessage -> IO ()-unknownMethod client msg = sendOnly_ client errorMsg where-	M.ReceivedMethodCall serial sender _ = msg-	errorMsg = M.Error-		Const.errorUnknownMethod-		serial sender-		[]--@ \subsubsection{Replies}--@ 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.--<<message handlers>>=-handleMessage c msg@(M.ReceivedMethodReturn _ _ msg') =-	gotReply c (M.methodReturnSerial msg') msg-handleMessage c msg@(M.ReceivedError        _ _ msg') =-	gotReply c (M.errorSerial msg') msg--<<imports>>=-import Data.Maybe (isJust)--<<DBus/Client.hs>>=-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 ()--@ \subsubsection{Signals}--Signals are dispatched to the list of active signal handlers.--<<message handlers>>=-handleMessage c msg@(M.ReceivedSignal _ _ _) = let-	Client _ _ _ _ mvar = c-	in MV.withMVar mvar $ mapM_ (\cb -> forkIO (cb msg))--@-\section{Name reservation}--<<imports>>=-import qualified DBus.NameReservation as NR--<<DBus/Client.hs>>=--- | 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.--- --- 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 -> [NR.RequestNameFlag]-            -> IO NR.RequestNameReply-requestName client name flags = do-	reply <- callBlocking_ client $ NR.requestName name flags-	case NR.mkRequestNameReply reply of-		Nothing -> error $ "Invalid reply to RequestName"-		Just x  -> return x--<<DBus/Client.hs>>=--- | Clients may also release names they've requested.--- -releaseName :: Client -> T.BusName -> IO NR.ReleaseNameReply-releaseName client name = do-	reply <- callBlocking_ client $ NR.releaseName name-	case NR.mkReleaseNameReply reply of-		Nothing -> error $ "Invalid reply to ReleaseName"-		Just x  -> return x--<<exports>>=-  -- * Name reservation-, requestName-, releaseName--@ \subsection{Listening for Signals}--Before the bus forwards any signals to this client, the client must send a-match rule to the bus. The rule is kept around so the correct callback can-be found when the signal is received.--<<imports>>=-import qualified DBus.MatchRule as MR-import Data.Maybe (fromJust)--<<DBus/Client.hs>>=--- | Perform some computation every time this client receives a matching--- signal.--- -onSignal :: Client -> MR.MatchRule-         -> (T.BusName -> M.Signal -> IO ())-         -> IO ()-onSignal client rule callback = let-	(Client _ _ _ _ mvar) = client-	rule' = rule { MR.matchType = Just MR.Signal }-	-	callback' msg@(M.ReceivedSignal _ sender signal)-		| MR.matches rule' msg = callback (fromJust sender) signal-	callback' _ = return ()-	-	in do-		callBlocking_ client $ MR.addMatch rule'-		MV.modifyMVar_ mvar $ return . (callback' :)--<<exports>>=-  -- * Receiving signals-, onSignal--@-\section{Remote objects and proxies}--TODO: document this section--<<exports>>=-  -- * Remote objects and proxies-, RemoteObject (..)-, Proxy (..)--<<DBus/Client.hs>>=-data RemoteObject = RemoteObject T.BusName T.ObjectPath-data Proxy = Proxy RemoteObject T.InterfaceName--@ \subsection{Method calls}--<<DBus/Client.hs>>=-buildMethodCall :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]-                -> M.MethodCall-buildMethodCall proxy name flags body = msg where-	Proxy (RemoteObject dest path) iface = proxy-	msg = M.MethodCall path name (Just iface) (Just dest)-		(Set.fromList flags) body--<<DBus/Client.hs>>=--- | As 'call', except that the proxy's information is used to--- build the message.--- -callProxy :: Client -> Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]-          -> (M.Error -> IO ())-          -> (M.MethodReturn -> IO ())-          -> IO ()-callProxy client proxy name flags body onError onReturn = let-	msg = buildMethodCall proxy name flags body-	in call client msg onError onReturn--<<DBus/Client.hs>>=--- | As 'callBlocking', except that the proxy's information is used--- to build the message.--- -callProxyBlocking :: Client -> Proxy -> T.MemberName -> [M.Flag]-                  -> [T.Variant]-                  -> IO (Either M.Error M.MethodReturn)-callProxyBlocking client proxy name flags body =-	callBlocking client $ buildMethodCall proxy name flags body--<<DBus/Client.hs>>=--- | As 'callBlocking_', except that the proxy's information is used--- to build the message.--- -callProxyBlocking_ :: Client -> Proxy -> T.MemberName -> [M.Flag]-                   -> [T.Variant]-                   -> IO M.MethodReturn-callProxyBlocking_ client proxy name flags body =-	callBlocking_ client $ buildMethodCall proxy name flags body--<<exports>>=-, callProxy-, callProxyBlocking-, callProxyBlocking_--@ \subsection{Signals}--<<DBus/Client.hs>>=-onSignalFrom :: Client -> Proxy -> T.MemberName -> (M.Signal -> IO ())-             -> IO ()-onSignalFrom client proxy member io = onSignal client rule io' where-	Proxy (RemoteObject dest path) iface = proxy-	rule = MR.MatchRule-		{ MR.matchType = Nothing-		, MR.matchSender = Just dest-		, MR.matchInterface = Just iface-		, MR.matchMember = Just member-		, MR.matchPath = Just path-		, MR.matchDestination = Nothing-		, MR.matchParameters = []-		}-	io' _ msg = io msg--<<exports>>=-, onSignalFrom--@-\section{Exporting local objects}--FIXME: the introspection stuff is \emph{really} ugly.--<<imports>>=-import qualified DBus.Introspection as I--<<initialize client>>=-export client ("/") rootObject--<<DBus/Client.hs>>=-rootObject :: LocalObject-rootObject = LocalObject $ Map.fromList [(ifaceName, interface)] where-	ifaceName = "org.freedesktop.DBus.Introspectable"-	memberName =  "Introspect"-	interface = Interface $ Map.fromList [(memberName, impl)]-	-	method = I.Method memberName [] [I.Parameter "xml" "s"]-	iface = I.Interface ifaceName [method] [] []-	-	impl = Method "" "s" $ \call' -> do-		let Client _ _ _ mvar _ = methodCallClient call'-		paths <- fmap Map.keys $ MV.readMVar mvar-		-		let paths' = filter (/= "/") paths-		let Just xml = I.toXML $ I.Object "/" [iface]-			[I.Object p [] [] | p <- paths']-		replyReturn call' [T.toVariant xml]--<<exports>>=-  -- * Exporting local objects-, LocalObject (..)-, Interface (..)-, Member (..)-, export--<<imports>>=-import qualified Data.Text.Lazy as TL--<<DBus/Client.hs>>=-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--<<DBus/Client.hs>>=--- | Export a set of interfaces on the bus. Whenever a method call is--- received which matches the object's path, interface, and member name,--- one of its members will be called.--- --- Exported objects automatically implement the--- @org.freedesktop.DBus.Introspectable@ interface.--- -export :: Client -> T.ObjectPath -> LocalObject -> IO ()-export client@(Client _ _ _ mvar _) path obj = MV.modifyMVar_ mvar $-	return . Map.insert path (onMethodCall client (addIntrospectable path obj))--<<DBus/Client.hs>>=-addIntrospectable :: T.ObjectPath -> LocalObject -> LocalObject-addIntrospectable path (LocalObject ifaces) = LocalObject ifaces' where-	ifaces' = Map.insertWith (\_ x -> x) name iface ifaces-	name = "org.freedesktop.DBus.Introspectable"-	iface = Interface $ Map.fromList [("Introspect", impl)]-	impl = Method "" "s" $ \call' -> do-		let Just xml = I.toXML . introspect path . methodCallObject $ call'-		replyReturn call' [T.toVariant xml]--<<DBus/Client.hs>>=-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}--<<DBus/Client.hs>>=-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.--<<DBus/Client.hs>>=-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--<<DBus/Client.hs>>=-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-		--<<DBus/Client.hs>>=--- | Send a successful return reply for a method call.--- -replyReturn :: MethodCall -> [T.Variant] -> IO ()-replyReturn call' body = reply where-	replyInvalid = M.Error-		Const.errorFailed-		(methodCallSerial call')-		(methodCallSender call')-		[T.toVariant $ TL.pack "Method return didn't match signature."]-	-	replyValid = M.MethodReturn-		(methodCallSerial call')-		(methodCallSender call')-		body-	-	sendReply :: M.Message a => a -> IO ()-	sendReply = sendOnly_ (methodCallClient call')-	-	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--<<DBus/Client.hs>>=--- | Send an error reply for a method call.--- -replyError :: MethodCall -> T.ErrorName -> [T.Variant] -> IO ()-replyError call' name body = sendOnly_ c reply where-	c = methodCallClient call'-	reply = M.Error-		name-		(methodCallSerial call')-		(methodCallSender call')-		body--<<exports>>=-  -- ** Responding to method calls-, MethodCall (..)-, replyReturn-, replyError--@ \end{document}
hs/DBus/Client.hs view
@@ -1,457 +1,491 @@-{--  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/>.--}-+-- 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-        (   -- * Clients-            Client-          , clientName--          , mkClient--            -- ** Sending messages-          , call-          , callBlocking-          , callBlocking_--            -- *** Emitting signals-          , emitSignal--            -- * Name reservation-          , requestName-          , releaseName--            -- * Receiving signals-          , onSignal--            -- * Remote objects and proxies-          , RemoteObject (..)-          , Proxy (..)--          , callProxy-          , callProxyBlocking-          , callProxyBlocking_--          , onSignalFrom--            -- * Exporting local objects-          , LocalObject (..)-          , Interface (..)-          , Member (..)-          , export--            -- ** Responding to method calls-          , MethodCall (..)-          , replyReturn-          , replyError+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE TypeFamilies #-}+module DBus.Client (+	  module DBus.Bus+	, module DBus.Types+	, module DBus.Message+	  -- * Clients+	, Client+	, C.Connection+	, clientName+	, newClient+	, DBus+	, DBusException+	, runDBus+	, getClient+	, processMessage+	, send+	, send_+	, receive+	, mainLoop+	, call+	, callBlocking+	, callBlocking_+	-- * Handling signals+	, onSignal+	-- * Name reservation+	, NR.RequestNameFlag (..)+	, NR.RequestNameReply (..)+	, NR.ReleaseNameReply (..)+	, requestName+	, releaseName+	, requestName_+	, releaseName_+	  -- * Exporting local objects+	, Object (..)+	, Interface (..)+	, Member (..)+	, Method (..)+	, export+	, object+	, interface+	, method+	  -- ** Responding to method calls+	, MethodCtx (..)+	, replyReturn+	, replyError+	, Proxy (..)+	, callProxy+	, callProxyBlocking+	, callProxyBlocking_+	, onProxySignal+	) where+import DBus.Bus+import DBus.Types+import DBus.Message -        ) 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.Introspection as I+import qualified DBus.MatchRule as MR import qualified DBus.Message as M+import qualified DBus.NameReservation as NR import qualified DBus.Types as T--import Control.Concurrent (forkIO)-import Control.Monad (forever)-import qualified Data.Set as Set-import qualified DBus.Constants as Const-+import qualified DBus.Wire as W+import qualified Control.Concurrent.MVar as MV+import qualified Data.Map as Map+import Control.Monad (liftM, ap, forever)+import Control.Monad.IO.Class (liftIO)+import qualified Control.Monad.IO.Class as MIO+import qualified Control.Monad.Reader as R+import qualified Control.Applicative as A+import Data.Typeable (Typeable)+import qualified Control.Exception as Exc+import qualified Control.Monad.Error as E import Data.Maybe (isJust)--import qualified DBus.NameReservation as NR--import qualified DBus.MatchRule as MR-import Data.Maybe (fromJust)--import qualified DBus.Introspection as I--import qualified Data.Text.Lazy as TL---type Callback = (M.ReceivedMessage -> IO ())-+import qualified Data.Set as Set+import Data.Monoid (mconcat) -- | 'Client's are opaque handles to an open connection and other internal -- state.--- -data Client = Client C.Connection T.BusName-        (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--clientName :: Client -> T.BusName-clientName (Client _ x _ _ _) = x-+data Client = Client+	{ clientConnection :: C.Connection+	, clientName :: T.BusName+	, clientCallbacks :: MV.MVar (Map.Map M.Serial MessageHandler)+	, clientObjects :: MV.MVar (Map.Map T.ObjectPath Object)+	, clientSignalHandlers :: MV.MVar [MessageHandler]+	}+type MessageHandler = (M.ReceivedMessage -> DBus ()) -- | Create a new 'Client' from an open connection and bus name. The weird---- signature allows 'mkClient' to use the computations in "DBus.Bus"+-- signature allows @newClient@ to use the computations in "DBus.Bus" -- directly, without unpacking:--- +-- -- @--- client <- mkClient =<< getSessionBus+-- client <- newClient =<< 'getSessionBus' -- @--- +-- -- Only one client should be created for any given connection. Otherwise, -- they will compete to receive messages.--- -mkClient :: (C.Connection, T.BusName) -> IO Client-mkClient (c, name) = do-        replies <- MV.newMVar Map.empty-        exports <- MV.newMVar Map.empty-        signals <- MV.newMVar []-        let client = Client c name replies exports signals-        forkIO $ forever (receiveMessages client)+newClient :: (C.Connection, T.BusName) -> IO Client+newClient (c, name) = do+	callbacks <- MV.newMVar Map.empty+	objects <- MV.newMVar Map.empty+	signals <- MV.newMVar []+	let client = Client c name callbacks objects signals+	liftIO $ MV.modifyMVar_ objects $ return . Map.insert "/" rootObject+	return client+newtype DBus a = DBus { unDBus :: R.ReaderT Client IO a } -        export client ("/") rootObject+instance Monad DBus where+	return = DBus . return+	(>>=) (DBus m) f = DBus $ m >>= unDBus . f -        return client+instance MIO.MonadIO DBus where+	liftIO = DBus . MIO.liftIO -send_ :: M.Message a => Client -> (M.Serial -> IO b) -> a -> IO b-send_ c f msg = do-        result <- C.send (clientConnection c) f msg-        case result of-                Left x -> error $ show x-                Right x -> return x+instance Functor DBus where+	fmap = liftM -sendOnly_ :: M.Message a => Client -> a -> IO ()-sendOnly_ c = send_ c (const $ return ())+instance A.Applicative DBus where+	pure = return+	(<*>) = ap+data DBusException+	= MarshalFailed W.MarshalError+	| UnmarshalFailed W.UnmarshalError+	| MethodCallFailed M.Error+	| InvalidRequestNameReply M.MethodReturn+	| InvalidReleaseNameReply M.MethodReturn+	deriving (Show, Eq, Typeable)+instance Exc.Exception DBusException+instance E.MonadError DBus where+	type E.ErrorType DBus = DBusException+	throwError = MIO.liftIO . Exc.throwIO+	catchError dbus h = do+		c <- getClient+		liftIO $ Exc.catch+			(runDBus c dbus)+			(runDBus c . h)+-- | Run a DBus computation with the given client callbacks. Errors+-- encountered while running will be thrown as exceptions, using the+-- 'DBusException' type.+--+-- Use the 'E.MonadError' instance for 'DBus' to handle errors inside+-- the computation.+runDBus :: Client -> DBus a -> IO a+runDBus c (DBus m) = R.runReaderT m c +getClient :: DBus Client+getClient = DBus R.ask++getConnection :: DBus C.Connection+getConnection = fmap clientConnection getClient+-- | Run message handlers with the received message. If any method reply+-- callbacks or signal handlers are found, they will be run in the current+-- thread.+processMessage :: M.ReceivedMessage -> DBus ()+processMessage received = p received where+	p (M.ReceivedUnknown _ _ _) = return ()+	p (M.ReceivedMethodReturn _ _ msg) = reply $ M.methodReturnSerial msg+	p (M.ReceivedError _ _ msg) = reply $ M.errorSerial msg+	p (M.ReceivedSignal _ _ _) = do+		mvar <- fmap clientSignalHandlers getClient+		handlers <- liftIO $ MV.readMVar mvar+		mapM_ ($ received) handlers+	p (M.ReceivedMethodCall _ _ msg) = do+		mvar <- fmap clientObjects getClient+		objects <- liftIO $ MV.readMVar mvar+		case findMethod objects msg of+			Just (obj, m) -> onMethodCall obj m received+			Nothing -> unknownMethod received+	reply s = onReply s received+-- | A wrapper around 'C.send'.+send :: M.Message msg => (M.Serial -> DBus a) -> msg -> DBus a+send onSerial msg = do+	c <- getConnection+	client <- getClient+	sent <- liftIO $ C.send c (runDBus client . onSerial) msg+	case sent of+		Left err -> E.throwError $ MarshalFailed err+		Right a -> return a++-- | A wrapper around 'C.send', which does not allow the message serial+-- to be recorded. This is a useful shortcut when sending messages which+-- are not expected to receive a reply.+send_ :: M.Message msg => msg -> DBus ()+send_ = send (const $ return ())+-- | A wrapper around 'C.receive'.+receive :: DBus M.ReceivedMessage+receive = do+	c <- getConnection+	parsed <- liftIO $ C.receive c+	case parsed of+		Left err -> E.throwError $ UnmarshalFailed err+		Right msg -> return msg+-- | Run in a loop forever, processing messages.+--+-- This is commonly run in a separate thread, ie+--+-- > client <- newClient =<< getSessionBus+-- > forkIO $ runDBus client mainLoop+mainLoop :: DBus ()+mainLoop = forever $ receive >>= processMessage -- | Perform an asynchronous method call. One of the provided computations -- will be performed depending on what message type the destination sends -- back.--- -call :: Client -> M.MethodCall-     -> (M.Error -> IO ())-     -> (M.MethodReturn -> IO ())-     -> IO ()-call client msg onError onReturn = send_ client addCallback msg where-        Client _ _ mvar _ _ = client-        addCallback s = MV.modifyMVar_ mvar $ return . Map.insert s callback-        -        callback (M.ReceivedError        _ _ msg') = onError msg'-        callback (M.ReceivedMethodReturn _ _ msg') = onReturn msg'-        callback _ = return ()---- | Similar to 'call', except that it waits for the reply and returns it--- in the current 'IO' thread.--- -callBlocking :: Client -> M.MethodCall -> IO (Either M.Error M.MethodReturn)-callBlocking client msg = do-        mvar <- MV.newEmptyMVar-        call client msg-                (MV.putMVar mvar . Left)-                (MV.putMVar mvar . Right)-        MV.takeMVar mvar---- | Similar to 'callBlocking', except that an exception is raised if the--- destination sends back an error.--- -callBlocking_ :: Client -> M.MethodCall -> IO M.MethodReturn-callBlocking_ client msg = do-        reply <- callBlocking client msg-        case reply of-                Left x -> error . TL.unpack . M.errorMessage $ x-                Right x -> return x--emitSignal :: Client -> M.Signal -> IO ()-emitSignal = sendOnly_--receiveMessages :: Client -> IO ()-receiveMessages client = do-        received <- C.receive $ clientConnection client-        case received of-                Left x -> error $ show x-                Right x -> handleMessage client x--handleMessage :: Client -> M.ReceivedMessage -> IO ()-handleMessage client msg@(M.ReceivedMethodCall _ _ call') = do-        let Client _ _ _ mvar _ = client-        objects <- MV.readMVar mvar-        case Map.lookup (M.methodCallPath call') objects of-                Just x  -> x msg-                Nothing -> unknownMethod client msg--handleMessage c msg@(M.ReceivedMethodReturn _ _ msg') =-        gotReply c (M.methodReturnSerial msg') msg-handleMessage c msg@(M.ReceivedError        _ _ msg') =-        gotReply c (M.errorSerial msg') msg--handleMessage c msg@(M.ReceivedSignal _ _ _) = let-        Client _ _ _ _ mvar = c-        in MV.withMVar mvar $ mapM_ (\cb -> forkIO (cb msg))--handleMessage _ _ = return ()--unknownMethod :: Client -> M.ReceivedMessage -> IO ()-unknownMethod client msg = sendOnly_ client errorMsg where-        M.ReceivedMethodCall serial sender _ = msg-        errorMsg = M.Error-                Const.errorUnknownMethod-                serial sender-                []--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 ()---- | 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.--- --- 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 -> [NR.RequestNameFlag]-            -> IO NR.RequestNameReply-requestName client name flags = do-        reply <- callBlocking_ client $ NR.requestName name flags-        case NR.mkRequestNameReply reply of-                Nothing -> error $ "Invalid reply to RequestName"-                Just x  -> return x---- | Clients may also release names they've requested.--- -releaseName :: Client -> T.BusName -> IO NR.ReleaseNameReply-releaseName client name = do-        reply <- callBlocking_ client $ NR.releaseName name-        case NR.mkReleaseNameReply reply of-                Nothing -> error $ "Invalid reply to ReleaseName"-                Just x  -> return x+call :: M.MethodCall+     -> (M.Error -> DBus ())+     -> (M.MethodReturn -> DBus ())+     -> DBus ()+call msg onError onReturn = send addCallback msg where+	cb (M.ReceivedError _ _ msg') = onError msg'+	cb (M.ReceivedMethodReturn _ _ msg') = onReturn msg'+	cb _ = return ()+	+	addCallback s = do+		mvar <- fmap clientCallbacks getClient+		liftIO $ MV.modifyMVar_ mvar $ return . Map.insert s cb+onReply :: M.Serial -> M.ReceivedMessage -> DBus ()+onReply serial msg = do+	mvar <- fmap clientCallbacks getClient+	maybeCB <- liftIO $ 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 maybeCB of+		Just cb -> cb msg+		Nothing -> return ()+-- | Sends a method call, and then blocks until a reply is received. Use+-- this when the receive/process loop is running in a separate thread.+callBlocking :: M.MethodCall -> DBus (Either M.Error M.MethodReturn)+callBlocking msg = do+	mvar <- liftIO $ MV.newEmptyMVar+	call msg+		(liftIO . MV.putMVar mvar . Left)+		(liftIO . MV.putMVar mvar . Right)+	liftIO $ MV.takeMVar mvar +-- | A variant of 'callBlocking', which throws an exception if the+-- remote client returns 'M.Error'.+callBlocking_ :: M.MethodCall -> DBus M.MethodReturn+callBlocking_ msg = do+	reply <- callBlocking msg+	case reply of+		Left err -> E.throwError $ MethodCallFailed err+		Right x -> return x -- | Perform some computation every time this client receives a matching -- signal.--- -onSignal :: Client -> MR.MatchRule-         -> (T.BusName -> M.Signal -> IO ())-         -> IO ()-onSignal client rule callback = let-        (Client _ _ _ _ mvar) = client-        rule' = rule { MR.matchType = Just MR.Signal }-        -        callback' msg@(M.ReceivedSignal _ sender signal)-                | MR.matches rule' msg = callback (fromJust sender) signal-        callback' _ = return ()-        -        in do-                callBlocking_ client $ MR.addMatch rule'-                MV.modifyMVar_ mvar $ return . (callback' :)--data RemoteObject = RemoteObject T.BusName T.ObjectPath-data Proxy = Proxy RemoteObject T.InterfaceName--buildMethodCall :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]-                -> M.MethodCall-buildMethodCall proxy name flags body = msg where-        Proxy (RemoteObject dest path) iface = proxy-        msg = M.MethodCall path name (Just iface) (Just dest)-                (Set.fromList flags) body---- | As 'call', except that the proxy's information is used to--- build the message.--- -callProxy :: Client -> Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]-          -> (M.Error -> IO ())-          -> (M.MethodReturn -> IO ())-          -> IO ()-callProxy client proxy name flags body onError onReturn = let-        msg = buildMethodCall proxy name flags body-        in call client msg onError onReturn---- | As 'callBlocking', except that the proxy's information is used--- to build the message.--- -callProxyBlocking :: Client -> Proxy -> T.MemberName -> [M.Flag]-                  -> [T.Variant]-                  -> IO (Either M.Error M.MethodReturn)-callProxyBlocking client proxy name flags body =-        callBlocking client $ buildMethodCall proxy name flags body---- | As 'callBlocking_', except that the proxy's information is used--- to build the message.--- -callProxyBlocking_ :: Client -> Proxy -> T.MemberName -> [M.Flag]-                   -> [T.Variant]-                   -> IO M.MethodReturn-callProxyBlocking_ client proxy name flags body =-        callBlocking_ client $ buildMethodCall proxy name flags body--onSignalFrom :: Client -> Proxy -> T.MemberName -> (M.Signal -> IO ())-             -> IO ()-onSignalFrom client proxy member io = onSignal client rule io' where-        Proxy (RemoteObject dest path) iface = proxy-        rule = MR.MatchRule-                { MR.matchType = Nothing-                , MR.matchSender = Just dest-                , MR.matchInterface = Just iface-                , MR.matchMember = Just member-                , MR.matchPath = Just path-                , MR.matchDestination = Nothing-                , MR.matchParameters = []-                }-        io' _ msg = io msg--rootObject :: LocalObject-rootObject = LocalObject $ Map.fromList [(ifaceName, interface)] where-        ifaceName = "org.freedesktop.DBus.Introspectable"-        memberName =  "Introspect"-        interface = Interface $ Map.fromList [(memberName, impl)]-        -        method = I.Method memberName [] [I.Parameter "xml" "s"]-        iface = I.Interface ifaceName [method] [] []-        -        impl = Method "" "s" $ \call' -> do-                let Client _ _ _ mvar _ = methodCallClient call'-                paths <- fmap Map.keys $ MV.readMVar mvar-                -                let paths' = filter (/= "/") paths-                let Just xml = I.toXML $ I.Object "/" [iface]-                        [I.Object p [] [] | p <- paths']-                replyReturn call' [T.toVariant xml]--newtype LocalObject = LocalObject (Map.Map T.InterfaceName Interface)+onSignal :: MR.MatchRule+	 -> (T.BusName -> M.Signal -> DBus ())+	 -> DBus ()+onSignal rule h = addHandler where+	rule' = rule { MR.matchType = Just MR.Signal }+	+	handler msg@(M.ReceivedSignal _ (Just sender) signal)+		| MR.matches rule' msg = h sender signal+	handler _ = return ()+	+	addHandler = do+		callBlocking_ $ MR.addMatch rule'+		mvar <- fmap clientSignalHandlers getClient+		liftIO $ MV.modifyMVar_ mvar $ return . (handler :)+requestName :: T.BusName+            -> [NR.RequestNameFlag]+            -> (M.Error -> DBus ())+            -> (NR.RequestNameReply -> DBus ())+            -> DBus ()+requestName name flags onError callback =+	call (NR.requestName name flags) onError $ \reply -> +	case NR.mkRequestNameReply reply of+		Nothing -> E.throwError $ InvalidRequestNameReply reply+		Just x -> callback x+releaseName :: T.BusName+            -> (M.Error -> DBus ())+            -> (NR.ReleaseNameReply -> DBus ())+            -> DBus ()+releaseName name onError callback =+	call (NR.releaseName name) onError $ \reply ->+	case NR.mkReleaseNameReply reply of+		Nothing -> E.throwError $ InvalidReleaseNameReply reply+		Just x -> callback x+requestName_ :: T.BusName -> [NR.RequestNameFlag] -> DBus NR.RequestNameReply+requestName_ name flags = do+	reply <- callBlocking_ $ NR.requestName name flags+	case NR.mkRequestNameReply reply of+		Nothing -> E.throwError $ InvalidRequestNameReply reply+		Just x -> return x+releaseName_ :: T.BusName -> DBus NR.ReleaseNameReply+releaseName_ name = do+	reply <- callBlocking_ $ NR.releaseName name+	case NR.mkReleaseNameReply reply of+		Nothing -> E.throwError $ InvalidReleaseNameReply reply+		Just x -> return x+newtype Object = Object (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-+	= MemberMethod Method+	| MemberSignal T.Signature+data Method = Method T.Signature T.Signature (MethodCtx -> DBus ()) -- | Export a set of interfaces on the bus. Whenever a method call is -- received which matches the object's path, interface, and member name, -- one of its members will be called.--- +-- -- Exported objects automatically implement the -- @org.freedesktop.DBus.Introspectable@ interface.--- -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 = "org.freedesktop.DBus.Introspectable"-        iface = Interface $ Map.fromList [("Introspect", impl)]-        impl = Method "" "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+export :: T.ObjectPath -> Object -> DBus ()+export path obj = do+	let obj' = addIntrospectable path obj+	mvar <- fmap clientObjects getClient+	liftIO $ MV.modifyMVar_ mvar $ return . Map.insert path obj'+object :: [(T.InterfaceName, Interface)] -> Object+object = Object . Map.fromList -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-                +interface :: [(T.MemberName, Member)] -> Interface+interface = Interface . Map.fromList +method :: T.Signature -- ^ Input signature+       -> T.Signature -- ^ Output signature+       -> (MethodCtx -> DBus ()) -- ^ Implementation+       -> Member+method inSig outSig cb = MemberMethod $ Method inSig outSig cb+data MethodCtx = MethodCtx+	{ methodCtxObject :: Object+	, methodCtxMethod :: Method+	, methodCtxSerial :: M.Serial+	, methodCtxSender :: Maybe T.BusName+	, methodCtxFlags  :: Set.Set M.Flag+	, methodCtxBody   :: [T.Variant]+	} -- | Send a successful return reply for a method call.--- -replyReturn :: MethodCall -> [T.Variant] -> IO ()-replyReturn call' body = reply where-        replyInvalid = M.Error-                Const.errorFailed-                (methodCallSerial call')-                (methodCallSender call')-                [T.toVariant $ TL.pack "Method return didn't match signature."]-        -        replyValid = M.MethodReturn-                (methodCallSerial call')-                (methodCallSender call')-                body-        -        sendReply :: M.Message a => a -> IO ()-        sendReply = sendOnly_ (methodCallClient call')-        -        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---- | Send an error reply for a method call.--- -replyError :: MethodCall -> T.ErrorName -> [T.Variant] -> IO ()-replyError call' name body = sendOnly_ c reply where-        c = methodCallClient call'-        reply = M.Error-                name-                (methodCallSerial call')-                (methodCallSender call')-                body-+replyReturn :: MethodCtx -> [T.Variant] -> DBus ()+replyReturn call' body = if valid then sendReply else sendError where+	sendError = replyError call' Const.errorFailed+		[T.toVariant ("Method return didn't match signature." :: String)]+	+	sendReply = send_ $ M.MethodReturn+		(methodCtxSerial call')+		(methodCtxSender call')+		body+	+	(Method _ outSig _) = methodCtxMethod call'+	valid = listSig body == Just outSig+replyError :: MethodCtx -> T.ErrorName -> [T.Variant] -> DBus ()+replyError call' name body = send_ $ M.Error+	name+	(methodCtxSerial call')+	(methodCtxSender call')+	body+unknownMethod :: M.ReceivedMessage -> DBus ()+unknownMethod msg = send_ errorMsg where+	M.ReceivedMethodCall serial sender _ = msg+	errorMsg = M.Error+		Const.errorUnknownMethod+		serial sender+		[]+findMethod :: Map.Map T.ObjectPath Object -> M.MethodCall -> Maybe (Object, Method)+findMethod objects call' = do+	Object obj <- Map.lookup (M.methodCallPath call') objects+	ifaceName <- M.methodCallInterface call'+	Interface iface <- Map.lookup ifaceName obj+	member <- Map.lookup (M.methodCallMember call') iface+	case member of+		MemberMethod m -> return (Object obj, m)+		_ -> Nothing+onMethodCall :: Object -> Method -> M.ReceivedMessage -> DBus ()+onMethodCall obj method' received = runCall where+	M.ReceivedMethodCall serial sender msg = received+	sig = listSig $ M.methodCallBody msg+	Method inSig _ cb = method'+	+	call' = MethodCtx obj method' serial sender+		(M.methodCallFlags msg)+		(M.methodCallBody msg)+	+	runCall = if sig == Just inSig+		then cb call'+		else replyError call' Const.errorInvalidArgs []+addIntrospectable :: T.ObjectPath -> Object -> Object+addIntrospectable path (Object ifaces) = Object ifaces' where+	ifaces' = Map.insertWith (\_ x -> x) name iface ifaces+	name = Const.interfaceIntrospectable+	iface = interface [("Introspect", impl)]+	impl = method "" "s" $ \call' -> do+		let Just xml = I.toXML . introspect path . methodCtxObject $ call'+		replyReturn call' [T.toVariant xml]+introspect :: T.ObjectPath -> Object -> I.Object+introspect path obj = I.Object path interfaces [] where+	Object 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, (MemberMethod (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, (MemberSignal sig)) = [I.Signal name+		(map introspectParam (T.signatureTypes sig))]+	introspectSignal _ = []+	+	introspectParam = I.Parameter "" . T.mkSignature_ . T.typeCode+rootObject :: Object+rootObject = object [(ifaceName, interface [(memberName, impl)])] where+	ifaceName = Const.interfaceIntrospectable+	memberName =  "Introspect"+	+	methodXML = I.Method memberName [] [I.Parameter "xml" "s"]+	ifaceXML = I.Interface ifaceName [methodXML] [] []+	+	impl = method "" "s" $ \call' -> do+		mvar <- fmap clientObjects getClient+		paths <- liftIO $ fmap Map.keys $ MV.readMVar mvar+		+		let paths' = filter (/= "/") paths+		let Just xml = I.toXML $ I.Object "/" [ifaceXML]+			[I.Object p [] [] | p <- paths']+		replyReturn call' [T.toVariant xml]+data Proxy = Proxy+	{ proxyName :: T.BusName+	, proxyObjectPath :: T.ObjectPath+	, proxyInterface :: T.InterfaceName+	}+	deriving (Show, Eq)+-- | As 'call', except that the proxy's information is used to+-- build the message.+callProxy :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]+          -> (M.Error -> DBus ())+          -> (M.MethodReturn -> DBus ())+          -> DBus ()+callProxy proxy name flags body onError onReturn = let+	msg = buildMethodCall proxy name flags body+	in call msg onError onReturn+-- | As 'callBlocking', except that the proxy's information is used+-- to build the message.+callProxyBlocking :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]+                  -> DBus (Either M.Error M.MethodReturn)+callProxyBlocking proxy name flags body =+	callBlocking $ buildMethodCall proxy name flags body+-- | As 'callBlocking_', except that the proxy's information is used+-- to build the message.+callProxyBlocking_ :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]+                   -> DBus M.MethodReturn+callProxyBlocking_ proxy name flags body =+	callBlocking_ $ buildMethodCall proxy name flags body+-- | As 'onSIgnal', except that the proxy's information is used+-- to build the match rule.+onProxySignal :: Proxy -> T.MemberName -> (M.Signal -> DBus ())+             -> DBus ()+onProxySignal proxy member handler = onSignal rule handler' where+	Proxy dest path iface = proxy+	rule = MR.MatchRule+		{ MR.matchType = Nothing+		, MR.matchSender = Just dest+		, MR.matchInterface = Just iface+		, MR.matchMember = Just member+		, MR.matchPath = Just path+		, MR.matchDestination = Nothing+		, MR.matchParameters = []+		}+	handler' _ msg = handler msg+buildMethodCall :: Proxy -> T.MemberName -> [M.Flag] -> [T.Variant]+                -> M.MethodCall+buildMethodCall proxy name flags body = msg where+	Proxy dest path iface = proxy+	msg = M.MethodCall path name (Just iface) (Just dest)+		(Set.fromList flags) body+listSig :: [T.Variant] -> Maybe T.Signature+listSig = T.mkSignature . mconcat . map (T.typeCode . T.variantType)