From 59fdf2c6df03f9d3c2786237693dfde5e149da50 Mon Sep 17 00:00:00 2001 From: Pavel Koneski Date: Sun, 25 Oct 2026 13:40:04 -0700 Subject: [PATCH] Keep the frame stack balanced when tracing is switched --- src/core/IronPython/Runtime/PythonContext.cs | 8 ++-- .../Runtime/PythonTracebackListener.cs | 4 +- .../suite/modules/system_related/test_sys.py | 46 +++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/core/IronPython/Runtime/PythonContext.cs b/src/core/IronPython/Runtime/PythonContext.cs index ae9f9fb99..29cbd56df 100644 --- a/src/core/IronPython/Runtime/PythonContext.cs +++ b/src/core/IronPython/Runtime/PythonContext.cs @@ -3849,13 +3849,13 @@ public PythonTracebackListenersDispatcher(PythonContext parent) { void Debugging.ITraceCallback.OnTraceEvent(Debugging.TraceEventKind kind, string name, string sourceFileName, SourceSpan sourceSpan, Func> scopeCallback, object payload, object customPayload) { var listener = _parent._tracebackListeners.Value; - if (listener == null && (_parent.PythonOptions.Tracing || _parent._tracebackListenersCount > 0)) { - // If tracing is enabled (globally or via SetTrace on any thread), we need to register a dummy - // traceback listener for this thread, because of the FunctionStack handling done there. + if (listener == null) { + // Code compiled for tracing pushes and pops its frames on the FunctionStack only through the listener, so this thread + // needs a dummy listener even without a trace function. This must not depend on whether tracing is on right now. _parent._tracebackListeners.Value = listener = new PythonTracebackListener(_parent, null, isDummyListener: true); } - listener?.OnTraceEvent(kind, name, sourceFileName, sourceSpan, scopeCallback, payload, customPayload); + listener.OnTraceEvent(kind, name, sourceFileName, sourceSpan, scopeCallback, payload, customPayload); } } diff --git a/src/core/IronPython/Runtime/PythonTracebackListener.cs b/src/core/IronPython/Runtime/PythonTracebackListener.cs index e8d6b0055..77ced61c4 100644 --- a/src/core/IronPython/Runtime/PythonTracebackListener.cs +++ b/src/core/IronPython/Runtime/PythonTracebackListener.cs @@ -64,7 +64,7 @@ internal bool ExceptionThrown { } } - // dummy listener used to maintain the frame stack + // dummy listener used to maintain the frame stack on a thread without a trace function internal bool IsDummyListener { get; } #if PROFILE_SUPPORT @@ -135,7 +135,7 @@ public void OnTraceEvent(Debugging.TraceEventKind kind, string name, string sour pyFrame._lineNo = sourceSpan.Start.Line; } - if (traceDispatchObject != null && !_exceptionThrown) { + if (traceDispatchObject != null && !_exceptionThrown && !IsDummyListener) { DispatchTrace(thread, kind, payload, traceDispatch, traceDispatchObject, pyFrame); } } finally { diff --git a/tests/suite/modules/system_related/test_sys.py b/tests/suite/modules/system_related/test_sys.py index c2f6daf6d..065f2d341 100644 --- a/tests/suite/modules/system_related/test_sys.py +++ b/tests/suite/modules/system_related/test_sys.py @@ -253,6 +253,52 @@ def f(): out = subprocess.check_output([sys.executable, '-X:Debug', '-c', code]) self.assertEqual(out.strip(), b'True') + def test_settrace_no_frames(self): + # A traced function that calls sys.settrace(None) leaves no frames behind on the frame stack + def stop(): + sys.settrace(None) + def f(): + stop() + + sys.settrace(lambda *args: None) + try: + f() + finally: + sys.settrace(None) + self.assertEqual(sys._getframe(0).f_code.co_name, 'test_settrace_no_frames') + + def test_no_trace_events_after_settrace_none(self): + # After sys.settrace(None), no trace events are delivered, not even to frames that have a local trace function, + # also while another thread is still being traced + import threading + events = [] + def tracer(frame, event, arg): + events.append((event, frame.f_code.co_name)) + return tracer + def stop(): + sys.settrace(None) + def f(): + stop() + return 1 + + traced, finished = threading.Event(), threading.Event() + def other(): + sys.settrace(lambda *args: None) + traced.set() + finished.wait() + sys.settrace(None) + thread = threading.Thread(target=other) + thread.start() + traced.wait() + sys.settrace(tracer) + try: + f() + finally: + sys.settrace(None) + finished.set() + thread.join() + self.assertEqual(events, [('call', 'f'), ('line', 'f'), ('call', 'stop'), ('line', 'stop')]) + @skipUnlessIronPython() @unittest.expectedFailure # dummy getrefcount removed in https://github.com/IronLanguages/ironpython3/pull/577 def test_getrefcount(self):