haskellnotificationslifetimedbusio-monad

Do I have to keep the caller of DBus.Client.export alive for as long as I want the exported interface at the object path to work?


If not, what keeps alive the functions I export to implement the interface?


I am implementing a notification server in Haskell, and at the moment I have something like this,

startServer :: IORef Notifications -> IO ()
startServer notifications = do
    client <- connectSession
    reply <- requestName client "org.freedesktop.Notifications" [nameDoNotQueue]
    export client "/org/freedesktop/Notifications" defaultInterface {
          interfaceName = "org.freedesktop.Notifications",
          interfaceMethods = [
          autoMethod "GetServerInformation" getServerInformation,
          autoMethod "GetCapabilities" getCapabilities,
          makeMethod "Notify" (signature_ notifyInSig) (signature_ notifyOutSig) (notify notifications)
        ]
    }
    when (reply == NamePrimaryOwner) $ forever $ threadDelay oneSec
      where
        oneSec = 1000000

which evolved from a skinnier, but standalone example that you can find in an question of mine and in the accepted answer.

At that time, the forever $ threadDelay oneSec was there because I was experimenting directly in main, so I didn't want the executable to return, because I needed time to send notifications via notify-send and see if notify was indeed doing its job.

But now that I've got some way further with my project, I'm not sure I need that anymore.

The way the code above is used at the moment is like this,

-- In the IO monad; not really in main, but it's ok to think of it as main itself, I think
withAsync (startServer notifications)
          (const $ fancyShow notifications)

where notifications is a IORef wrapping some state (roughly a [a]) that is mutated by both notify (exported by startServer as per the first snippet above) and fancyShow: the former populates the inside of the IORef with new notifications as they come, and the latter polls that IORef every 1/10 of a second and empties it, at the same time taking care of showing the notifications graphically.

Now fancyShow is the thing that uses forever and hence never returns, so I don't see why I should keep startServer from returning; after all,

Here I start having some concerns...

Once startServer has returned, the mechanism to recieve the notifications and putting them in the IORef is in place, whereas fancyShow keeps running forever, doing its job of pulling things out of the IORef (and showing it on screen), while... notify keeps doing its job of filling the IORef as the notifications come?

But who's keeping notify alive?

Is export effectively putting notify (and the other two methods, fwiw) in a "safe place", offloading the reponsibility of keeping them alive to... something else?

I would be tempted to think that the answer is "yes, I don't need to keep startServer from returning".

On the other hand, the existence of DBus.Client.unexport makes me wonder whether I am supposed to make use of it at all and, if so, wehther I'm supposed to use it in export's caller, hence implying I should not return from it.

But back to the previous hand, this notification server seems indeed to use a returning startServer, and the waiting is done only in main.

And again my question remains: where does export put the code to run notify? And what determines the lifetime of that?


Solution

  • The connectSession call uses forkIO to start a thread that listens to requests in a loop. The relevant code is buried in connectWith':

    connectWith' opts addr = do
        ...
        threadID <- forkIO $ do
            client <- readMVar clientMVar
            threadRunner (mainLoop client)
        ...
    

    where, by default, threadRunner is just another name for forever.

    The export call just modifies some data structures via a shared IORef:

    export client path interface =
      atomicModifyIORef_ (clientObjects client) $ addInterface path interface
    

    so the looping thread knows about the exported interface and can dispatch appropriate requests to it. Calling unexport just undoes those changes, so the looping thread stops dispatching to the interface.

    The looping thread works by accepting messages on the socket and then calling dispatch:

    mainLoop client = do
        ...
        received <- Control.Exception.try (DBus.Socket.receive sock)
        msg <- case received of
            ...
            Right msg -> return msg
        dispatch client msg
    

    and dispatch works by looking up the destination in the aforementioned data structures and forking a thread to run the appropriate callback (e.g., your notify function):

    dispatch client = go where
        ...
        go (ReceivedMethodCall serial msg) = do
            pathInfo <- readIORef (clientObjects client)
            ...
            _ <- forkIO $ case findMethodForCall (clientInterfaces client) pathInfo msg of
                Right Method { methodHandler = handler } ->
                  runReaderT (handler msg) client >>= sendResult
                ...
        ...
    

    Because, in a Haskell program, all threads forked via forkIO are killed when the main thread exits, all you need to do in order to keep servicing requests is to prevent the main thread from exiting. As long as the main thread exists, the looping thread will keep running (literally forever) in the background, accepting messages and forkIOing threads to call your notify function.

    So, as you've correctly guessed, it is safe to remove the forever loop from startServer, call startServer directly from the main thread in main (without forking or asyncing anything), and allow startServer to return. As long as the main thread keeps doing stuff in your application without exiting, everything should "just work", as if there's a magical oracle calling notify from forked threads on your behalf. In particular, if fancyShow runs forever in a loop, then you should be able to do without the async like so:

    do ...
       startServer notifications   -- modify this so it returns immediately
       fancyShow notifications     -- then this function runs "forever"