-------------------------------------------------------------------------- {- | Module : Control.Concurrent.StdInOut Copyright : (c) 2010 Galois, Inc. License : BSD-style (see the file libraries/base/LICENSE) Maintainer : John Launchbury, john@galois.com Stability : experimental Portability : concurrency A low-tech concurrent interface to the console. When multiple threads want input, they send messages to the console with the format > <thread-id>:request The user supplies input to any requesting thread in a similar way: > <thread-id>:response At any time, the user can enter @!!@ to obtain a listing of all the active prompts. Any input not of either of these forms is discarded. > example :: IO () > example = setupStdinout processes > > processes :: IO () > processes = do > forkIO $ (prompt "Enter something" >> return ()) > forkIO $ (prompt "Something else" >> return ()) > prompt "quit" -- When the main thread dies, > return () -- the whole interaction ends -} -------------------------------------------------------------------------- {-# OPTIONS_GHC -fno-warn-unused-do-bind #-} module Control.Concurrent.StdInOut ( setupStdInOut -- :: IO a -> IO a , prompt -- :: String -> IO String , putStrLine -- :: String -> IO () ) where import System.IO(hSetBuffering,BufferMode(..),stdout) import System.IO.Unsafe(unsafePerformIO) -- for a global variable import Control.Monad(forever,join) import Control.Exception(finally) import Control.Concurrent.MonadIO import Control.Concurrent.STM.MonadIO flag :: String flag = "!!" -- User input to list active prompts timeDelay :: Int timeDelay = 100000 -- 1/10th sec -- 'std' is a concurrent variable that holds the current input line for -- other processes to consider whether they want to grab it. The -- process 'forever inputScan' maintains it. type Stdinout = TMVar String {-# NOINLINE std #-} std :: Stdinout std = unsafePerformIO $ newEmptyTMVar -- global location for input strings -- | 'setupStdInOut' establishes the context for 'prompt', by running -- a daemon while its argument is executing. The daemon is terminated -- once the argument to 'setupStdInOut' finishes. setupStdInOut :: IO a -> IO a setupStdInOut procs = do hSetBuffering stdout LineBuffering -- prevents character interleaving tid <- fork $ forever inputScan -- daemon putting stdin into std procs `finally` killThread tid inputScan :: IO (Maybe String) inputScan = do str <- getLine -- get next input line from stdin putTMVar std str -- make the input available to others threadDelay timeDelay -- give others time to grab it tryTakeTMVar std -- clear the input if still present -- | 'prompt' is the main user level function of the module. The function -- prints its argument on stdout, prefixed by its process number. The user -- similarly selects the recipient by prefixing the process number, -- e.g. "23:". Active prompts will reprompt when !! is entered. prompt :: HasFork io => String -> io String prompt text = do name <- myThreadNumber putStrLine (name ++ text) (join . atomically) $ do str <- takeTMVarSTM std -- Grab the input string to examine case match name str of Just inp -> return (return inp) -- Exit with the string contents Nothing -> do check (str==flag) -- Test whether to reprint the prompt putTMVarSTM std flag -- Replace the flag for others to see return $ do threadDelay (2*timeDelay) -- Wait for the flag to flush prompt text -- Recurse, to reprint the prompt myThreadNumber :: HasFork io => io String myThreadNumber = do tid <- myThreadId return $ drop (length "ThreadId ") (show tid ++ ":") match :: String -> String -> Maybe String match n s = if n == take (length n) s then Just (drop (length n) s) else Nothing -- | 'putStrLine' sends output to stdout, ensuring that lines are whole -- and uninterrupted (including the final newline). putStrLine :: MonadIO io => String -> io () putStrLine s = liftIO $ putStr (s ++ "\n")