summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/collector.py5
-rw-r--r--coverage/ctracer/tracer.c13
-rw-r--r--coverage/pytracer.py16
3 files changed, 28 insertions, 6 deletions
diff --git a/coverage/collector.py b/coverage/collector.py
index 90c32756..cea341a1 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -375,7 +375,10 @@ class Collector(object):
def abs_file_dict(d):
"""Return a dict like d, but with keys modified by `abs_file`."""
- return dict((abs_file(k), v) for k, v in iitems(d))
+ # The call to list() ensures that the GIL protects the dictionary
+ # iterator against concurrent modifications by tracers running
+ # in other threads.
+ return dict((abs_file(k), v) for k, v in list(iitems(d)))
if self.branch:
covdata.add_arcs(abs_file_dict(self.data))
diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c
index 625a45a6..0ade7412 100644
--- a/coverage/ctracer/tracer.c
+++ b/coverage/ctracer/tracer.c
@@ -833,6 +833,14 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
return RET_OK;
#endif
+ if (!self->started) {
+ /* If CTracer.stop() has been called from another thread, the tracer
+ is still active in the current thread. Let's deactivate ourselves
+ now. */
+ PyEval_SetTrace(NULL, NULL);
+ return RET_OK;
+ }
+
#if WHAT_LOG || TRACE_LOG
PyObject * ascii = NULL;
#endif
@@ -1027,7 +1035,10 @@ static PyObject *
CTracer_stop(CTracer *self, PyObject *args_unused)
{
if (self->started) {
- PyEval_SetTrace(NULL, NULL);
+ /* Set the started flag only. The actual call to
+ PyEval_SetTrace(NULL, NULL) is delegated to the callback
+ itself to ensure that it called from the right thread.
+ */
self->started = FALSE;
}
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index b41f4059..8f1b4b98 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -68,7 +68,10 @@ class PyTracer(object):
def _trace(self, frame, event, arg_unused):
"""The trace function passed to sys.settrace."""
- if self.stopped:
+ if (self.stopped and sys.gettrace() == self._trace):
+ # The PyTrace.stop() method has been called, possibly by another
+ # thread, let's deactivate ourselves now.
+ sys.settrace(None)
return
if self.last_exc_back:
@@ -152,7 +155,15 @@ class PyTracer(object):
def stop(self):
"""Stop this Tracer."""
+ # Get the activate tracer callback before setting the stop flag to be
+ # able to detect if the tracer was changed prior to stopping it.
+ tf = sys.gettrace()
+
+ # Set the stop flag. The actual call to sys.settrace(None) will happen
+ # in the self._trace callback itself to make sure to call it from the
+ # right thread.
self.stopped = True
+
if self.threading and self.thread.ident != self.threading.currentThread().ident:
# Called on a different thread than started us: we can't unhook
# ourselves, but we've set the flag that we should stop, so we
@@ -163,7 +174,6 @@ class PyTracer(object):
# PyPy clears the trace function before running atexit functions,
# so don't warn if we are in atexit on PyPy and the trace function
# has changed to None.
- tf = sys.gettrace()
dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None)
if (not dont_warn) and tf != self._trace:
self.warn(
@@ -171,8 +181,6 @@ class PyTracer(object):
slug="trace-changed",
)
- sys.settrace(None)
-
def activity(self):
"""Has there been any activity?"""
return self._activity