hgdbmi-0.2: src/Gdbmi/IO.hs
-- | Control execution of a GDB instance, send commands and receive results, notifications and stream information.
--
-- Due to <http://sourceware.org/bugzilla/show_bug.cgi?id=8759> the first command issued should be to set the output terminal for GDB to \/dev\/null. Unfortunatelly, there is no MI command for this, so we have to resort to the CLI command \"tty\". For example:
--
-- >>> ctx <- setup config callback
-- >>> resp <- send_command ctx (cli_command "tty /dev/null")
-- >>> when (respClass resp /= RCDone) (error ("unexpected response: " ++ show resp))
--
module Gdbmi.IO
-- exports {{{1
(
Context, Config(..), Callback(..)
, default_config
, setup, shutdown, send_command
) where
-- imports {{{1
import Control.Applicative ((<*>), (<$>))
import Control.Concurrent (forkIO, killThread, ThreadId, MVar, newEmptyMVar, tryTakeMVar, putMVar, takeMVar)
import Control.Concurrent.STM (TVar, TChan, TMVar, newEmptyTMVar, newTVarIO, newTChanIO, atomically, takeTMVar, readTVar, writeTVar, writeTChan, readTChan, putTMVar)
import Control.Exception (catchJust)
import Control.Exception.Base (AsyncException(ThreadKilled))
import Control.Monad (replicateM_, when)
import Control.Monad.Fix (mfix)
import Data.List (partition)
import Prelude hiding (catch, interact)
import System.IO (Handle, hSetBuffering, BufferMode(LineBuffering), hPutStr, hWaitForInput, hGetLine, IOMode(WriteMode), stdout, openFile, hFlush, hClose)
import System.Posix.IO (fdToHandle, createPipe)
import System.Process (ProcessHandle, runProcess, waitForProcess)
import qualified Gdbmi.Commands as C
import qualified Gdbmi.Representation as R
import qualified Gdbmi.Semantics as S
data Context = Context { -- {{{1
-- gdb process {{{2
ctxProcess :: ProcessHandle
, ctxCommandPipe :: Handle
, ctxOutputPipe :: Handle
, ctxLog :: Maybe Handle
-- callback
, ctxCallback :: Callback
-- threads
, ctxCommandThread :: ThreadId
, ctxOutputThread :: ThreadId
, ctxCurrentJob :: MVar Job
, ctxFinished :: MVar ()
-- jobs
, ctxNextToken :: TVar R.Token
, ctxJobs :: TChan Job
}
data Job = Job {
jobCommand :: R.Command
, jobResponse :: TMVar R.Response
, jobToken :: R.Token
}
-- | Call-back functions for asynchronous GDB output.
--
-- The call-backs are called in a separate thread per GDB output, so they may block.
--
-- Stop events are 'Gdbmi.Representation.Notification' events with 'R.NotificationClass' 'R.Exec' and 'R.AsyncClass' 'R.ACStop'.
-- If 'cbStopped' is given stop events are delivered to that call-back instead of 'cbNotify'.
data Callback -- {{{1
= Callback {
cbStream :: [R.Stream] -> IO () -- ^ call-back for 'Gdbmi.Representation.Stream' events
, cbNotify :: [R.Notification] -> IO () -- ^ call-back for 'Gdbmi.Representation.Notification' events
, cbStopped :: Maybe ([S.Stopped] -> IO ()) -- ^ optionally a special call-back for 'Gdbmi.Semantics.Stopped' events
}
-- | Configuration
data Config -- {{{1
= Config {
confCommandLine :: [String] -- ^ command line to execute. The library will add \"--interpreter mi\".
, confLogfile :: Maybe FilePath -- ^ optinonally a file path to a log file for GDB\/MI input and output. \'-\' means stdout.
}
default_config :: Config -- {{{2
-- | Default configuration: "gdb" command line, no log file
default_config = Config ["gdb"] Nothing
setup :: Config -> Callback -> IO Context -- {{{1
-- | Launch a GDB instance in Machine Interface mode.
--
-- The child process is run in a new session to avoid receiving SIGINTs when issuing -exec-interrupt.
setup config callback = do
(commandR, commandW) <- createPipe >>= asHandles
(outputR, outputW) <- createPipe >>= asHandles
phandle <- runProcess "setsid" (confCommandLine config ++ ["--interpreter", "mi"])
Nothing Nothing
(Just commandR)
(Just outputW)
Nothing
mapM_ (`hSetBuffering` LineBuffering) [commandW, outputR]
logH <- case (confLogfile config) of
Nothing -> return $ Nothing
Just "-" -> return $ Just stdout
Just f -> fmap Just $ openFile f WriteMode
currentJob <- newEmptyMVar
finished <- newEmptyMVar
nextToken <- newTVarIO 0
jobs <- newTChanIO
ctx <- mfix (\ctx -> do
itid <- forkIO (handleCommands ctx)
otid <- forkIO (handleOutput ctx)
return $ Context phandle commandW outputR logH callback itid otid currentJob finished nextToken jobs
)
return ctx
where
asHandles (f1, f2) = do
h1 <- fdToHandle f1
h2 <- fdToHandle f2
return (h1, h2)
shutdown :: Context -> IO () -- {{{1
-- | Shut down the GDB instance and all resources associated with the 'Context'.
shutdown ctx = do
mapM_ (killThread . ($ctx)) [ctxCommandThread, ctxOutputThread]
replicateM_ 2 (takeMVar (ctxFinished ctx))
writeCommand ctx C.gdb_exit 0
_ <- waitForProcess (ctxProcess ctx)
putMVar (ctxFinished ctx) ()
case ctxLog ctx of
Nothing -> return ()
Just handle ->
if handle /= stdout
then hClose handle
else return ()
send_command :: Context -> R.Command -> IO R.Response -- {{{1
-- | Send a GDB command and wait for the response.
--
-- This function is thread safe, i.e., it can be called by multiple threads in an interleaved fashion.
send_command ctx command = checkShutdown >> sendCommand >>= receiveResponse
where
checkShutdown = do
finished <- tryTakeMVar (ctxFinished ctx)
case finished of
Nothing -> return ()
Just () -> error "context has already been shut down"
sendCommand = atomically $ do
token <- readTVar (ctxNextToken ctx)
writeTVar (ctxNextToken ctx) (if token == maxBound then 0 else token + 1)
response <- newEmptyTMVar
writeTChan (ctxJobs ctx) $ Job command response token
return response
receiveResponse = atomically . takeTMVar
-- implementation {{{1
handleCommands :: Context -> IO () -- {{{2
handleCommands ctx = handleKill ctx $ do
job <- atomically $ readTChan (ctxJobs ctx)
putMVar (ctxCurrentJob ctx) job
writeCommand ctx (jobCommand job) (jobToken job)
handleCommands ctx
handleOutput :: Context -> IO () -- {{{2
handleOutput ctx = handleKill ctx $ do
output <- readOutput ctx
_ <- callBack ctx output
case R.output_response output of
Nothing -> return ()
Just response -> do
maybJob <- tryTakeMVar (ctxCurrentJob ctx)
case maybJob of
Nothing -> error "result record lost!"
Just job ->
if (R.get_token output /= Just (jobToken job))
then error $ "token missmatch! " ++ show (R.get_token output) ++ " vs. " ++ show (jobToken job)
else atomically $ putTMVar (jobResponse job) response
handleOutput ctx
callBack :: Context -> R.Output -> IO ()
callBack ctx output = forkIO go >> return ()
where
go =
let
callbacks = ctxCallback ctx
streamsCb = cbStream callbacks
notifyCb = cbNotify callbacks
stoppedCbMb = cbStopped callbacks
streams = R.output_stream output
notifications = R.output_notification output
(stops, others) = partition ((&&) <$> (R.Exec==) . R.notiClass <*> (R.ACStop==) . R.notiAsyncClass) notifications
Just stops' = sequence $ map (S.notification_stopped . R.notiResults) stops
in case stoppedCbMb of
Nothing -> do
when (not (null streams)) (streamsCb streams)
when (not (null notifications)) (notifyCb notifications)
Just stoppedCb -> do
when (not (null streams)) (streamsCb streams)
when (not (null others)) (notifyCb others)
when (not (null stops')) (stoppedCb stops')
handleKill :: Context -> IO () -> IO ()
handleKill ctx action = catchJust select action handler
where
select :: AsyncException -> Maybe ()
select ThreadKilled = Just ()
select _ = Nothing
handler :: () -> IO ()
handler _ = putMVar (ctxFinished ctx) ()
writeCommand :: Context -> R.Command -> R.Token -> IO () -- {{{2
writeCommand ctx cmd token =
let cmdstr = (R.render_command . C.set_token token) cmd in
do
debugLog ctx True cmdstr
hPutStr (ctxCommandPipe ctx) cmdstr
readOutput :: Context -> IO R.Output -- {{{2
readOutput ctx = do
_ <- hWaitForInput (ctxOutputPipe ctx) (-1)
str <- outputString (ctxOutputPipe ctx)
debugLog ctx False str
return (R.parse_output str)
where
outputString handle = outputLines handle >>= return . unlines
outputLines handle = do
line <- hGetLine handle
if line == "(gdb) "
then return [line]
else outputLines handle >>= return . (line:)
debugLog :: Context -> Bool -> String -> IO () -- {{{2
debugLog ctx io text =
let
prefix = if io then "/i " else "/o "
line = ((unlines . map (prefix++) . lines) text)
in
case (ctxLog ctx) of
Nothing -> return ()
Just h -> hPutStr h line >> hFlush h