Skip to content

Commit f025dba

Browse files
authored
gh-149230: _remote_debugging: Fix async-aware for tasks in non-main threads (#149235)
1 parent 6f8c964 commit f025dba

2 files changed

Lines changed: 156 additions & 2 deletions

File tree

Lib/test/test_external_inspection.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,160 @@ def matches_awaited_by_pattern(task):
14371437
finally:
14381438
_cleanup_sockets(client_socket, server_socket)
14391439

1440+
@skip_if_not_supported
1441+
@unittest.skipIf(
1442+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
1443+
"Test only runs on Linux with process_vm_readv support",
1444+
)
1445+
def test_async_global_awaited_by_from_non_main_thread(self):
1446+
port = find_unused_port()
1447+
script = textwrap.dedent(
1448+
f"""\
1449+
import asyncio
1450+
import socket
1451+
import threading
1452+
import time
1453+
1454+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1455+
sock.connect(('localhost', {port}))
1456+
1457+
async def worker_main():
1458+
task = asyncio.create_task(
1459+
asyncio.sleep(10_000),
1460+
name="worker task",
1461+
)
1462+
await asyncio.sleep(0)
1463+
sock.sendall(f"ready:{{threading.get_native_id()}}\\n".encode())
1464+
await task
1465+
1466+
def run_worker_loop():
1467+
asyncio.run(worker_main())
1468+
1469+
threading.Thread(
1470+
target=run_worker_loop,
1471+
name="async-worker",
1472+
daemon=True,
1473+
).start()
1474+
time.sleep(10_000)
1475+
"""
1476+
)
1477+
1478+
with os_helper.temp_dir() as work_dir:
1479+
script_dir = os.path.join(work_dir, "script_pkg")
1480+
os.mkdir(script_dir)
1481+
1482+
server_socket = _create_server_socket(port)
1483+
script_name = _make_test_script(script_dir, "script", script)
1484+
client_socket = None
1485+
1486+
try:
1487+
with _managed_subprocess([sys.executable, script_name]) as p:
1488+
client_socket, _ = server_socket.accept()
1489+
server_socket.close()
1490+
server_socket = None
1491+
1492+
response = _wait_for_signal(client_socket, b"ready:")
1493+
worker_thread_id = int(
1494+
response.split(b"ready:", 1)[1].splitlines()[0]
1495+
)
1496+
1497+
for _ in busy_retry(SHORT_TIMEOUT):
1498+
all_awaited_by = get_all_awaited_by(p.pid)
1499+
if any(
1500+
task.task_name == "worker task"
1501+
for info in all_awaited_by
1502+
if info.thread_id == worker_thread_id
1503+
for task in info.awaited_by
1504+
):
1505+
break
1506+
else:
1507+
self.fail(
1508+
"get_all_awaited_by() did not report "
1509+
"the asyncio task from the non-main thread"
1510+
)
1511+
finally:
1512+
_cleanup_sockets(client_socket, server_socket)
1513+
1514+
@skip_if_not_supported
1515+
@unittest.skipIf(
1516+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
1517+
"Test only runs on Linux with process_vm_readv support",
1518+
)
1519+
def test_async_remote_stack_trace_from_non_main_thread(self):
1520+
port = find_unused_port()
1521+
script = textwrap.dedent(
1522+
f"""\
1523+
import asyncio
1524+
import socket
1525+
import threading
1526+
import time
1527+
1528+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1529+
sock.connect(('localhost', {port}))
1530+
1531+
def blocking_call():
1532+
sock.sendall(f"ready:{{threading.get_native_id()}}\\n".encode())
1533+
time.sleep(10_000)
1534+
1535+
async def worker_task():
1536+
await asyncio.sleep(0)
1537+
blocking_call()
1538+
1539+
async def worker_main():
1540+
task = asyncio.create_task(
1541+
worker_task(),
1542+
name="worker task",
1543+
)
1544+
await task
1545+
1546+
def run_worker_loop():
1547+
asyncio.run(worker_main())
1548+
1549+
threading.Thread(
1550+
target=run_worker_loop,
1551+
name="async-worker",
1552+
daemon=True,
1553+
).start()
1554+
time.sleep(10_000)
1555+
"""
1556+
)
1557+
1558+
with os_helper.temp_dir() as work_dir:
1559+
script_dir = os.path.join(work_dir, "script_pkg")
1560+
os.mkdir(script_dir)
1561+
1562+
server_socket = _create_server_socket(port)
1563+
script_name = _make_test_script(script_dir, "script", script)
1564+
client_socket = None
1565+
1566+
try:
1567+
with _managed_subprocess([sys.executable, script_name]) as p:
1568+
client_socket, _ = server_socket.accept()
1569+
server_socket.close()
1570+
server_socket = None
1571+
1572+
response = _wait_for_signal(client_socket, b"ready:")
1573+
worker_thread_id = int(
1574+
response.split(b"ready:", 1)[1].splitlines()[0]
1575+
)
1576+
1577+
for _ in busy_retry(SHORT_TIMEOUT):
1578+
stack_trace = get_async_stack_trace(p.pid)
1579+
if any(
1580+
task.task_name == "worker task"
1581+
for info in stack_trace
1582+
if info.thread_id == worker_thread_id
1583+
for task in info.awaited_by
1584+
):
1585+
break
1586+
else:
1587+
self.fail(
1588+
"get_async_stack_trace() did not report "
1589+
"the running asyncio task from the non-main thread"
1590+
)
1591+
finally:
1592+
_cleanup_sockets(client_socket, server_socket)
1593+
14401594
@skip_if_not_supported
14411595
@unittest.skipIf(
14421596
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,

Modules/_remote_debugging/threads.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ iterate_threads(
3434

3535
if (0 > _Py_RemoteDebug_PagedReadRemoteMemory(
3636
&unwinder->handle,
37-
unwinder->interpreter_addr + (uintptr_t)unwinder->debug_offsets.interpreter_state.threads_main,
37+
unwinder->interpreter_addr + (uintptr_t)unwinder->debug_offsets.interpreter_state.threads_head,
3838
sizeof(void*),
3939
&thread_state_addr))
4040
{
41-
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read main thread state");
41+
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read threads head");
4242
return -1;
4343
}
4444

0 commit comments

Comments
 (0)