Rodrigo Mesquita pushed to branch wip/romes/multi-threaded-debugger at Glasgow Haskell Compiler / GHC
Commits:
-
c3176c10
by Rodrigo Mesquita at 2026-09-23T17:27:47+01:00
-
5d3cb7fa
by Rodrigo Mesquita at 2026-09-23T17:28:04+01:00
-
3ffdef29
by Rodrigo Mesquita at 2026-09-23T17:28:15+01:00
-
4a826f19
by Rodrigo Mesquita at 2026-10-01T11:22:37+01:00
-
a3e3a66f
by Rodrigo Mesquita at 2026-10-01T16:36:23+01:00
-
ee339734
by Rodrigo Mesquita at 2026-10-01T17:33:52+01:00
-
aa7ab6ca
by Rodrigo Mesquita at 2026-10-01T17:35:10+01:00
-
a7150ad3
by Rodrigo Mesquita at 2026-10-05T16:21:30+01:00
6 changed files:
- compiler/GHC/Runtime/Eval.hs
- compiler/GHC/Runtime/Interpreter.hs
- libraries/ghci/GHCi/Message.hs
- libraries/ghci/GHCi/Run.hs
- libraries/ghci/GHCi/Run/Breakpoints.hs
- libraries/ghci/GHCi/Server.hs
Changes:
| ... | ... | @@ -188,12 +188,13 @@ execOptions = ExecOptions |
| 188 | 188 | -- by default we just care about breakpoints hit in this eval thread
|
| 189 | 189 | }
|
| 190 | 190 | |
| 191 | --- | Run a statement in the current interactive context.
|
|
| 191 | +-- | Run a statement in the current interactive context, as a new thread in the interpreter.
|
|
| 192 | +-- Returns an action which blocks waiting for the execution to finish and yield a result.
|
|
| 192 | 193 | execStmt
|
| 193 | 194 | :: GhcMonad m
|
| 194 | 195 | => String -- ^ a statement (bind or expression)
|
| 195 | 196 | -> ExecOptions
|
| 196 | - -> m ExecResult
|
|
| 197 | + -> m (m ExecResult)
|
|
| 197 | 198 | execStmt input exec_opts@ExecOptions{..} = do
|
| 198 | 199 | hsc_env <- getSession
|
| 199 | 200 | |
| ... | ... | @@ -204,13 +205,15 @@ execStmt input exec_opts@ExecOptions{..} = do |
| 204 | 205 | |
| 205 | 206 | case mb_stmt of
|
| 206 | 207 | -- empty statement / comment
|
| 207 | - Nothing -> return (ExecComplete (Right []) 0)
|
|
| 208 | + Nothing -> pure (pure (ExecComplete (Right []) 0))
|
|
| 208 | 209 | Just stmt -> execStmt' stmt input exec_opts
|
| 209 | 210 | |
| 210 | 211 | -- | Like `execStmt`, but takes a parsed statement as argument. Useful when
|
| 211 | 212 | -- doing preprocessing on the AST before execution, e.g. in GHCi (see
|
| 212 | 213 | -- GHCi.UI.runStmt).
|
| 213 | -execStmt' :: GhcMonad m => GhciLStmt GhcPs -> String -> ExecOptions -> m ExecResult
|
|
| 214 | +--
|
|
| 215 | +-- Returns an action which blocks waiting for the execution to finish and yield a result.
|
|
| 216 | +execStmt' :: GhcMonad m => GhciLStmt GhcPs -> String -> ExecOptions -> m (m ExecResult)
|
|
| 214 | 217 | execStmt' stmt stmt_text ExecOptions{..} = do
|
| 215 | 218 | hsc_env <- getSession
|
| 216 | 219 | let interp = hscInterp hsc_env
|
| ... | ... | @@ -226,21 +229,22 @@ execStmt' stmt stmt_text ExecOptions{..} = do |
| 226 | 229 | case r of
|
| 227 | 230 | Nothing ->
|
| 228 | 231 | -- empty statement / comment
|
| 229 | - return (ExecComplete (Right []) 0)
|
|
| 232 | + pure (pure (ExecComplete (Right []) 0))
|
|
| 230 | 233 | Just (ids, hval, fix_env) -> do
|
| 231 | 234 | updateFixityEnv fix_env
|
| 232 | 235 | |
| 233 | 236 | let eval_opts = initEvalOpts idflags' (enableGhcStepMode execSingleStep)
|
| 234 | 237 | (enableIsolateThreadBreaks execIsolateMode)
|
| 235 | - status <- liftIO $ evalStmt interp eval_opts (execWrap hval)
|
|
| 238 | + rtid <- liftIO $ evalStmt interp eval_opts (execWrap hval)
|
|
| 236 | 239 | |
| 237 | 240 | let ic = hsc_IC hsc_env
|
| 238 | 241 | bindings = (ic_tythings ic, ic_gre_cache ic)
|
| 239 | 242 | |
| 240 | 243 | size = ghciHistSize idflags'
|
| 241 | - |
|
| 242 | - handleRunStatus execSingleStep eval_opts stmt_text bindings ids
|
|
| 243 | - status (emptyHistory size)
|
|
| 244 | + pure $ do
|
|
| 245 | + status <- liftIO (waitThreadResult interp rtid)
|
|
| 246 | + handleRunStatus execSingleStep eval_opts stmt_text bindings ids
|
|
| 247 | + status (emptyHistory size)
|
|
| 244 | 248 | |
| 245 | 249 | runDecls :: GhcMonad m => String -> m [TyThing]
|
| 246 | 250 | runDecls = runDeclsWithLocation "<interactive>" 1
|
| ... | ... | @@ -326,16 +330,16 @@ handleRunStatus step eval_opts expr bindings final_ids status history0 = do |
| 326 | 330 | return (ExecComplete (Left (fromSerializableException e)) alloc)
|
| 327 | 331 | |
| 328 | 332 | -- Nothing case: we stopped when an exception was raised, not at a breakpoint.
|
| 329 | - EvalBreak apStack_ref Nothing resume_ctxt ccs -> do
|
|
| 330 | - resume_ctxt_fhv <- liftIO $ mkFinalizedHValue interp resume_ctxt
|
|
| 331 | - apStack_fhv <- liftIO $ mkFinalizedHValue interp apStack_ref
|
|
| 333 | + EvalBreak apStack_ref Nothing resume_tid ccs -> do
|
|
| 334 | + resume_tid_fhv <- liftIO $ mkFinalizedHValue interp resume_tid
|
|
| 335 | + apStack_fhv <- liftIO $ mkFinalizedHValue interp apStack_ref
|
|
| 332 | 336 | let span = mkGeneralSrcSpan (fsLit "<unknown>")
|
| 333 | 337 | (hsc_env1, break_ids) <- liftIO $
|
| 334 | 338 | bindLocalsAtBreakpoint hsc_env apStack_fhv span Nothing
|
| 335 | 339 | let
|
| 336 | 340 | resume = Resume
|
| 337 | 341 | { resumeStmt = expr
|
| 338 | - , resumeContext = resume_ctxt_fhv
|
|
| 342 | + , resumeContext = resume_tid_fhv
|
|
| 339 | 343 | , resumeBindings = bindings
|
| 340 | 344 | , resumeFinalIds = final_ids
|
| 341 | 345 | , resumeApStack = apStack_fhv
|
| ... | ... | @@ -356,7 +360,7 @@ handleRunStatus step eval_opts expr bindings final_ids status history0 = do |
| 356 | 360 | -- The interpreter yields on a breakpoint if:
|
| 357 | 361 | -- - the breakpoint was explicitly enabled (in @BreakArray@)
|
| 358 | 362 | -- - or one of the stepping options in @EvalOpts@ caused us to stop at one
|
| 359 | - EvalBreak apStack_ref (Just eval_break) resume_ctxt ccs -> do
|
|
| 363 | + EvalBreak apStack_ref (Just eval_break) resume_tid ccs -> do
|
|
| 360 | 364 | let ibi = evalBreakpointToId eval_break
|
| 361 | 365 | let hug = hsc_HUG hsc_env
|
| 362 | 366 | info_brks <- liftIO $ readIModBreaks hug ibi
|
| ... | ... | @@ -368,8 +372,8 @@ handleRunStatus step eval_opts expr bindings final_ids status history0 = do |
| 368 | 372 | breakArray <- getBreakArray interp ibi info_brks
|
| 369 | 373 | breakpointStatus interp breakArray (ibi_info_index ibi)
|
| 370 | 374 | |
| 371 | - apStack_fhv <- liftIO $ mkFinalizedHValue interp apStack_ref
|
|
| 372 | - resume_ctxt_fhv <- liftIO $ mkFinalizedHValue interp resume_ctxt
|
|
| 375 | + apStack_fhv <- liftIO $ mkFinalizedHValue interp apStack_ref
|
|
| 376 | + resume_tid_fhv <- liftIO $ mkFinalizedHValue interp resume_tid
|
|
| 373 | 377 | |
| 374 | 378 | -- This breakpoint is enabled or we mean to break here;
|
| 375 | 379 | -- we want to stop instead of just logging it.
|
| ... | ... | @@ -382,7 +386,7 @@ handleRunStatus step eval_opts expr bindings final_ids status history0 = do |
| 382 | 386 | let
|
| 383 | 387 | resume = Resume
|
| 384 | 388 | { resumeStmt = expr
|
| 385 | - , resumeContext = resume_ctxt_fhv
|
|
| 389 | + , resumeContext = resume_tid_fhv
|
|
| 386 | 390 | , resumeBindings = bindings
|
| 387 | 391 | , resumeFinalIds = final_ids
|
| 388 | 392 | , resumeApStack = apStack_fhv
|
| ... | ... | @@ -398,7 +402,9 @@ handleRunStatus step eval_opts expr bindings final_ids status history0 = do |
| 398 | 402 | return (ExecBreak break_ids (Just ibi) resume)
|
| 399 | 403 | else do
|
| 400 | 404 | -- resume with the same step type
|
| 401 | - status <- liftIO $ GHCi.resumeStmt interp eval_opts resume_ctxt_fhv
|
|
| 405 | + status <- liftIO $ do
|
|
| 406 | + GHCi.resumeStmt interp eval_opts resume_tid_fhv
|
|
| 407 | + GHCi.waitThreadResult interp resume_tid_fhv
|
|
| 402 | 408 | history <- if not tracing then pure history0 else do
|
| 403 | 409 | history1 <- liftIO $ mkHistory hug apStack_fhv ibi
|
| 404 | 410 | let !history' = history1 `consBL` history0
|
| ... | ... | @@ -414,7 +420,9 @@ resumeExec :: GhcMonad m |
| 414 | 420 | -> ThreadBreaksIsolationMode
|
| 415 | 421 | -> Maybe Int
|
| 416 | 422 | -> Resume
|
| 417 | - -> m ExecResult
|
|
| 423 | + -> m (m ExecResult)
|
|
| 424 | + -- ^ Returns an action which blocks waiting for the resumed thread
|
|
| 425 | + -- to break again or yield a result
|
|
| 418 | 426 | resumeExec step isolateMode mbCnt r = do
|
| 419 | 427 | hsc_env <- getSession
|
| 420 | 428 | let ic = hsc_IC hsc_env
|
| ... | ... | @@ -438,7 +446,7 @@ resumeExec step isolateMode mbCnt r = do |
| 438 | 446 | |
| 439 | 447 | case r of
|
| 440 | 448 | Resume { resumeStmt = expr
|
| 441 | - , resumeContext = fhv
|
|
| 449 | + , resumeContext = rtid
|
|
| 442 | 450 | , resumeBindings = bindings
|
| 443 | 451 | , resumeFinalIds = final_ids
|
| 444 | 452 | , resumeApStack = apStack
|
| ... | ... | @@ -454,17 +462,19 @@ resumeExec step isolateMode mbCnt r = do |
| 454 | 462 | |
| 455 | 463 | let eval_opts = initEvalOpts dflags (enableGhcStepMode step)
|
| 456 | 464 | (enableIsolateThreadBreaks isolateMode)
|
| 457 | - status <- liftIO $ GHCi.resumeStmt interp eval_opts fhv
|
|
| 458 | - let prevHistoryLst = fromListBL 50 hist
|
|
| 459 | - hug = hsc_HUG hsc_env
|
|
| 460 | - hist' = case mb_brkpt of
|
|
| 461 | - Nothing -> pure prevHistoryLst
|
|
| 462 | - Just bi
|
|
| 463 | - | breakHere False step span -> do
|
|
| 464 | - hist1 <- liftIO (mkHistory hug apStack bi)
|
|
| 465 | - return $ hist1 `consBL` fromListBL 50 hist
|
|
| 466 | - | otherwise -> pure prevHistoryLst
|
|
| 467 | - handleRunStatus step eval_opts expr bindings final_ids status =<< hist'
|
|
| 465 | + liftIO $ GHCi.resumeStmt interp eval_opts rtid
|
|
| 466 | + pure $ do
|
|
| 467 | + status <- liftIO $ GHCi.waitThreadResult interp rtid
|
|
| 468 | + let prevHistoryLst = fromListBL 50 hist
|
|
| 469 | + hug = hsc_HUG hsc_env
|
|
| 470 | + hist' = case mb_brkpt of
|
|
| 471 | + Nothing -> pure prevHistoryLst
|
|
| 472 | + Just bi
|
|
| 473 | + | breakHere False step span -> do
|
|
| 474 | + hist1 <- liftIO (mkHistory hug apStack bi)
|
|
| 475 | + return $ hist1 `consBL` fromListBL 50 hist
|
|
| 476 | + | otherwise -> pure prevHistoryLst
|
|
| 477 | + handleRunStatus step eval_opts expr bindings final_ids status =<< hist'
|
|
| 468 | 478 | |
| 469 | 479 | setupBreakpoint :: GhcMonad m => Interp -> InternalBreakpointId -> Int -> m () -- #19157
|
| 470 | 480 | setupBreakpoint interp ibi cnt = do
|
| ... | ... | @@ -1275,7 +1285,9 @@ compileParsedExprRemote expr@(L loc _) = withSession $ \hsc_env -> do |
| 1275 | 1285 | |
| 1276 | 1286 | updateFixityEnv fix_env
|
| 1277 | 1287 | let eval_opts = initEvalOpts dflags EvalStepNone True
|
| 1278 | - status <- liftIO $ evalStmt interp eval_opts (EvalThis hvals_io)
|
|
| 1288 | + status <- liftIO $ do
|
|
| 1289 | + rtid <- evalStmt interp eval_opts (EvalThis hvals_io)
|
|
| 1290 | + waitThreadResult interp rtid
|
|
| 1279 | 1291 | case status of
|
| 1280 | 1292 | EvalComplete _ (EvalSuccess [hval]) -> return hval
|
| 1281 | 1293 | EvalComplete _ (EvalException e) ->
|
| ... | ... | @@ -11,6 +11,7 @@ module GHC.Runtime.Interpreter |
| 11 | 11 | , evalStmt, EvalStatus_(..), EvalStatus, EvalResult(..), EvalExpr(..)
|
| 12 | 12 | , resumeStmt
|
| 13 | 13 | , abandonStmt
|
| 14 | + , waitThreadResult
|
|
| 14 | 15 | , evalIO
|
| 15 | 16 | , evalString
|
| 16 | 17 | , evalStringToIOString
|
| ... | ... | @@ -286,11 +287,11 @@ evalStmt |
| 286 | 287 | :: Interp
|
| 287 | 288 | -> EvalOpts
|
| 288 | 289 | -> EvalExpr ForeignHValue
|
| 289 | - -> IO (EvalStatus_ [ForeignHValue] [HValueRef])
|
|
| 290 | + -> IO (ForeignRef ThreadId)
|
|
| 290 | 291 | evalStmt interp opts foreign_expr = do
|
| 291 | - status <- withExpr foreign_expr $ \expr ->
|
|
| 292 | + r <- withExpr foreign_expr $ \expr ->
|
|
| 292 | 293 | interpCmd interp (EvalStmt opts expr)
|
| 293 | - handleEvalStatus interp status
|
|
| 294 | + mkFinalizedHValue interp r
|
|
| 294 | 295 | where
|
| 295 | 296 | withExpr :: EvalExpr ForeignHValue -> (EvalExpr HValueRef -> IO a) -> IO a
|
| 296 | 297 | withExpr (EvalThis fhv) cont =
|
| ... | ... | @@ -304,17 +305,25 @@ resumeStmt |
| 304 | 305 | :: Interp
|
| 305 | 306 | -> EvalOpts
|
| 306 | 307 | -> ForeignRef ThreadId
|
| 307 | - -> IO (EvalStatus_ [ForeignHValue] [HValueRef])
|
|
| 308 | + -> IO ()
|
|
| 308 | 309 | resumeStmt interp opts resume_tid = do
|
| 309 | - status <- withForeignRef resume_tid $ \rhv ->
|
|
| 310 | + withForeignRef resume_tid $ \rhv ->
|
|
| 310 | 311 | interpCmd interp (ResumeStmt opts rhv)
|
| 311 | - handleEvalStatus interp status
|
|
| 312 | 312 | |
| 313 | 313 | abandonStmt :: Interp -> ForeignRef ThreadId -> IO ()
|
| 314 | 314 | abandonStmt interp resume_tid =
|
| 315 | 315 | withForeignRef resume_tid $ \rhv ->
|
| 316 | 316 | interpCmd interp (AbandonStmt rhv)
|
| 317 | 317 | |
| 318 | +waitThreadResult
|
|
| 319 | + :: Interp
|
|
| 320 | + -> ForeignRef ThreadId
|
|
| 321 | + -> IO (EvalStatus_ [ForeignHValue] [HValueRef])
|
|
| 322 | +waitThreadResult interp tid = do
|
|
| 323 | + status <- withForeignRef tid $ \rhv -> do
|
|
| 324 | + interpCmd interp (WaitThreadResult rhv)
|
|
| 325 | + handleEvalStatus interp status
|
|
| 326 | + |
|
| 318 | 327 | handleEvalStatus
|
| 319 | 328 | :: Interp
|
| 320 | 329 | -> EvalStatus [HValueRef]
|
| ... | ... | @@ -417,8 +426,9 @@ whereFrom interp ref = |
| 417 | 426 | seqHValue :: Interp -> UnitEnv -> Logger -> ForeignHValue -> IO (EvalResult ())
|
| 418 | 427 | seqHValue interp unit_env logger ref =
|
| 419 | 428 | withForeignRef ref $ \hval -> do
|
| 420 | - status <- interpCmd interp (Seq hval)
|
|
| 421 | - handleSeqHValueStatus interp unit_env logger status
|
|
| 429 | + rtid <- interpCmd interp (Seq hval)
|
|
| 430 | + status <- interpCmd interp (WaitThreadResult rtid)
|
|
| 431 | + handleSeqHValueStatus interp unit_env logger (clearEvalStatus status)
|
|
| 422 | 432 | |
| 423 | 433 | evalBreakpointToId :: EvalBreakpoint -> InternalBreakpointId
|
| 424 | 434 | evalBreakpointToId eval_break =
|
| ... | ... | @@ -460,9 +470,10 @@ handleSeqHValueStatus interp unit_env logger eval_status = |
| 460 | 470 | Just modbreaks -> put =<< getBreakLoc (readIModModBreaks hug) ibi modbreaks
|
| 461 | 471 | |
| 462 | 472 | -- resume the seq (:force) processing in the iserv process
|
| 463 | - withForeignRef resume_ctxt_fhv $ \hval -> do
|
|
| 464 | - status <- interpCmd interp (ResumeSeq hval)
|
|
| 465 | - handleSeqHValueStatus interp unit_env logger status
|
|
| 473 | + withForeignRef resume_ctxt_fhv $ \rtid -> do
|
|
| 474 | + interpCmd interp (ResumeSeq rtid)
|
|
| 475 | + status <- interpCmd interp (WaitThreadResult rtid)
|
|
| 476 | + handleSeqHValueStatus interp unit_env logger (clearEvalStatus status)
|
|
| 466 | 477 | (EvalComplete _ r) -> return r
|
| 467 | 478 | |
| 468 | 479 | |
| ... | ... | @@ -771,3 +782,9 @@ readIModModBreaks hug mod = imodBreaks_modBreaks . expectJust <$> readIModBreaks |
| 771 | 782 | fromEvalResult :: EvalResult a -> IO a
|
| 772 | 783 | fromEvalResult (EvalException e) = throwIO (fromSerializableException e)
|
| 773 | 784 | fromEvalResult (EvalSuccess a) = return a
|
| 785 | + |
|
| 786 | +clearEvalStatus :: EvalStatus a -> EvalStatus ()
|
|
| 787 | +clearEvalStatus = \case
|
|
| 788 | + EvalComplete w (EvalException se) -> EvalComplete w (EvalException se)
|
|
| 789 | + EvalComplete w (EvalSuccess _) -> EvalComplete w (EvalSuccess ())
|
|
| 790 | + EvalBreak ap mb rt rccs -> EvalBreak ap mb rt rccs |
| ... | ... | @@ -62,6 +62,8 @@ import Data.Dynamic |
| 62 | 62 | import Data.Typeable (TypeRep)
|
| 63 | 63 | import Data.IORef
|
| 64 | 64 | import Data.Map (Map)
|
| 65 | +import Data.IntMap (IntMap)
|
|
| 66 | +import qualified Data.IntMap as IM
|
|
| 65 | 67 | import Foreign
|
| 66 | 68 | import GHC.Generics
|
| 67 | 69 | import GHC.Stack.CCS
|
| ... | ... | @@ -132,13 +134,13 @@ data Message a where |
| 132 | 134 | EvalStmt
|
| 133 | 135 | :: EvalOpts
|
| 134 | 136 | -> EvalExpr HValueRef {- IO [a] -}
|
| 135 | - -> Message (EvalStatus [HValueRef]) {- [a] -}
|
|
| 137 | + -> Message (RemoteRef ThreadId) {- [a] -}
|
|
| 136 | 138 | |
| 137 | 139 | -- | Resume evaluation of a statement after a breakpoint
|
| 138 | 140 | ResumeStmt
|
| 139 | 141 | :: EvalOpts
|
| 140 | 142 | -> RemoteRef ThreadId
|
| 141 | - -> Message (EvalStatus [HValueRef])
|
|
| 143 | + -> Message ()
|
|
| 142 | 144 | |
| 143 | 145 | -- | Abandon evaluation of a statement after a breakpoint
|
| 144 | 146 | AbandonStmt
|
| ... | ... | @@ -240,12 +242,23 @@ data Message a where |
| 240 | 242 | -- | Evaluate something. This is used to support :force in GHCi.
|
| 241 | 243 | Seq
|
| 242 | 244 | :: HValueRef
|
| 243 | - -> Message (EvalStatus ())
|
|
| 245 | + -> Message (RemoteRef ThreadId)
|
|
| 244 | 246 | |
| 245 | 247 | -- | Resume forcing a free variable in a breakpoint (#2950)
|
| 246 | 248 | ResumeSeq
|
| 247 | 249 | :: RemoteRef ThreadId
|
| 248 | - -> Message (EvalStatus ())
|
|
| 250 | + -> Message ()
|
|
| 251 | + |
|
| 252 | + WaitThreadResult
|
|
| 253 | + :: RemoteRef ThreadId
|
|
| 254 | + -> Message (EvalStatus [HValueRef])
|
|
| 255 | + |
|
| 256 | + WaitAnyThreadBreak
|
|
| 257 | + :: Message EvalBreak
|
|
| 258 | + |
|
| 259 | + WaitThreadResultOrAnyThreadBreak
|
|
| 260 | + :: RemoteRef ThreadId
|
|
| 261 | + -> Message (EvalStatus [HValueRef])
|
|
| 249 | 262 | |
| 250 | 263 | -- | User-defined request encoded as a tag/payload pair. This is left
|
| 251 | 264 | -- uninterpreted by GHC and is meant for GHC API applications to be able to supply
|
| ... | ... | @@ -724,6 +737,7 @@ data Pipe = Pipe |
| 724 | 737 | { getSome :: !(IO ByteString)
|
| 725 | 738 | , putAll :: !(B.Builder -> IO ())
|
| 726 | 739 | , pipeLeftovers :: !(IORef (Maybe ByteString))
|
| 740 | + -- , waitingStatus :: !(IORef (Map (RemoteRef ThreadId) (MVar (EvalStatus ()))))
|
|
| 727 | 741 | }
|
| 728 | 742 | |
| 729 | 743 | -- | Make a 'Pipe' from a 'Handle' to read and a 'Handle' to write.
|
| ... | ... | @@ -744,8 +758,19 @@ mkPipeFromContinuations getSome putAll = do |
| 744 | 758 | |
| 745 | 759 | remoteCall :: Binary a => Pipe -> Message a -> IO a
|
| 746 | 760 | remoteCall pipe msg = do
|
| 747 | - writePipe pipe (putMessage msg)
|
|
| 748 | - readPipe pipe get
|
|
| 761 | + -- synchronous, write, read
|
|
| 762 | + writePipe pipe (putMessage msg) -- write goes in the handle
|
|
| 763 | + readPipe pipe get -- instead, this takes from the channel
|
|
| 764 | + -- the channel is fed from the handle by a different thread that creates a
|
|
| 765 | + -- stream of notifications and a stream of replies.
|
|
| 766 | + --
|
|
| 767 | + -- Since a reply will only come in response to a write request that happens
|
|
| 768 | + -- while the entire is thing is locked, we'll only get 1 reply per write, and
|
|
| 769 | + -- in fact the channel of replies is just 1 cell (MVar)
|
|
| 770 | + --
|
|
| 771 | + -- ATOMIC:
|
|
| 772 | + -- write pipe
|
|
| 773 | + -- read reply -- will never have more than 1 reply, because the Lock guarantees only 1 write.
|
|
| 749 | 774 | |
| 750 | 775 | writePipe :: Pipe -> Put -> IO ()
|
| 751 | 776 | writePipe Pipe{..} put = putAll $ execPut put
|
| ... | ... | @@ -755,6 +780,11 @@ remoteTHCall pipe msg = do |
| 755 | 780 | writePipe pipe (putTHMessage msg)
|
| 756 | 781 | readPipe pipe get
|
| 757 | 782 | |
| 783 | +-- todo: this will have to drain the pipe and write to a queue?
|
|
| 784 | +-- Normal replies pushed to reply channel
|
|
| 785 | +-- Remote call instead of ReadPipe, reads from the channel.
|
|
| 786 | +--
|
|
| 787 | +-- We need to drain channel for notifications...
|
|
| 758 | 788 | readPipe :: Pipe -> Get a -> IO a
|
| 759 | 789 | readPipe Pipe{..} get = do
|
| 760 | 790 | leftovers <- readIORef pipeLeftovers
|
| ... | ... | @@ -128,6 +128,15 @@ run m = case m of |
| 128 | 128 | Seq ref -> doSeq ref
|
| 129 | 129 | ResumeSeq ref -> resumeSeq ref
|
| 130 | 130 | |
| 131 | + WaitThreadResult tid -> localRef tid >>= atomically . readThreadEvalStatus
|
|
| 132 | + WaitAnyThreadBreak -> atomically $ readAnyThreadEvalBreak
|
|
| 133 | + WaitThreadResultOrAnyThreadBreak t -> do
|
|
| 134 | + tid <- localRef t
|
|
| 135 | + atomically $
|
|
| 136 | + readThreadEvalStatus tid
|
|
| 137 | + `orElse`
|
|
| 138 | + (EvalPaused <$> readAnyThreadEvalBreak)
|
|
| 139 | + |
|
| 131 | 140 | Shutdown -> unexpectedMessage m
|
| 132 | 141 | RunTH {} -> unexpectedMessage m
|
| 133 | 142 | RunModFinalizers {} -> unexpectedMessage m
|
| ... | ... | @@ -206,7 +215,7 @@ tryEval io = do |
| 206 | 215 | |
| 207 | 216 | --------------------------------------------------------------------------------
|
| 208 | 217 | |
| 209 | -evalStmt :: EvalOpts -> EvalExpr HValueRef -> IO (EvalStatus [HValueRef])
|
|
| 218 | +evalStmt :: EvalOpts -> EvalExpr HValueRef -> IO (RemoteRef ThreadId)
|
|
| 210 | 219 | evalStmt opts expr = do
|
| 211 | 220 | io <- mkIO expr
|
| 212 | 221 | sandboxIO opts $ do
|
| ... | ... | @@ -248,16 +257,16 @@ evalStringToString r str = do |
| 248 | 257 | -- The UI process has more and therefore also can show more
|
| 249 | 258 | -- information about the breakpoint than the current iserv
|
| 250 | 259 | -- process.
|
| 251 | -doSeq :: RemoteRef a -> IO (EvalStatus ())
|
|
| 252 | -doSeq ref = clearEvalStatus <$> do
|
|
| 260 | +doSeq :: RemoteRef a -> IO (RemoteRef ThreadId)
|
|
| 261 | +doSeq ref = do
|
|
| 253 | 262 | sandboxIO evalOptsSeq $ do
|
| 254 | 263 | _ <- (void $ evaluate =<< localRef ref)
|
| 255 | 264 | return []
|
| 256 | 265 | |
| 257 | 266 | -- | Process a ResumeSeq message. Continue the :force processing #2950
|
| 258 | 267 | -- after a breakpoint.
|
| 259 | -resumeSeq :: RemoteRef ThreadId -> IO (EvalStatus ())
|
|
| 260 | -resumeSeq hvref = clearEvalStatus <$> resumeStmt evalOptsSeq hvref
|
|
| 268 | +resumeSeq :: RemoteRef ThreadId -> IO ()
|
|
| 269 | +resumeSeq hvref = resumeStmt evalOptsSeq hvref
|
|
| 261 | 270 | |
| 262 | 271 | evalOptsSeq :: EvalOpts
|
| 263 | 272 | evalOptsSeq = EvalOpts
|
| ... | ... | @@ -296,15 +305,16 @@ abandonStmt hvref = do |
| 296 | 305 | |
| 297 | 306 | --------------------------------------------------------------------------------
|
| 298 | 307 | |
| 299 | -resumeStmt :: EvalOpts -> RemoteRef ThreadId -> IO (EvalStatus [HValueRef])
|
|
| 308 | +resumeStmt :: EvalOpts -> RemoteRef ThreadId -> IO ()
|
|
| 300 | 309 | resumeStmt opts rtid = do
|
| 301 | 310 | resumeThreadId <- localRef rtid
|
| 302 | 311 | ResumeContext{..} <- getThreadResumeContext resumeThreadId
|
| 303 | - withBreakAction opts resumeThreadId $ \waitForResult ->
|
|
| 304 | - mask_ $ do
|
|
| 305 | - putMVar resumeBreakMVar () -- this awakens the stopped thread...
|
|
| 306 | - redirectInterrupts resumeThreadId $
|
|
| 307 | - waitForResult
|
|
| 312 | + setBreakAction opts resumeThreadId -- See Note [TODO: all break options must be set every time]
|
|
| 313 | + -- withBreakAction opts resumeThreadId $ \waitForResult ->
|
|
| 314 | + -- mask_ $ do
|
|
| 315 | + putMVar resumeBreakMVar () -- this awakens the stopped thread...
|
|
| 316 | + -- redirectInterrupts resumeThreadId $
|
|
| 317 | + -- waitForResult
|
|
| 308 | 318 | |
| 309 | 319 | -- When running a computation, we redirect ^C exceptions to the running
|
| 310 | 320 | -- thread. ToDo: we might want a way to continue even if the target
|
| ... | ... | @@ -316,26 +326,51 @@ resumeStmt opts rtid = do |
| 316 | 326 | -- only while we execute the user's code. We can't afford to lose the final
|
| 317 | 327 | -- putMVar, otherwise deadlock ensues. (#1583, #1922, #1946)
|
| 318 | 328 | |
| 319 | -sandboxIO :: EvalOpts -> IO [HValueRef] -> IO (EvalStatus [HValueRef])
|
|
| 329 | +sandboxIO :: EvalOpts -> IO [HValueRef] -> IO (RemoteRef ThreadId)
|
|
| 320 | 330 | sandboxIO opts io
|
| 321 | 331 | | useSandboxThread opts
|
| 322 | 332 | = do
|
| 323 | 333 | wait <- newEmptyMVar
|
| 324 | 334 | |
| 335 | + -- If we are evaluating expressions (sandboxIO), then make sure the
|
|
| 336 | + -- global breakpoint action is set.
|
|
| 337 | + --
|
|
| 338 | + -- TODO: Is this thread unsafe? Already running threads may access this
|
|
| 339 | + -- pointer as we write it when evaluating a new expression.
|
|
| 340 | + --
|
|
| 341 | + -- A solution would be to have an "initialize interpreter" command
|
|
| 342 | + -- which sets this at the start. A different solution is to have a
|
|
| 343 | + -- global lock and only write this once here, then lock.
|
|
| 344 | + poke breakPointIOAction globalBreakStablePtr
|
|
| 345 | + |
|
| 325 | 346 | tid <- forkIO $ do
|
| 326 | 347 | |
| 327 | 348 | takeMVar wait -- don't run anything in this thread before
|
| 328 | - -- `withBreakAction` has a chance to set up.
|
|
| 349 | + -- `setBreakAction` has a chance to set up.
|
|
| 329 | 350 | |
| 330 | 351 | tid <- myThreadId
|
| 331 | 352 | labelThread tid "GHCi sandbox"
|
| 353 | + |
|
| 354 | + -- WHEN THE THREAD IS FINISHED, write the value itself.
|
|
| 355 | + --
|
|
| 356 | + -- we only write the TID status map in two places:
|
|
| 357 | + --
|
|
| 358 | + -- When the "main" thread finishes (here)
|
|
| 359 | + -- When any thread hits a breakpoint (global break action)
|
|
| 332 | 360 | unsafeUnmask runIt >>= writeThreadEvalStatus tid
|
| 361 | + -- TODO: This unsafe-unmask is very dubious. Consult with Andrea. I
|
|
| 362 | + -- don't think we should be uninterruptable masking interp commands
|
|
| 363 | + -- somewhere else. This should just be runIt?
|
|
| 333 | 364 | |
| 334 | - -- We are running in uninterruptibleMask
|
|
| 335 | - withBreakAction opts tid $ \waitForResult -> do
|
|
| 336 | - putMVar wait ()
|
|
| 337 | - redirectInterrupts tid $ unsafeUnmask $
|
|
| 338 | - waitForResult
|
|
| 365 | + -- We are running in uninterruptibleMask -- TODO WHY??
|
|
| 366 | + setBreakAction opts tid
|
|
| 367 | + putMVar wait ()
|
|
| 368 | + |
|
| 369 | + mkRemoteRef tid
|
|
| 370 | + -- withBreakAction opts tid $ \waitForResult -> do
|
|
| 371 | + -- putMVar wait ()
|
|
| 372 | + -- redirectInterrupts tid $ unsafeUnmask $
|
|
| 373 | + -- waitForResult
|
|
| 339 | 374 | |
| 340 | 375 | | otherwise
|
| 341 | 376 | = do
|
| ... | ... | @@ -349,7 +384,17 @@ sandboxIO opts io |
| 349 | 384 | -- tracing, etc.) need the expression to be running in a
|
| 350 | 385 | -- separate thread, so debugging is only enabled when
|
| 351 | 386 | -- using the sandbox.
|
| 352 | - runIt
|
|
| 387 | + tid <- forkIO (pure ())
|
|
| 388 | + runIt >>= writeThreadEvalStatus tid
|
|
| 389 | + mkRemoteRef tid
|
|
| 390 | + -- We `runIt` on the main thread, but fork a new thread to store to the
|
|
| 391 | + -- result with `writeThreadEvalStatus` to fit the asynchronous API. We
|
|
| 392 | + -- can't use myThreadId because if the user happens to runIt twice
|
|
| 393 | + -- before awaiting, the second result will clobber the first.
|
|
| 394 | + |
|
| 395 | + -- TODO We have to be careful evacuating this map bc there may be
|
|
| 396 | + -- uncollected results from finished threads. So the thread map can't
|
|
| 397 | + -- be weak refs ? Or maybe because of the RemoteRef it's fine. Ah, prob.
|
|
| 353 | 398 | where
|
| 354 | 399 | runIt = measureAlloc $ tryEval $ rethrow opts $ clearCCS io
|
| 355 | 400 | |
| ... | ... | @@ -404,17 +449,17 @@ sandboxIO opts io |
| 404 | 449 | -- whenever "main" or one of the threads spawned by "main" hit a breakpoint,
|
| 405 | 450 | -- rather than just "main" being observed. This should be used for the main
|
| 406 | 451 | -- debuggee execution of a debugger.
|
| 407 | -withBreakAction :: EvalOpts -> ThreadId -> (IO (EvalStatus [HValueRef]) -> IO a) -> IO a
|
|
| 408 | -withBreakAction opts tid@(ThreadId tid#) act
|
|
| 409 | - = bracket setBreakAction resetBreakAction (\_ -> act waitForResult)
|
|
| 452 | +-- withBreakAction :: EvalOpts -> ThreadId -> (IO (EvalStatus [HValueRef]) -> IO a) -> IO a
|
|
| 453 | +-- withBreakAction opts tid@(ThreadId tid#) act = do
|
|
| 454 | + |
|
| 455 | +setBreakAction :: EvalOpts -> ThreadId -> IO ()
|
|
| 456 | +setBreakAction opts tid@(ThreadId tid#) = do
|
|
| 457 | + ctx <- getThreadResumeContext tid
|
|
| 458 | + setBreakAction' ctx
|
|
| 459 | + -- bracket _ (resetBreakAction ctx) (\_ -> act (waitForResult ctx))
|
|
| 410 | 460 | where
|
| 411 | - setBreakAction = do
|
|
| 412 | - poke breakPointIOAction globalBreakStablePtr
|
|
| 413 | - -- TODO: This is poke thread unsafe, as one thread might be accessing this
|
|
| 414 | - -- global variable while this thread tries to overwrite it. We should
|
|
| 415 | - -- rather do it when the interpreter process/internal is initialized
|
|
| 416 | - -- somehow.
|
|
| 417 | - |
|
| 461 | + setBreakAction' ctx = do
|
|
| 462 | + -- TODO: TERRIBLY NOT MULTI-THREAD FRIENDLY. Each thread needs its own RTS flag exceptionFlag, like we have for step-in or step-out.
|
|
| 418 | 463 | runIf breakOnException $ \_ -> poke exceptionFlag 1
|
| 419 | 464 | -- Breaking on exceptions is not enabled by default, since it
|
| 420 | 465 | -- might be a bit surprising. The exception flag is turned off
|
| ... | ... | @@ -422,32 +467,34 @@ withBreakAction opts tid@(ThreadId tid#) act |
| 422 | 467 | |
| 423 | 468 | runIf# singleStep rts_enableStopNextBreakpoint
|
| 424 | 469 | runIf# stepOut rts_enableStopAfterReturn
|
| 425 | - runIf isolateThreadBreaks setIsolatedThread
|
|
| 426 | 470 | |
| 427 | - resetBreakAction () = do
|
|
| 428 | - poke exceptionFlag 0
|
|
| 471 | + runIfCtx ctx isolateThreadBreaks setIsolatedCtx
|
|
| 472 | + |
|
| 473 | + resetBreakAction ctx () = do
|
|
| 474 | + poke exceptionFlag 0 -- just wrong, expection flag should be per-thread
|
|
| 429 | 475 | rts_disableStopAfterReturn tid#
|
| 430 | 476 | rts_disableStopNextBreakpoint tid#
|
| 431 | - runIf isolateThreadBreaks unsetIsolatedThread
|
|
| 477 | + runIfCtx ctx isolateThreadBreaks unsetIsolatedCtx
|
|
| 432 | 478 | |
| 433 | 479 | -- freeStablePtr stablePtr TODO: we never free the global stablePtr for
|
| 434 | 480 | -- global action. Maybe we should have a de-init for clean up.
|
| 435 | 481 | -- TODO: don't set it in `setBreakAction` either. Should be on
|
| 436 | 482 | -- interpreter initialization.
|
| 437 | 483 | |
| 438 | - runIf pred what = when (pred opts) (what tid)
|
|
| 439 | - runIf# pred what = when (pred opts) (what tid#)
|
|
| 484 | + runIf pred what = when (pred opts) (what tid)
|
|
| 485 | + runIf# pred what = when (pred opts) (what tid#)
|
|
| 486 | + runIfCtx ctx pred what = when (pred opts) (what ctx)
|
|
| 440 | 487 | |
| 441 | - waitForResult
|
|
| 488 | + waitForResult ctx
|
|
| 442 | 489 | | isolateThreadBreaks opts
|
| 443 | 490 | = atomically $
|
| 444 | - readThreadEvalStatus tid
|
|
| 491 | + readCtxEvalStatus ctx
|
|
| 445 | 492 | |
| 446 | 493 | | otherwise
|
| 447 | 494 | = atomically $
|
| 448 | 495 | (EvalPaused <$> readAnyThreadEvalBreak)
|
| 449 | 496 | `orElse`
|
| 450 | - readThreadEvalStatus tid
|
|
| 497 | + readCtxEvalStatus ctx
|
|
| 451 | 498 | |
| 452 | 499 | -- While we're waiting for the sandbox thread to return a result, if
|
| 453 | 500 | -- the current thread receives an asynchronous exception we re-throw
|
| ... | ... | @@ -476,9 +523,3 @@ redirectInterrupts target wait = do |
| 476 | 523 | Nothing -> wait
|
| 477 | 524 | Just target -> do throwTo target (e :: SomeException); wait
|
| 478 | 525 | |
| 479 | -clearEvalStatus :: EvalStatus a -> EvalStatus ()
|
|
| 480 | -clearEvalStatus = \case
|
|
| 481 | - EvalComplete w (EvalException se) -> EvalComplete w (EvalException se)
|
|
| 482 | - EvalComplete w (EvalSuccess _) -> EvalComplete w (EvalSuccess ())
|
|
| 483 | - EvalBreak ap mb rt rccs -> EvalBreak ap mb rt rccs
|
|
| 484 | - |
| ... | ... | @@ -8,10 +8,12 @@ module GHCi.Run.Breakpoints |
| 8 | 8 | -- * Per-thread breakpoints
|
| 9 | 9 | , readThreadEvalStatus, readAnyThreadEvalBreak
|
| 10 | 10 | , writeThreadEvalStatus
|
| 11 | + , readCtxEvalStatus, writeCtxEvalStatus
|
|
| 11 | 12 | |
| 12 | 13 | -- ** Isolating thread breakpoints
|
| 13 | 14 | , withIsolatedThread
|
| 14 | 15 | , setIsolatedThread, unsetIsolatedThread
|
| 16 | + , setIsolatedCtx, unsetIsolatedCtx
|
|
| 15 | 17 | |
| 16 | 18 | -- ** Querying the global thread breakpoint map
|
| 17 | 19 | , listOtherPausedThreads
|
| ... | ... | @@ -29,18 +31,15 @@ import GHCi.Message |
| 29 | 31 | import GHCi.RemoteTypes
|
| 30 | 32 | |
| 31 | 33 | import qualified Data.Map as Map
|
| 32 | -import qualified Data.Set as Set
|
|
| 33 | 34 | import Control.Concurrent
|
| 34 | 35 | import Control.Exception
|
| 35 | 36 | import qualified Data.ByteString.Short.Internal as BS
|
| 36 | -#if defined(PROFILING)
|
|
| 37 | -import GHC.Data.ShortByteString
|
|
| 38 | -#endif
|
|
| 39 | 37 | import GHC.Exts
|
| 40 | 38 | import GHC.Stack
|
| 41 | 39 | import Foreign hiding (void)
|
| 42 | 40 | import GHC.Conc.Sync
|
| 43 | 41 | import GHC.IO hiding ( bracket )
|
| 42 | +import Control.Monad
|
|
| 44 | 43 | |
| 45 | 44 | --------------------------------------------------------------------------------
|
| 46 | 45 | -- * Global break action
|
| ... | ... | @@ -72,7 +71,7 @@ globalBreakStablePtr = unsafePerformIO $ newStablePtr globalBreakAction |
| 72 | 71 | globalBreakAction :: BreakpointCallback
|
| 73 | 72 | globalBreakAction info_mod# info_mod_uid# infox# is_exception apStack = do
|
| 74 | 73 | tid <- myThreadId
|
| 75 | - ResumeContext{..} <- getThreadResumeContext tid
|
|
| 74 | + ctx@ResumeContext{..} <- getThreadResumeContext tid
|
|
| 76 | 75 | resume_r <- mkRemoteRef tid
|
| 77 | 76 | apStack_r <- mkRemoteRef apStack
|
| 78 | 77 | ccs <- toRemotePtr <$> getCCSOf apStack
|
| ... | ... | @@ -83,7 +82,7 @@ globalBreakAction info_mod# info_mod_uid# infox# is_exception apStack = do |
| 83 | 82 | info_mod <- BS.packCString (Ptr info_mod#)
|
| 84 | 83 | info_mod_uid <- BS.packCString (Ptr info_mod_uid#)
|
| 85 | 84 | pure (Just (EvalBreakpoint info_mod info_mod_uid (I# infox#)))
|
| 86 | - writeThreadEvalStatus tid $ EvalBreak apStack_r mb_breakpoint resume_r ccs
|
|
| 85 | + writeCtxEvalStatus ctx $ EvalBreak apStack_r mb_breakpoint resume_r ccs
|
|
| 87 | 86 | |
| 88 | 87 | -- Block until this thread is resumed (by the thread which took the
|
| 89 | 88 | -- `ResumeContext` from the `statusMVar`).
|
| ... | ... | @@ -109,14 +108,19 @@ globalBreakAction info_mod# info_mod_uid# infox# is_exception apStack = do |
| 109 | 108 | -- Succeeds when the given thread writes to the global debugger map an eval result.
|
| 110 | 109 | readThreadEvalStatus :: ThreadId -> STM (EvalStatus [HValueRef])
|
| 111 | 110 | readThreadEvalStatus tid = do
|
| 112 | - GlobalDbgStatus{..} <- readTVar globalDbgStatusVar
|
|
| 113 | - case Map.lookup tid threadEvalStatuses of
|
|
| 111 | + ctxs <- readTVar threadContextsVar
|
|
| 112 | + case Map.lookup tid ctxs of
|
|
| 113 | + Just ctx -> readCtxEvalStatus ctx
|
|
| 114 | + Nothing -> retry -- to write a thread break or result, the
|
|
| 115 | + -- 'threadContextsVar' entry will be created
|
|
| 116 | + |
|
| 117 | +-- | Like 'readThreadEvalStatus', but when the 'ResumeContext' is already
|
|
| 118 | +-- available (to avoid looking it up unnecessarily).
|
|
| 119 | +readCtxEvalStatus :: ResumeContext -> STM (EvalStatus [HValueRef])
|
|
| 120 | +readCtxEvalStatus ResumeContext{resumeEvalStatus} =
|
|
| 121 | + readTVar resumeEvalStatus >>= \case
|
|
| 114 | 122 | Just r -> do
|
| 115 | - writeTVar globalDbgStatusVar
|
|
| 116 | - GlobalDbgStatus
|
|
| 117 | - { threadEvalStatuses = Map.delete tid threadEvalStatuses -- clear it
|
|
| 118 | - , isolatedThreads
|
|
| 119 | - }
|
|
| 123 | + writeTVar resumeEvalStatus Nothing -- clear it
|
|
| 120 | 124 | pure r
|
| 121 | 125 | Nothing -> retry -- until someone writes the result for this thread
|
| 122 | 126 | |
| ... | ... | @@ -126,38 +130,46 @@ readThreadEvalStatus tid = do |
| 126 | 130 | -- The debugger will be waiting for threads to break using `readAnyThreadEvalBreak`.
|
| 127 | 131 | --
|
| 128 | 132 | -- Succeeds when any thread hits a breakpoint.
|
| 133 | +--
|
|
| 134 | +-- TODO: Instead of reading all elems of the map, we could have the global
|
|
| 135 | +-- break action write breakpoints hit into a global `TChan ThreadId`, and then
|
|
| 136 | +-- `readAnyThreadEvalBreak` would read off of this channel breakpoints that
|
|
| 137 | +-- were hit, and do the lookup on the map by ThreadId, retrying if it has
|
|
| 138 | +-- already been handled (i.e. set to Nothing)
|
|
| 129 | 139 | readAnyThreadEvalBreak :: STM EvalBreak
|
| 130 | 140 | readAnyThreadEvalBreak = do
|
| 131 | - dbgs <- readTVar globalDbgStatusVar
|
|
| 141 | + ctxs <- readTVar threadContextsVar
|
|
| 132 | 142 | -- try reading any thread's 'EvalBreak' and retry if none have yielded one yet
|
| 133 | - foldr (\x next -> readStatus dbgs x `orElse` next) retry (Map.toList (threadEvalStatuses dbgs))
|
|
| 143 | + foldl' (\next ctx -> next `orElse` readBreak ctx) retry (Map.elems ctxs)
|
|
| 134 | 144 | where
|
| 135 | - readStatus GlobalDbgStatus{..} (tid, _) -- don't look at isolated threads
|
|
| 136 | - | Set.member tid isolatedThreads
|
|
| 137 | - = retry
|
|
| 138 | - |
|
| 139 | - readStatus GlobalDbgStatus{..} (tid, status)
|
|
| 140 | - = do
|
|
| 141 | - case status of
|
|
| 142 | - EvalComplete{} -> retry -- successes can be read with `readThreadEvalStatus`
|
|
| 143 | - EvalPaused b -> do
|
|
| 144 | - writeTVar globalDbgStatusVar
|
|
| 145 | - GlobalDbgStatus
|
|
| 146 | - { threadEvalStatuses = Map.delete tid threadEvalStatuses
|
|
| 147 | - , isolatedThreads
|
|
| 148 | - }
|
|
| 149 | - pure b
|
|
| 145 | + readBreak ResumeContext{resumeEvalStatus, resumeIsolated} = do
|
|
| 146 | + isolated <- readTVar resumeIsolated
|
|
| 147 | + if isolated
|
|
| 148 | + then retry -- don't look at isolated threads
|
|
| 149 | + else readTVar resumeEvalStatus >>= \case
|
|
| 150 | + Just (EvalPaused b) -> do
|
|
| 151 | + writeTVar resumeEvalStatus Nothing
|
|
| 152 | + pure b
|
|
| 153 | + Just EvalComplete{} ->
|
|
| 154 | + retry -- successes can be read with `readThreadEvalStatus`
|
|
| 155 | + Nothing ->
|
|
| 156 | + retry -- no status for this thread yet
|
|
| 150 | 157 | |
| 151 | 158 | writeThreadEvalStatus :: ThreadId -> EvalStatus [HValueRef] -> IO ()
|
| 152 | -writeThreadEvalStatus tid status =
|
|
| 159 | +writeThreadEvalStatus tid status = do
|
|
| 160 | + -- get or create a 'ResumeContext' for the thread (e.g. we may write the
|
|
| 161 | + -- EvalComplete without breakpoints occurring, so the map won't have an entry):
|
|
| 162 | + ctx <- getThreadResumeContext tid
|
|
| 163 | + writeCtxEvalStatus ctx status
|
|
| 164 | + |
|
| 165 | +-- | Like 'writeThreadEvalStatus', but for when the 'ResumeContext' is already
|
|
| 166 | +-- available
|
|
| 167 | +writeCtxEvalStatus :: ResumeContext -> EvalStatus [HValueRef] -> IO ()
|
|
| 168 | +writeCtxEvalStatus ResumeContext{resumeEvalStatus} status =
|
|
| 153 | 169 | atomically $ do
|
| 154 | - GlobalDbgStatus{..} <- readTVar globalDbgStatusVar
|
|
| 155 | - writeTVar globalDbgStatusVar $
|
|
| 156 | - GlobalDbgStatus
|
|
| 157 | - { threadEvalStatuses =
|
|
| 158 | - Map.insertWith (\_ _ -> error "writeThreadEvalStatus: should be impossible")
|
|
| 159 | - tid status threadEvalStatuses
|
|
| 160 | - , isolatedThreads }
|
|
| 170 | + readTVar resumeEvalStatus >>= \case
|
|
| 171 | + Just{} -> error "writeCtxEvalStatus: should be impossible"
|
|
| 172 | + Nothing -> writeTVar resumeEvalStatus (Just status)
|
|
| 161 | 173 | |
| 162 | 174 | |
| 163 | 175 | -- ** Isolating thread breakpoints ---------------------------------------------
|
| ... | ... | @@ -174,23 +186,15 @@ withIsolatedThread tid = bracket_ (setIsolatedThread tid) (unsetIsolatedThread t |
| 174 | 186 | -- expression to run should not be seen at all by the debugger (e.g. when
|
| 175 | 187 | -- evaluating an expr to :force a variable)
|
| 176 | 188 | setIsolatedThread :: ThreadId -> IO ()
|
| 177 | -setIsolatedThread tid = atomically $ do
|
|
| 178 | - GlobalDbgStatus{..} <- readTVar globalDbgStatusVar
|
|
| 179 | - writeTVar globalDbgStatusVar
|
|
| 180 | - GlobalDbgStatus
|
|
| 181 | - { threadEvalStatuses
|
|
| 182 | - , isolatedThreads = Set.insert tid isolatedThreads
|
|
| 183 | - }
|
|
| 189 | +setIsolatedThread = getThreadResumeContext >=> setIsolatedCtx
|
|
| 184 | 190 | |
| 185 | 191 | -- | Undoes 'setIsolatedThread'
|
| 186 | 192 | unsetIsolatedThread :: ThreadId -> IO ()
|
| 187 | -unsetIsolatedThread tid = atomically $ do
|
|
| 188 | - GlobalDbgStatus{..} <- readTVar globalDbgStatusVar
|
|
| 189 | - writeTVar globalDbgStatusVar
|
|
| 190 | - GlobalDbgStatus
|
|
| 191 | - { threadEvalStatuses
|
|
| 192 | - , isolatedThreads = Set.delete tid isolatedThreads
|
|
| 193 | - }
|
|
| 193 | +unsetIsolatedThread = getThreadResumeContext >=> unsetIsolatedCtx
|
|
| 194 | + |
|
| 195 | +setIsolatedCtx, unsetIsolatedCtx :: ResumeContext -> IO ()
|
|
| 196 | +setIsolatedCtx ctx = atomically $ writeTVar (resumeIsolated ctx) True
|
|
| 197 | +unsetIsolatedCtx ctx = atomically $ writeTVar (resumeIsolated ctx) False
|
|
| 194 | 198 | |
| 195 | 199 | -- ** Querying the global thread breakpoint map --------------------------------
|
| 196 | 200 | |
| ... | ... | @@ -201,64 +205,74 @@ unsetIsolatedThread tid = atomically $ do |
| 201 | 205 | -- the global thread mapping) and are asking about which other threads are
|
| 202 | 206 | -- paused to list them.
|
| 203 | 207 | listOtherPausedThreads :: IO [ThreadId]
|
| 204 | -listOtherPausedThreads = do
|
|
| 205 | - m <- atomically $ readTVar globalDbgStatusVar
|
|
| 206 | - pure $ Map.keys $ Map.filter isBreak (threadEvalStatuses m)
|
|
| 208 | +listOtherPausedThreads = atomically $ do
|
|
| 209 | + ctxs <- readTVar threadContextsVar
|
|
| 210 | + paused <- mapM isBreak (Map.toList ctxs)
|
|
| 211 | + pure [tid | (tid, True) <- paused]
|
|
| 207 | 212 | where
|
| 208 | - isBreak EvalBreak{} = True
|
|
| 209 | - isBreak EvalComplete{} = False
|
|
| 210 | - |
|
| 211 | --- ** Global thread exec status map --------------------------------------------
|
|
| 212 | - |
|
| 213 | --- | When we launch a new thread using the debugger, we map ... TODO ... all
|
|
| 214 | --- threads launched by that main stmt may further spawn threads, and any of
|
|
| 215 | --- them might hit a breakpoint (break array toggled, or per-thread step in
|
|
| 216 | --- might be used, or pause button, etc...).
|
|
| 217 | ---
|
|
| 218 | --- When we launch that main thread we also write its result to this map. So
|
|
| 219 | --- this map will only contain the results of threads we explicitly care to
|
|
| 220 | --- observe the result of. Not all threads will write their results here,
|
|
| 221 | --- certainly not ones spawned unknowningly by the main stmt.
|
|
| 222 | --- ...
|
|
| 223 | --- todo words
|
|
| 224 | -globalDbgStatusVar :: TVar GlobalDbgStatus
|
|
| 225 | -globalDbgStatusVar = unsafePerformIO $ newTVarIO $ GlobalDbgStatus Map.empty Set.empty
|
|
| 226 | -{-# NOINLINE globalDbgStatusVar #-}
|
|
| 227 | - |
|
| 228 | -data GlobalDbgStatus = GlobalDbgStatus
|
|
| 229 | - { threadEvalStatuses :: !(Map.Map ThreadId (EvalStatus [HValueRef]))
|
|
| 230 | - -- ^ Whenever a thread hits a breakpoint it inserts in this map its 'EvalBreak'.
|
|
| 231 | - -- We can additionally observe a thread's result by calling
|
|
| 232 | - -- `writeThreadEvalStatus tid (EvalComplete ...)` when the main expression
|
|
| 233 | - -- finishes evaluating, see e.g. 'sandboxIO'.
|
|
| 234 | - , isolatedThreads :: !(Set.Set ThreadId)
|
|
| 235 | - -- ^ Threads in this map are meant to be ignored by 'readAnyThreadEvalBreak'.
|
|
| 236 | - -- That is, even if one of the threads in this set hits a breakpoint, we
|
|
| 237 | - -- can only read its 'EvalBreak' with @'readThreadEvalStatus' tid@ directly.
|
|
| 238 | - }
|
|
| 213 | + isBreak (tid, ResumeContext{resumeEvalStatus}) = do
|
|
| 214 | + status <- readTVar resumeEvalStatus
|
|
| 215 | + pure (tid, case status of
|
|
| 216 | + Just EvalBreak{} -> True
|
|
| 217 | + Just EvalComplete{} -> False
|
|
| 218 | + Nothing -> False)
|
|
| 239 | 219 | |
| 240 | 220 | --------------------------------------------------------------------------------
|
| 241 | 221 | -- * Per-thread resume-ing
|
| 242 | 222 | --------------------------------------------------------------------------------
|
| 243 | 223 | |
| 244 | 224 | getThreadResumeContext :: ThreadId -> IO ResumeContext
|
| 245 | -getThreadResumeContext tid = modifyMVar globalBreakActionMap $ \gbm0 -> do
|
|
| 246 | - case Map.lookup tid gbm0 of
|
|
| 225 | +getThreadResumeContext tid = do
|
|
| 226 | + ctxs0 <- readTVarIO threadContextsVar
|
|
| 227 | + case Map.lookup tid ctxs0 of
|
|
| 228 | + Just ctx -> pure ctx -- common case: hence read without transaction
|
|
| 247 | 229 | Nothing -> do
|
| 248 | - r <- ResumeContext <$> newEmptyMVar
|
|
| 249 | - let !gbm1 = Map.insert tid r gbm0
|
|
| 250 | - pure (gbm1, r)
|
|
| 251 | - Just ctx -> do
|
|
| 252 | - pure (gbm0, ctx)
|
|
| 230 | + new_ctx <- ResumeContext <$> newEmptyMVar
|
|
| 231 | + <*> newTVarIO Nothing
|
|
| 232 | + <*> newTVarIO False
|
|
| 233 | + atomically $ do -- write new thread context atomically
|
|
| 234 | + ctxs1 <- readTVar threadContextsVar
|
|
| 235 | + case Map.lookup tid ctxs1 of
|
|
| 236 | + Just ctx -> pure ctx -- already exists
|
|
| 237 | + Nothing -> do
|
|
| 238 | + let !ctxs2 = Map.insert tid new_ctx ctxs1
|
|
| 239 | + writeTVar threadContextsVar ctxs2
|
|
| 240 | + pure new_ctx
|
|
| 253 | 241 | |
| 254 | --- | A global mapping from thread ids to the MVars on which they block when stopped.
|
|
| 255 | -globalBreakActionMap :: MVar (Map.Map ThreadId ResumeContext)
|
|
| 256 | -globalBreakActionMap = unsafePerformIO $ newMVar Map.empty
|
|
| 257 | -{-# NOINLINE globalBreakActionMap #-}
|
|
| 242 | +-- | A global mapping from threads to their per-thread state 'ResumeContext'
|
|
| 243 | +-- (that e.g. contain the MVars on which they block when stopped)
|
|
| 244 | +threadContextsVar :: TVar (Map.Map ThreadId ResumeContext)
|
|
| 245 | +threadContextsVar = unsafePerformIO $ newTVarIO Map.empty
|
|
| 246 | +{-# NOINLINE threadContextsVar #-}
|
|
| 258 | 247 | |
| 259 | -newtype ResumeContext = ResumeContext
|
|
| 260 | - { resumeBreakMVar :: MVar ()
|
|
| 248 | +-- | Global context for a thread, which is created and inserted in the global
|
|
| 249 | +-- 'threadContextsVar' when a thread hits a breakpoint (and, thus, the
|
|
| 250 | +-- 'globalBreakAction') is run, or when 'withBreakAction' sets the thread
|
|
| 251 | +-- isolation (e.g. in 'sandboxIO'), or in other calls to
|
|
| 252 | +-- 'getThreadResumeContext', the function which gets or creates a thread
|
|
| 253 | +-- 'ResumeContext'.
|
|
| 254 | +--
|
|
| 255 | +-- TODO: Rename ResumeContext to something like ThreadContext or
|
|
| 256 | +-- ThreadDbgContext or (Thread)BreakContext, and fields to something like
|
|
| 257 | +-- dbgEvalStatus, threadIsolated, ...
|
|
| 258 | +data ResumeContext = ResumeContext
|
|
| 259 | + { resumeBreakMVar :: !(MVar ())
|
|
| 261 | 260 | -- ^ A thread that hits a breakpoint blocks reading its corresponding MVar
|
| 262 | - -- (gotten from the 'globalBreakActionMap').
|
|
| 261 | + -- (gotten from the 'threadContextsVar').
|
|
| 263 | 262 | -- The debugger can unblock that thread by signaling its MVar.
|
| 263 | + , resumeEvalStatus :: !(TVar (Maybe (EvalStatus [HValueRef])))
|
|
| 264 | + -- ^ When a thread hits a breakpoint, the 'globalBreakAction' is run and
|
|
| 265 | + -- writes an 'EvalBreak' in this @TVar@ (which was found in 'threadContextsVar').
|
|
| 266 | + --
|
|
| 267 | + -- Additionally, the 'EvalComplete' result of a thread may be written here
|
|
| 268 | + -- using `writeThreadEvalStatus tid (EvalComplete ...)` after the main
|
|
| 269 | + -- expression finishes evaluating, see e.g. 'sandboxIO'.
|
|
| 270 | + --
|
|
| 271 | + -- The variable should be emptied (i.e. set to 'Nothing') by whoever
|
|
| 272 | + -- reads the status.
|
|
| 273 | + , resumeIsolated :: !(TVar Bool)
|
|
| 274 | + -- ^ When set, this thread is meant to be ignored by 'readAnyThreadEvalBreak'.
|
|
| 275 | + -- That is, if a thread with resumeIsolated=True hits a breakpoint, its
|
|
| 276 | + -- 'EvalBreak' can only be read with @'readThreadEvalStatus' tid@ for this
|
|
| 277 | + -- thread's @tid@ directly.
|
|
| 264 | 278 | } |
| ... | ... | @@ -70,6 +70,14 @@ servWithCustom verbose hook pipe restore customHandler = loop |
| 70 | 70 | |
| 71 | 71 | when verbose $ trace ("msg: " ++ (show msg))
|
| 72 | 72 | case msg of
|
| 73 | + -- The expressions which block waiting for a result musn't block the
|
|
| 74 | + -- server thread, so send them off and notify over the wire when done.
|
|
| 75 | + --
|
|
| 76 | + -- The other end (see 'remoteCall') will be processing notifications, and
|
|
| 77 | + -- it will use the returned pointer as the index for the matching notification.
|
|
| 78 | + WaitThreadResult {} -> forkIO (run msg >>= (notification =<< myThreadId)) >>= reply . mkRemoteRef
|
|
| 79 | + WaitAnyThreadBreak {} -> forkIO (run msg >>= (notification =<< myThreadId)) >>= reply . mkRemoteRef
|
|
| 80 | + WaitThreadResultOrAnyThreadBreak {} -> forkIO (run msg >>= (notification =<< myThreadId)) >>= reply . mkRemoteRef
|
|
| 73 | 81 | CustomMessage tag payload -> handleCustom tag payload
|
| 74 | 82 | Shutdown -> return ()
|
| 75 | 83 | RunTH st q ty loc -> wrapRunTH $ runTH pipe st q ty loc
|