Skip to content

Commit 14bb76d

Browse files
authored
Ensure a delegate built for the old tracing mode doesn't replace the new one (#2118)
1 parent 8adac0f commit 14bb76d

4 files changed

Lines changed: 71 additions & 10 deletions

File tree

‎src/core/IronPython/Runtime/FunctionCode.cs‎

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -682,15 +682,22 @@ internal void LazyCompileFirstTarget(PythonFunction function) {
682682
/// <summary>
683683
/// Updates the delegate based upon current Python context settings for recursion enforcement and for tracing.
684684
/// </summary>
685+
/// <remarks>
686+
/// With <paramref name="forceCreation"/> false, this is the update done by switching tracing on or off (<see cref="UpdateAllCode"/>)
687+
/// and it always sets the target. With <paramref name="forceCreation"/> true, the update is not synchronized with such a switch,
688+
/// so the delegate is set as the target only if the tracing mode it was built for is still current (see <see cref="PublishTarget"/>).
689+
/// </remarks>
685690
/// <returns>
686-
/// The delegate set as the target, or null if only a lazy target was set (only when <paramref name="forceCreation"/> is false).
687-
/// A caller that needs the delegate should use this result rather than read <see cref="Target"/> again: another thread may change
688-
/// it at any time, e.g. by switching tracing on or off.
691+
/// The delegate for the tracing mode that was current when the update started, or null if only a lazy target was set (only when
692+
/// <paramref name="forceCreation"/> is false). A caller that needs the delegate should use this result rather than read
693+
/// <see cref="Target"/> again: another thread may change it at any time, e.g. by switching tracing on or off.
689694
/// </returns>
690695
internal Delegate UpdateDelegate(PythonContext context, bool forceCreation) {
696+
// Read only once: the delegate is built for this mode, even if another thread switches tracing on or off meanwhile.
697+
bool tracing = context.EnableTracing;
691698
Delegate finalTarget;
692699

693-
if (context.EnableTracing && _lambda != null) {
700+
if (tracing && _lambda != null) {
694701
if (_tracingLambda == null) {
695702
if (!forceCreation) {
696703
// 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) {
723730

724731
finalTarget = AddRecursionCheck(context, finalTarget);
725732

726-
SetTarget(finalTarget);
733+
if (forceCreation) {
734+
PublishTarget(context, tracing, finalTarget);
735+
} else {
736+
SetTarget(finalTarget);
737+
}
727738
return finalTarget;
728739
}
729740

741+
742+
/// <summary>
743+
/// Sets the target to a delegate built for the given tracing mode, unless that mode is no longer current.
744+
/// </summary>
745+
/// <remarks>
746+
/// Used for updates that are not synchronized with switching tracing on or off (<see cref="UpdateAllCode"/>), so that a delegate
747+
/// built for the previous mode cannot replace the target set for the new one. Such a delegate stays in its cache
748+
/// (<see cref="_normalDelegate"/> or <see cref="_tracingDelegate"/>) and is used when the mode switches back.
749+
/// </remarks>
750+
/// <param name="tracing">
751+
/// The tracing mode that <paramref name="target"/> was built for: true for the tracing delegate, false for the normal one.
752+
/// </param>
753+
/// <param name="target">
754+
/// The delegate for the target, with the recursion check already added (see <see cref="AddRecursionCheck"/>).
755+
/// </param>
756+
internal void PublishTarget(PythonContext context, bool tracing, Delegate target) {
757+
if (context.EnableTracing != tracing) {
758+
return;
759+
}
760+
761+
SetTarget(target);
762+
Thread.MemoryBarrier();
763+
while (context.EnableTracing != tracing) {
764+
tracing = context.EnableTracing;
765+
UpdateDelegate(context, forceCreation: false);
766+
Thread.MemoryBarrier();
767+
}
768+
}
769+
770+
730771
/// <summary>
731772
/// Sets a target that creates the delegate for the current mode (tracing or not) when it is first needed.
732773
/// </summary>
@@ -751,7 +792,7 @@ private void SetLazyTarget() {
751792
internal void SetDebugTarget(PythonContext context, Delegate target) {
752793
_normalDelegate = target;
753794

754-
SetTarget(AddRecursionCheck(context, target));
795+
PublishTarget(context, tracing: false, AddRecursionCheck(context, target));
755796
}
756797

757798
/// <summary>
@@ -963,11 +1004,11 @@ public TargetUpdaterForCompilation(PythonContext context, FunctionCode code) {
9631004
}
9641005

9651006
public void SetCompiledTarget(object sender, LightLambdaCompileEventArgs e) {
966-
_code.SetTarget(_code.AddRecursionCheck(_context, _code._normalDelegate = e.Compiled));
1007+
_code.PublishTarget(_context, tracing: false, _code.AddRecursionCheck(_context, _code._normalDelegate = e.Compiled));
9671008
}
9681009

9691010
public void SetCompiledTargetTracing(object sender, LightLambdaCompileEventArgs e) {
970-
_code.SetTarget(_code.AddRecursionCheck(_context, _code._tracingDelegate = e.Compiled));
1011+
_code.PublishTarget(_context, tracing: true, _code.AddRecursionCheck(_context, _code._tracingDelegate = e.Compiled));
9711012
}
9721013
}
9731014

‎src/core/IronPython/Runtime/Operations/PythonOps.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1595,8 +1595,8 @@ static Func<CodeContext, CodeContext> GetClassCode(CodeContext/*!*/ context, Fun
15951595
return target ?? (Func<CodeContext, CodeContext>)funcCode.UpdateDelegate(context.LanguageContext, true);
15961596
} else {
15971597
if (funcCode.Target == null) {
1598-
funcCode.SetTarget(body);
15991598
funcCode._normalDelegate = body;
1599+
funcCode.PublishTarget(context.LanguageContext, tracing: false, body);
16001600
}
16011601
return body;
16021602
}

‎src/core/IronPython/Runtime/PythonContext.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3895,7 +3895,7 @@ internal void SetTrace(object o) {
38953895
var newCounted = newTraceListener?.IsDummyListener == false;
38963896

38973897
if (oldCounted != newCounted) {
3898-
_tracebackListenersCount += newCounted ? 1 : -1;
3898+
Interlocked.Add(ref _tracebackListenersCount, newCounted ? 1 : -1);
38993899
}
39003900

39013901
if (EnableTracing != oldEnableTracing) {

‎tests/suite/modules/system_related/test_sys.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,26 @@ class Boom(Exception): pass
233233
# sys.settrace(None) gave the class body a function's call target.
234234
self.assertTrue(issubclass(f(), Exception))
235235

236+
@skipUnlessIronPython()
237+
def test_settrace_function_Xdebug(self):
238+
"""With -X:Debug, a function defined while tracing is on is traced."""
239+
import subprocess
240+
code = """import sys
241+
events = []
242+
def tracer(frame, event, arg):
243+
events.append((event, frame.f_code.co_name))
244+
return tracer
245+
sys.settrace(tracer)
246+
def f():
247+
return 1
248+
f()
249+
sys.settrace(None)
250+
print(('call', 'f') in events)
251+
"""
252+
# Before the fix, defining f under -X:Debug gave it its normal (untraced) delegate, even with tracing on.
253+
out = subprocess.check_output([sys.executable, '-X:Debug', '-c', code])
254+
self.assertEqual(out.strip(), b'True')
255+
236256
@skipUnlessIronPython()
237257
@unittest.expectedFailure # dummy getrefcount removed in https://github.com/IronLanguages/ironpython3/pull/577
238258
def test_getrefcount(self):

0 commit comments

Comments
 (0)