Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions src/core/IronPython/Runtime/PythonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3848,10 +3848,10 @@ public PythonTracebackListenersDispatcher(PythonContext parent) {
void Debugging.ITraceCallback.OnTraceEvent(Debugging.TraceEventKind kind, string name, string sourceFileName, SourceSpan sourceSpan, Func<IDictionary<object, object>> scopeCallback, object payload, object customPayload) {
var listener = _parent._tracebackListeners.Value;

if (listener == null && _parent.PythonOptions.Tracing) {
// If tracing without sys.set_trace() is enabled, we need to register a dummy traceback listener,
// because of the FunctionStack handling done there.
_parent._tracebackListeners.Value = listener = new PythonTracebackListener(_parent, null);
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.
_parent._tracebackListeners.Value = listener = new PythonTracebackListener(_parent, null, isDummyListener: true);
}

listener?.OnTraceEvent(kind, name, sourceFileName, sourceSpan, scopeCallback, payload, customPayload);
Expand Down Expand Up @@ -3881,17 +3881,20 @@ internal void SetTrace(object o) {
// If CurrentPythonFrame is not null then we're currently inside a traceback, and
// enabling trace while inside a traceback is only allowed through sys.call_tracing()
var pyThread = PythonOps.GetFunctionStackNoCreate();
if (pyThread == null || (oldTraceListener == null || !oldTraceListener.InTraceBack)) {
_tracebackListeners.Value = newTraceListener = new PythonTracebackListener(this, o);
if (pyThread == null || oldTraceListener == null || !oldTraceListener.InTraceBack) {
_tracebackListeners.Value = newTraceListener = new PythonTracebackListener(this, o, isDummyListener: false);
}
}

// global
lock (_codeUpdateLock) {
var oldEnableTracing = EnableTracing;

if ((oldTraceListener != null) != (newTraceListener != null)) {
_tracebackListenersCount += (newTraceListener != null) ? 1 : -1;
var oldCounted = oldTraceListener?.IsDummyListener == false;
var newCounted = newTraceListener?.IsDummyListener == false;

if (oldCounted != newCounted) {
_tracebackListenersCount += newCounted ? 1 : -1;
}

if (EnableTracing != oldEnableTracing) {
Expand Down
8 changes: 6 additions & 2 deletions src/core/IronPython/Runtime/PythonTracebackListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ internal sealed class PythonTracebackListener : Debugging.ITraceCallback {
private TracebackDelegate _traceDispatch;
private bool _inTraceBack;
private bool _exceptionThrown;

#if PROFILE_SUPPORT
private bool _profile;
#endif

internal PythonTracebackListener(PythonContext pythonContext, object traceObject) {
internal PythonTracebackListener(PythonContext pythonContext, object traceObject, bool isDummyListener) {
_pythonContext = pythonContext;
IsDummyListener = isDummyListener;

if (traceObject != null) {
_traceObject = traceObject;
Expand Down Expand Up @@ -63,6 +64,9 @@ internal bool ExceptionThrown {
}
}

// dummy listener used to maintain the frame stack
internal bool IsDummyListener { get; }

#if PROFILE_SUPPORT
internal void SetProfile(TracebackDelegate traceDispatch) {
_traceDispatch = traceDispatch;
Expand Down
3 changes: 3 additions & 0 deletions tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,6 @@ NotParallelSafe=true # Uses fixed file, directory, and environment variable name
[IronPython.modules.system_related.test_sys_getframe]
IsolationLevel=PROCESS # https://github.com/IronLanguages/ironpython3/issues/489
FullFrames=true

[IronPython.modules.system_related.test_trace_listener_count]
IsolationLevel=PROCESS
72 changes: 72 additions & 0 deletions tests/suite/modules/system_related/test_trace_listener_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Licensed to the .NET Foundation under one or more agreements.
# The .NET Foundation licenses this file to you under the Apache 2.0 License.
# See the LICENSE file in the project root for more information.

import sys
import threading
import typing

from iptest import IronPythonTestCase, run_test

class TraceListenerCountTest(IronPythonTestCase):
def test_typing_lookup_on_thread_while_main_thread_traced(self):
# https://github.com/IronLanguages/ironpython3/issues/2072
results = []

def tracer(frame, event, arg):
return tracer

def do_check():
results.append(isinstance(None, typing.Iterable))

thread = threading.Thread(target=do_check)
sys.settrace(tracer)
try:
thread.start()
thread.join(5)
self.assertFalse(thread.is_alive())
finally:
sys.settrace(None)

self.assertEqual(results, [False])

def test_dummy_listener_replaced_by_thread_trace(self):
# https://github.com/IronLanguages/ironpython3/pull/2075
worker_trace_set = threading.Event()
allow_probe = threading.Event()
probe_events = []

def main_trace(frame, event, arg):
return main_trace

def worker_trace(frame, event, arg):
if frame.f_code.co_name == 'probe':
probe_events.append(event)
return worker_trace

def worker():
try:
sys.settrace(worker_trace)
worker_trace_set.set()
allow_probe.wait()

namespace = {}
exec('def probe():\n pass', namespace)
namespace['probe']()
finally:
sys.settrace(None)

thread = threading.Thread(target=worker)
sys.settrace(main_trace)
try:
thread.start()
self.assertTrue(worker_trace_set.wait(5))
finally:
sys.settrace(None)
allow_probe.set()
thread.join(5)
self.assertFalse(thread.is_alive())

self.assertIn('call', probe_events)

run_test(__name__)
Loading