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
(export
ed 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,
IORef
, so startServer
returning doesn't invalidate that state,client
seems to have no other reason to exist than to be passed to export
, which is an IO
action that does it's job whether or not startServer
returns,reply
is used in my example precisely to decide whether to let startServer
return or not, but I guess I could just always have startServer
return, and return precisely that reply
to the caller to inform it of whether the export
succeded or not.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?
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 forkIO
ing 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"