Skip to content

Commit 457ca99

Browse files
committed
More tests
1 parent f6ac8cc commit 457ca99

File tree

3 files changed

+157
-18
lines changed

3 files changed

+157
-18
lines changed

Lib/test/test_external_inspection.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919

2020
import subprocess
2121

22+
# Profiling mode constants
23+
PROFILING_MODE_WALL = 0
24+
PROFILING_MODE_CPU = 1
25+
PROFILING_MODE_GIL = 2
26+
2227
try:
2328
from concurrent import interpreters
2429
except ImportError:
@@ -1747,7 +1752,8 @@ def busy():
17471752

17481753
attempts = 10
17491754
try:
1750-
unwinder = RemoteUnwinder(p.pid, all_threads=True, cpu_time=True)
1755+
unwinder = RemoteUnwinder(p.pid, all_threads=True, mode=PROFILING_MODE_CPU,
1756+
skip_non_matching_threads=False)
17511757
for _ in range(attempts):
17521758
traces = unwinder.get_stack_trace()
17531759
# Check if any thread is running
@@ -1780,5 +1786,117 @@ def busy():
17801786
p.terminate()
17811787
p.wait(timeout=SHORT_TIMEOUT)
17821788

1789+
@unittest.skipIf(
1790+
sys.platform not in ("linux", "darwin", "win32"),
1791+
"Test only runs on unsupported platforms (not Linux, macOS, or Windows)",
1792+
)
1793+
@unittest.skipIf(sys.platform == "android", "Android raises Linux-specific exception")
1794+
def test_thread_status_gil_detection(self):
1795+
port = find_unused_port()
1796+
script = textwrap.dedent(
1797+
f"""\
1798+
import time, sys, socket, threading
1799+
import os
1800+
1801+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1802+
sock.connect(('localhost', {port}))
1803+
1804+
def sleeper():
1805+
tid = threading.get_native_id()
1806+
sock.sendall(f'ready:sleeper:{{tid}}\\n'.encode())
1807+
time.sleep(10000)
1808+
1809+
def busy():
1810+
tid = threading.get_native_id()
1811+
sock.sendall(f'ready:busy:{{tid}}\\n'.encode())
1812+
x = 0
1813+
while True:
1814+
x = x + 1
1815+
time.sleep(0.5)
1816+
1817+
t1 = threading.Thread(target=sleeper)
1818+
t2 = threading.Thread(target=busy)
1819+
t1.start()
1820+
t2.start()
1821+
sock.sendall(b'ready:main\\n')
1822+
t1.join()
1823+
t2.join()
1824+
sock.close()
1825+
"""
1826+
)
1827+
with os_helper.temp_dir() as work_dir:
1828+
script_dir = os.path.join(work_dir, "script_pkg")
1829+
os.mkdir(script_dir)
1830+
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1831+
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1832+
server_socket.bind(("localhost", port))
1833+
server_socket.settimeout(SHORT_TIMEOUT)
1834+
server_socket.listen(1)
1835+
1836+
script_name = _make_test_script(script_dir, "thread_status_script", script)
1837+
client_socket = None
1838+
try:
1839+
p = subprocess.Popen([sys.executable, script_name])
1840+
client_socket, _ = server_socket.accept()
1841+
server_socket.close()
1842+
response = b""
1843+
sleeper_tid = None
1844+
busy_tid = None
1845+
while True:
1846+
chunk = client_socket.recv(1024)
1847+
response += chunk
1848+
if b"ready:main" in response and b"ready:sleeper" in response and b"ready:busy" in response:
1849+
# Parse TIDs from the response
1850+
for line in response.split(b"\n"):
1851+
if line.startswith(b"ready:sleeper:"):
1852+
try:
1853+
sleeper_tid = int(line.split(b":")[-1])
1854+
except Exception:
1855+
pass
1856+
elif line.startswith(b"ready:busy:"):
1857+
try:
1858+
busy_tid = int(line.split(b":")[-1])
1859+
except Exception:
1860+
pass
1861+
break
1862+
1863+
attempts = 10
1864+
try:
1865+
unwinder = RemoteUnwinder(p.pid, all_threads=True, mode=PROFILING_MODE_GIL,
1866+
skip_non_matching_threads=False)
1867+
for _ in range(attempts):
1868+
traces = unwinder.get_stack_trace()
1869+
# Check if any thread is running
1870+
if any(thread_info.status == 0 for interpreter_info in traces
1871+
for thread_info in interpreter_info.threads):
1872+
break
1873+
time.sleep(0.5) # Give a bit of time to let threads settle
1874+
except PermissionError:
1875+
self.skipTest(
1876+
"Insufficient permissions to read the stack trace"
1877+
)
1878+
1879+
1880+
# Find threads and their statuses
1881+
statuses = {}
1882+
for interpreter_info in traces:
1883+
for thread_info in interpreter_info.threads:
1884+
statuses[thread_info.thread_id] = thread_info.status
1885+
1886+
self.assertIsNotNone(sleeper_tid, "Sleeper thread id not received")
1887+
self.assertIsNotNone(busy_tid, "Busy thread id not received")
1888+
self.assertIn(sleeper_tid, statuses, "Sleeper tid not found in sampled threads")
1889+
self.assertIn(busy_tid, statuses, "Busy tid not found in sampled threads")
1890+
self.assertEqual(statuses[sleeper_tid], 2, "Sleeper thread should be idle (1)")
1891+
self.assertEqual(statuses[busy_tid], 0, "Busy thread should be running (0)")
1892+
1893+
finally:
1894+
if client_socket is not None:
1895+
client_socket.close()
1896+
p.terminate()
1897+
p.wait(timeout=SHORT_TIMEOUT)
1898+
1899+
1900+
17831901
if __name__ == "__main__":
17841902
unittest.main()

