https://github.com/python/cpython/commit/2bc836523105a2197a1f987cc03911bece74b35e
commit: 2bc836523105a2197a1f987cc03911bece74b35e
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2025-05-04T00:51:57Z
summary:

 GH-91048: Add utils for printing the call stack for asyncio tasks (#133284)

files:
A Lib/asyncio/tools.py
A Lib/test/test_asyncio/test_tools.py
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst
A Modules/_remotedebuggingmodule.c
A PCbuild/_remotedebugging.vcxproj
A PCbuild/_remotedebugging.vcxproj.filters
D Modules/_testexternalinspection.c
D PCbuild/_testexternalinspection.vcxproj
D PCbuild/_testexternalinspection.vcxproj.filters
M Doc/whatsnew/3.14.rst
M Lib/asyncio/__main__.py
M Lib/test/test_external_inspection.py
M Lib/test/test_sys.py
M Modules/Setup
M Modules/Setup.stdlib.in
M PCbuild/pcbuild.proj
M PCbuild/pcbuild.sln
M Tools/build/generate_stdlib_module_names.py
M configure
M configure.ac

diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 87c31d32e2264d..81581b30d2194b 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 69f5a30cfe5095..7d980bc401ae3b 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 @@ def interrupt(self) -> None:
 
 
 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 00000000000000..16440b594ad993
--- /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 00000000000000..2caf56172c9193
--- /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 aa05db972f068d..4e82f567e1f429 100644
--- a/Lib/test/test_external_inspection.py
+++ b/Lib/test/test_external_inspection.py
@@ -4,7 +4,8 @@
 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 @@
 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 @@ def new_eager_loop():
 
                 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 @@ async def main():
             # 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 @@ async def main():
                 # 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 @@ async def main():
                      "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 56413d00823f4a..10c3e0e9a1d2bb 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 00000000000000..1d45868b7b27bc
--- /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 65c22d48ba0bb7..c3e0d9eb9344a9 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 33e60f37d19922..be4fb513e592e1 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
similarity index 94%
rename from Modules/_testexternalinspection.c
rename to Modules/_remotedebuggingmodule.c
index b65c5821443ebf..0e055ae1604d5f 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
similarity index 97%
rename from PCbuild/_testexternalinspection.vcxproj
rename to PCbuild/_remotedebugging.vcxproj
index d5f347ecfec2c7..a16079f7c6c869 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
similarity index 90%
rename from PCbuild/_testexternalinspection.vcxproj.filters
rename to PCbuild/_remotedebugging.vcxproj.filters
index feb4343e5c2b8c..888e2cd478aa4e 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 1bf430e03debc8..eec213d7bac612 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 803bb149c905cb..d2bfb9472b10ee 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 9873890837fa8e..761eecba96f291 100644
--- a/Tools/build/generate_stdlib_module_names.py
+++ b/Tools/build/generate_stdlib_module_names.py
@@ -34,7 +34,7 @@
     '_testlimitedcapi',
     '_testmultiphase',
     '_testsinglephase',
-    '_testexternalinspection',
+    '_remotedebugging',
     '_xxtestfuzz',
     'idlelib.idle_test',
     'test',
diff --git a/configure b/configure
index 7dbb35f9f45f4b..3b74554d5a2e64 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 65f265045ba318..ed5c65ecbcc2be 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],

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to