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):