desktop-portal-0.1.0.0: test/Desktop/Portal/TestUtil.hs
module Desktop.Portal.TestUtil
( successResponse,
toVariantMap,
toVariantText,
TestHandle,
client,
withTestBus,
withMethodResponse,
savingRequestArguments,
savingRequestArguments_,
sendSignal,
)
where
import Control.Concurrent (newEmptyMVar, tryPutMVar, tryReadMVar)
import Control.Exception (finally)
import Control.Monad (unless, void)
import Control.Monad.IO.Class (MonadIO (..))
import DBus (BusName, InterfaceName, IsVariant (fromVariant), MemberName, MethodCall (..), ObjectPath, Variant, formatBusName, objectPath_, toVariant)
import DBus.Client (Client, Interface (..), Reply (..), RequestNameReply (..), connectSession, defaultInterface, disconnect, emit, export, makeMethod, nameDoNotQueue, requestName, unexport)
import DBus.Internal.Message (Signal (..))
import DBus.Internal.Types (Atom (AtomText), Signature (..), Value (ValueMap), Variant (Variant))
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Word (Word32)
import Desktop.Portal qualified as Portal
import GHC.IO.Handle (hGetLine)
import System.Environment (lookupEnv, setEnv)
import System.Process (StdStream (..), createProcess, proc, std_out, terminateProcess)
data TestHandle = TestHandle
{ serverClient :: Client,
clientClient :: Portal.Client
}
client :: TestHandle -> Portal.Client
client c = c.clientClient
withTestBus :: (TestHandle -> IO ()) -> IO ()
withTestBus cmd = do
let dbusArgs =
[ "--print-address",
"--nopidfile",
"--nofork",
"--syslog-only",
"--config-file=test/dbus-config.xml"
]
(_, Just hOut, _, ph) <-
createProcess (proc "dbus-daemon" dbusArgs) {std_out = CreatePipe}
oldSessionAddr <- lookupEnv sessionAddressEnv
flip finally (stopDbus oldSessionAddr ph) $ do
addrLine <- hGetLine hOut
setEnv sessionAddressEnv addrLine
serverClient <- connectSession
flip finally (disconnect serverClient) $ do
clientClient <- Portal.connect
flip finally (Portal.disconnect clientClient) $ do
requestName serverClient portalBusName [nameDoNotQueue] >>= \case
NamePrimaryOwner -> cmd TestHandle {serverClient, clientClient}
reply -> fail ("Can't get portal name: " <> show reply)
where
stopDbus oldSessionAddr ph = do
terminateProcess ph
maybe (pure ()) (setEnv sessionAddressEnv) oldSessionAddr
withMethodResponse :: TestHandle -> InterfaceName -> MemberName -> [Variant] -> IO () -> IO ()
withMethodResponse handle interfaceName methodName methodResponse cmd = do
export
handle.serverClient
portalObjectPath
defaultInterface
{ interfaceName,
interfaceMethods = [makeMethod methodName (Signature []) (Signature []) handleMethodCall]
}
cmd
unexport handle.serverClient portalObjectPath
where
handleMethodCall methodCall = do
emitResponseSignal handle methodCall methodResponse
pure (ReplyReturn [toVariant (methodRequestHandle methodCall)])
savingRequestArguments :: TestHandle -> InterfaceName -> MemberName -> IO () -> IO [Variant]
savingRequestArguments handle interfaceName methodName cmd = do
savingRequestArguments_ handle interfaceName methodName True cmd
savingRequestArguments_ :: TestHandle -> InterfaceName -> MemberName -> Bool -> IO () -> IO [Variant]
savingRequestArguments_ handle interfaceName methodName hasResponse cmd = do
argsVar <- newEmptyMVar
export
handle.serverClient
portalObjectPath
defaultInterface
{ interfaceName,
interfaceMethods = [makeMethod methodName (Signature []) (Signature []) (handleMethodCall argsVar)]
}
cmd
unexport handle.serverClient portalObjectPath
tryReadMVar argsVar >>= \case
Just args -> pure (removeHandleToken args)
Nothing -> fail "No method was called during the callback!"
where
handleMethodCall argsVar methodCall = do
putSucceeded <- liftIO $ tryPutMVar argsVar (methodCallBody methodCall)
unless putSucceeded $
fail "Method arguments already saved: is more than one method being called?"
if hasResponse
then do
emitResponseSignal handle methodCall [toVariant (1 :: Word32)]
pure (ReplyReturn [toVariant (methodRequestHandle methodCall)])
else do
pure (ReplyReturn [])
removeHandleToken = \case
args
| hasResponse && (not (null args)),
Variant (ValueMap kt vt argsMap) <- args !! (length args - 1) ->
take (length args - 1) args <> [Variant (ValueMap kt vt (Map.delete (AtomText "handle_token") argsMap))]
| otherwise ->
args
sendSignal :: TestHandle -> InterfaceName -> MemberName -> [Variant] -> IO ()
sendSignal handle signalInterface signalMember signalBody =
emit
handle.serverClient
Signal
{ signalPath = "/org/freedesktop/portal/desktop",
signalInterface,
signalMember,
signalSender = Just portalBusName,
signalDestination = Just (Portal.clientName handle.clientClient),
signalBody
}
emitResponseSignal :: MonadIO m => TestHandle -> MethodCall -> [Variant] -> m ()
emitResponseSignal handle methodCall signalBody = do
void . liftIO $
emit
handle.serverClient
Signal
{ signalPath = methodRequestHandle methodCall,
signalInterface = "org.freedesktop.portal.Request",
signalMember = "Response",
signalSender = Just portalBusName,
signalDestination = methodCallSender methodCall,
signalBody
}
methodRequestHandle :: MethodCall -> ObjectPath
methodRequestHandle methodCall =
objectPath_ $
"/org/freedesktop/portal/desktop/request/"
<> escapeClientName senderClientName
<> "/"
<> handleToken
where
args = methodCallBody methodCall
Just (optionsArg :: Map Text Variant) = fromVariant (args !! (length args - 1))
Just (handleToken :: String) = Map.lookup "handle_token" optionsArg >>= fromVariant
Just senderClientName = methodCall.methodCallSender
escapeClientName =
map (\case '.' -> '_'; c -> c) . drop 1 . formatBusName
sessionAddressEnv :: String
sessionAddressEnv = "DBUS_SESSION_BUS_ADDRESS"
portalObjectPath :: ObjectPath
portalObjectPath = "/org/freedesktop/portal/desktop"
portalBusName :: BusName
portalBusName = "org.freedesktop.portal.Desktop"
successResponse :: [(Text, Variant)] -> [Variant]
successResponse pairs =
[ toVariant (0 :: Word32), -- success code
toVariantMap pairs
]
toVariantMap :: [(Text, Variant)] -> Variant
toVariantMap = toVariant . Map.fromList
-- | Specialised 'toVariant' to avoid need for type assertions.
toVariantText :: Text -> Variant
toVariantText = toVariant