]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-91048: Add utils for printing the call stack for asyncio tasks (#133284)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Sun, 4 May 2025 00:51:57 +0000 (02:51 +0200)
committerGitHub <noreply@github.com>
Sun, 4 May 2025 00:51:57 +0000 (00:51 +0000)
17 files changed:
Doc/whatsnew/3.14.rst
Lib/asyncio/__main__.py
Lib/asyncio/tools.py [new file with mode: 0644]
Lib/test/test_asyncio/test_tools.py [new file with mode: 0644]
Lib/test/test_external_inspection.py
Lib/test/test_sys.py
Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst [new file with mode: 0644]
Modules/Setup
Modules/Setup.stdlib.in
Modules/_remotedebuggingmodule.c [moved from Modules/_testexternalinspection.c with 94% similarity]
PCbuild/_remotedebugging.vcxproj [moved from PCbuild/_testexternalinspection.vcxproj with 97% similarity]
PCbuild/_remotedebugging.vcxproj.filters [moved from PCbuild/_testexternalinspection.vcxproj.filters with 90% similarity]
PCbuild/pcbuild.proj
PCbuild/pcbuild.sln
Tools/build/generate_stdlib_module_names.py
configure
configure.ac

index 87c31d32e2264d634cf0c380ed265354cc7a191e..81581b30d2194b8c8a4b9dafa70d958afa5da3e4 100644 (file)
@@ -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
index 69f5a30cfe509591f3c1878ec6ae2807c8d569a9..7d980bc401ae3b0ea1802571ecd940809f13fa6b 100644 (file)
@@ -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 (file)
index 0000000..16440b5
--- /dev/null
@@ -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 (file)
index 0000000..2caf561
--- /dev/null
@@ -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)
index aa05db972f068d73fbdd2e04c6fd77aa96ab6bdb..4e82f567e1f429548676ee1839368ec73caeb6d7 100644 (file)
@@ -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__":
index 56413d00823f4a47c262c23fb3d4e999cd5bbb39..10c3e0e9a1d2bbf9e57db23d7cfbb52c98a035f3 100644 (file)
@@ -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 (file)
index 0000000..1d45868
--- /dev/null
@@ -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.
index 65c22d48ba0bb7f639f8c5c6e556c92608fb88d7..c3e0d9eb9344a9f1a4d34926628cc83bb3208c58 100644 (file)
@@ -286,7 +286,7 @@ PYTHONPATH=$(COREPYTHONPATH)
 #_testcapi _testcapimodule.c
 #_testimportmultiple _testimportmultiple.c
 #_testmultiphase _testmultiphase.c
-#_testexternalinspection _testexternalinspection.c
+#_remotedebugging _remotedebuggingmodule.c
 #_testsinglephase _testsinglephase.c
 
 # ---
index 33e60f37d19922bb1233ab92ffcc63f611d32478..be4fb513e592e1fb48c50ae8f2b93e423c3a8750 100644 (file)
@@ -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
 @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.
similarity index 94%
rename from Modules/_testexternalinspection.c
rename to Modules/_remotedebuggingmodule.c
index b65c5821443ebfa72f0c688d715e58b74f04f5d6..0e055ae1604d5f07896544151643b4999fc93760 100644 (file)
@@ -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) {
similarity index 97%
rename from PCbuild/_testexternalinspection.vcxproj
rename to PCbuild/_remotedebugging.vcxproj
index d5f347ecfec2c7b5438a8670f3a41e5ecb97c976..a16079f7c6c869bf17f8b0f540ef18898c37002d 100644 (file)
@@ -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" />
similarity index 90%
rename from PCbuild/_testexternalinspection.vcxproj.filters
rename to PCbuild/_remotedebugging.vcxproj.filters
index feb4343e5c2b8c04437f9489ebe604d1bcb8fe39..888e2cd478aa4e5ce5dd361898bf46699c804597 100644 (file)
@@ -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">
index 1bf430e03debc8e92dcf39f53401cd729333c437..eec213d7bac6122a81cbdc1122a02dcf786c41b9 100644 (file)
@@ -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)">
index 803bb149c905cb27199c4cab09a10e415428a2a8..d2bfb9472b10ee211273b85573bef67f59b4424c 100644 (file)
@@ -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
index 9873890837fa8ed37618e148c24febe35c228a24..761eecba96f291132d148cb480c50919f32c9074 100644 (file)
@@ -34,7 +34,7 @@ IGNORE = {
     '_testlimitedcapi',
     '_testmultiphase',
     '_testsinglephase',
-    '_testexternalinspection',
+    '_remotedebugging',
     '_xxtestfuzz',
     'idlelib.idle_test',
     'test',
index 7dbb35f9f45f4bc185b0e7932b4732511182d6ca..3b74554d5a2e6442c2db95ec3e253cd293abbb52 100755 (executable)
--- 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
 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
index 65f265045ba318bfe06cb822a9ea5f8ae472c1a8..ed5c65ecbcc2be9381be8c89d105c8cc3d0010d3 100644 (file)
@@ -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],