module DBus.Client
(
Client(..)
, DBusR
, PathInfo(..)
, pathInterfaces
, pathChildren
, pathLens
, findPath
, Interface(..)
, defaultInterface
, connect
, connectSystem
, connectSession
, connectStarter
, disconnect
, call
, call_
, callNoReply
, getProperty
, getPropertyValue
, setProperty
, setPropertyValue
, getAllProperties
, getAllPropertiesMap
, buildPropertiesInterface
, export
, unexport
, Method(..)
, makeMethod
, AutoMethod
, autoMethod
, autoMethodWithMsg
, Property(..)
, autoProperty
, readOnlyProperty
, Reply(..)
, throwError
, SignalHandler
, addMatch
, removeMatch
, emit
, listen
, MatchRule
, formatMatchRule
, matchAny
, matchSender
, matchDestination
, matchPath
, matchInterface
, matchMember
, matchPathNamespace
, buildIntrospectionObject
, buildIntrospectionInterface
, buildIntrospectionMethod
, buildIntrospectionProperty
, buildIntrospectableInterface
, requestName
, releaseName
, RequestNameFlag
, nameAllowReplacement
, nameReplaceExisting
, nameDoNotQueue
, RequestNameReply(..)
, ReleaseNameReply(..)
, ClientError
, clientError
, clientErrorMessage
, clientErrorFatal
, ClientOptions
, clientSocketOptions
, clientThreadRunner
, defaultClientOptions
, connectWith
, dbusName
, dbusPath
, ErrorName
, errorFailed
, errorInvalidParameters
, errorUnknownMethod
) where
import Control.Applicative
import Control.Arrow
import Control.Concurrent
import qualified Control.Exception
import Control.Exception (SomeException, throwIO)
import Control.Lens
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Except
import Data.Bits ((.|.))
import Data.Coerce
import Data.Foldable hiding (forM_, and)
import Data.Function
import Data.Functor ((<$>))
import Data.IORef
import Data.List (intercalate, isPrefixOf)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe
import Data.Monoid
import Data.String
import qualified Data.Traversable as T
import Data.Typeable (Typeable)
import Data.Unique
import Data.Word (Word32)
import Prelude hiding (foldl, foldr, concat)
import DBus
import DBus.Internal.Message
import qualified DBus.Internal.Types as T
import qualified DBus.Introspection as I
import qualified DBus.Socket
import DBus.Transport (TransportOpen, SocketTransport)
data ClientError = ClientError
{ clientErrorMessage :: String
, clientErrorFatal :: Bool
}
deriving (Eq, Show, Typeable)
instance Control.Exception.Exception ClientError
clientError :: String -> ClientError
clientError msg = ClientError msg True
data Client = Client
{ clientSocket :: DBus.Socket.Socket
, clientPendingCalls :: IORef (Map Serial (MVar (Either MethodError MethodReturn)))
, clientSignalHandlers :: IORef (Map Unique SignalHandler)
, clientObjects :: IORef PathInfo
, clientThreadID :: ThreadId
, clientInterfaces :: [Interface]
}
type DBusR a = ReaderT Client IO a
data ClientOptions t = ClientOptions
{
clientSocketOptions :: DBus.Socket.SocketOptions t
, clientThreadRunner :: IO () -> IO ()
, clientBuildInterfaces :: Client -> [Interface]
}
type FormattedMatchRule = String
data SignalHandler =
SignalHandler Unique FormattedMatchRule (IORef Bool) (Signal -> IO ())
data Method = Method
{ methodName :: MemberName
, inSignature :: Signature
, outSignature :: Signature
, methodHandler :: MethodCall -> DBusR Reply
}
data Property = Property
{ propertyName :: MemberName
, propertyType :: Type
, propertyGetter :: Maybe (IO Variant)
, propertySetter :: Maybe (Variant -> IO ())
}
data Reply
= ReplyReturn [Variant]
| ReplyError ErrorName [Variant]
data Interface = Interface
{ interfaceName :: InterfaceName
, interfaceMethods :: [Method]
, interfaceProperties :: [Property]
, interfaceSignals :: [I.Signal]
}
defaultInterface :: Interface
defaultInterface =
Interface { interfaceName = ""
, interfaceMethods = []
, interfaceProperties = []
, interfaceSignals = []
}
data PathInfo = PathInfo
{ _pathInterfaces :: [Interface]
, _pathChildren :: Map String PathInfo
}
instance Eq PathInfo where
a == b = null (_pathInterfaces a) &&
null (_pathInterfaces b) &&
M.null (_pathChildren a) &&
M.null (_pathChildren b)
makeLenses ''PathInfo
emptyPathInfo :: PathInfo
emptyPathInfo = PathInfo
{ _pathInterfaces = []
, _pathChildren = M.empty
}
traverseElement
:: Applicative f
=> (a -> Maybe PathInfo -> f (Maybe PathInfo))
-> String
-> a
-> PathInfo
-> f PathInfo
traverseElement nothingHandler pathElement =
pathChildren . at pathElement . nothingHandler
lookupNothingHandler
:: (a -> Const (Data.Monoid.First PathInfo) b)
-> Maybe a
-> Const (Data.Monoid.First PathInfo) (Maybe b)
lookupNothingHandler = _Just
modifyNothingHandler ::
(PathInfo -> Identity PathInfo)
-> Maybe PathInfo
-> Identity (Maybe PathInfo)
modifyNothingHandler = non emptyPathInfo
pathLens ::
Applicative f =>
ObjectPath
-> ((PathInfo -> f PathInfo) -> Maybe PathInfo -> f (Maybe PathInfo))
-> (PathInfo -> f PathInfo)
-> PathInfo
-> f PathInfo
pathLens path nothingHandler =
foldl (\f pathElem -> f . traverseElement nothingHandler pathElem) id $
T.pathElements path
modifyPathInfoLens
:: ObjectPath
-> (PathInfo -> Identity PathInfo) -> PathInfo -> Identity PathInfo
modifyPathInfoLens path = pathLens path modifyNothingHandler
modifyPathInterfacesLens
:: ObjectPath
-> ([Interface] -> Identity [Interface])
-> PathInfo
-> Identity PathInfo
modifyPathInterfacesLens path = modifyPathInfoLens path . pathInterfaces
addInterface :: ObjectPath -> Interface -> PathInfo -> PathInfo
addInterface path interface =
over (modifyPathInterfacesLens path) (interface :)
findPath :: ObjectPath -> PathInfo -> Maybe PathInfo
findPath path = preview (pathLens path lookupNothingHandler)
findByGetterAndName ::
(Coercible a2 a1, Eq a1, Foldable t) =>
t a3 -> (a3 -> a2) -> a1 -> Maybe a3
findByGetterAndName options getter name =
find ((== name) . coerce . getter) options
findInterface :: [Interface] -> InterfaceName -> PathInfo -> Maybe Interface
findInterface alwaysPresent (T.InterfaceName name) info =
findByGetterAndName (_pathInterfaces info ++ alwaysPresent) interfaceName name
findMethod :: MemberName -> Interface -> Maybe Method
findMethod (T.MemberName name) interface =
findByGetterAndName (interfaceMethods interface) methodName name
findProperty :: MemberName -> Interface -> Maybe Property
findProperty (T.MemberName name) interface =
findByGetterAndName (interfaceProperties interface) propertyName name
connectSystem :: IO Client
connectSystem = do
env <- getSystemAddress
case env of
Nothing -> throwIO (clientError "connectSystem: DBUS_SYSTEM_BUS_ADDRESS is invalid.")
Just addr -> connect addr
connectSession :: IO Client
connectSession = do
env <- getSessionAddress
case env of
Nothing -> throwIO (clientError "connectSession: DBUS_SESSION_BUS_ADDRESS is missing or invalid.")
Just addr -> connect addr
connectStarter :: IO Client
connectStarter = do
env <- getStarterAddress
case env of
Nothing -> throwIO (clientError "connectStarter: DBUS_STARTER_ADDRESS is missing or invalid.")
Just addr -> connect addr
connect :: Address -> IO Client
connect = connectWith defaultClientOptions
connectWith :: TransportOpen t => ClientOptions t -> Address -> IO Client
connectWith opts addr = do
sock <- DBus.Socket.openWith (clientSocketOptions opts) addr
pendingCalls <- newIORef M.empty
signalHandlers <- newIORef M.empty
objects <- newIORef $ PathInfo [] M.empty
let threadRunner = clientThreadRunner opts
clientMVar <- newEmptyMVar
threadID <- forkIO $ do
client <- readMVar clientMVar
threadRunner (mainLoop client)
let client = Client
{ clientSocket = sock
, clientPendingCalls = pendingCalls
, clientSignalHandlers = signalHandlers
, clientObjects = objects
, clientThreadID = threadID
, clientInterfaces = clientBuildInterfaces opts client
}
putMVar clientMVar client
callNoReply client (methodCall dbusPath dbusInterface "Hello")
{ methodCallDestination = Just dbusName
}
return client
makeErrorReply :: ErrorName -> Reply
makeErrorReply errorName = ReplyError errorName []
buildPropertiesInterface :: Client -> Interface
buildPropertiesInterface client =
let alwaysPresent = clientInterfaces client
getPropertyObjF propertyInterfaceName memberName path info =
findInterfaceAtPath alwaysPresent info path
(Just $ fromString propertyInterfaceName) >>=
(maybeToEither errorUnknownMethod . findProperty (fromString memberName))
getPropertyObj propertyInterfaceName memberName path =
getPropertyObjF propertyInterfaceName memberName path <$>
readIORef (clientObjects client)
callGet MethodCall { methodCallPath = path }
propertyInterfaceName memberName =
left makeErrorReply <$>
runExceptT (do
property <- ExceptT $ getPropertyObj propertyInterfaceName
memberName path
ExceptT $ sequenceA $ maybeToEither errorNotAuthorized $
propertyGetter property)
callSet MethodCall { methodCallPath = path }
propertyInterfaceName memberName value =
left makeErrorReply <$>
runExceptT (do
property <- ExceptT $ getPropertyObj propertyInterfaceName memberName path
setter <- ExceptT $ return $ maybeToEither errorNotAuthorized $
propertySetter property
lift $ setter value)
callGetAll MethodCall { methodCallPath = path } propertyInterfaceName =
left makeErrorReply <$>
runExceptT (do
info <- lift $ readIORef (clientObjects client)
propertyInterface <-
ExceptT $ return $ findInterfaceAtPath alwaysPresent info path $
Just $ fromString propertyInterfaceName
let properties = interfaceProperties propertyInterface
nameGetters :: [IO (String, Variant)]
nameGetters = [ (coerce name,) <$> getter |
Property { propertyName = name
, propertyGetter = Just getter
} <- properties]
lift $ M.fromList <$> T.sequenceA nameGetters)
in
defaultInterface
{ interfaceName = propertiesInterfaceName
, interfaceMethods =
[ autoMethodWithMsg "Get" callGet
, autoMethodWithMsg "GetAll" callGetAll
, autoMethodWithMsg "Set" callSet
]
, interfaceSignals =
[ I.Signal
{ I.signalName = "PropertiesChanged"
, I.signalArgs =
[ I.SignalArg
{ I.signalArgName = "interface_name"
, I.signalArgType = T.TypeString
}
, I.SignalArg
{ I.signalArgName = "changed_properties"
, I.signalArgType = T.TypeDictionary T.TypeString T.TypeVariant
}
, I.SignalArg
{ I.signalArgName = "invalidated_properties"
, I.signalArgType = T.TypeArray T.TypeString
}
]
}
]
}
buildIntrospectableInterface :: Client -> Interface
buildIntrospectableInterface client =
defaultInterface
{ interfaceName = introspectableInterfaceName
, interfaceMethods = [ autoMethodWithMsg "Introspect" callIntrospect ]
} where
callIntrospect MethodCall { methodCallPath = path } = do
info <- readIORef (clientObjects client)
return $ left makeErrorReply $ do
targetInfo <- maybeToEither errorUnknownObject $ findPath path info
maybeToEither errorUnknownObject $ I.formatXML $
buildIntrospectionObject defaultInterfaces
targetInfo (T.pathElements path)
defaultInterfaces = map buildIntrospectionInterface $ clientInterfaces client
defaultClientOptions :: ClientOptions SocketTransport
defaultClientOptions = ClientOptions
{ clientSocketOptions = DBus.Socket.defaultSocketOptions
, clientThreadRunner = forever
, clientBuildInterfaces =
\client -> map ($ client) [buildPropertiesInterface, buildIntrospectableInterface]
}
disconnect :: Client -> IO ()
disconnect client = do
killThread (clientThreadID client)
disconnect' client
disconnect' :: Client -> IO ()
disconnect' client = do
pendingCalls <- atomicModifyIORef (clientPendingCalls client) (\p -> (M.empty, p))
forM_ (M.toList pendingCalls) $ \(k, v) ->
putMVar v (Left (methodError k errorDisconnected))
atomicWriteIORef (clientSignalHandlers client) M.empty
atomicWriteIORef (clientObjects client) emptyPathInfo
DBus.Socket.close (clientSocket client)
mainLoop :: Client -> IO ()
mainLoop client = do
let sock = clientSocket client
received <- Control.Exception.try (DBus.Socket.receive sock)
msg <- case received of
Left err -> do
disconnect' client
throwIO (clientError (DBus.Socket.socketErrorMessage err))
Right msg -> return msg
dispatch client msg
dispatch :: Client -> ReceivedMessage -> IO ()
dispatch client = go where
go (ReceivedMethodReturn _ msg) = dispatchReply (methodReturnSerial msg) (Right msg)
go (ReceivedMethodError _ msg) = dispatchReply (methodErrorSerial msg) (Left msg)
go (ReceivedSignal _ msg) = do
handlers <- readIORef (clientSignalHandlers client)
forM_ (M.toAscList handlers) (\(_, SignalHandler _ _ _ h) -> forkIO $ void $ h msg)
go (ReceivedMethodCall serial msg) = do
pathInfo <- readIORef (clientObjects client)
let sender = methodCallSender msg
sendResult reply =
case reply of
ReplyReturn vs -> send_ client (methodReturn serial)
{ methodReturnDestination = sender
, methodReturnBody = vs
} (\_ -> return ())
ReplyError name vs -> send_ client (methodError serial name)
{ methodErrorDestination = sender
, methodErrorBody = vs
} (\_ -> return ())
_ <- forkIO $ case findMethodForCall (clientInterfaces client) pathInfo msg of
Right Method { methodHandler = handler } ->
runReaderT (handler msg) client >>= sendResult
Left errName -> send_ client
(methodError serial errName) { methodErrorDestination = sender }
(\_ -> return ())
return ()
go _ = return ()
dispatchReply serial result = do
pending <- atomicModifyIORef
(clientPendingCalls client)
(\p -> case M.lookup serial p of
Nothing -> (p, Nothing)
Just mvar -> (M.delete serial p, Just mvar))
case pending of
Just mvar -> putMVar mvar result
Nothing -> return ()
findInterfaceAtPath
:: [Interface]
-> PathInfo
-> ObjectPath
-> Maybe InterfaceName
-> Either ErrorName Interface
findInterfaceAtPath defaultInterfaces info path name =
maybeToEither errorUnknownObject (findPath path info) >>=
(maybeToEither errorUnknownInterface .
maybe (const Nothing) (findInterface defaultInterfaces) name)
findMethodForCall ::
[Interface] -> PathInfo -> MethodCall -> Either ErrorName Method
findMethodForCall defaultInterfaces info
MethodCall { methodCallInterface = interface
, methodCallMember = member
, methodCallPath = path
} =
findInterfaceAtPath defaultInterfaces info path interface >>=
(maybeToEither errorUnknownMethod . findMethod member)
data RequestNameFlag
= AllowReplacement
| ReplaceExisting
| DoNotQueue
deriving (Eq, Show)
nameAllowReplacement :: RequestNameFlag
nameAllowReplacement = AllowReplacement
nameReplaceExisting :: RequestNameFlag
nameReplaceExisting = ReplaceExisting
nameDoNotQueue :: RequestNameFlag
nameDoNotQueue = DoNotQueue
data RequestNameReply
= NamePrimaryOwner
| NameInQueue
| NameExists
| NameAlreadyOwner
| UnknownRequestNameReply Word32
deriving (Eq, Show)
data ReleaseNameReply
= NameReleased
| NameNonExistent
| NameNotOwner
| UnknownReleaseNameReply Word32
deriving (Eq, Show)
encodeFlags :: [RequestNameFlag] -> Word32
encodeFlags = foldr ((.|.) . flagValue) 0 where
flagValue AllowReplacement = 0x1
flagValue ReplaceExisting = 0x2
flagValue DoNotQueue = 0x4
requestName :: Client -> BusName -> [RequestNameFlag] -> IO RequestNameReply
requestName client name flags = do
reply <- call_ client (methodCall dbusPath dbusInterface "RequestName")
{ methodCallDestination = Just dbusName
, methodCallBody = [toVariant name, toVariant (encodeFlags flags)]
}
var <- case listToMaybe (methodReturnBody reply) of
Just x -> return x
Nothing -> throwIO (clientError "requestName: received empty response")
{ clientErrorFatal = False
}
code <- case fromVariant var of
Just x -> return x
Nothing -> throwIO (clientError ("requestName: received invalid response code " ++ showsPrec 11 var ""))
{ clientErrorFatal = False
}
return $ case code :: Word32 of
1 -> NamePrimaryOwner
2 -> NameInQueue
3 -> NameExists
4 -> NameAlreadyOwner
_ -> UnknownRequestNameReply code
releaseName :: Client -> BusName -> IO ReleaseNameReply
releaseName client name = do
reply <- call_ client (methodCall dbusPath dbusInterface "ReleaseName")
{ methodCallDestination = Just dbusName
, methodCallBody = [toVariant name]
}
var <- case listToMaybe (methodReturnBody reply) of
Just x -> return x
Nothing -> throwIO (clientError "releaseName: received empty response")
{ clientErrorFatal = False
}
code <- case fromVariant var of
Just x -> return x
Nothing -> throwIO (clientError ("releaseName: received invalid response code " ++ showsPrec 11 var ""))
{ clientErrorFatal = False
}
return $ case code :: Word32 of
1 -> NameReleased
2 -> NameNonExistent
3 -> NameNotOwner
_ -> UnknownReleaseNameReply code
send_ :: Message msg => Client -> msg -> (Serial -> IO a) -> IO a
send_ client msg io = do
result <- Control.Exception.try (DBus.Socket.send (clientSocket client) msg io)
case result of
Right x -> return x
Left err -> throwIO (clientError (DBus.Socket.socketErrorMessage err))
{ clientErrorFatal = DBus.Socket.socketErrorFatal err
}
call :: Client -> MethodCall -> IO (Either MethodError MethodReturn)
call client msg = do
let safeMsg = msg
{ methodCallReplyExpected = True
}
mvar <- newEmptyMVar
let ref = clientPendingCalls client
serial <- send_ client safeMsg (\serial -> atomicModifyIORef ref (\p -> (M.insert serial mvar p, serial)))
Control.Exception.onException
(takeMVar mvar)
(atomicModifyIORef_ ref (M.delete serial))
call_ :: Client -> MethodCall -> IO MethodReturn
call_ client msg = do
result <- call client msg
case result of
Left err -> throwIO (clientError ("Call failed: " ++ methodErrorMessage err))
{ clientErrorFatal = methodErrorName err == errorDisconnected
}
Right ret -> return ret
callNoReply :: Client -> MethodCall -> IO ()
callNoReply client msg = do
let safeMsg = msg
{ methodCallReplyExpected = False
}
send_ client safeMsg (\_ -> return ())
orDefaultInterface :: Maybe InterfaceName -> InterfaceName
orDefaultInterface = fromMaybe "org.freedesktop.DBus"
dummyMethodError :: MethodError
dummyMethodError =
MethodError { methodErrorName = errorName_ "org.ClientTypeMismatch"
, methodErrorSerial = T.Serial 1
, methodErrorSender = Nothing
, methodErrorDestination = Nothing
, methodErrorBody = []
}
unpackVariant :: IsValue a => MethodCall -> Variant -> Either MethodError a
unpackVariant MethodCall { methodCallSender = sender } variant =
maybeToEither dummyMethodError { methodErrorBody =
[variant, toVariant $ show $ variantType variant]
, methodErrorSender = sender
} $ fromVariant variant
getProperty :: Client -> MethodCall -> IO (Either MethodError Variant)
getProperty client
msg@MethodCall { methodCallInterface = interface
, methodCallMember = member
} =
(>>= (unpackVariant msg . head . methodReturnBody)) <$>
call client msg { methodCallInterface = Just propertiesInterfaceName
, methodCallMember = getMemberName
, methodCallBody = [ toVariant (coerce (orDefaultInterface interface) :: String)
, toVariant (coerce member :: String)
]
}
getPropertyValue :: IsValue a => Client -> MethodCall -> IO (Either MethodError a)
getPropertyValue client msg =
(>>= unpackVariant msg) <$> getProperty client msg
setProperty :: Client -> MethodCall -> Variant -> IO (Either MethodError MethodReturn)
setProperty client
msg@MethodCall { methodCallInterface = interface
, methodCallMember = member
} value =
call client msg { methodCallInterface = Just propertiesInterfaceName
, methodCallMember = setMemberName
, methodCallBody =
[ toVariant (coerce (orDefaultInterface interface) :: String)
, toVariant (coerce member :: String)
, value
]
}
setPropertyValue
:: IsValue a
=> Client -> MethodCall -> a -> IO (Maybe MethodError)
setPropertyValue client msg v = eitherToMaybe <$> setProperty client msg (toVariant v)
where eitherToMaybe (Left a) = Just a
eitherToMaybe (Right _) = Nothing
getAllProperties :: Client -> MethodCall -> IO (Either MethodError MethodReturn)
getAllProperties client
msg@MethodCall { methodCallInterface = interface } =
call client msg { methodCallInterface = Just propertiesInterfaceName
, methodCallMember = getAllMemberName
, methodCallBody = [toVariant (coerce (orDefaultInterface interface) :: String)]
}
getAllPropertiesMap :: Client -> MethodCall -> IO (Either MethodError (M.Map String Variant))
getAllPropertiesMap client msg =
(>>= (maybeToEither dummyMethodError . fromVariant . head . methodReturnBody))
<$> getAllProperties client msg
addMatch :: Client -> MatchRule -> (Signal -> IO ()) -> IO SignalHandler
addMatch client rule io = do
let formatted = case formatMatchRule rule of
"" -> "type='signal'"
x -> "type='signal'," ++ x
handlerId <- newUnique
registered <- newIORef True
let handler = SignalHandler handlerId formatted registered (\msg -> when (checkMatchRule rule msg) (io msg))
atomicModifyIORef (clientSignalHandlers client) (\hs -> (M.insert handlerId handler hs, ()))
_ <- call_ client (methodCall dbusPath dbusInterface "AddMatch")
{ methodCallDestination = Just dbusName
, methodCallBody = [toVariant formatted]
}
return handler
removeMatch :: Client -> SignalHandler -> IO ()
removeMatch client (SignalHandler handlerId formatted registered _) = do
shouldUnregister <- atomicModifyIORef registered (\wasRegistered -> (False, wasRegistered))
when shouldUnregister $ do
atomicModifyIORef (clientSignalHandlers client) (\hs -> (M.delete handlerId hs, ()))
_ <- call_ client (methodCall dbusPath dbusInterface "RemoveMatch")
{ methodCallDestination = Just dbusName
, methodCallBody = [toVariant formatted]
}
return ()
listen :: Client -> MatchRule -> (Signal -> IO ()) -> IO ()
listen client rule io = void $ addMatch client rule io
emit :: Client -> Signal -> IO ()
emit client msg = send_ client msg (\_ -> return ())
data MatchRule = MatchRule
{
matchSender :: Maybe BusName
, matchDestination :: Maybe BusName
, matchPath :: Maybe ObjectPath
, matchInterface :: Maybe InterfaceName
, matchMember :: Maybe MemberName
, matchPathNamespace :: Maybe ObjectPath
}
instance Show MatchRule where
showsPrec d rule = showParen (d > 10) (showString "MatchRule " . shows (formatMatchRule rule))
formatMatchRule :: MatchRule -> String
formatMatchRule rule = intercalate "," predicates where
predicates = catMaybes
[ f "sender" matchSender formatBusName
, f "destination" matchDestination formatBusName
, f "path" matchPath formatObjectPath
, f "interface" matchInterface formatInterfaceName
, f "member" matchMember formatMemberName
, f "path_namespace" matchPathNamespace formatObjectPath
]
f :: String -> (MatchRule -> Maybe a) -> (a -> String) -> Maybe String
f key get text = do
val <- fmap text (get rule)
return (concat [key, "='", val, "'"])
matchAny :: MatchRule
matchAny = MatchRule Nothing Nothing Nothing Nothing Nothing Nothing
checkMatchRule :: MatchRule -> Signal -> Bool
checkMatchRule rule msg = and
[ maybe True (\x -> signalSender msg == Just x) (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)
, maybe True (`pathPrefix` signalPath msg) (matchPathNamespace rule)
] where
pathPrefix = isPrefixOf `on` formatObjectPath
data MethodExc = MethodExc ErrorName [Variant]
deriving (Show, Eq, Typeable)
instance Control.Exception.Exception MethodExc
throwError :: ErrorName
-> String
-> [Variant]
-> IO a
throwError name message extra = Control.Exception.throwIO (MethodExc name (toVariant message : extra))
returnInvalidParameters :: Monad m => m Reply
returnInvalidParameters = return $ ReplyError errorInvalidParameters []
class AutoMethod a where
funTypes :: a -> ([Type], [Type])
apply :: a -> [Variant] -> DBusR Reply
handleTopLevelReturn :: IsVariant a => a -> [Variant]
handleTopLevelReturn value =
case toVariant value of
T.Variant (T.ValueStructure xs) -> fmap T.Variant xs
v -> [v]
instance IsValue a => AutoMethod (IO a) where
funTypes io = funTypes (lift io :: DBusR a)
apply io = apply (lift io :: DBusR a)
instance IsValue a => AutoMethod (DBusR a) where
funTypes _ = ([], outTypes) where
aType :: Type
aType = typeOf (undefined :: a)
outTypes =
case aType of
TypeStructure ts -> ts
_ -> [aType]
apply io [] = ReplyReturn . handleTopLevelReturn <$> io
apply _ _ = returnInvalidParameters
instance IsValue a => AutoMethod (IO (Either Reply a)) where
funTypes io = funTypes (lift io :: DBusR (Either Reply a))
apply io = apply (lift io :: DBusR (Either Reply a))
instance IsValue a => AutoMethod (DBusR (Either Reply a)) where
funTypes _ = ([], outTypes) where
aType :: Type
aType = typeOf (undefined :: a)
outTypes =
case aType of
TypeStructure ts -> ts
_ -> [aType]
apply io [] = either id (ReplyReturn . handleTopLevelReturn) <$> io
apply _ _ = returnInvalidParameters
instance (IsValue a, AutoMethod fn) => AutoMethod (a -> fn) where
funTypes fn = cased where
cased = 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)
apply _ [] = returnInvalidParameters
apply fn (v:vs) = case fromVariant v of
Just v' -> apply (fn v') vs
Nothing -> returnInvalidParameters
autoMethod :: (AutoMethod fn) => MemberName -> fn -> Method
autoMethod name fun = autoMethodWithMsg name $ const fun
autoMethodWithMsg :: (AutoMethod fn) => MemberName -> (MethodCall -> fn) -> Method
autoMethodWithMsg name fun = makeMethod name inSig outSig io where
(typesIn, typesOut) = funTypes (fun undefined)
inSig = fromMaybe (invalid "input") $ signature typesIn
outSig = fromMaybe (invalid "output") $ signature typesOut
io msg = apply (fun msg) (methodCallBody msg)
invalid label = error (concat
[ "Method "
, "."
, formatMemberName name
, " has an invalid "
, label
, " signature."])
autoProperty
:: forall v. (IsValue v)
=> MemberName -> Maybe (IO v) -> Maybe (v -> IO ()) -> Property
autoProperty name mgetter msetter =
Property name propType (fmap toVariant <$> mgetter) (variantSetter <$> msetter)
where propType = typeOf (undefined :: v)
variantSetter setter =
let newFun variant = maybe (return ()) setter (fromVariant variant)
in newFun
readOnlyProperty :: (IsValue v) => MemberName -> IO v -> Property
readOnlyProperty name getter = autoProperty name (Just getter) Nothing
makeMethod
:: MemberName
-> Signature
-> Signature
-> (MethodCall -> DBusR Reply)
-> Method
makeMethod name inSig outSig io = Method name inSig outSig
(\msg -> do
fromReader <- ask
lift $ Control.Exception.catch
(Control.Exception.catch
(runReaderT (io msg) fromReader)
(\(MethodExc name' vs') -> return (ReplyError name' vs')))
(\exc -> return (ReplyError errorFailed
[toVariant (show (exc :: SomeException))])))
export :: Client -> ObjectPath -> Interface -> IO ()
export client path interface =
atomicModifyIORef_ (clientObjects client) $ addInterface path interface
unexport :: Client -> ObjectPath -> IO ()
unexport client path = atomicModifyIORef_ (clientObjects client) clear
where clear = over (modifyPathInterfacesLens path) $ const []
buildIntrospectionObject :: [I.Interface] -> PathInfo -> [String] -> I.Object
buildIntrospectionObject defaultInterfaces
PathInfo
{ _pathInterfaces = interfaces
, _pathChildren = infoChildren
} elems =
I.Object
{ I.objectPath = T.fromElements elems
, I.objectInterfaces =
(if null interfaces then [] else defaultInterfaces) ++
map buildIntrospectionInterface interfaces
, I.objectChildren = M.elems $ M.mapWithKey recurseFromString infoChildren
}
where recurseFromString stringNode nodeInfo =
buildIntrospectionObject defaultInterfaces nodeInfo $ elems ++ [stringNode]
buildIntrospectionInterface :: Interface -> I.Interface
buildIntrospectionInterface Interface
{ interfaceName = name
, interfaceMethods = methods
, interfaceProperties = properties
, interfaceSignals = signals
} =
I.Interface
{ I.interfaceName = name
, I.interfaceMethods = map buildIntrospectionMethod methods
, I.interfaceProperties = map buildIntrospectionProperty properties
, I.interfaceSignals = signals
}
buildIntrospectionProperty :: Property -> I.Property
buildIntrospectionProperty (Property memberName ptype getter setter) =
I.Property { I.propertyName = coerce memberName
, I.propertyType = ptype
, I.propertyRead = isJust getter
, I.propertyWrite = isJust setter
}
buildIntrospectionMethod :: Method -> I.Method
buildIntrospectionMethod Method
{ methodName = name
, inSignature = inSig
, outSignature = outSig
} = I.Method
{ I.methodName = name
, I.methodArgs = zipWith makeMethodArg ['a'..'z'] $ inTuples ++ outTuples
}
where inTuples = map (, I.In) $ coerce inSig
outTuples = map (, I.Out) $ coerce outSig
makeMethodArg nameChar (t, dir) =
I.MethodArg { I.methodArgName = [nameChar]
, I.methodArgType = t
, I.methodArgDirection = dir
}
errorFailed :: ErrorName
errorFailed = errorName_ "org.freedesktop.DBus.Error.Failed"
errorDisconnected :: ErrorName
errorDisconnected = errorName_ "org.freedesktop.DBus.Error.Disconnected"
errorUnknownObject :: ErrorName
errorUnknownObject = errorName_ "org.freedesktop.DBus.Error.UnknownObject"
errorUnknownInterface :: ErrorName
errorUnknownInterface = errorName_ "org.freedesktop.DBus.Error.UnknownInterface"
errorUnknownMethod :: ErrorName
errorUnknownMethod = errorName_ "org.freedesktop.DBus.Error.UnknownMethod"
errorInvalidParameters :: ErrorName
errorInvalidParameters = errorName_ "org.freedesktop.DBus.Error.InvalidParameters"
errorNotAuthorized :: ErrorName
errorNotAuthorized = errorName_ "org.freedesktop.DBus.Error.NotAuthorized"
dbusName :: BusName
dbusName = busName_ "org.freedesktop.DBus"
dbusPath :: ObjectPath
dbusPath = objectPath_ "/org/freedesktop/DBus"
dbusInterface :: InterfaceName
dbusInterface = interfaceName_ "org.freedesktop.DBus"
introspectableInterfaceName :: InterfaceName
introspectableInterfaceName = interfaceName_ "org.freedesktop.DBus.Introspectable"
propertiesInterfaceName :: InterfaceName
propertiesInterfaceName = fromString "org.freedesktop.DBus.Properties"
getAllMemberName :: MemberName
getAllMemberName = fromString "GetAll"
getMemberName :: MemberName
getMemberName = fromString "Get"
setMemberName :: MemberName
setMemberName = fromString "Set"
maybeToEither :: b -> Maybe a -> Either b a
maybeToEither = flip maybe Right . Left
atomicModifyIORef_ :: IORef a -> (a -> a) -> IO ()
atomicModifyIORef_ ref fn = atomicModifyIORef ref (fn &&& const ())
#if !MIN_VERSION_base(4,6,0)
atomicWriteIORef :: IORef a -> a -> IO ()
atomicWriteIORef ref x = atomicModifyIORef ref $ const x &&& const ()
#endif