aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--Doc/whatsnew/3.14.rst99
-rw-r--r--Lib/asyncio/__main__.py32
-rw-r--r--Lib/asyncio/tools.py212
-rw-r--r--Lib/test/test_asyncio/test_tools.py839
-rw-r--r--Lib/test/test_external_inspection.py45
-rw-r--r--Lib/test/test_sys.py2
-rw-r--r--Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst6
-rw-r--r--Modules/Setup2
-rw-r--r--Modules/Setup.stdlib.in2
-rw-r--r--Modules/_remotedebuggingmodule.c (renamed from Modules/_testexternalinspection.c)100
-rw-r--r--PCbuild/_remotedebugging.vcxproj (renamed from PCbuild/_testexternalinspection.vcxproj)4
-rw-r--r--PCbuild/_remotedebugging.vcxproj.filters (renamed from PCbuild/_testexternalinspection.vcxproj.filters)2
-rw-r--r--PCbuild/pcbuild.proj4
-rw-r--r--PCbuild/pcbuild.sln2
-rw-r--r--Tools/build/generate_stdlib_module_names.py2
-rwxr-xr-xconfigure40
-rw-r--r--configure.ac4
17 files changed, 1308 insertions, 89 deletions
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 87c31d32e22..81581b30d21 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -543,6 +543,105 @@ configuration mechanisms).
.. seealso::
:pep:`741`.
+.. _whatsnew314-asyncio-introspection:
+
+Asyncio introspection capabilities
+----------------------------------
+
+Added a new command-line interface to inspect running Python processes using
+asynchronous tasks, available via:
+
+.. code-block:: bash
+
+ python -m asyncio ps PID
+
+This tool inspects the given process ID (PID) and displays information about
+currently running asyncio tasks. It outputs a task table: a flat
+listing of all tasks, their names, their coroutine stacks, and which tasks are
+awaiting them.
+
+.. code-block:: bash
+
+ python -m asyncio pstree PID
+
+This tool fetches the same information, but renders a visual async call tree,
+showing coroutine relationships in a hierarchical format. This command is
+particularly useful for debugging long-running or stuck asynchronous programs.
+It can help developers quickly identify where a program is blocked, what tasks
+are pending, and how coroutines are chained together.
+
+For example given this code:
+
+.. code-block:: python
+
+ import asyncio
+
+ async def play(track):
+ await asyncio.sleep(5)
+ print(f"🎡 Finished: {track}")
+
+ async def album(name, tracks):
+ async with asyncio.TaskGroup() as tg:
+ for track in tracks:
+ tg.create_task(play(track), name=track)
+
+ async def main():
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(
+ album("Sundowning", ["TNDNBTG", "Levitate"]), name="Sundowning")
+ tg.create_task(
+ album("TMBTE", ["DYWTYLM", "Aqua Regia"]), name="TMBTE")
+
+ if __name__ == "__main__":
+ asyncio.run(main())
+
+Executing the new tool on the running process will yield a table like this:
+
+.. code-block:: bash
+
+ python -m asyncio ps 12345
+
+ tid task id task name coroutine chain awaiter name awaiter id
+ ---------------------------------------------------------------------------------------------------------------------------------------
+ 8138752 0x564bd3d0210 Task-1 0x0
+ 8138752 0x564bd3d0410 Sundowning _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
+ 8138752 0x564bd3d0610 TMBTE _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
+ 8138752 0x564bd3d0810 TNDNBTG _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
+ 8138752 0x564bd3d0a10 Levitate _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
+ 8138752 0x564bd3e0550 DYWTYLM _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
+ 8138752 0x564bd3e0710 Aqua Regia _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
+
+
+or:
+
+.. code-block:: bash
+
+ python -m asyncio pstree 12345
+
+ └── (T) Task-1
+ └── main
+ └── __aexit__
+ └── _aexit
+ β”œβ”€β”€ (T) Sundowning
+ β”‚ └── album
+ β”‚ └── __aexit__
+ β”‚ └── _aexit
+ β”‚ β”œβ”€β”€ (T) TNDNBTG
+ β”‚ └── (T) Levitate
+ └── (T) TMBTE
+ └── album
+ └── __aexit__
+ └── _aexit
+ β”œβ”€β”€ (T) DYWTYLM
+ └── (T) Aqua Regia
+
+If a cycle is detected in the async await graph (which could indicate a
+programming issue), the tool raises an error and lists the cycle paths that
+prevent tree construction.
+
+(Contributed by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
+Gomez Macias in :gh:`91048`.)
+
.. _whatsnew314-tail-call:
A new type of interpreter
diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py
index 69f5a30cfe5..7d980bc401a 100644
--- a/Lib/asyncio/__main__.py
+++ b/Lib/asyncio/__main__.py
@@ -1,5 +1,7 @@
+import argparse
import ast
import asyncio
+import asyncio.tools
import concurrent.futures
import contextvars
import inspect
@@ -140,6 +142,36 @@ class REPLThread(threading.Thread):
if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ prog="python3 -m asyncio",
+ description="Interactive asyncio shell and CLI tools",
+ )
+ subparsers = parser.add_subparsers(help="sub-commands", dest="command")
+ ps = subparsers.add_parser(
+ "ps", help="Display a table of all pending tasks in a process"
+ )
+ ps.add_argument("pid", type=int, help="Process ID to inspect")
+ pstree = subparsers.add_parser(
+ "pstree", help="Display a tree of all pending tasks in a process"
+ )
+ pstree.add_argument("pid", type=int, help="Process ID to inspect")
+ args = parser.parse_args()
+ match args.command:
+ case "ps":
+ asyncio.tools.display_awaited_by_tasks_table(args.pid)
+ sys.exit(0)
+ case "pstree":
+ asyncio.tools.display_awaited_by_tasks_tree(args.pid)
+ sys.exit(0)
+ case None:
+ pass # continue to the interactive shell
+ case _:
+ # shouldn't happen as an invalid command-line wouldn't parse
+ # but let's keep it for the next person adding a command
+ print(f"error: unhandled command {args.command}", file=sys.stderr)
+ parser.print_usage(file=sys.stderr)
+ sys.exit(1)
+
sys.audit("cpython.run_stdin")
if os.getenv('PYTHON_BASIC_REPL'):
diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py
new file mode 100644
index 00000000000..16440b594ad
--- /dev/null
+++ b/Lib/asyncio/tools.py
@@ -0,0 +1,212 @@
+"""Tools to analyze tasks running in asyncio programs."""
+
+from dataclasses import dataclass
+from collections import defaultdict
+from itertools import count
+from enum import Enum
+import sys
+from _remotedebugging import get_all_awaited_by
+
+
+class NodeType(Enum):
+ COROUTINE = 1
+ TASK = 2
+
+
+@dataclass(frozen=True)
+class CycleFoundException(Exception):
+ """Raised when there is a cycle when drawing the call tree."""
+ cycles: list[list[int]]
+ id2name: dict[int, str]
+
+
+# ─── indexing helpers ───────────────────────────────────────────
+def _index(result):
+ id2name, awaits = {}, []
+ for _thr_id, tasks in result:
+ for tid, tname, awaited in tasks:
+ id2name[tid] = tname
+ for stack, parent_id in awaited:
+ awaits.append((parent_id, stack, tid))
+ return id2name, awaits
+
+
+def _build_tree(id2name, awaits):
+ id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
+ children = defaultdict(list)
+ cor_names = defaultdict(dict) # (parent) -> {frame: node}
+ cor_id_seq = count(1)
+
+ def _cor_node(parent_key, frame_name):
+ """Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
+ bucket = cor_names[parent_key]
+ if frame_name in bucket:
+ return bucket[frame_name]
+ node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
+ id2label[node_key] = frame_name
+ children[parent_key].append(node_key)
+ bucket[frame_name] = node_key
+ return node_key
+
+ # lay down parent ➜ …frames… ➜ child paths
+ for parent_id, stack, child_id in awaits:
+ cur = (NodeType.TASK, parent_id)
+ for frame in reversed(stack): # outer-most β†’ inner-most
+ cur = _cor_node(cur, frame)
+ child_key = (NodeType.TASK, child_id)
+ if child_key not in children[cur]:
+ children[cur].append(child_key)
+
+ return id2label, children
+
+
+def _roots(id2label, children):
+ all_children = {c for kids in children.values() for c in kids}
+ return [n for n in id2label if n not in all_children]
+
+# ─── detect cycles in the task-to-task graph ───────────────────────
+def _task_graph(awaits):
+ """Return {parent_task_id: {child_task_id, …}, …}."""
+ g = defaultdict(set)
+ for parent_id, _stack, child_id in awaits:
+ g[parent_id].add(child_id)
+ return g
+
+
+def _find_cycles(graph):
+ """
+ Depth-first search for back-edges.
+
+ Returns a list of cycles (each cycle is a list of task-ids) or an
+ empty list if the graph is acyclic.
+ """
+ WHITE, GREY, BLACK = 0, 1, 2
+ color = defaultdict(lambda: WHITE)
+ path, cycles = [], []
+
+ def dfs(v):
+ color[v] = GREY
+ path.append(v)
+ for w in graph.get(v, ()):
+ if color[w] == WHITE:
+ dfs(w)
+ elif color[w] == GREY: # back-edge β†’ cycle!
+ i = path.index(w)
+ cycles.append(path[i:] + [w]) # make a copy
+ color[v] = BLACK
+ path.pop()
+
+ for v in list(graph):
+ if color[v] == WHITE:
+ dfs(v)
+ return cycles
+
+
+# ─── PRINT TREE FUNCTION ───────────────────────────────────────
+def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
+ """
+ Build a list of strings for pretty-print a async call tree.
+
+ The call tree is produced by `get_all_async_stacks()`, prefixing tasks
+ with `task_emoji` and coroutine frames with `cor_emoji`.
+ """
+ id2name, awaits = _index(result)
+ g = _task_graph(awaits)
+ cycles = _find_cycles(g)
+ if cycles:
+ raise CycleFoundException(cycles, id2name)
+ labels, children = _build_tree(id2name, awaits)
+
+ def pretty(node):
+ flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
+ return f"{flag} {labels[node]}"
+
+ def render(node, prefix="", last=True, buf=None):
+ if buf is None:
+ buf = []
+ buf.append(f"{prefix}{'└── ' if last else 'β”œβ”€β”€ '}{pretty(node)}")
+ new_pref = prefix + (" " if last else "β”‚ ")
+ kids = children.get(node, [])
+ for i, kid in enumerate(kids):
+ render(kid, new_pref, i == len(kids) - 1, buf)
+ return buf
+
+ return [render(root) for root in _roots(labels, children)]
+
+
+def build_task_table(result):
+ id2name, awaits = _index(result)
+ table = []
+ for tid, tasks in result:
+ for task_id, task_name, awaited in tasks:
+ if not awaited:
+ table.append(
+ [
+ tid,
+ hex(task_id),
+ task_name,
+ "",
+ "",
+ "0x0"
+ ]
+ )
+ for stack, awaiter_id in awaited:
+ coroutine_chain = " -> ".join(stack)
+ awaiter_name = id2name.get(awaiter_id, "Unknown")
+ table.append(
+ [
+ tid,
+ hex(task_id),
+ task_name,
+ coroutine_chain,
+ awaiter_name,
+ hex(awaiter_id),
+ ]
+ )
+
+ return table
+
+def _print_cycle_exception(exception: CycleFoundException):
+ print("ERROR: await-graph contains cycles – cannot print a tree!", file=sys.stderr)
+ print("", file=sys.stderr)
+ for c in exception.cycles:
+ inames = " β†’ ".join(exception.id2name.get(tid, hex(tid)) for tid in c)
+ print(f"cycle: {inames}", file=sys.stderr)
+
+
+def _get_awaited_by_tasks(pid: int) -> list:
+ try:
+ return get_all_awaited_by(pid)
+ except RuntimeError as e:
+ while e.__context__ is not None:
+ e = e.__context__
+ print(f"Error retrieving tasks: {e}")
+ sys.exit(1)
+
+
+def display_awaited_by_tasks_table(pid: int) -> None:
+ """Build and print a table of all pending tasks under `pid`."""
+
+ tasks = _get_awaited_by_tasks(pid)
+ table = build_task_table(tasks)
+ # Print the table in a simple tabular format
+ print(
+ f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
+ )
+ print("-" * 135)
+ for row in table:
+ print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")
+
+
+def display_awaited_by_tasks_tree(pid: int) -> None:
+ """Build and print a tree of all pending tasks under `pid`."""
+
+ tasks = _get_awaited_by_tasks(pid)
+ try:
+ result = build_async_tree(tasks)
+ except CycleFoundException as e:
+ _print_cycle_exception(e)
+ sys.exit(1)
+
+ for tree in result:
+ print("\n".join(tree))
diff --git a/Lib/test/test_asyncio/test_tools.py b/Lib/test/test_asyncio/test_tools.py
new file mode 100644
index 00000000000..2caf56172c9
--- /dev/null
+++ b/Lib/test/test_asyncio/test_tools.py
@@ -0,0 +1,839 @@
+import unittest
+
+from asyncio import tools
+
+
+# mock output of get_all_awaited_by function.
+TEST_INPUTS_TREE = [
+ [
+ # test case containing a task called timer being awaited in two
+ # different subtasks part of a TaskGroup (root1 and root2) which call
+ # awaiter functions.
+ (
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ (
+ 3,
+ "timer",
+ [
+ [["awaiter3", "awaiter2", "awaiter"], 4],
+ [["awaiter1_3", "awaiter1_2", "awaiter1"], 5],
+ [["awaiter1_3", "awaiter1_2", "awaiter1"], 6],
+ [["awaiter3", "awaiter2", "awaiter"], 7],
+ ],
+ ),
+ (
+ 8,
+ "root1",
+ [[["_aexit", "__aexit__", "main"], 2]],
+ ),
+ (
+ 9,
+ "root2",
+ [[["_aexit", "__aexit__", "main"], 2]],
+ ),
+ (
+ 4,
+ "child1_1",
+ [
+ [
+ ["_aexit", "__aexit__", "blocho_caller", "bloch"],
+ 8,
+ ]
+ ],
+ ),
+ (
+ 6,
+ "child2_1",
+ [
+ [
+ ["_aexit", "__aexit__", "blocho_caller", "bloch"],
+ 8,
+ ]
+ ],
+ ),
+ (
+ 7,
+ "child1_2",
+ [
+ [
+ ["_aexit", "__aexit__", "blocho_caller", "bloch"],
+ 9,
+ ]
+ ],
+ ),
+ (
+ 5,
+ "child2_2",
+ [
+ [
+ ["_aexit", "__aexit__", "blocho_caller", "bloch"],
+ 9,
+ ]
+ ],
+ ),
+ ],
+ ),
+ (0, []),
+ ),
+ (
+ [
+ [
+ "└── (T) Task-1",
+ " └── main",
+ " └── __aexit__",
+ " └── _aexit",
+ " β”œβ”€β”€ (T) root1",
+ " β”‚ └── bloch",
+ " β”‚ └── blocho_caller",
+ " β”‚ └── __aexit__",
+ " β”‚ └── _aexit",
+ " β”‚ β”œβ”€β”€ (T) child1_1",
+ " β”‚ β”‚ └── awaiter",
+ " β”‚ β”‚ └── awaiter2",
+ " β”‚ β”‚ └── awaiter3",
+ " β”‚ β”‚ └── (T) timer",
+ " β”‚ └── (T) child2_1",
+ " β”‚ └── awaiter1",
+ " β”‚ └── awaiter1_2",
+ " β”‚ └── awaiter1_3",
+ " β”‚ └── (T) timer",
+ " └── (T) root2",
+ " └── bloch",
+ " └── blocho_caller",
+ " └── __aexit__",
+ " └── _aexit",
+ " β”œβ”€β”€ (T) child1_2",
+ " β”‚ └── awaiter",
+ " β”‚ └── awaiter2",
+ " β”‚ └── awaiter3",
+ " β”‚ └── (T) timer",
+ " └── (T) child2_2",
+ " └── awaiter1",
+ " └── awaiter1_2",
+ " └── awaiter1_3",
+ " └── (T) timer",
+ ]
+ ]
+ ),
+ ],
+ [
+ # test case containing two roots
+ (
+ (
+ 9,
+ [
+ (5, "Task-5", []),
+ (6, "Task-6", [[["main2"], 5]]),
+ (7, "Task-7", [[["main2"], 5]]),
+ (8, "Task-8", [[["main2"], 5]]),
+ ],
+ ),
+ (
+ 10,
+ [
+ (1, "Task-1", []),
+ (2, "Task-2", [[["main"], 1]]),
+ (3, "Task-3", [[["main"], 1]]),
+ (4, "Task-4", [[["main"], 1]]),
+ ],
+ ),
+ (11, []),
+ (0, []),
+ ),
+ (
+ [
+ [
+ "└── (T) Task-5",
+ " └── main2",
+ " β”œβ”€β”€ (T) Task-6",
+ " β”œβ”€β”€ (T) Task-7",
+ " └── (T) Task-8",
+ ],
+ [
+ "└── (T) Task-1",
+ " └── main",
+ " β”œβ”€β”€ (T) Task-2",
+ " β”œβ”€β”€ (T) Task-3",
+ " └── (T) Task-4",
+ ],
+ ]
+ ),
+ ],
+ [
+ # test case containing two roots, one of them without subtasks
+ (
+ [
+ (1, [(2, "Task-5", [])]),
+ (
+ 3,
+ [
+ (4, "Task-1", []),
+ (5, "Task-2", [[["main"], 4]]),
+ (6, "Task-3", [[["main"], 4]]),
+ (7, "Task-4", [[["main"], 4]]),
+ ],
+ ),
+ (8, []),
+ (0, []),
+ ]
+ ),
+ (
+ [
+ ["└── (T) Task-5"],
+ [
+ "└── (T) Task-1",
+ " └── main",
+ " β”œβ”€β”€ (T) Task-2",
+ " β”œβ”€β”€ (T) Task-3",
+ " └── (T) Task-4",
+ ],
+ ]
+ ),
+ ],
+]
+
+TEST_INPUTS_CYCLES_TREE = [
+ [
+ # this test case contains a cycle: two tasks awaiting each other.
+ (
+ [
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ (
+ 3,
+ "a",
+ [[["awaiter2"], 4], [["main"], 2]],
+ ),
+ (4, "b", [[["awaiter"], 3]]),
+ ],
+ ),
+ (0, []),
+ ]
+ ),
+ ([[4, 3, 4]]),
+ ],
+ [
+ # this test case contains two cycles
+ (
+ [
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ (
+ 3,
+ "A",
+ [[["nested", "nested", "task_b"], 4]],
+ ),
+ (
+ 4,
+ "B",
+ [
+ [["nested", "nested", "task_c"], 5],
+ [["nested", "nested", "task_a"], 3],
+ ],
+ ),
+ (5, "C", [[["nested", "nested"], 6]]),
+ (
+ 6,
+ "Task-2",
+ [[["nested", "nested", "task_b"], 4]],
+ ),
+ ],
+ ),
+ (0, []),
+ ]
+ ),
+ ([[4, 3, 4], [4, 6, 5, 4]]),
+ ],
+]
+
+TEST_INPUTS_TABLE = [
+ [
+ # test case containing a task called timer being awaited in two
+ # different subtasks part of a TaskGroup (root1 and root2) which call
+ # awaiter functions.
+ (
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ (
+ 3,
+ "timer",
+ [
+ [["awaiter3", "awaiter2", "awaiter"], 4],
+ [["awaiter1_3", "awaiter1_2", "awaiter1"], 5],
+ [["awaiter1_3", "awaiter1_2", "awaiter1"], 6],
+ [["awaiter3", "awaiter2", "awaiter"], 7],
+ ],
+ ),
+ (
+ 8,
+ "root1",
+ [[["_aexit", "__aexit__", "main"], 2]],
+ ),
+ (
+ 9,
+ "root2",
+ [[["_aexit", "__aexit__", "main"], 2]],
+ ),
+ (
+ 4,
+ "child1_1",
+ [
+ [
+ ["_aexit", "__aexit__", "blocho_caller", "bloch"],
+ 8,
+ ]
+ ],
+ ),
+ (
+ 6,
+ "child2_1",
+ [
+ [
+ ["_aexit", "__aexit__", "blocho_caller", "bloch"],
+ 8,
+ ]
+ ],
+ ),
+ (
+ 7,
+ "child1_2",
+ [
+ [
+ ["_aexit", "__aexit__", "blocho_caller", "bloch"],
+ 9,
+ ]
+ ],
+ ),
+ (
+ 5,
+ "child2_2",
+ [
+ [
+ ["_aexit", "__aexit__", "blocho_caller", "bloch"],
+ 9,
+ ]
+ ],
+ ),
+ ],
+ ),
+ (0, []),
+ ),
+ (
+ [
+ [1, "0x2", "Task-1", "", "", "0x0"],
+ [
+ 1,
+ "0x3",
+ "timer",
+ "awaiter3 -> awaiter2 -> awaiter",
+ "child1_1",
+ "0x4",
+ ],
+ [
+ 1,
+ "0x3",
+ "timer",
+ "awaiter1_3 -> awaiter1_2 -> awaiter1",
+ "child2_2",
+ "0x5",
+ ],
+ [
+ 1,
+ "0x3",
+ "timer",
+ "awaiter1_3 -> awaiter1_2 -> awaiter1",
+ "child2_1",
+ "0x6",
+ ],
+ [
+ 1,
+ "0x3",
+ "timer",
+ "awaiter3 -> awaiter2 -> awaiter",
+ "child1_2",
+ "0x7",
+ ],
+ [
+ 1,
+ "0x8",
+ "root1",
+ "_aexit -> __aexit__ -> main",
+ "Task-1",
+ "0x2",
+ ],
+ [
+ 1,
+ "0x9",
+ "root2",
+ "_aexit -> __aexit__ -> main",
+ "Task-1",
+ "0x2",
+ ],
+ [
+ 1,
+ "0x4",
+ "child1_1",
+ "_aexit -> __aexit__ -> blocho_caller -> bloch",
+ "root1",
+ "0x8",
+ ],
+ [
+ 1,
+ "0x6",
+ "child2_1",
+ "_aexit -> __aexit__ -> blocho_caller -> bloch",
+ "root1",
+ "0x8",
+ ],
+ [
+ 1,
+ "0x7",
+ "child1_2",
+ "_aexit -> __aexit__ -> blocho_caller -> bloch",
+ "root2",
+ "0x9",
+ ],
+ [
+ 1,
+ "0x5",
+ "child2_2",
+ "_aexit -> __aexit__ -> blocho_caller -> bloch",
+ "root2",
+ "0x9",
+ ],
+ ]
+ ),
+ ],
+ [
+ # test case containing two roots
+ (
+ (
+ 9,
+ [
+ (5, "Task-5", []),
+ (6, "Task-6", [[["main2"], 5]]),
+ (7, "Task-7", [[["main2"], 5]]),
+ (8, "Task-8", [[["main2"], 5]]),
+ ],
+ ),
+ (
+ 10,
+ [
+ (1, "Task-1", []),
+ (2, "Task-2", [[["main"], 1]]),
+ (3, "Task-3", [[["main"], 1]]),
+ (4, "Task-4", [[["main"], 1]]),
+ ],
+ ),
+ (11, []),
+ (0, []),
+ ),
+ (
+ [
+ [9, "0x5", "Task-5", "", "", "0x0"],
+ [9, "0x6", "Task-6", "main2", "Task-5", "0x5"],
+ [9, "0x7", "Task-7", "main2", "Task-5", "0x5"],
+ [9, "0x8", "Task-8", "main2", "Task-5", "0x5"],
+ [10, "0x1", "Task-1", "", "", "0x0"],
+ [10, "0x2", "Task-2", "main", "Task-1", "0x1"],
+ [10, "0x3", "Task-3", "main", "Task-1", "0x1"],
+ [10, "0x4", "Task-4", "main", "Task-1", "0x1"],
+ ]
+ ),
+ ],
+ [
+ # test case containing two roots, one of them without subtasks
+ (
+ [
+ (1, [(2, "Task-5", [])]),
+ (
+ 3,
+ [
+ (4, "Task-1", []),
+ (5, "Task-2", [[["main"], 4]]),
+ (6, "Task-3", [[["main"], 4]]),
+ (7, "Task-4", [[["main"], 4]]),
+ ],
+ ),
+ (8, []),
+ (0, []),
+ ]
+ ),
+ (
+ [
+ [1, "0x2", "Task-5", "", "", "0x0"],
+ [3, "0x4", "Task-1", "", "", "0x0"],
+ [3, "0x5", "Task-2", "main", "Task-1", "0x4"],
+ [3, "0x6", "Task-3", "main", "Task-1", "0x4"],
+ [3, "0x7", "Task-4", "main", "Task-1", "0x4"],
+ ]
+ ),
+ ],
+ # CASES WITH CYCLES
+ [
+ # this test case contains a cycle: two tasks awaiting each other.
+ (
+ [
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ (
+ 3,
+ "a",
+ [[["awaiter2"], 4], [["main"], 2]],
+ ),
+ (4, "b", [[["awaiter"], 3]]),
+ ],
+ ),
+ (0, []),
+ ]
+ ),
+ (
+ [
+ [1, "0x2", "Task-1", "", "", "0x0"],
+ [1, "0x3", "a", "awaiter2", "b", "0x4"],
+ [1, "0x3", "a", "main", "Task-1", "0x2"],
+ [1, "0x4", "b", "awaiter", "a", "0x3"],
+ ]
+ ),
+ ],
+ [
+ # this test case contains two cycles
+ (
+ [
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ (
+ 3,
+ "A",
+ [[["nested", "nested", "task_b"], 4]],
+ ),
+ (
+ 4,
+ "B",
+ [
+ [["nested", "nested", "task_c"], 5],
+ [["nested", "nested", "task_a"], 3],
+ ],
+ ),
+ (5, "C", [[["nested", "nested"], 6]]),
+ (
+ 6,
+ "Task-2",
+ [[["nested", "nested", "task_b"], 4]],
+ ),
+ ],
+ ),
+ (0, []),
+ ]
+ ),
+ (
+ [
+ [1, "0x2", "Task-1", "", "", "0x0"],
+ [
+ 1,
+ "0x3",
+ "A",
+ "nested -> nested -> task_b",
+ "B",
+ "0x4",
+ ],
+ [
+ 1,
+ "0x4",
+ "B",
+ "nested -> nested -> task_c",
+ "C",
+ "0x5",
+ ],
+ [
+ 1,
+ "0x4",
+ "B",
+ "nested -> nested -> task_a",
+ "A",
+ "0x3",
+ ],
+ [
+ 1,
+ "0x5",
+ "C",
+ "nested -> nested",
+ "Task-2",
+ "0x6",
+ ],
+ [
+ 1,
+ "0x6",
+ "Task-2",
+ "nested -> nested -> task_b",
+ "B",
+ "0x4",
+ ],
+ ]
+ ),
+ ],
+]
+
+
+class TestAsyncioToolsTree(unittest.TestCase):
+
+ def test_asyncio_utils(self):
+ for input_, tree in TEST_INPUTS_TREE:
+ with self.subTest(input_):
+ self.assertEqual(tools.build_async_tree(input_), tree)
+
+ def test_asyncio_utils_cycles(self):
+ for input_, cycles in TEST_INPUTS_CYCLES_TREE:
+ with self.subTest(input_):
+ try:
+ tools.build_async_tree(input_)
+ except tools.CycleFoundException as e:
+ self.assertEqual(e.cycles, cycles)
+
+
+class TestAsyncioToolsTable(unittest.TestCase):
+ def test_asyncio_utils(self):
+ for input_, table in TEST_INPUTS_TABLE:
+ with self.subTest(input_):
+ self.assertEqual(tools.build_task_table(input_), table)
+
+
+class TestAsyncioToolsBasic(unittest.TestCase):
+ def test_empty_input_tree(self):
+ """Test build_async_tree with empty input."""
+ result = []
+ expected_output = []
+ self.assertEqual(tools.build_async_tree(result), expected_output)
+
+ def test_empty_input_table(self):
+ """Test build_task_table with empty input."""
+ result = []
+ expected_output = []
+ self.assertEqual(tools.build_task_table(result), expected_output)
+
+ def test_only_independent_tasks_tree(self):
+ input_ = [(1, [(10, "taskA", []), (11, "taskB", [])])]
+ expected = [["└── (T) taskA"], ["└── (T) taskB"]]
+ result = tools.build_async_tree(input_)
+ self.assertEqual(sorted(result), sorted(expected))
+
+ def test_only_independent_tasks_table(self):
+ input_ = [(1, [(10, "taskA", []), (11, "taskB", [])])]
+ self.assertEqual(
+ tools.build_task_table(input_),
+ [[1, "0xa", "taskA", "", "", "0x0"], [1, "0xb", "taskB", "", "", "0x0"]],
+ )
+
+ def test_single_task_tree(self):
+ """Test build_async_tree with a single task and no awaits."""
+ result = [
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ ],
+ )
+ ]
+ expected_output = [
+ [
+ "└── (T) Task-1",
+ ]
+ ]
+ self.assertEqual(tools.build_async_tree(result), expected_output)
+
+ def test_single_task_table(self):
+ """Test build_task_table with a single task and no awaits."""
+ result = [
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ ],
+ )
+ ]
+ expected_output = [[1, "0x2", "Task-1", "", "", "0x0"]]
+ self.assertEqual(tools.build_task_table(result), expected_output)
+
+ def test_cycle_detection(self):
+ """Test build_async_tree raises CycleFoundException for cyclic input."""
+ result = [
+ (
+ 1,
+ [
+ (2, "Task-1", [[["main"], 3]]),
+ (3, "Task-2", [[["main"], 2]]),
+ ],
+ )
+ ]
+ with self.assertRaises(tools.CycleFoundException) as context:
+ tools.build_async_tree(result)
+ self.assertEqual(context.exception.cycles, [[3, 2, 3]])
+
+ def test_complex_tree(self):
+ """Test build_async_tree with a more complex tree structure."""
+ result = [
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ (3, "Task-2", [[["main"], 2]]),
+ (4, "Task-3", [[["main"], 3]]),
+ ],
+ )
+ ]
+ expected_output = [
+ [
+ "└── (T) Task-1",
+ " └── main",
+ " └── (T) Task-2",
+ " └── main",
+ " └── (T) Task-3",
+ ]
+ ]
+ self.assertEqual(tools.build_async_tree(result), expected_output)
+
+ def test_complex_table(self):
+ """Test build_task_table with a more complex tree structure."""
+ result = [
+ (
+ 1,
+ [
+ (2, "Task-1", []),
+ (3, "Task-2", [[["main"], 2]]),
+ (4, "Task-3", [[["main"], 3]]),
+ ],
+ )
+ ]
+ expected_output = [
+ [1, "0x2", "Task-1", "", "", "0x0"],
+ [1, "0x3", "Task-2", "main", "Task-1", "0x2"],
+ [1, "0x4", "Task-3", "main", "Task-2", "0x3"],
+ ]
+ self.assertEqual(tools.build_task_table(result), expected_output)
+
+ def test_deep_coroutine_chain(self):
+ input_ = [
+ (
+ 1,
+ [
+ (10, "leaf", [[["c1", "c2", "c3", "c4", "c5"], 11]]),
+ (11, "root", []),
+ ],
+ )
+ ]
+ expected = [
+ [
+ "└── (T) root",
+ " └── c5",
+ " └── c4",
+ " └── c3",
+ " └── c2",
+ " └── c1",
+ " └── (T) leaf",
+ ]
+ ]
+ result = tools.build_async_tree(input_)
+ self.assertEqual(result, expected)
+
+ def test_multiple_cycles_same_node(self):
+ input_ = [
+ (
+ 1,
+ [
+ (1, "Task-A", [[["call1"], 2]]),
+ (2, "Task-B", [[["call2"], 3]]),
+ (3, "Task-C", [[["call3"], 1], [["call4"], 2]]),
+ ],
+ )
+ ]
+ with self.assertRaises(tools.CycleFoundException) as ctx:
+ tools.build_async_tree(input_)
+ cycles = ctx.exception.cycles
+ self.assertTrue(any(set(c) == {1, 2, 3} for c in cycles))
+
+ def test_table_output_format(self):
+ input_ = [(1, [(1, "Task-A", [[["foo"], 2]]), (2, "Task-B", [])])]
+ table = tools.build_task_table(input_)
+ for row in table:
+ self.assertEqual(len(row), 6)
+ self.assertIsInstance(row[0], int) # thread ID
+ self.assertTrue(
+ isinstance(row[1], str) and row[1].startswith("0x")
+ ) # hex task ID
+ self.assertIsInstance(row[2], str) # task name
+ self.assertIsInstance(row[3], str) # coroutine chain
+ self.assertIsInstance(row[4], str) # awaiter name
+ self.assertTrue(
+ isinstance(row[5], str) and row[5].startswith("0x")
+ ) # hex awaiter ID
+
+
+class TestAsyncioToolsEdgeCases(unittest.TestCase):
+
+ def test_task_awaits_self(self):
+ """A task directly awaits itself – should raise a cycle."""
+ input_ = [(1, [(1, "Self-Awaiter", [[["loopback"], 1]])])]
+ with self.assertRaises(tools.CycleFoundException) as ctx:
+ tools.build_async_tree(input_)
+ self.assertIn([1, 1], ctx.exception.cycles)
+
+ def test_task_with_missing_awaiter_id(self):
+ """Awaiter ID not in task list – should not crash, just show 'Unknown'."""
+ input_ = [(1, [(1, "Task-A", [[["coro"], 999]])])] # 999 not defined
+ table = tools.build_task_table(input_)
+ self.assertEqual(len(table), 1)
+ self.assertEqual(table[0][4], "Unknown")
+
+ def test_duplicate_coroutine_frames(self):
+ """Same coroutine frame repeated under a parent – should deduplicate."""
+ input_ = [
+ (
+ 1,
+ [
+ (1, "Task-1", [[["frameA"], 2], [["frameA"], 3]]),
+ (2, "Task-2", []),
+ (3, "Task-3", []),
+ ],
+ )
+ ]
+ tree = tools.build_async_tree(input_)
+ # Both children should be under the same coroutine node
+ flat = "\n".join(tree[0])
+ self.assertIn("frameA", flat)
+ self.assertIn("Task-2", flat)
+ self.assertIn("Task-1", flat)
+
+ flat = "\n".join(tree[1])
+ self.assertIn("frameA", flat)
+ self.assertIn("Task-3", flat)
+ self.assertIn("Task-1", flat)
+
+ def test_task_with_no_name(self):
+ """Task with no name in id2name – should still render with fallback."""
+ input_ = [(1, [(1, "root", [[["f1"], 2]]), (2, None, [])])]
+ # If name is None, fallback to string should not crash
+ tree = tools.build_async_tree(input_)
+ self.assertIn("(T) None", "\n".join(tree[0]))
+
+ def test_tree_rendering_with_custom_emojis(self):
+ """Pass custom emojis to the tree renderer."""
+ input_ = [(1, [(1, "MainTask", [[["f1", "f2"], 2]]), (2, "SubTask", [])])]
+ tree = tools.build_async_tree(input_, task_emoji="🧡", cor_emoji="πŸ”")
+ flat = "\n".join(tree[0])
+ self.assertIn("🧡 MainTask", flat)
+ self.assertIn("πŸ” f1", flat)
+ self.assertIn("πŸ” f2", flat)
+ self.assertIn("🧡 SubTask", flat)
diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py
index aa05db972f0..4e82f567e1f 100644
--- a/Lib/test/test_external_inspection.py
+++ b/Lib/test/test_external_inspection.py
@@ -4,7 +4,8 @@ import textwrap
import importlib
import sys
import socket
-from test.support import os_helper, SHORT_TIMEOUT, busy_retry
+from unittest.mock import ANY
+from test.support import os_helper, SHORT_TIMEOUT, busy_retry, requires_gil_enabled
from test.support.script_helper import make_script
from test.support.socket_helper import find_unused_port
@@ -13,13 +14,13 @@ import subprocess
PROCESS_VM_READV_SUPPORTED = False
try:
- from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
- from _testexternalinspection import get_stack_trace
- from _testexternalinspection import get_async_stack_trace
- from _testexternalinspection import get_all_awaited_by
+ from _remotedebugging import PROCESS_VM_READV_SUPPORTED
+ from _remotedebugging import get_stack_trace
+ from _remotedebugging import get_async_stack_trace
+ from _remotedebugging import get_all_awaited_by
except ImportError:
raise unittest.SkipTest(
- "Test only runs when _testexternalinspection is available")
+ "Test only runs when _remotedebuggingmodule is available")
def _make_test_script(script_dir, script_basename, source):
to_return = make_script(script_dir, script_basename, source)
@@ -184,13 +185,13 @@ class TestGetStackTrace(unittest.TestCase):
root_task = "Task-1"
expected_stack_trace = [
- ["c5", "c4", "c3", "c2"],
- "c2_root",
+ ['c5', 'c4', 'c3', 'c2'],
+ 'c2_root',
[
- [["main"], root_task, []],
- [["c1"], "sub_main_1", [[["main"], root_task, []]]],
- [["c1"], "sub_main_2", [[["main"], root_task, []]]],
- ],
+ [['_aexit', '__aexit__', 'main'], root_task, []],
+ [['c1'], 'sub_main_1', [[['_aexit', '__aexit__', 'main'], root_task, []]]],
+ [['c1'], 'sub_main_2', [[['_aexit', '__aexit__', 'main'], root_task, []]]],
+ ]
]
self.assertEqual(stack_trace, expected_stack_trace)
@@ -397,12 +398,15 @@ class TestGetStackTrace(unittest.TestCase):
# sets are unordered, so we want to sort "awaited_by"s
stack_trace[2].sort(key=lambda x: x[1])
- expected_stack_trace = [
- ['deep', 'c1', 'run_one_coro'], 'Task-2', [[['main'], 'Task-1', []]]
+ expected_stack_trace = [
+ ['deep', 'c1', 'run_one_coro'],
+ 'Task-2',
+ [[['staggered_race', 'main'], 'Task-1', []]]
]
self.assertEqual(stack_trace, expected_stack_trace)
@skip_if_not_supported
+ @requires_gil_enabled("gh-133359: occasionally flaky on AMD64")
@unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support")
def test_async_global_awaited_by(self):
@@ -516,19 +520,19 @@ class TestGetStackTrace(unittest.TestCase):
# expected: at least 1000 pending tasks
self.assertGreaterEqual(len(entries), 1000)
# the first three tasks stem from the code structure
- self.assertIn(('Task-1', []), entries)
- self.assertIn(('server task', [[['main'], 'Task-1', []]]), entries)
- self.assertIn(('echo client spam', [[['main'], 'Task-1', []]]), entries)
+ self.assertIn((ANY, 'Task-1', []), entries)
+ self.assertIn((ANY, 'server task', [[['_aexit', '__aexit__', 'main'], ANY]]), entries)
+ self.assertIn((ANY, 'echo client spam', [[['_aexit', '__aexit__', 'main'], ANY]]), entries)
- expected_stack = [[['echo_client_spam'], 'echo client spam', [[['main'], 'Task-1', []]]]]
- tasks_with_stack = [task for task in entries if task[1] == expected_stack]
+ expected_stack = [[['_aexit', '__aexit__', 'echo_client_spam'], ANY]]
+ tasks_with_stack = [task for task in entries if task[2] == expected_stack]
self.assertGreaterEqual(len(tasks_with_stack), 1000)
# the final task will have some random number, but it should for
# sure be one of the echo client spam horde (In windows this is not true
# for some reason)
if sys.platform != "win32":
- self.assertEqual([[['echo_client_spam'], 'echo client spam', [[['main'], 'Task-1', []]]]], entries[-1][1])
+ self.assertEqual([[['_aexit', '__aexit__', 'echo_client_spam'], ANY]], entries[-1][2])
except PermissionError:
self.skipTest(
"Insufficient permissions to read the stack trace")
@@ -544,7 +548,6 @@ class TestGetStackTrace(unittest.TestCase):
"Test only runs on Linux with process_vm_readv support")
def test_self_trace(self):
stack_trace = get_stack_trace(os.getpid())
- print(stack_trace)
self.assertEqual(stack_trace[0], "test_self_trace")
if __name__ == "__main__":
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 56413d00823..10c3e0e9a1d 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -1960,7 +1960,7 @@ def _supports_remote_attaching():
PROCESS_VM_READV_SUPPORTED = False
try:
- from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
+ from _remotedebuggingmodule import PROCESS_VM_READV_SUPPORTED
except ImportError:
pass
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst
new file mode 100644
index 00000000000..1d45868b7b2
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst
@@ -0,0 +1,6 @@
+Add a new ``python -m asyncio ps PID`` command-line interface to inspect
+asyncio tasks in a running Python process. Displays a flat table of await
+relationships. A variant showing a tree view is also available as
+``python -m asyncio pstree PID``. Both are useful for debugging async
+code. Patch by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
+Gomez Macias.
diff --git a/Modules/Setup b/Modules/Setup
index 65c22d48ba0..c3e0d9eb934 100644
--- a/Modules/Setup
+++ b/Modules/Setup
@@ -286,7 +286,7 @@ PYTHONPATH=$(COREPYTHONPATH)
#_testcapi _testcapimodule.c
#_testimportmultiple _testimportmultiple.c
#_testmultiphase _testmultiphase.c
-#_testexternalinspection _testexternalinspection.c
+#_remotedebugging _remotedebuggingmodule.c
#_testsinglephase _testsinglephase.c
# ---
diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in
index 33e60f37d19..be4fb513e59 100644
--- a/Modules/Setup.stdlib.in
+++ b/Modules/Setup.stdlib.in
@@ -33,6 +33,7 @@
# Modules that should always be present (POSIX and Windows):
@MODULE_ARRAY_TRUE@array arraymodule.c
@MODULE__ASYNCIO_TRUE@_asyncio _asynciomodule.c
+@MODULE__REMOTEDEBUGGING_TRUE@_remotedebugging _remotedebuggingmodule.c
@MODULE__BISECT_TRUE@_bisect _bisectmodule.c
@MODULE__CSV_TRUE@_csv _csv.c
@MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c
@@ -186,7 +187,6 @@
@MODULE__TESTIMPORTMULTIPLE_TRUE@_testimportmultiple _testimportmultiple.c
@MODULE__TESTMULTIPHASE_TRUE@_testmultiphase _testmultiphase.c
@MODULE__TESTSINGLEPHASE_TRUE@_testsinglephase _testsinglephase.c
-@MODULE__TESTEXTERNALINSPECTION_TRUE@_testexternalinspection _testexternalinspection.c
@MODULE__CTYPES_TEST_TRUE@_ctypes_test _ctypes/_ctypes_test.c
# Limited API template modules; must be built as shared modules.
diff --git a/Modules/_testexternalinspection.c b/Modules/_remotedebuggingmodule.c
index b65c5821443..0e055ae1604 100644
--- a/Modules/_testexternalinspection.c
+++ b/Modules/_remotedebuggingmodule.c
@@ -152,9 +152,9 @@ read_char(proc_handle_t *handle, uintptr_t address, char *result)
}
static int
-read_int(proc_handle_t *handle, uintptr_t address, int *result)
+read_sized_int(proc_handle_t *handle, uintptr_t address, void *result, size_t size)
{
- int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(int), result);
+ int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, size, result);
if (res < 0) {
return -1;
}
@@ -345,7 +345,7 @@ parse_coro_chain(
uintptr_t gen_type_addr;
int err = read_ptr(
handle,
- coro_address + sizeof(void*),
+ coro_address + offsets->pyobject.ob_type,
&gen_type_addr);
if (err) {
return -1;
@@ -376,11 +376,13 @@ parse_coro_chain(
}
Py_DECREF(name);
- int gi_frame_state;
- err = read_int(
+ int8_t gi_frame_state;
+ err = read_sized_int(
handle,
coro_address + offsets->gen_object.gi_frame_state,
- &gi_frame_state);
+ &gi_frame_state,
+ sizeof(int8_t)
+ );
if (err) {
return -1;
}
@@ -427,7 +429,7 @@ parse_coro_chain(
uintptr_t gi_await_addr_type_addr;
int err = read_ptr(
handle,
- gi_await_addr + sizeof(void*),
+ gi_await_addr + offsets->pyobject.ob_type,
&gi_await_addr_type_addr);
if (err) {
return -1;
@@ -470,7 +472,8 @@ parse_task_awaited_by(
struct _Py_DebugOffsets* offsets,
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
uintptr_t task_address,
- PyObject *awaited_by
+ PyObject *awaited_by,
+ int recurse_task
);
@@ -480,7 +483,8 @@ parse_task(
struct _Py_DebugOffsets* offsets,
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
uintptr_t task_address,
- PyObject *render_to
+ PyObject *render_to,
+ int recurse_task
) {
char is_task;
int err = read_char(
@@ -508,8 +512,13 @@ parse_task(
Py_DECREF(call_stack);
if (is_task) {
- PyObject *tn = parse_task_name(
- handle, offsets, async_offsets, task_address);
+ PyObject *tn = NULL;
+ if (recurse_task) {
+ tn = parse_task_name(
+ handle, offsets, async_offsets, task_address);
+ } else {
+ tn = PyLong_FromUnsignedLongLong(task_address);
+ }
if (tn == NULL) {
goto err;
}
@@ -550,21 +559,23 @@ parse_task(
goto err;
}
- PyObject *awaited_by = PyList_New(0);
- if (awaited_by == NULL) {
- goto err;
- }
- if (PyList_Append(result, awaited_by)) {
+ if (recurse_task) {
+ PyObject *awaited_by = PyList_New(0);
+ if (awaited_by == NULL) {
+ goto err;
+ }
+ if (PyList_Append(result, awaited_by)) {
+ Py_DECREF(awaited_by);
+ goto err;
+ }
+ /* we can operate on a borrowed one to simplify cleanup */
Py_DECREF(awaited_by);
- goto err;
- }
- /* we can operate on a borrowed one to simplify cleanup */
- Py_DECREF(awaited_by);
- if (parse_task_awaited_by(handle, offsets, async_offsets,
- task_address, awaited_by)
- ) {
- goto err;
+ if (parse_task_awaited_by(handle, offsets, async_offsets,
+ task_address, awaited_by, 1)
+ ) {
+ goto err;
+ }
}
Py_DECREF(result);
@@ -581,7 +592,8 @@ parse_tasks_in_set(
struct _Py_DebugOffsets* offsets,
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
uintptr_t set_addr,
- PyObject *awaited_by
+ PyObject *awaited_by,
+ int recurse_task
) {
uintptr_t set_obj;
if (read_py_ptr(
@@ -642,7 +654,9 @@ parse_tasks_in_set(
offsets,
async_offsets,
key_addr,
- awaited_by)
+ awaited_by,
+ recurse_task
+ )
) {
return -1;
}
@@ -666,7 +680,8 @@ parse_task_awaited_by(
struct _Py_DebugOffsets* offsets,
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
uintptr_t task_address,
- PyObject *awaited_by
+ PyObject *awaited_by,
+ int recurse_task
) {
uintptr_t task_ab_addr;
int err = read_py_ptr(
@@ -696,7 +711,9 @@ parse_task_awaited_by(
offsets,
async_offsets,
task_address + async_offsets->asyncio_task_object.task_awaited_by,
- awaited_by)
+ awaited_by,
+ recurse_task
+ )
) {
return -1;
}
@@ -715,7 +732,9 @@ parse_task_awaited_by(
offsets,
async_offsets,
sub_task,
- awaited_by)
+ awaited_by,
+ recurse_task
+ )
) {
return -1;
}
@@ -1060,15 +1079,24 @@ append_awaited_by_for_thread(
return -1;
}
- PyObject *result_item = PyTuple_New(2);
+ PyObject* task_id = PyLong_FromUnsignedLongLong(task_addr);
+ if (task_id == NULL) {
+ Py_DECREF(tn);
+ Py_DECREF(current_awaited_by);
+ return -1;
+ }
+
+ PyObject *result_item = PyTuple_New(3);
if (result_item == NULL) {
Py_DECREF(tn);
Py_DECREF(current_awaited_by);
+ Py_DECREF(task_id);
return -1;
}
- PyTuple_SET_ITEM(result_item, 0, tn); // steals ref
- PyTuple_SET_ITEM(result_item, 1, current_awaited_by); // steals ref
+ PyTuple_SET_ITEM(result_item, 0, task_id); // steals ref
+ PyTuple_SET_ITEM(result_item, 1, tn); // steals ref
+ PyTuple_SET_ITEM(result_item, 2, current_awaited_by); // steals ref
if (PyList_Append(result, result_item)) {
Py_DECREF(result_item);
return -1;
@@ -1076,7 +1104,7 @@ append_awaited_by_for_thread(
Py_DECREF(result_item);
if (parse_task_awaited_by(handle, debug_offsets, async_offsets,
- task_addr, current_awaited_by))
+ task_addr, current_awaited_by, 0))
{
return -1;
}
@@ -1499,7 +1527,7 @@ get_async_stack_trace(PyObject* self, PyObject* args)
if (parse_task_awaited_by(
handle, &local_debug_offsets, &local_async_debug,
- running_task_addr, awaited_by)
+ running_task_addr, awaited_by, 1)
) {
goto result_err;
}
@@ -1526,13 +1554,13 @@ static PyMethodDef methods[] = {
static struct PyModuleDef module = {
.m_base = PyModuleDef_HEAD_INIT,
- .m_name = "_testexternalinspection",
+ .m_name = "_remotedebugging",
.m_size = -1,
.m_methods = methods,
};
PyMODINIT_FUNC
-PyInit__testexternalinspection(void)
+PyInit__remotedebugging(void)
{
PyObject* mod = PyModule_Create(&module);
if (mod == NULL) {
diff --git a/PCbuild/_testexternalinspection.vcxproj b/PCbuild/_remotedebugging.vcxproj
index d5f347ecfec..a16079f7c6c 100644
--- a/PCbuild/_testexternalinspection.vcxproj
+++ b/PCbuild/_remotedebugging.vcxproj
@@ -68,7 +68,7 @@
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{4D7C112F-3083-4D9E-9754-9341C14D9B39}</ProjectGuid>
- <RootNamespace>_testexternalinspection</RootNamespace>
+ <RootNamespace>_remotedebugging</RootNamespace>
<Keyword>Win32Proj</Keyword>
<SupportPGO>false</SupportPGO>
</PropertyGroup>
@@ -93,7 +93,7 @@
<_ProjectFileVersion>10.0.30319.1</_ProjectFileVersion>
</PropertyGroup>
<ItemGroup>
- <ClCompile Include="..\Modules\_testexternalinspection.c" />
+ <ClCompile Include="..\Modules\_remotedebuggingmodule.c" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\PC\python_nt.rc" />
diff --git a/PCbuild/_testexternalinspection.vcxproj.filters b/PCbuild/_remotedebugging.vcxproj.filters
index feb4343e5c2..888e2cd478a 100644
--- a/PCbuild/_testexternalinspection.vcxproj.filters
+++ b/PCbuild/_remotedebugging.vcxproj.filters
@@ -9,7 +9,7 @@
</Filter>
</ItemGroup>
<ItemGroup>
- <ClCompile Include="..\Modules\_testexternalinspection.c" />
+ <ClCompile Include="..\Modules\_remotedebuggingmodule.c" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\PC\python_nt.rc">
diff --git a/PCbuild/pcbuild.proj b/PCbuild/pcbuild.proj
index 1bf430e03de..eec213d7bac 100644
--- a/PCbuild/pcbuild.proj
+++ b/PCbuild/pcbuild.proj
@@ -66,7 +66,7 @@
<!-- pyshellext.dll -->
<Projects Include="pyshellext.vcxproj" />
<!-- Extension modules -->
- <ExtensionModules Include="_asyncio;_zoneinfo;_decimal;_elementtree;_multiprocessing;_overlapped;pyexpat;_queue;select;unicodedata;winsound;_uuid;_wmi" />
+ <ExtensionModules Include="_asyncio;_remotedebugging;_zoneinfo;_decimal;_elementtree;_multiprocessing;_overlapped;pyexpat;_queue;select;unicodedata;winsound;_uuid;_wmi" />
<ExtensionModules Include="_ctypes" Condition="$(IncludeCTypes)" />
<!-- Extension modules that require external sources -->
<ExternalModules Include="_bz2;_lzma;_sqlite3" />
@@ -79,7 +79,7 @@
<ExtensionModules Include="@(ExternalModules->'%(Identity)')" Condition="$(IncludeExternals)" />
<Projects Include="@(ExtensionModules->'%(Identity).vcxproj')" Condition="$(IncludeExtensions)" />
<!-- Test modules -->
- <TestModules Include="_ctypes_test;_testbuffer;_testcapi;_testlimitedcapi;_testexternalinspection;_testinternalcapi;_testembed;_testimportmultiple;_testmultiphase;_testsinglephase;_testconsole;_testclinic;_testclinic_limited" />
+ <TestModules Include="_ctypes_test;_testbuffer;_testcapi;_testlimitedcapi;_testinternalcapi;_testembed;_testimportmultiple;_testmultiphase;_testsinglephase;_testconsole;_testclinic;_testclinic_limited" />
<TestModules Include="xxlimited" Condition="'$(Configuration)' == 'Release'" />
<TestModules Include="xxlimited_35" Condition="'$(Configuration)' == 'Release'" />
<Projects Include="@(TestModules->'%(Identity).vcxproj')" Condition="$(IncludeTests)">
diff --git a/PCbuild/pcbuild.sln b/PCbuild/pcbuild.sln
index 803bb149c90..d2bfb9472b1 100644
--- a/PCbuild/pcbuild.sln
+++ b/PCbuild/pcbuild.sln
@@ -81,7 +81,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testclinic", "_testclinic.
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testinternalcapi", "_testinternalcapi.vcxproj", "{900342D7-516A-4469-B1AD-59A66E49A25F}"
EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testexternalinspection", "_testexternalinspection.vcxproj", "{4D7C112F-3083-4D9E-9754-9341C14D9B39}"
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_remotedebugging", "_remotedebugging.vcxproj", "{4D7C112F-3083-4D9E-9754-9341C14D9B39}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testimportmultiple", "_testimportmultiple.vcxproj", "{36D0C52C-DF4E-45D0-8BC7-E294C3ABC781}"
EndProject
diff --git a/Tools/build/generate_stdlib_module_names.py b/Tools/build/generate_stdlib_module_names.py
index 9873890837f..761eecba96f 100644
--- a/Tools/build/generate_stdlib_module_names.py
+++ b/Tools/build/generate_stdlib_module_names.py
@@ -34,7 +34,7 @@ IGNORE = {
'_testlimitedcapi',
'_testmultiphase',
'_testsinglephase',
- '_testexternalinspection',
+ '_remotedebugging',
'_xxtestfuzz',
'idlelib.idle_test',
'test',
diff --git a/configure b/configure
index 7dbb35f9f45..3b74554d5a2 100755
--- a/configure
+++ b/configure
@@ -654,8 +654,8 @@ MODULE__XXTESTFUZZ_FALSE
MODULE__XXTESTFUZZ_TRUE
MODULE_XXSUBTYPE_FALSE
MODULE_XXSUBTYPE_TRUE
-MODULE__TESTEXTERNALINSPECTION_FALSE
-MODULE__TESTEXTERNALINSPECTION_TRUE
+MODULE__REMOTEDEBUGGING_FALSE
+MODULE__REMOTEDEBUGGING_TRUE
MODULE__TESTSINGLEPHASE_FALSE
MODULE__TESTSINGLEPHASE_TRUE
MODULE__TESTMULTIPHASE_FALSE
@@ -30684,7 +30684,7 @@ case $ac_sys_system in #(
py_cv_module__ctypes_test=n/a
- py_cv_module__testexternalinspection=n/a
+ py_cv_module__remotedebugging=n/a
py_cv_module__testimportmultiple=n/a
py_cv_module__testmultiphase=n/a
py_cv_module__testsinglephase=n/a
@@ -33449,44 +33449,44 @@ fi
printf "%s\n" "$py_cv_module__testsinglephase" >&6; }
- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module _testexternalinspection" >&5
-printf %s "checking for stdlib extension module _testexternalinspection... " >&6; }
- if test "$py_cv_module__testexternalinspection" != "n/a"
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module _remotedebugging" >&5
+printf %s "checking for stdlib extension module _remotedebugging... " >&6; }
+ if test "$py_cv_module__remotedebugging" != "n/a"
then :
if test "$TEST_MODULES" = yes
then :
if true
then :
- py_cv_module__testexternalinspection=yes
+ py_cv_module__remotedebugging=yes
else case e in #(
- e) py_cv_module__testexternalinspection=missing ;;
+ e) py_cv_module__remotedebugging=missing ;;
esac
fi
else case e in #(
- e) py_cv_module__testexternalinspection=disabled ;;
+ e) py_cv_module__remotedebugging=disabled ;;
esac
fi
fi
- as_fn_append MODULE_BLOCK "MODULE__TESTEXTERNALINSPECTION_STATE=$py_cv_module__testexternalinspection$as_nl"
- if test "x$py_cv_module__testexternalinspection" = xyes
+ as_fn_append MODULE_BLOCK "MODULE__REMOTEDEBUGGING_STATE=$py_cv_module__remotedebugging$as_nl"
+ if test "x$py_cv_module__remotedebugging" = xyes
then :
fi
- if test "$py_cv_module__testexternalinspection" = yes; then
- MODULE__TESTEXTERNALINSPECTION_TRUE=
- MODULE__TESTEXTERNALINSPECTION_FALSE='#'
+ if test "$py_cv_module__remotedebugging" = yes; then
+ MODULE__REMOTEDEBUGGING_TRUE=
+ MODULE__REMOTEDEBUGGING_FALSE='#'
else
- MODULE__TESTEXTERNALINSPECTION_TRUE='#'
- MODULE__TESTEXTERNALINSPECTION_FALSE=
+ MODULE__REMOTEDEBUGGING_TRUE='#'
+ MODULE__REMOTEDEBUGGING_FALSE=
fi
- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $py_cv_module__testexternalinspection" >&5
-printf "%s\n" "$py_cv_module__testexternalinspection" >&6; }
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $py_cv_module__remotedebugging" >&5
+printf "%s\n" "$py_cv_module__remotedebugging" >&6; }
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module xxsubtype" >&5
@@ -34119,8 +34119,8 @@ if test -z "${MODULE__TESTSINGLEPHASE_TRUE}" && test -z "${MODULE__TESTSINGLEPHA
as_fn_error $? "conditional \"MODULE__TESTSINGLEPHASE\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
fi
-if test -z "${MODULE__TESTEXTERNALINSPECTION_TRUE}" && test -z "${MODULE__TESTEXTERNALINSPECTION_FALSE}"; then
- as_fn_error $? "conditional \"MODULE__TESTEXTERNALINSPECTION\" was never defined.
+if test -z "${MODULE__REMOTEDEBUGGING_TRUE}" && test -z "${MODULE__REMOTEDEBUGGING_FALSE}"; then
+ as_fn_error $? "conditional \"MODULE__REMOTEDEBUGGING\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
fi
if test -z "${MODULE_XXSUBTYPE_TRUE}" && test -z "${MODULE_XXSUBTYPE_FALSE}"; then
diff --git a/configure.ac b/configure.ac
index 65f265045ba..ed5c65ecbcc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -7720,7 +7720,7 @@ AS_CASE([$ac_sys_system],
dnl (see Modules/Setup.stdlib.in).
PY_STDLIB_MOD_SET_NA(
[_ctypes_test],
- [_testexternalinspection],
+ [_remotedebugging],
[_testimportmultiple],
[_testmultiphase],
[_testsinglephase],
@@ -8082,7 +8082,7 @@ PY_STDLIB_MOD([_testbuffer], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([_testimportmultiple], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
PY_STDLIB_MOD([_testmultiphase], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
PY_STDLIB_MOD([_testsinglephase], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
-PY_STDLIB_MOD([_testexternalinspection], [test "$TEST_MODULES" = yes])
+PY_STDLIB_MOD([_remotedebugging], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([xxsubtype], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([_xxtestfuzz], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([_ctypes_test],