{-# LANGUAGE OverloadedStrings #-} module GDB ( GDB, gdbProcess, gdbStart, addInferior, gdbSession, ) where import Control.Concurrent import Control.Concurrent.STM import Control.Monad.IO.Class import Control.Monad.Identity import Control.Monad.Reader import Data.Char import Data.List import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Void import Text.Megaparsec hiding (State) import Text.Megaparsec.Char import System.IO.Error import System.Process import Output import Process data GDB = GDB { gdbProcess_ :: Process , gdbResult :: MVar (ResultClass, [(Text, MiValue)]) , gdbInferiors :: MVar [Inferior] , gdbThreadGroups :: TChan Text , gdbOnCrash :: Process -> IO () } gdbProcess :: GDB -> Process gdbProcess = gdbProcess_ data Inferior = Inferior { infProcess :: Process , infPid :: Pid , infThreadGroup :: Text , infThreads :: [Text] } data MiRecord = ResultRecord ResultClass [(Text, MiValue)] | ExecAsyncOutput Text [(Text, MiValue)] | StatusAsyncOutput Text [(Text, MiValue)] | NotifyAsyncOutput Text [(Text, MiValue)] | ConsoleStreamOutput Text | TargetStreamOutput Text | LogStreamOutput Text deriving (Show) data ResultClass = Done | Connected | Error | Exit deriving (Show) data MiValue = MiString Text | MiTuple [(Text, MiValue)] | MiList [MiValue] deriving (Show) gdbCmd :: String gdbCmd = "gdb --quiet --interpreter=mi3" gdbStart :: (MonadOutput m, MonadFail m) => (Process -> IO ()) -> m GDB gdbStart onCrash = do (Just hin, Just hout, Just herr, handle) <- liftIO $ createProcess (shell gdbCmd) { std_in = CreatePipe, std_out = CreatePipe, std_err = CreatePipe } pout <- liftIO $ newTVarIO [] let process = Process { procName = ProcNameGDB , procHandle = handle , procStdin = hin , procOutput = pout , procKillWith = Nothing , procNode = undefined } gdb <- GDB <$> pure process <*> liftIO newEmptyMVar <*> liftIO (newMVar []) <*> liftIO newTChanIO <*> pure onCrash out <- getOutput liftIO $ void $ forkIO $ flip runReaderT out $ lineReadingLoop process hout $ gdbLine gdb liftIO $ void $ forkIO $ flip runReaderT out $ lineReadingLoop process herr $ outProc OutputChildStderr process gdbCommand gdb "-gdb-set schedule-multiple on" gdbCommand gdb "-gdb-set mi-async on" gdbCommand gdb "-gdb-set non-stop on" gdbCommand gdb "-gdb-set print symbol-loading off" return gdb gdbLine :: GDB -> Text -> ReaderT Output IO () gdbLine _ "(gdb)" = return () gdbLine _ "(gdb) " = return () gdbLine gdb rline = either (outProc OutputError (gdbProcess gdb) . T.pack . errorBundlePretty) go $ runParser miOutputRecord "" rline where go = \case ResultRecord cls params -> liftIO $ putMVar (gdbResult gdb) (cls, params) ExecAsyncOutput cls params -> (cls,) <$> liftIO (readMVar (gdbInferiors gdb)) >>= \case ("stopped", infs) | Just (MiString "signal-received") <- lookup "reason" params , Just (MiString tid) <- lookup "thread-id" params , Just inf <- find (elem tid . infThreads) infs -> do -- It is needed to switch thread manually in non-stop mode, -- fork to avoid blocking further input and reply processing. out <- getOutput void $ liftIO $ forkIO $ do flip runReaderT out $ do gdbCommand gdb $ "-thread-select " <> tid gdbOnCrash gdb $ infProcess inf _ -> return () StatusAsyncOutput cls params -> outProc OutputChildInfo (gdbProcess gdb) $ "status: " <> cls <> " " <> T.pack (show params) NotifyAsyncOutput cls params -> case cls of "thread-group-added" | Just (MiString tgid) <- lookup "id" params -> do liftIO $ atomically $ writeTChan (gdbThreadGroups gdb) tgid "thread-group-exited" | Just (MiString tgid) <- lookup "id" params -> do liftIO $ modifyMVar_ (gdbInferiors gdb) $ return . filter ((/=tgid) . infThreadGroup) "thread-created" | Just (MiString tid) <- lookup "id" params , Just (MiString tgid) <- lookup "group-id" params -> liftIO $ modifyMVar_ (gdbInferiors gdb) $ return . map (\inf -> if infThreadGroup inf == tgid then inf { infThreads = tid : infThreads inf } else inf) "thread-exited" | Just (MiString tid) <- lookup "id" params , Just (MiString tgid) <- lookup "group-id" params -> liftIO $ modifyMVar_ (gdbInferiors gdb) $ return . map (\inf -> if infThreadGroup inf == tgid then inf { infThreads = filter (/=tid) $ infThreads inf } else inf) _ -> return () ConsoleStreamOutput line -> mapM_ (outLine OutputAlways Nothing) (T.lines line) TargetStreamOutput line -> mapM_ (outProc OutputChildStderr (gdbProcess gdb) . ("target-stream: " <>)) (T.lines line) LogStreamOutput line -> mapM_ (outProc OutputChildInfo (gdbProcess gdb) . ("log: " <>)) (T.lines line) addInferior :: MonadOutput m => GDB -> Process -> m () addInferior gdb process = do liftIO (getPid $ procHandle process) >>= \case Nothing -> outProc OutputError process $ "failed to get PID" Just pid -> do tgid <- liftIO (atomically $ tryReadTChan $ gdbThreadGroups gdb) >>= \case Just tgid -> return tgid Nothing -> do gdbCommand gdb $ "-add-inferior" liftIO $ atomically $ readTChan $ gdbThreadGroups gdb liftIO $ modifyMVar_ (gdbInferiors gdb) $ return . (:) Inferior { infProcess = process , infPid = pid , infThreadGroup = tgid , infThreads = [] } gdbCommand gdb $ "-target-attach --thread-group " <> tgid <> " " <> T.pack (show pid) gdbCommand gdb $ "-exec-continue --thread-group " <> tgid gdbCommand :: MonadOutput m => GDB -> Text -> m () gdbCommand gdb cmd = do send (gdbProcess gdb) cmd liftIO (takeMVar (gdbResult gdb)) >>= \case (Done, _) -> return () (Connected, _) -> outProc OutputChildInfo (gdbProcess gdb) "result connected" (Error, _) -> outProc OutputError (gdbProcess gdb) $ "command error: " <> cmd (Exit, _) -> outProc OutputError (gdbProcess gdb) "result exit" gdbSession :: MonadOutput m => GDB -> m () gdbSession gdb = do outPrompt "gdb> " loop "" outClearPrompt where loop prev = liftIO (catchIOError (Just <$> T.getLine) (\e -> if isEOFError e then return Nothing else ioError e)) >>= \case Just line -> do let cmd = if T.null line then prev else line gdbCommand gdb ("-interpreter-exec console \"" <> cmd <> "\"") loop cmd Nothing -> return () type MiParser = ParsecT Void MiStream Identity type MiStream = Text miOutputRecord :: MiParser MiRecord miOutputRecord = choice [ miResultRecord , miExecAsync , miStatusAsync , miNotifyAsync , miConsoleStream , miTargetStream , miLogStream ] miResultRecord :: MiParser MiRecord miResultRecord = char '^' >> ResultRecord <$> resultClass <*> many result miExecAsync, miStatusAsync, miNotifyAsync :: MiParser MiRecord miExecAsync = char '*' >> ExecAsyncOutput <$> miString <*> many (char ',' *> result) miStatusAsync = char '+' >> StatusAsyncOutput <$> miString <*> many (char ',' *> result) miNotifyAsync = char '=' >> NotifyAsyncOutput <$> miString <*> many (char ',' *> result) miConsoleStream, miTargetStream, miLogStream :: MiParser MiRecord miConsoleStream = char '~' >> ConsoleStreamOutput <$> miCString miTargetStream = char '@' >> TargetStreamOutput <$> miCString miLogStream = char '&' >> LogStreamOutput <$> miCString resultClass :: MiParser ResultClass resultClass = label "result-class" $ choice [ return Done <* string "done" , return Done <* string "running" -- equivalent to "done" per documentation , return Connected <* string "connected" , return Error <* string "error" , return Exit <* string "exit" ] result :: MiParser (Text, MiValue) result = (,) <$> miString <* char '=' <*> miValue miString :: MiParser Text miString = label "string" $ takeWhile1P Nothing (\x -> isAlphaNum x || x == '_' || x == '-') miCString :: MiParser Text miCString = label "c-string" $ do void $ char '"' let go = choice [ char '"' >> return [] , takeWhile1P Nothing (`notElem` ['\"', '\\']) >>= \s -> (s:) <$> go ,do void $ char '\\' c <- choice [ char '\\' >> return '\\' , char '"' >> return '"' , char 'n' >> return '\n' , char 'r' >> return '\r' , char 't' >> return '\t' ] ((T.singleton c) :) <$> go ] T.concat <$> go listOf :: MiParser a -> MiParser [a] listOf item = do x <- item (x:) <$> choice [ char ',' >> listOf item, return [] ] miTuple :: MiParser [(Text, MiValue)] miTuple = between (char '{') (char '}') $ listOf result <|> return [] miList :: MiParser [MiValue] miList = between (char '[') (char ']') $ listOf miValue <|> return [] miValue :: MiParser MiValue miValue = choice [ MiString <$> miCString , MiTuple <$> miTuple , MiList <$> miList ]