| ... |
... |
@@ -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
|
|
| ... |
... |
@@ -128,36 +132,38 @@ readThreadEvalStatus tid = do |
|
128
|
132
|
-- Succeeds when any thread hits a breakpoint.
|
|
129
|
133
|
readAnyThreadEvalBreak :: STM EvalBreak
|
|
130
|
134
|
readAnyThreadEvalBreak = do
|
|
131
|
|
- dbgs <- readTVar globalDbgStatusVar
|
|
|
135
|
+ ctxs <- readTVar threadContextsVar
|
|
132
|
136
|
-- 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))
|
|
|
137
|
+ foldr (\ctx next -> readBreak ctx `orElse` next) retry (Map.elems ctxs)
|
|
134
|
138
|
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
|
|
|
139
|
+ readBreak ResumeContext{resumeEvalStatus, resumeIsolated} = do
|
|
|
140
|
+ isolated <- readTVar resumeIsolated
|
|
|
141
|
+ if isolated
|
|
|
142
|
+ then retry -- don't look at isolated threads
|
|
|
143
|
+ else readTVar resumeEvalStatus >>= \case
|
|
|
144
|
+ Just (EvalPaused b) -> do
|
|
|
145
|
+ writeTVar resumeEvalStatus Nothing
|
|
|
146
|
+ pure b
|
|
|
147
|
+ Just EvalComplete{} ->
|
|
|
148
|
+ retry -- successes can be read with `readThreadEvalStatus`
|
|
|
149
|
+ Nothing ->
|
|
|
150
|
+ retry -- no status for this thread yet
|
|
150
|
151
|
|
|
151
|
152
|
writeThreadEvalStatus :: ThreadId -> EvalStatus [HValueRef] -> IO ()
|
|
152
|
|
-writeThreadEvalStatus tid status =
|
|
|
153
|
+writeThreadEvalStatus tid status = do
|
|
|
154
|
+ -- get or create a 'ResumeContext' for the thread (e.g. we may write the
|
|
|
155
|
+ -- EvalComplete without breakpoints occurring, so the map won't have an entry):
|
|
|
156
|
+ ctx <- getThreadResumeContext tid
|
|
|
157
|
+ writeCtxEvalStatus ctx status
|
|
|
158
|
+
|
|
|
159
|
+-- | Like 'writeThreadEvalStatus', but for when the 'ResumeContext' is already
|
|
|
160
|
+-- available
|
|
|
161
|
+writeCtxEvalStatus :: ResumeContext -> EvalStatus [HValueRef] -> IO ()
|
|
|
162
|
+writeCtxEvalStatus ResumeContext{resumeEvalStatus} status =
|
|
153
|
163
|
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 }
|
|
|
164
|
+ readTVar resumeEvalStatus >>= \case
|
|
|
165
|
+ Just{} -> error "writeCtxEvalStatus: should be impossible"
|
|
|
166
|
+ Nothing -> writeTVar resumeEvalStatus (Just status)
|
|
161
|
167
|
|
|
162
|
168
|
|
|
163
|
169
|
-- ** Isolating thread breakpoints ---------------------------------------------
|
| ... |
... |
@@ -174,23 +180,15 @@ withIsolatedThread tid = bracket_ (setIsolatedThread tid) (unsetIsolatedThread t |
|
174
|
180
|
-- expression to run should not be seen at all by the debugger (e.g. when
|
|
175
|
181
|
-- evaluating an expr to :force a variable)
|
|
176
|
182
|
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
|
|
- }
|
|
|
183
|
+setIsolatedThread = getThreadResumeContext >=> setIsolatedCtx
|
|
184
|
184
|
|
|
185
|
185
|
-- | Undoes 'setIsolatedThread'
|
|
186
|
186
|
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
|
|
- }
|
|
|
187
|
+unsetIsolatedThread = getThreadResumeContext >=> unsetIsolatedCtx
|
|
|
188
|
+
|
|
|
189
|
+setIsolatedCtx, unsetIsolatedCtx :: ResumeContext -> IO ()
|
|
|
190
|
+setIsolatedCtx ctx = atomically $ writeTVar (resumeIsolated ctx) True
|
|
|
191
|
+unsetIsolatedCtx ctx = atomically $ writeTVar (resumeIsolated ctx) False
|
|
194
|
192
|
|
|
195
|
193
|
-- ** Querying the global thread breakpoint map --------------------------------
|
|
196
|
194
|
|
| ... |
... |
@@ -201,64 +199,74 @@ unsetIsolatedThread tid = atomically $ do |
|
201
|
199
|
-- the global thread mapping) and are asking about which other threads are
|
|
202
|
200
|
-- paused to list them.
|
|
203
|
201
|
listOtherPausedThreads :: IO [ThreadId]
|
|
204
|
|
-listOtherPausedThreads = do
|
|
205
|
|
- m <- atomically $ readTVar globalDbgStatusVar
|
|
206
|
|
- pure $ Map.keys $ Map.filter isBreak (threadEvalStatuses m)
|
|
|
202
|
+listOtherPausedThreads = atomically $ do
|
|
|
203
|
+ ctxs <- readTVar threadContextsVar
|
|
|
204
|
+ paused <- mapM isBreak (Map.toList ctxs)
|
|
|
205
|
+ pure [tid | (tid, True) <- paused]
|
|
207
|
206
|
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
|
|
- }
|
|
|
207
|
+ isBreak (tid, ResumeContext{resumeEvalStatus}) = do
|
|
|
208
|
+ status <- readTVar resumeEvalStatus
|
|
|
209
|
+ pure (tid, case status of
|
|
|
210
|
+ Just EvalBreak{} -> True
|
|
|
211
|
+ Just EvalComplete{} -> False
|
|
|
212
|
+ Nothing -> False)
|
|
239
|
213
|
|
|
240
|
214
|
--------------------------------------------------------------------------------
|
|
241
|
215
|
-- * Per-thread resume-ing
|
|
242
|
216
|
--------------------------------------------------------------------------------
|
|
243
|
217
|
|
|
244
|
218
|
getThreadResumeContext :: ThreadId -> IO ResumeContext
|
|
245
|
|
-getThreadResumeContext tid = modifyMVar globalBreakActionMap $ \gbm0 -> do
|
|
246
|
|
- case Map.lookup tid gbm0 of
|
|
|
219
|
+getThreadResumeContext tid = do
|
|
|
220
|
+ ctxs0 <- readTVarIO threadContextsVar
|
|
|
221
|
+ case Map.lookup tid ctxs0 of
|
|
|
222
|
+ Just ctx -> pure ctx -- common case: hence read without transaction
|
|
247
|
223
|
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)
|
|
|
224
|
+ new_ctx <- ResumeContext <$> newEmptyMVar
|
|
|
225
|
+ <*> newTVarIO Nothing
|
|
|
226
|
+ <*> newTVarIO False
|
|
|
227
|
+ atomically $ do -- write new thread context atomically
|
|
|
228
|
+ ctxs1 <- readTVar threadContextsVar
|
|
|
229
|
+ case Map.lookup tid ctxs1 of
|
|
|
230
|
+ Just ctx -> pure ctx -- already exists
|
|
|
231
|
+ Nothing -> do
|
|
|
232
|
+ let !ctxs2 = Map.insert tid new_ctx ctxs1
|
|
|
233
|
+ writeTVar threadContextsVar ctxs2
|
|
|
234
|
+ pure new_ctx
|
|
253
|
235
|
|
|
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 #-}
|
|
|
236
|
+-- | A global mapping from threads to their per-thread state 'ResumeContext'
|
|
|
237
|
+-- (that e.g. contain the MVars on which they block when stopped)
|
|
|
238
|
+threadContextsVar :: TVar (Map.Map ThreadId ResumeContext)
|
|
|
239
|
+threadContextsVar = unsafePerformIO $ newTVarIO Map.empty
|
|
|
240
|
+{-# NOINLINE threadContextsVar #-}
|
|
258
|
241
|
|
|
259
|
|
-newtype ResumeContext = ResumeContext
|
|
260
|
|
- { resumeBreakMVar :: MVar ()
|
|
|
242
|
+-- | Global context for a thread, which is created and inserted in the global
|
|
|
243
|
+-- 'threadContextsVar' when a thread hits a breakpoint (and, thus, the
|
|
|
244
|
+-- 'globalBreakAction') is run, or when 'withBreakAction' sets the thread
|
|
|
245
|
+-- isolation (e.g. in 'sandboxIO'), or in other calls to
|
|
|
246
|
+-- 'getThreadResumeContext', the function which gets or creates a thread
|
|
|
247
|
+-- 'ResumeContext'.
|
|
|
248
|
+--
|
|
|
249
|
+-- TODO: Rename ResumeContext to something like ThreadContext or
|
|
|
250
|
+-- ThreadDbgContext or (Thread)BreakContext, and fields to something like
|
|
|
251
|
+-- dbgEvalStatus, threadIsolated, ...
|
|
|
252
|
+data ResumeContext = ResumeContext
|
|
|
253
|
+ { resumeBreakMVar :: !(MVar ())
|
|
261
|
254
|
-- ^ A thread that hits a breakpoint blocks reading its corresponding MVar
|
|
262
|
|
- -- (gotten from the 'globalBreakActionMap').
|
|
|
255
|
+ -- (gotten from the 'threadContextsVar').
|
|
263
|
256
|
-- The debugger can unblock that thread by signaling its MVar.
|
|
|
257
|
+ , resumeEvalStatus :: !(TVar (Maybe (EvalStatus [HValueRef])))
|
|
|
258
|
+ -- ^ When a thread hits a breakpoint, the 'globalBreakAction' is run and
|
|
|
259
|
+ -- writes an 'EvalBreak' in this @TVar@ (which was found in 'threadContextsVar').
|
|
|
260
|
+ --
|
|
|
261
|
+ -- Additionally, the 'EvalComplete' result of a thread may be written here
|
|
|
262
|
+ -- using `writeThreadEvalStatus tid (EvalComplete ...)` after the main
|
|
|
263
|
+ -- expression finishes evaluating, see e.g. 'sandboxIO'.
|
|
|
264
|
+ --
|
|
|
265
|
+ -- The variable should be emptied (i.e. set to 'Nothing') by whoever
|
|
|
266
|
+ -- reads the status.
|
|
|
267
|
+ , resumeIsolated :: !(TVar Bool)
|
|
|
268
|
+ -- ^ When set, this thread is meant to be ignored by 'readAnyThreadEvalBreak'.
|
|
|
269
|
+ -- That is, if a thread with resumeIsolated=True hits a breakpoint, its
|
|
|
270
|
+ -- 'EvalBreak' can only be read with @'readThreadEvalStatus' tid@ for this
|
|
|
271
|
+ -- thread's @tid@ directly.
|
|
264
|
272
|
} |