I'm writing some code with reactive-banana and gtk2hs that needs to read from a file handle. I need to have at least two threads (one to read keyboard events with reactive banana and one to read from the file handle), so at the moment I have code that looks something like this:
type EventSource a = (AddHandler a, a -> IO ())
fire :: EventSource a -> a -> IO ()
fire = snd
watch :: EventSource ByteString -> Handle -> IO ()
watch textIn pty = forever $
hGetLine pty >>= fire textIn >> threadWaitRead pty
With the following main function:
mainAxn :: IO ()
mainAxn = do
h <- openFile "foo" ReadMode
initGUI
win <- windowNew
txt <- textViewNew
containerAdd win txt
widgetShowAll win
(keyPress, textIn) <-
(,) <$> newAddHandler <*> newAddHandler
network <- setupNetwork keyPress textIn
actuate network
_ <- forkIO $ watch textIn h
_ <- win `on` keyPressEvent $
eventKeyVal >>= liftIO . fire keyPress >> return True
mainGUI
and my event network set up as follows:
setupNetwork :: EventSource KeyVal -> EventSource ByteString -> IO EventNetwork
setupNetwork keyPress textIn = compile $ do
ePressed <- fromAddHandler $ addHandler keyPress
eText <- fromAddHandler $ addHandler textIn
reactimate $ print <$> (filterJust $ keyToChar <$> ePressed)
reactimate $ print <$> eText
(except in my actual code, those reactimate
calls write to the TextView
built in mainAxn
). I found that I needed to build with -threaded
to make the event network correctly capture both text from textIn
and keypresses from keyPress
, which caused issues because it's not safe to modify objects from the gtk package concurrently.
At the moment, I have postGUIAsync
calls scattered throughout my code, and I've found that using postGUISync
causes the whole thing to deadlock --- I'm not sure why. I think it's because I end up calling postGUISync
inside of the same thread that ran mainGUI
.
It seems like it would be better to run all of the GUI stuff in its own thread and use the postGUI*
functions for every access to it. However, when I change the last line of mainAxn
to be
forkIO mainGUI
return ()
the program returns immediately when it hits the end of mainAxn
. I tried to fix that by using:
forkIO mainGUI
forever $ return ()
but then the gtk GUI never opens at all, and I don't understand why.
What's the right way to do this? What am I missing?
The basic problem here is that, in Haskell, as soon as main
exits, the entire program is torn down. The solution is simply to keep the main
thread open; e.g.
done <- newEmptyMVar
forkOS (mainGUI >> putMVar done ())
takeMVar done
I've also replaced forkIO
with forkOS
. GTK uses (OS-)thread-local state on Windows, so as a matter of defensive programming it is best to ensure that mainGUI
runs on a bound thread just in case one day you want to support Windows.