haskellserverfunctional-programmingdbus

How do I make a minimal working example for the a DBus server?


In the doc for DBus there's this example,

ping :: MethodCall -> IO Reply
ping _ = ReplyReturn []

sayHello :: String -> IO String
sayHello name = return ("Hello " ++ name ++ "!")

 export client "/hello_world"
   defaultInterface { interfaceName = "com.example.HelloWorld"
                    , interfaceMethods =
                      [ method "com.example.HelloWorld" "Ping" ping
                      , autoMethod "com.example.HelloWorld" "Hello" sayHello
                      ]
                    }

which should help me understand how to implement a dbus-based server.

However, the example seems a bit outdated in several respects:

But most importantly the example is incomplete. How do I make use of it?

The doc for export reads

Export the given Interface at the given ObjectPath

makes me think all I have to do is to put the last piece of the snippet (the call to export) in a main :: IO () function, and then keep it alive with _ <- getChar or something before return (), but I still have no clue how to... have those functions sayHello and ping be called. Looking at this answer I kinda feel that I should use dbus-send, but with whatever Haskell nonsense I've managed to compile based on the above code snippet, if I run

dbus-send --session --print-reply --dest="com.example.HelloWorld" /hello_world com.example.HelloWorld.Ping

I get

Error org.freedesktop.DBus.Error.ServiceUnknown: The name is not activatable

which, given my knowledge at the present time, can be because of some unrelated-to-Haskell set-up I supposed to do, as well as because I wrote Haskell nonsense.


Anyway, here's some code I managed to compile, but that doesn't actually do anything:

{-# LANGUAGE OverloadedStrings #-}

import DBus
import DBus.Client

ping :: MethodCall -> IO Reply
ping _ = return (ReplyReturn [])

sayHello :: String -> IO String
sayHello name = return ("Hello " ++ name ++ "!")

main :: IO ()
main = do
    client <- connectSession

    export client "/hello_world"
        defaultInterface { interfaceName = "com.example.HelloWorld"
                         , interfaceMethods =
                           [
                           autoMethod "com.example.HelloWorld" sayHello
                           ]
                         }
    _ <- getLine
    return ()

Solution

  • Error org.freedesktop.DBus.Error.ServiceUnknown: The name is not activatable
    

    The error message is talking about bus names, which are basically the "addresses" of each service (or even each client) on the bus. Most importantly, it means that the requested --dest= name is currently not present on the bus.

    (Alongside that, the error message also means dbus-daemon doesn't know how to start your service on-demand, but you can safely ignore that – "activation" is just the auto-start mechanism and is not relevant when you're manually starting your service.)

    Bus names are not set through configuration (that's only for auto-start); they're "claimed" by the service using a special bus call, every time it starts up and connects to the bus. Your process needs to call requestName (docs) to make that happen.

    Examples (I don't know enough Haskell to make much sense of them):

    (The interface name is not that – interfaces are just something your own code uses to group or distinguish methods, whereas the service name (aka bus name, aka unique name) is the actual "D-Bus address" of your whole process. For a simple 1-object, 1-interface service they're very likely to be identical, but if you browse through d-spy you'll see many examples where a service makes many objects available, and which have multiple interfaces, all at a single bus name.)

    Ideally, the bus name should be claimed after the initial objects have been set up and exported, to prevent cases where calls to your service fail with "no such object" or similar because it just happened to arrive a moment too early.