diff --git a/src/core/IronPython/Runtime/FunctionCode.cs b/src/core/IronPython/Runtime/FunctionCode.cs
index 4fef3b690..276f66248 100644
--- a/src/core/IronPython/Runtime/FunctionCode.cs
+++ b/src/core/IronPython/Runtime/FunctionCode.cs
@@ -682,15 +682,22 @@ internal void LazyCompileFirstTarget(PythonFunction function) {
///
/// Updates the delegate based upon current Python context settings for recursion enforcement and for tracing.
///
+ ///
+ /// With false, this is the update done by switching tracing on or off ()
+ /// and it always sets the target. With true, the update is not synchronized with such a switch,
+ /// so the delegate is set as the target only if the tracing mode it was built for is still current (see ).
+ ///
///
- /// The delegate set as the target, or null if only a lazy target was set (only when is false).
- /// A caller that needs the delegate should use this result rather than read again: another thread may change
- /// it at any time, e.g. by switching tracing on or off.
+ /// The delegate for the tracing mode that was current when the update started, or null if only a lazy target was set (only when
+ /// is false). A caller that needs the delegate should use this result rather than read
+ /// again: another thread may change it at any time, e.g. by switching tracing on or off.
///
internal Delegate UpdateDelegate(PythonContext context, bool forceCreation) {
+ // Read only once: the delegate is built for this mode, even if another thread switches tracing on or off meanwhile.
+ bool tracing = context.EnableTracing;
Delegate finalTarget;
- if (context.EnableTracing && _lambda != null) {
+ if (tracing && _lambda != null) {
if (_tracingLambda == null) {
if (!forceCreation) {
// the user just called sys.settrace(), don't force re-compilation of every method in the system. Instead
@@ -723,10 +730,44 @@ internal Delegate UpdateDelegate(PythonContext context, bool forceCreation) {
finalTarget = AddRecursionCheck(context, finalTarget);
- SetTarget(finalTarget);
+ if (forceCreation) {
+ PublishTarget(context, tracing, finalTarget);
+ } else {
+ SetTarget(finalTarget);
+ }
return finalTarget;
}
+
+ ///
+ /// Sets the target to a delegate built for the given tracing mode, unless that mode is no longer current.
+ ///
+ ///
+ /// Used for updates that are not synchronized with switching tracing on or off (), so that a delegate
+ /// built for the previous mode cannot replace the target set for the new one. Such a delegate stays in its cache
+ /// ( or ) and is used when the mode switches back.
+ ///
+ ///
+ /// The tracing mode that was built for: true for the tracing delegate, false for the normal one.
+ ///
+ ///
+ /// The delegate for the target, with the recursion check already added (see ).
+ ///
+ internal void PublishTarget(PythonContext context, bool tracing, Delegate target) {
+ if (context.EnableTracing != tracing) {
+ return;
+ }
+
+ SetTarget(target);
+ Thread.MemoryBarrier();
+ while (context.EnableTracing != tracing) {
+ tracing = context.EnableTracing;
+ UpdateDelegate(context, forceCreation: false);
+ Thread.MemoryBarrier();
+ }
+ }
+
+
///
/// Sets a target that creates the delegate for the current mode (tracing or not) when it is first needed.
///
@@ -751,7 +792,7 @@ private void SetLazyTarget() {
internal void SetDebugTarget(PythonContext context, Delegate target) {
_normalDelegate = target;
- SetTarget(AddRecursionCheck(context, target));
+ PublishTarget(context, tracing: false, AddRecursionCheck(context, target));
}
///
@@ -963,11 +1004,11 @@ public TargetUpdaterForCompilation(PythonContext context, FunctionCode code) {
}
public void SetCompiledTarget(object sender, LightLambdaCompileEventArgs e) {
- _code.SetTarget(_code.AddRecursionCheck(_context, _code._normalDelegate = e.Compiled));
+ _code.PublishTarget(_context, tracing: false, _code.AddRecursionCheck(_context, _code._normalDelegate = e.Compiled));
}
public void SetCompiledTargetTracing(object sender, LightLambdaCompileEventArgs e) {
- _code.SetTarget(_code.AddRecursionCheck(_context, _code._tracingDelegate = e.Compiled));
+ _code.PublishTarget(_context, tracing: true, _code.AddRecursionCheck(_context, _code._tracingDelegate = e.Compiled));
}
}
diff --git a/src/core/IronPython/Runtime/Operations/PythonOps.cs b/src/core/IronPython/Runtime/Operations/PythonOps.cs
index e9c5969aa..bdc02b079 100644
--- a/src/core/IronPython/Runtime/Operations/PythonOps.cs
+++ b/src/core/IronPython/Runtime/Operations/PythonOps.cs
@@ -1595,8 +1595,8 @@ static Func GetClassCode(CodeContext/*!*/ context, Fun
return target ?? (Func)funcCode.UpdateDelegate(context.LanguageContext, true);
} else {
if (funcCode.Target == null) {
- funcCode.SetTarget(body);
funcCode._normalDelegate = body;
+ funcCode.PublishTarget(context.LanguageContext, tracing: false, body);
}
return body;
}
diff --git a/src/core/IronPython/Runtime/PythonContext.cs b/src/core/IronPython/Runtime/PythonContext.cs
index 434549350..7a5d12b94 100644
--- a/src/core/IronPython/Runtime/PythonContext.cs
+++ b/src/core/IronPython/Runtime/PythonContext.cs
@@ -3888,7 +3888,7 @@ internal void SetTrace(object o) {
var newCounted = newTraceListener?.IsDummyListener == false;
if (oldCounted != newCounted) {
- _tracebackListenersCount += newCounted ? 1 : -1;
+ Interlocked.Add(ref _tracebackListenersCount, newCounted ? 1 : -1);
}
if (EnableTracing != oldEnableTracing) {
diff --git a/tests/suite/modules/system_related/test_sys.py b/tests/suite/modules/system_related/test_sys.py
index f48396e9a..c2f6daf6d 100644
--- a/tests/suite/modules/system_related/test_sys.py
+++ b/tests/suite/modules/system_related/test_sys.py
@@ -233,6 +233,26 @@ class Boom(Exception): pass
# sys.settrace(None) gave the class body a function's call target.
self.assertTrue(issubclass(f(), Exception))
+ @skipUnlessIronPython()
+ def test_settrace_function_Xdebug(self):
+ """With -X:Debug, a function defined while tracing is on is traced."""
+ import subprocess
+ code = """import sys
+events = []
+def tracer(frame, event, arg):
+ events.append((event, frame.f_code.co_name))
+ return tracer
+sys.settrace(tracer)
+def f():
+ return 1
+f()
+sys.settrace(None)
+print(('call', 'f') in events)
+"""
+ # Before the fix, defining f under -X:Debug gave it its normal (untraced) delegate, even with tracing on.
+ out = subprocess.check_output([sys.executable, '-X:Debug', '-c', code])
+ self.assertEqual(out.strip(), b'True')
+
@skipUnlessIronPython()
@unittest.expectedFailure # dummy getrefcount removed in https://github.com/IronLanguages/ironpython3/pull/577
def test_getrefcount(self):