Modules/_remote_debugging_module.c

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ typedef struct {
264264
int debug;
265265
int only_active_thread;
266266
int mode; // Use enum _ProfilingMode values
267+
int skip_non_matching_threads; // New option to skip threads that don't match mode
267268
RemoteDebuggingState *cached_state; // Cached module state
268269
#ifdef Py_GIL_DISABLED
269270
// TLBC cache invalidation tracking
@@ -2654,12 +2655,14 @@ unwind_stack_for_thread(
26542655
status = THREAD_STATE_RUNNING;
26552656
}
26562657

2657-
// Check if we should skip this thread based on mode
2658+
// Check if we should skip this thread based on mode and the new option
26582659
int should_skip = 0;
2659-
if (unwinder->mode == PROFILING_MODE_CPU && status != THREAD_STATE_RUNNING) {
2660-
should_skip = 1;
2661-
} else if (unwinder->mode == PROFILING_MODE_GIL && status != THREAD_STATE_RUNNING) {
2662-
should_skip = 1;
2660+
if (unwinder->skip_non_matching_threads) {
2661+
if (unwinder->mode == PROFILING_MODE_CPU && status != THREAD_STATE_RUNNING) {
2662+
should_skip = 1;
2663+
} else if (unwinder->mode == PROFILING_MODE_GIL && status != THREAD_STATE_RUNNING) {
2664+
should_skip = 1;
2665+
}
26632666
}
26642667

26652668
if (should_skip) {
@@ -2743,6 +2746,7 @@ _remote_debugging.RemoteUnwinder.__init__
27432746
only_active_thread: bool = False
27442747
mode: int = 0
27452748
debug: bool = False
2749+
skip_non_matching_threads: bool = True
27462750
27472751
Initialize a new RemoteUnwinder object for debugging a remote Python process.
27482752
@@ -2755,6 +2759,8 @@ Initialize a new RemoteUnwinder object for debugging a remote Python process.
27552759
Cannot be used together with all_threads=True.
27562760
debug: If True, chain exceptions to explain the sequence of events that
27572761
lead to the exception.
2762+
skip_non_matching_threads: If True, skip threads that don't match the selected mode.
2763+
If False, include all threads regardless of mode.
27582764
27592765
The RemoteUnwinder provides functionality to inspect and debug a running Python
27602766
process, including examining thread states, stack frames and other runtime data.
@@ -2770,8 +2776,9 @@ static int
27702776
_remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
27712777
int pid, int all_threads,
27722778
int only_active_thread,
2773-
int mode, int debug)
2774-
/*[clinic end generated code: output=784e9990115aa569 input=d082d792d2ba9924]*/
2779+
int mode, int debug,
2780+
int skip_non_matching_threads)
2781+
/*[clinic end generated code: output=abf5ea5cd58bcb36 input=08fb6ace023ec3b5]*/
27752782
{
27762783
// Validate that all_threads and only_active_thread are not both True
27772784
if (all_threads && only_active_thread) {
@@ -2791,6 +2798,7 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
27912798
self->debug = debug;
27922799
self->only_active_thread = only_active_thread;
27932800
self->mode = mode;
2801+
self->skip_non_matching_threads = skip_non_matching_threads;
27942802
self->cached_state = NULL;
27952803
if (_Py_RemoteDebug_InitProcHandle(&self->handle, pid) < 0) {
27962804
set_exception_cause(self, PyExc_RuntimeError, "Failed to initialize process handle");

Modules/clinic/_remote_debugging_module.c.h

Lines changed: 23 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)