From 2bc836523105a2197a1f987cc03911bece74b35e Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 4 May 2025 02:51:57 +0200 Subject: GH-91048: Add utils for printing the call stack for asyncio tasks (#133284) --- Doc/whatsnew/3.14.rst | 99 ++ Lib/asyncio/__main__.py | 32 + Lib/asyncio/tools.py | 212 +++ Lib/test/test_asyncio/test_tools.py | 839 +++++++++++ Lib/test/test_external_inspection.py | 45 +- Lib/test/test_sys.py | 2 +- .../2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst | 6 + Modules/Setup | 2 +- Modules/Setup.stdlib.in | 2 +- Modules/_remotedebuggingmodule.c | 1579 ++++++++++++++++++++ Modules/_testexternalinspection.c | 1551 ------------------- PCbuild/_remotedebugging.vcxproj | 114 ++ PCbuild/_remotedebugging.vcxproj.filters | 20 + PCbuild/_testexternalinspection.vcxproj | 114 -- PCbuild/_testexternalinspection.vcxproj.filters | 20 - PCbuild/pcbuild.proj | 4 +- PCbuild/pcbuild.sln | 2 +- Tools/build/generate_stdlib_module_names.py | 2 +- configure | 40 +- configure.ac | 4 +- 20 files changed, 2954 insertions(+), 1735 deletions(-) create mode 100644 Lib/asyncio/tools.py create mode 100644 Lib/test/test_asyncio/test_tools.py create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst create mode 100644 Modules/_remotedebuggingmodule.c delete mode 100644 Modules/_testexternalinspection.c create mode 100644 PCbuild/_remotedebugging.vcxproj create mode 100644 PCbuild/_remotedebugging.vcxproj.filters delete mode 100644 PCbuild/_testexternalinspection.vcxproj delete mode 100644 PCbuild/_testexternalinspection.vcxproj.filters diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 87c31d3..81581b3 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 69f5a30..7d980bc 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -1,5 +1,7 @@ +import argparse import ast import asyncio +import asyncio.tools import concurrent.futures import contextvars import inspect @@ -140,6 +142,36 @@ class REPLThread(threading.Thread): if __name__ == '__main__': + parser = argparse.ArgumentParser( + prog="python3 -m asyncio", + description="Interactive asyncio shell and CLI tools", + ) + subparsers = parser.add_subparsers(help="sub-commands", dest="command") + ps = subparsers.add_parser( + "ps", help="Display a table of all pending tasks in a process" + ) + ps.add_argument("pid", type=int, help="Process ID to inspect") + pstree = subparsers.add_parser( + "pstree", help="Display a tree of all pending tasks in a process" + ) + pstree.add_argument("pid", type=int, help="Process ID to inspect") + args = parser.parse_args() + match args.command: + case "ps": + asyncio.tools.display_awaited_by_tasks_table(args.pid) + sys.exit(0) + case "pstree": + asyncio.tools.display_awaited_by_tasks_tree(args.pid) + sys.exit(0) + case None: + pass # continue to the interactive shell + case _: + # shouldn't happen as an invalid command-line wouldn't parse + # but let's keep it for the next person adding a command + print(f"error: unhandled command {args.command}", file=sys.stderr) + parser.print_usage(file=sys.stderr) + sys.exit(1) + sys.audit("cpython.run_stdin") if os.getenv('PYTHON_BASIC_REPL'): diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py new file mode 100644 index 0000000..16440b5 --- /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 0000000..2caf561 --- /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 aa05db9..4e82f56 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -4,7 +4,8 @@ import textwrap import importlib import sys import socket -from test.support import os_helper, SHORT_TIMEOUT, busy_retry +from unittest.mock import ANY +from test.support import os_helper, SHORT_TIMEOUT, busy_retry, requires_gil_enabled from test.support.script_helper import make_script from test.support.socket_helper import find_unused_port @@ -13,13 +14,13 @@ import subprocess PROCESS_VM_READV_SUPPORTED = False try: - from _testexternalinspection import PROCESS_VM_READV_SUPPORTED - from _testexternalinspection import get_stack_trace - from _testexternalinspection import get_async_stack_trace - from _testexternalinspection import get_all_awaited_by + from _remotedebugging import PROCESS_VM_READV_SUPPORTED + from _remotedebugging import get_stack_trace + from _remotedebugging import get_async_stack_trace + from _remotedebugging import get_all_awaited_by except ImportError: raise unittest.SkipTest( - "Test only runs when _testexternalinspection is available") + "Test only runs when _remotedebuggingmodule is available") def _make_test_script(script_dir, script_basename, source): to_return = make_script(script_dir, script_basename, source) @@ -184,13 +185,13 @@ class TestGetStackTrace(unittest.TestCase): root_task = "Task-1" expected_stack_trace = [ - ["c5", "c4", "c3", "c2"], - "c2_root", + ['c5', 'c4', 'c3', 'c2'], + 'c2_root', [ - [["main"], root_task, []], - [["c1"], "sub_main_1", [[["main"], root_task, []]]], - [["c1"], "sub_main_2", [[["main"], root_task, []]]], - ], + [['_aexit', '__aexit__', 'main'], root_task, []], + [['c1'], 'sub_main_1', [[['_aexit', '__aexit__', 'main'], root_task, []]]], + [['c1'], 'sub_main_2', [[['_aexit', '__aexit__', 'main'], root_task, []]]], + ] ] self.assertEqual(stack_trace, expected_stack_trace) @@ -397,12 +398,15 @@ class TestGetStackTrace(unittest.TestCase): # sets are unordered, so we want to sort "awaited_by"s stack_trace[2].sort(key=lambda x: x[1]) - expected_stack_trace = [ - ['deep', 'c1', 'run_one_coro'], 'Task-2', [[['main'], 'Task-1', []]] + expected_stack_trace = [ + ['deep', 'c1', 'run_one_coro'], + 'Task-2', + [[['staggered_race', 'main'], 'Task-1', []]] ] self.assertEqual(stack_trace, expected_stack_trace) @skip_if_not_supported + @requires_gil_enabled("gh-133359: occasionally flaky on AMD64") @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support") def test_async_global_awaited_by(self): @@ -516,19 +520,19 @@ class TestGetStackTrace(unittest.TestCase): # expected: at least 1000 pending tasks self.assertGreaterEqual(len(entries), 1000) # the first three tasks stem from the code structure - self.assertIn(('Task-1', []), entries) - self.assertIn(('server task', [[['main'], 'Task-1', []]]), entries) - self.assertIn(('echo client spam', [[['main'], 'Task-1', []]]), entries) + self.assertIn((ANY, 'Task-1', []), entries) + self.assertIn((ANY, 'server task', [[['_aexit', '__aexit__', 'main'], ANY]]), entries) + self.assertIn((ANY, 'echo client spam', [[['_aexit', '__aexit__', 'main'], ANY]]), entries) - expected_stack = [[['echo_client_spam'], 'echo client spam', [[['main'], 'Task-1', []]]]] - tasks_with_stack = [task for task in entries if task[1] == expected_stack] + expected_stack = [[['_aexit', '__aexit__', 'echo_client_spam'], ANY]] + tasks_with_stack = [task for task in entries if task[2] == expected_stack] self.assertGreaterEqual(len(tasks_with_stack), 1000) # the final task will have some random number, but it should for # sure be one of the echo client spam horde (In windows this is not true # for some reason) if sys.platform != "win32": - self.assertEqual([[['echo_client_spam'], 'echo client spam', [[['main'], 'Task-1', []]]]], entries[-1][1]) + self.assertEqual([[['_aexit', '__aexit__', 'echo_client_spam'], ANY]], entries[-1][2]) except PermissionError: self.skipTest( "Insufficient permissions to read the stack trace") @@ -544,7 +548,6 @@ class TestGetStackTrace(unittest.TestCase): "Test only runs on Linux with process_vm_readv support") def test_self_trace(self): stack_trace = get_stack_trace(os.getpid()) - print(stack_trace) self.assertEqual(stack_trace[0], "test_self_trace") if __name__ == "__main__": diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 56413d0..10c3e0e 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 0000000..1d45868 --- /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 65c22d4..c3e0d9e 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 33e60f3..be4fb51 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/_remotedebuggingmodule.c b/Modules/_remotedebuggingmodule.c new file mode 100644 index 0000000..0e055ae --- /dev/null +++ b/Modules/_remotedebuggingmodule.c @@ -0,0 +1,1579 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +#ifndef Py_BUILD_CORE_BUILTIN +# define Py_BUILD_CORE_MODULE 1 +#endif +#include "Python.h" +#include // _Py_DebugOffsets +#include // FRAME_SUSPENDED_YIELD_FROM +#include // FRAME_OWNED_BY_CSTACK +#include // struct llist_node +#include // Py_TAG_BITS +#include "../Python/remote_debug.h" + +#ifndef HAVE_PROCESS_VM_READV +# define HAVE_PROCESS_VM_READV 0 +#endif + +struct _Py_AsyncioModuleDebugOffsets { + struct _asyncio_task_object { + uint64_t size; + uint64_t task_name; + uint64_t task_awaited_by; + uint64_t task_is_task; + uint64_t task_awaited_by_is_set; + uint64_t task_coro; + uint64_t task_node; + } asyncio_task_object; + struct _asyncio_interpreter_state { + uint64_t size; + uint64_t asyncio_tasks_head; + } asyncio_interpreter_state; + struct _asyncio_thread_state { + uint64_t size; + uint64_t asyncio_running_loop; + uint64_t asyncio_running_task; + uint64_t asyncio_tasks_head; + } asyncio_thread_state; +}; + +// Helper to chain exceptions and avoid repetitions +static void +chain_exceptions(PyObject *exception, const char *string) +{ + PyObject *exc = PyErr_GetRaisedException(); + PyErr_SetString(exception, string); + _PyErr_ChainExceptions1(exc); +} + +// Get the PyAsyncioDebug section address for any platform +static uintptr_t +_Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle) +{ + uintptr_t address; + +#ifdef MS_WINDOWS + // On Windows, search for asyncio debug in executable or DLL + address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio"); +#elif defined(__linux__) + // On Linux, search for asyncio debug in executable or DLL + address = search_linux_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython"); +#elif defined(__APPLE__) && TARGET_OS_OSX + // On macOS, try libpython first, then fall back to python + address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython"); + if (address == 0) { + PyErr_Clear(); + address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython"); + } +#else + Py_UNREACHABLE(); +#endif + + return address; +} + +static int +read_string( + proc_handle_t *handle, + _Py_DebugOffsets* debug_offsets, + uintptr_t address, + char* buffer, + Py_ssize_t size +) { + Py_ssize_t len; + int result = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address + debug_offsets->unicode_object.length, + sizeof(Py_ssize_t), + &len + ); + if (result < 0) { + return -1; + } + if (len >= size) { + PyErr_SetString(PyExc_RuntimeError, "Buffer too small"); + return -1; + } + size_t offset = debug_offsets->unicode_object.asciiobject_size; + result = _Py_RemoteDebug_ReadRemoteMemory(handle, address + offset, len, buffer); + if (result < 0) { + return -1; + } + buffer[len] = '\0'; + return 0; +} + +static inline int +read_ptr(proc_handle_t *handle, uintptr_t address, uintptr_t *ptr_addr) +{ + int result = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(void*), ptr_addr); + if (result < 0) { + return -1; + } + return 0; +} + +static inline int +read_Py_ssize_t(proc_handle_t *handle, uintptr_t address, Py_ssize_t *size) +{ + int result = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(Py_ssize_t), size); + if (result < 0) { + return -1; + } + return 0; +} + +static int +read_py_ptr(proc_handle_t *handle, uintptr_t address, uintptr_t *ptr_addr) +{ + if (read_ptr(handle, address, ptr_addr)) { + return -1; + } + *ptr_addr &= ~Py_TAG_BITS; + return 0; +} + +static int +read_char(proc_handle_t *handle, uintptr_t address, char *result) +{ + int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(char), result); + if (res < 0) { + return -1; + } + return 0; +} + +static int +read_sized_int(proc_handle_t *handle, uintptr_t address, void *result, size_t size) +{ + int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, size, result); + if (res < 0) { + return -1; + } + return 0; +} + +static int +read_unsigned_long(proc_handle_t *handle, uintptr_t address, unsigned long *result) +{ + int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(unsigned long), result); + if (res < 0) { + return -1; + } + return 0; +} + +static int +read_pyobj(proc_handle_t *handle, uintptr_t address, PyObject *ptr_addr) +{ + int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(PyObject), ptr_addr); + if (res < 0) { + return -1; + } + return 0; +} + +static PyObject * +read_py_str( + proc_handle_t *handle, + _Py_DebugOffsets* debug_offsets, + uintptr_t address, + Py_ssize_t max_len +) { + assert(max_len > 0); + + PyObject *result = NULL; + + char *buf = (char *)PyMem_RawMalloc(max_len); + if (buf == NULL) { + PyErr_NoMemory(); + return NULL; + } + if (read_string(handle, debug_offsets, address, buf, max_len)) { + goto err; + } + + result = PyUnicode_FromString(buf); + if (result == NULL) { + goto err; + } + + PyMem_RawFree(buf); + assert(result != NULL); + return result; + +err: + PyMem_RawFree(buf); + return NULL; +} + +static long +read_py_long(proc_handle_t *handle, _Py_DebugOffsets* offsets, uintptr_t address) +{ + unsigned int shift = PYLONG_BITS_IN_DIGIT; + + Py_ssize_t size; + uintptr_t lv_tag; + + int bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, address + offsets->long_object.lv_tag, + sizeof(uintptr_t), + &lv_tag); + if (bytes_read < 0) { + return -1; + } + + int negative = (lv_tag & 3) == 2; + size = lv_tag >> 3; + + if (size == 0) { + return 0; + } + + digit *digits = (digit *)PyMem_RawMalloc(size * sizeof(digit)); + if (!digits) { + PyErr_NoMemory(); + return -1; + } + + bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address + offsets->long_object.ob_digit, + sizeof(digit) * size, + digits + ); + if (bytes_read < 0) { + goto error; + } + + long long value = 0; + + // In theory this can overflow, but because of llvm/llvm-project#16778 + // we can't use __builtin_mul_overflow because it fails to link with + // __muloti4 on aarch64. In practice this is fine because all we're + // testing here are task numbers that would fit in a single byte. + for (Py_ssize_t i = 0; i < size; ++i) { + long long factor = digits[i] * (1UL << (Py_ssize_t)(shift * i)); + value += factor; + } + PyMem_RawFree(digits); + if (negative) { + value *= -1; + } + return (long)value; +error: + PyMem_RawFree(digits); + return -1; +} + +static PyObject * +parse_task_name( + proc_handle_t *handle, + _Py_DebugOffsets* offsets, + struct _Py_AsyncioModuleDebugOffsets* async_offsets, + uintptr_t task_address +) { + uintptr_t task_name_addr; + int err = read_py_ptr( + handle, + task_address + async_offsets->asyncio_task_object.task_name, + &task_name_addr); + if (err) { + return NULL; + } + + // The task name can be a long or a string so we need to check the type + + PyObject task_name_obj; + err = read_pyobj( + handle, + task_name_addr, + &task_name_obj); + if (err) { + return NULL; + } + + unsigned long flags; + err = read_unsigned_long( + handle, + (uintptr_t)task_name_obj.ob_type + offsets->type_object.tp_flags, + &flags); + if (err) { + return NULL; + } + + if ((flags & Py_TPFLAGS_LONG_SUBCLASS)) { + long res = read_py_long(handle, offsets, task_name_addr); + if (res == -1) { + chain_exceptions(PyExc_RuntimeError, "Failed to get task name"); + return NULL; + } + return PyUnicode_FromFormat("Task-%d", res); + } + + if(!(flags & Py_TPFLAGS_UNICODE_SUBCLASS)) { + PyErr_SetString(PyExc_RuntimeError, "Invalid task name object"); + return NULL; + } + + return read_py_str( + handle, + offsets, + task_name_addr, + 255 + ); +} + +static int +parse_coro_chain( + proc_handle_t *handle, + struct _Py_DebugOffsets* offsets, + struct _Py_AsyncioModuleDebugOffsets* async_offsets, + uintptr_t coro_address, + PyObject *render_to +) { + assert((void*)coro_address != NULL); + + uintptr_t gen_type_addr; + int err = read_ptr( + handle, + coro_address + offsets->pyobject.ob_type, + &gen_type_addr); + if (err) { + return -1; + } + + uintptr_t gen_name_addr; + err = read_py_ptr( + handle, + coro_address + offsets->gen_object.gi_name, + &gen_name_addr); + if (err) { + return -1; + } + + PyObject *name = read_py_str( + handle, + offsets, + gen_name_addr, + 255 + ); + if (name == NULL) { + return -1; + } + + if (PyList_Append(render_to, name)) { + Py_DECREF(name); + return -1; + } + Py_DECREF(name); + + int8_t gi_frame_state; + err = read_sized_int( + handle, + coro_address + offsets->gen_object.gi_frame_state, + &gi_frame_state, + sizeof(int8_t) + ); + if (err) { + return -1; + } + + if (gi_frame_state == FRAME_SUSPENDED_YIELD_FROM) { + char owner; + err = read_char( + handle, + coro_address + offsets->gen_object.gi_iframe + + offsets->interpreter_frame.owner, + &owner + ); + if (err) { + return -1; + } + if (owner != FRAME_OWNED_BY_GENERATOR) { + PyErr_SetString( + PyExc_RuntimeError, + "generator doesn't own its frame \\_o_/"); + return -1; + } + + uintptr_t stackpointer_addr; + err = read_py_ptr( + handle, + coro_address + offsets->gen_object.gi_iframe + + offsets->interpreter_frame.stackpointer, + &stackpointer_addr); + if (err) { + return -1; + } + + if ((void*)stackpointer_addr != NULL) { + uintptr_t gi_await_addr; + err = read_py_ptr( + handle, + stackpointer_addr - sizeof(void*), + &gi_await_addr); + if (err) { + return -1; + } + + if ((void*)gi_await_addr != NULL) { + uintptr_t gi_await_addr_type_addr; + int err = read_ptr( + handle, + gi_await_addr + offsets->pyobject.ob_type, + &gi_await_addr_type_addr); + if (err) { + return -1; + } + + if (gen_type_addr == gi_await_addr_type_addr) { + /* This needs an explanation. We always start with parsing + native coroutine / generator frames. Ultimately they + are awaiting on something. That something can be + a native coroutine frame or... an iterator. + If it's the latter -- we can't continue building + our chain. So the condition to bail out of this is + to do that when the type of the current coroutine + doesn't match the type of whatever it points to + in its cr_await. + */ + err = parse_coro_chain( + handle, + offsets, + async_offsets, + gi_await_addr, + render_to + ); + if (err) { + return -1; + } + } + } + } + + } + + return 0; +} + + +static int +parse_task_awaited_by( + proc_handle_t *handle, + struct _Py_DebugOffsets* offsets, + struct _Py_AsyncioModuleDebugOffsets* async_offsets, + uintptr_t task_address, + PyObject *awaited_by, + int recurse_task +); + + +static int +parse_task( + proc_handle_t *handle, + struct _Py_DebugOffsets* offsets, + struct _Py_AsyncioModuleDebugOffsets* async_offsets, + uintptr_t task_address, + PyObject *render_to, + int recurse_task +) { + char is_task; + int err = read_char( + handle, + task_address + async_offsets->asyncio_task_object.task_is_task, + &is_task); + if (err) { + return -1; + } + + PyObject* result = PyList_New(0); + if (result == NULL) { + return -1; + } + + PyObject *call_stack = PyList_New(0); + if (call_stack == NULL) { + goto err; + } + if (PyList_Append(result, call_stack)) { + Py_DECREF(call_stack); + goto err; + } + /* we can operate on a borrowed one to simplify cleanup */ + Py_DECREF(call_stack); + + if (is_task) { + 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; + } + if (PyList_Append(result, tn)) { + Py_DECREF(tn); + goto err; + } + Py_DECREF(tn); + + uintptr_t coro_addr; + err = read_py_ptr( + handle, + task_address + async_offsets->asyncio_task_object.task_coro, + &coro_addr); + if (err) { + goto err; + } + + if ((void*)coro_addr != NULL) { + err = parse_coro_chain( + handle, + offsets, + async_offsets, + coro_addr, + call_stack + ); + if (err) { + goto err; + } + + if (PyList_Reverse(call_stack)) { + goto err; + } + } + } + + if (PyList_Append(render_to, result)) { + goto err; + } + + 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); + + if (parse_task_awaited_by(handle, offsets, async_offsets, + task_address, awaited_by, 1) + ) { + goto err; + } + } + Py_DECREF(result); + + return 0; + +err: + Py_DECREF(result); + return -1; +} + +static int +parse_tasks_in_set( + proc_handle_t *handle, + struct _Py_DebugOffsets* offsets, + struct _Py_AsyncioModuleDebugOffsets* async_offsets, + uintptr_t set_addr, + PyObject *awaited_by, + int recurse_task +) { + uintptr_t set_obj; + if (read_py_ptr( + handle, + set_addr, + &set_obj) + ) { + return -1; + } + + Py_ssize_t num_els; + if (read_Py_ssize_t( + handle, + set_obj + offsets->set_object.used, + &num_els) + ) { + return -1; + } + + Py_ssize_t set_len; + if (read_Py_ssize_t( + handle, + set_obj + offsets->set_object.mask, + &set_len) + ) { + return -1; + } + set_len++; // The set contains the `mask+1` element slots. + + uintptr_t table_ptr; + if (read_ptr( + handle, + set_obj + offsets->set_object.table, + &table_ptr) + ) { + return -1; + } + + Py_ssize_t i = 0; + Py_ssize_t els = 0; + while (i < set_len) { + uintptr_t key_addr; + if (read_py_ptr(handle, table_ptr, &key_addr)) { + return -1; + } + + if ((void*)key_addr != NULL) { + Py_ssize_t ref_cnt; + if (read_Py_ssize_t(handle, table_ptr, &ref_cnt)) { + return -1; + } + + if (ref_cnt) { + // if 'ref_cnt=0' it's a set dummy marker + + if (parse_task( + handle, + offsets, + async_offsets, + key_addr, + awaited_by, + recurse_task + ) + ) { + return -1; + } + + if (++els == num_els) { + break; + } + } + } + + table_ptr += sizeof(void*) * 2; + i++; + } + return 0; +} + + +static int +parse_task_awaited_by( + proc_handle_t *handle, + struct _Py_DebugOffsets* offsets, + struct _Py_AsyncioModuleDebugOffsets* async_offsets, + uintptr_t task_address, + PyObject *awaited_by, + int recurse_task +) { + uintptr_t task_ab_addr; + int err = read_py_ptr( + handle, + task_address + async_offsets->asyncio_task_object.task_awaited_by, + &task_ab_addr); + if (err) { + return -1; + } + + if ((void*)task_ab_addr == NULL) { + return 0; + } + + char awaited_by_is_a_set; + err = read_char( + handle, + task_address + async_offsets->asyncio_task_object.task_awaited_by_is_set, + &awaited_by_is_a_set); + if (err) { + return -1; + } + + if (awaited_by_is_a_set) { + if (parse_tasks_in_set( + handle, + offsets, + async_offsets, + task_address + async_offsets->asyncio_task_object.task_awaited_by, + awaited_by, + recurse_task + ) + ) { + return -1; + } + } else { + uintptr_t sub_task; + if (read_py_ptr( + handle, + task_address + async_offsets->asyncio_task_object.task_awaited_by, + &sub_task) + ) { + return -1; + } + + if (parse_task( + handle, + offsets, + async_offsets, + sub_task, + awaited_by, + recurse_task + ) + ) { + return -1; + } + } + + return 0; +} + +static int +parse_code_object( + proc_handle_t *handle, + PyObject* result, + struct _Py_DebugOffsets* offsets, + uintptr_t address, + uintptr_t* previous_frame +) { + uintptr_t address_of_function_name; + int bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address + offsets->code_object.name, + sizeof(void*), + &address_of_function_name + ); + if (bytes_read < 0) { + return -1; + } + + if ((void*)address_of_function_name == NULL) { + PyErr_SetString(PyExc_RuntimeError, "No function name found"); + return -1; + } + + PyObject* py_function_name = read_py_str( + handle, offsets, address_of_function_name, 256); + if (py_function_name == NULL) { + return -1; + } + + if (PyList_Append(result, py_function_name) == -1) { + Py_DECREF(py_function_name); + return -1; + } + Py_DECREF(py_function_name); + + return 0; +} + +static int +parse_frame_object( + proc_handle_t *handle, + PyObject* result, + struct _Py_DebugOffsets* offsets, + uintptr_t address, + uintptr_t* previous_frame +) { + int err; + + Py_ssize_t bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address + offsets->interpreter_frame.previous, + sizeof(void*), + previous_frame + ); + if (bytes_read < 0) { + return -1; + } + + char owner; + if (read_char(handle, address + offsets->interpreter_frame.owner, &owner)) { + return -1; + } + + if (owner >= FRAME_OWNED_BY_INTERPRETER) { + return 0; + } + + uintptr_t address_of_code_object; + err = read_py_ptr( + handle, + address + offsets->interpreter_frame.executable, + &address_of_code_object + ); + if (err) { + return -1; + } + + if ((void*)address_of_code_object == NULL) { + return 0; + } + + return parse_code_object( + handle, result, offsets, address_of_code_object, previous_frame); +} + +static int +parse_async_frame_object( + proc_handle_t *handle, + PyObject* result, + struct _Py_DebugOffsets* offsets, + uintptr_t address, + uintptr_t* previous_frame, + uintptr_t* code_object +) { + int err; + + Py_ssize_t bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address + offsets->interpreter_frame.previous, + sizeof(void*), + previous_frame + ); + if (bytes_read < 0) { + return -1; + } + + char owner; + bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, address + offsets->interpreter_frame.owner, sizeof(char), &owner); + if (bytes_read < 0) { + return -1; + } + + if (owner == FRAME_OWNED_BY_CSTACK || owner == FRAME_OWNED_BY_INTERPRETER) { + return 0; // C frame + } + + if (owner != FRAME_OWNED_BY_GENERATOR + && owner != FRAME_OWNED_BY_THREAD) { + PyErr_Format(PyExc_RuntimeError, "Unhandled frame owner %d.\n", owner); + return -1; + } + + err = read_py_ptr( + handle, + address + offsets->interpreter_frame.executable, + code_object + ); + if (err) { + return -1; + } + + assert(code_object != NULL); + if ((void*)*code_object == NULL) { + return 0; + } + + if (parse_code_object( + handle, result, offsets, *code_object, previous_frame)) { + return -1; + } + + return 1; +} + +static int +read_async_debug( + proc_handle_t *handle, + struct _Py_AsyncioModuleDebugOffsets* async_debug +) { + uintptr_t async_debug_addr = _Py_RemoteDebug_GetAsyncioDebugAddress(handle); + if (!async_debug_addr) { + return -1; + } + + size_t size = sizeof(struct _Py_AsyncioModuleDebugOffsets); + int result = _Py_RemoteDebug_ReadRemoteMemory(handle, async_debug_addr, size, async_debug); + return result; +} + +static int +find_running_frame( + proc_handle_t *handle, + uintptr_t runtime_start_address, + _Py_DebugOffsets* local_debug_offsets, + uintptr_t *frame +) { + uint64_t interpreter_state_list_head = + local_debug_offsets->runtime_state.interpreters_head; + + uintptr_t address_of_interpreter_state; + int bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, + runtime_start_address + interpreter_state_list_head, + sizeof(void*), + &address_of_interpreter_state); + if (bytes_read < 0) { + return -1; + } + + if (address_of_interpreter_state == 0) { + PyErr_SetString(PyExc_RuntimeError, "No interpreter state found"); + return -1; + } + + uintptr_t address_of_thread; + bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address_of_interpreter_state + + local_debug_offsets->interpreter_state.threads_main, + sizeof(void*), + &address_of_thread); + if (bytes_read < 0) { + return -1; + } + + // No Python frames are available for us (can happen at tear-down). + if ((void*)address_of_thread != NULL) { + int err = read_ptr( + handle, + address_of_thread + local_debug_offsets->thread_state.current_frame, + frame); + if (err) { + return -1; + } + return 0; + } + + *frame = (uintptr_t)NULL; + return 0; +} + +static int +find_running_task( + proc_handle_t *handle, + uintptr_t runtime_start_address, + _Py_DebugOffsets *local_debug_offsets, + struct _Py_AsyncioModuleDebugOffsets *async_offsets, + uintptr_t *running_task_addr +) { + *running_task_addr = (uintptr_t)NULL; + + uint64_t interpreter_state_list_head = + local_debug_offsets->runtime_state.interpreters_head; + + uintptr_t address_of_interpreter_state; + int bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, + runtime_start_address + interpreter_state_list_head, + sizeof(void*), + &address_of_interpreter_state); + if (bytes_read < 0) { + return -1; + } + + if (address_of_interpreter_state == 0) { + PyErr_SetString(PyExc_RuntimeError, "No interpreter state found"); + return -1; + } + + uintptr_t address_of_thread; + bytes_read = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address_of_interpreter_state + + local_debug_offsets->interpreter_state.threads_head, + sizeof(void*), + &address_of_thread); + if (bytes_read < 0) { + return -1; + } + + uintptr_t address_of_running_loop; + // No Python frames are available for us (can happen at tear-down). + if ((void*)address_of_thread == NULL) { + return 0; + } + + bytes_read = read_py_ptr( + handle, + address_of_thread + + async_offsets->asyncio_thread_state.asyncio_running_loop, + &address_of_running_loop); + if (bytes_read == -1) { + return -1; + } + + // no asyncio loop is now running + if ((void*)address_of_running_loop == NULL) { + return 0; + } + + int err = read_ptr( + handle, + address_of_thread + + async_offsets->asyncio_thread_state.asyncio_running_task, + running_task_addr); + if (err) { + return -1; + } + + return 0; +} + +static int +append_awaited_by_for_thread( + proc_handle_t *handle, + uintptr_t head_addr, + struct _Py_DebugOffsets *debug_offsets, + struct _Py_AsyncioModuleDebugOffsets *async_offsets, + PyObject *result +) { + struct llist_node task_node; + + if (0 > _Py_RemoteDebug_ReadRemoteMemory( + handle, + head_addr, + sizeof(task_node), + &task_node)) + { + return -1; + } + + size_t iteration_count = 0; + const size_t MAX_ITERATIONS = 2 << 15; // A reasonable upper bound + while ((uintptr_t)task_node.next != head_addr) { + if (++iteration_count > MAX_ITERATIONS) { + PyErr_SetString(PyExc_RuntimeError, "Task list appears corrupted"); + return -1; + } + + if (task_node.next == NULL) { + PyErr_SetString( + PyExc_RuntimeError, + "Invalid linked list structure reading remote memory"); + return -1; + } + + uintptr_t task_addr = (uintptr_t)task_node.next + - async_offsets->asyncio_task_object.task_node; + + PyObject *tn = parse_task_name( + handle, + debug_offsets, + async_offsets, + task_addr); + if (tn == NULL) { + return -1; + } + + PyObject *current_awaited_by = PyList_New(0); + if (current_awaited_by == NULL) { + Py_DECREF(tn); + return -1; + } + + 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, 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; + } + Py_DECREF(result_item); + + if (parse_task_awaited_by(handle, debug_offsets, async_offsets, + task_addr, current_awaited_by, 0)) + { + return -1; + } + + // onto the next one... + if (0 > _Py_RemoteDebug_ReadRemoteMemory( + handle, + (uintptr_t)task_node.next, + sizeof(task_node), + &task_node)) + { + return -1; + } + } + + return 0; +} + +static int +append_awaited_by( + proc_handle_t *handle, + unsigned long tid, + uintptr_t head_addr, + struct _Py_DebugOffsets *debug_offsets, + struct _Py_AsyncioModuleDebugOffsets *async_offsets, + PyObject *result) +{ + PyObject *tid_py = PyLong_FromUnsignedLong(tid); + if (tid_py == NULL) { + return -1; + } + + PyObject *result_item = PyTuple_New(2); + if (result_item == NULL) { + Py_DECREF(tid_py); + return -1; + } + + PyObject* awaited_by_for_thread = PyList_New(0); + if (awaited_by_for_thread == NULL) { + Py_DECREF(tid_py); + Py_DECREF(result_item); + return -1; + } + + PyTuple_SET_ITEM(result_item, 0, tid_py); // steals ref + PyTuple_SET_ITEM(result_item, 1, awaited_by_for_thread); // steals ref + if (PyList_Append(result, result_item)) { + Py_DECREF(result_item); + return -1; + } + Py_DECREF(result_item); + + if (append_awaited_by_for_thread( + handle, + head_addr, + debug_offsets, + async_offsets, + awaited_by_for_thread)) + { + return -1; + } + + return 0; +} + +static PyObject* +get_all_awaited_by(PyObject* self, PyObject* args) +{ +#if (!defined(__linux__) && !defined(__APPLE__)) && !defined(MS_WINDOWS) || \ + (defined(__linux__) && !HAVE_PROCESS_VM_READV) + PyErr_SetString( + PyExc_RuntimeError, + "get_all_awaited_by is not implemented on this platform"); + return NULL; +#endif + + int pid; + if (!PyArg_ParseTuple(args, "i", &pid)) { + return NULL; + } + + proc_handle_t the_handle; + proc_handle_t *handle = &the_handle; + if (_Py_RemoteDebug_InitProcHandle(handle, pid) < 0) { + return 0; + } + + PyObject *result = NULL; + + uintptr_t runtime_start_addr = _Py_RemoteDebug_GetPyRuntimeAddress(handle); + if (runtime_start_addr == 0) { + if (!PyErr_Occurred()) { + PyErr_SetString( + PyExc_RuntimeError, "Failed to get .PyRuntime address"); + } + goto result_err; + } + struct _Py_DebugOffsets local_debug_offsets; + + if (_Py_RemoteDebug_ReadDebugOffsets(handle, &runtime_start_addr, &local_debug_offsets)) { + chain_exceptions(PyExc_RuntimeError, "Failed to read debug offsets"); + goto result_err; + } + + struct _Py_AsyncioModuleDebugOffsets local_async_debug; + if (read_async_debug(handle, &local_async_debug)) { + chain_exceptions(PyExc_RuntimeError, "Failed to read asyncio debug offsets"); + goto result_err; + } + + result = PyList_New(0); + if (result == NULL) { + goto result_err; + } + + uint64_t interpreter_state_list_head = + local_debug_offsets.runtime_state.interpreters_head; + + uintptr_t interpreter_state_addr; + if (0 > _Py_RemoteDebug_ReadRemoteMemory( + handle, + runtime_start_addr + interpreter_state_list_head, + sizeof(void*), + &interpreter_state_addr)) + { + goto result_err; + } + + uintptr_t thread_state_addr; + unsigned long tid = 0; + if (0 > _Py_RemoteDebug_ReadRemoteMemory( + handle, + interpreter_state_addr + + local_debug_offsets.interpreter_state.threads_head, + sizeof(void*), + &thread_state_addr)) + { + goto result_err; + } + + uintptr_t head_addr; + while (thread_state_addr != 0) { + if (0 > _Py_RemoteDebug_ReadRemoteMemory( + handle, + thread_state_addr + + local_debug_offsets.thread_state.native_thread_id, + sizeof(tid), + &tid)) + { + goto result_err; + } + + head_addr = thread_state_addr + + local_async_debug.asyncio_thread_state.asyncio_tasks_head; + + if (append_awaited_by(handle, tid, head_addr, &local_debug_offsets, + &local_async_debug, result)) + { + goto result_err; + } + + if (0 > _Py_RemoteDebug_ReadRemoteMemory( + handle, + thread_state_addr + local_debug_offsets.thread_state.next, + sizeof(void*), + &thread_state_addr)) + { + goto result_err; + } + } + + head_addr = interpreter_state_addr + + local_async_debug.asyncio_interpreter_state.asyncio_tasks_head; + + // On top of a per-thread task lists used by default by asyncio to avoid + // contention, there is also a fallback per-interpreter list of tasks; + // any tasks still pending when a thread is destroyed will be moved to the + // per-interpreter task list. It's unlikely we'll find anything here, but + // interesting for debugging. + if (append_awaited_by(handle, 0, head_addr, &local_debug_offsets, + &local_async_debug, result)) + { + goto result_err; + } + + _Py_RemoteDebug_CleanupProcHandle(handle); + return result; + +result_err: + Py_XDECREF(result); + _Py_RemoteDebug_CleanupProcHandle(handle); + return NULL; +} + +static PyObject* +get_stack_trace(PyObject* self, PyObject* args) +{ +#if (!defined(__linux__) && !defined(__APPLE__)) && !defined(MS_WINDOWS) || \ + (defined(__linux__) && !HAVE_PROCESS_VM_READV) + PyErr_SetString( + PyExc_RuntimeError, + "get_stack_trace is not supported on this platform"); + return NULL; +#endif + + int pid; + if (!PyArg_ParseTuple(args, "i", &pid)) { + return NULL; + } + + proc_handle_t the_handle; + proc_handle_t *handle = &the_handle; + if (_Py_RemoteDebug_InitProcHandle(handle, pid) < 0) { + return 0; + } + + PyObject* result = NULL; + + uintptr_t runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(handle); + if (runtime_start_address == 0) { + if (!PyErr_Occurred()) { + PyErr_SetString( + PyExc_RuntimeError, "Failed to get .PyRuntime address"); + } + goto result_err; + } + struct _Py_DebugOffsets local_debug_offsets; + + if (_Py_RemoteDebug_ReadDebugOffsets(handle, &runtime_start_address, &local_debug_offsets)) { + chain_exceptions(PyExc_RuntimeError, "Failed to read debug offsets"); + goto result_err; + } + + uintptr_t address_of_current_frame; + if (find_running_frame( + handle, runtime_start_address, &local_debug_offsets, + &address_of_current_frame) + ) { + goto result_err; + } + + result = PyList_New(0); + if (result == NULL) { + goto result_err; + } + + while ((void*)address_of_current_frame != NULL) { + if (parse_frame_object( + handle, + result, + &local_debug_offsets, + address_of_current_frame, + &address_of_current_frame) + < 0) + { + Py_DECREF(result); + goto result_err; + } + } + +result_err: + _Py_RemoteDebug_CleanupProcHandle(handle); + return result; +} + +static PyObject* +get_async_stack_trace(PyObject* self, PyObject* args) +{ +#if (!defined(__linux__) && !defined(__APPLE__)) && !defined(MS_WINDOWS) || \ + (defined(__linux__) && !HAVE_PROCESS_VM_READV) + PyErr_SetString( + PyExc_RuntimeError, + "get_stack_trace is not supported on this platform"); + return NULL; +#endif + int pid; + + if (!PyArg_ParseTuple(args, "i", &pid)) { + return NULL; + } + + proc_handle_t the_handle; + proc_handle_t *handle = &the_handle; + if (_Py_RemoteDebug_InitProcHandle(handle, pid) < 0) { + return 0; + } + + PyObject *result = NULL; + + uintptr_t runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(handle); + if (runtime_start_address == 0) { + if (!PyErr_Occurred()) { + PyErr_SetString( + PyExc_RuntimeError, "Failed to get .PyRuntime address"); + } + goto result_err; + } + struct _Py_DebugOffsets local_debug_offsets; + + if (_Py_RemoteDebug_ReadDebugOffsets(handle, &runtime_start_address, &local_debug_offsets)) { + chain_exceptions(PyExc_RuntimeError, "Failed to read debug offsets"); + goto result_err; + } + + struct _Py_AsyncioModuleDebugOffsets local_async_debug; + if (read_async_debug(handle, &local_async_debug)) { + chain_exceptions(PyExc_RuntimeError, "Failed to read asyncio debug offsets"); + goto result_err; + } + + result = PyList_New(1); + if (result == NULL) { + goto result_err; + } + PyObject* calls = PyList_New(0); + if (calls == NULL) { + goto result_err; + } + if (PyList_SetItem(result, 0, calls)) { /* steals ref to 'calls' */ + Py_DECREF(calls); + goto result_err; + } + + uintptr_t running_task_addr = (uintptr_t)NULL; + if (find_running_task( + handle, runtime_start_address, &local_debug_offsets, &local_async_debug, + &running_task_addr) + ) { + chain_exceptions(PyExc_RuntimeError, "Failed to find running task"); + goto result_err; + } + + if ((void*)running_task_addr == NULL) { + PyErr_SetString(PyExc_RuntimeError, "No running task found"); + goto result_err; + } + + uintptr_t running_coro_addr; + if (read_py_ptr( + handle, + running_task_addr + local_async_debug.asyncio_task_object.task_coro, + &running_coro_addr + )) { + chain_exceptions(PyExc_RuntimeError, "Failed to read running task coro"); + goto result_err; + } + + if ((void*)running_coro_addr == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Running task coro is NULL"); + goto result_err; + } + + // note: genobject's gi_iframe is an embedded struct so the address to + // the offset leads directly to its first field: f_executable + uintptr_t address_of_running_task_code_obj; + if (read_py_ptr( + handle, + running_coro_addr + local_debug_offsets.gen_object.gi_iframe, + &address_of_running_task_code_obj + )) { + goto result_err; + } + + if ((void*)address_of_running_task_code_obj == NULL) { + PyErr_SetString(PyExc_RuntimeError, "Running task code object is NULL"); + goto result_err; + } + + uintptr_t address_of_current_frame; + if (find_running_frame( + handle, runtime_start_address, &local_debug_offsets, + &address_of_current_frame) + ) { + chain_exceptions(PyExc_RuntimeError, "Failed to find running frame"); + goto result_err; + } + + uintptr_t address_of_code_object; + while ((void*)address_of_current_frame != NULL) { + int res = parse_async_frame_object( + handle, + calls, + &local_debug_offsets, + address_of_current_frame, + &address_of_current_frame, + &address_of_code_object + ); + + if (res < 0) { + chain_exceptions(PyExc_RuntimeError, "Failed to parse async frame object"); + goto result_err; + } + + if (address_of_code_object == address_of_running_task_code_obj) { + break; + } + } + + PyObject *tn = parse_task_name( + handle, &local_debug_offsets, &local_async_debug, running_task_addr); + if (tn == NULL) { + goto result_err; + } + if (PyList_Append(result, tn)) { + Py_DECREF(tn); + goto result_err; + } + Py_DECREF(tn); + + PyObject* awaited_by = PyList_New(0); + if (awaited_by == NULL) { + goto result_err; + } + if (PyList_Append(result, awaited_by)) { + Py_DECREF(awaited_by); + goto result_err; + } + Py_DECREF(awaited_by); + + if (parse_task_awaited_by( + handle, &local_debug_offsets, &local_async_debug, + running_task_addr, awaited_by, 1) + ) { + goto result_err; + } + + _Py_RemoteDebug_CleanupProcHandle(handle); + return result; + +result_err: + _Py_RemoteDebug_CleanupProcHandle(handle); + Py_XDECREF(result); + return NULL; +} + + +static PyMethodDef methods[] = { + {"get_stack_trace", get_stack_trace, METH_VARARGS, + "Get the Python stack from a given pod"}, + {"get_async_stack_trace", get_async_stack_trace, METH_VARARGS, + "Get the asyncio stack from a given pid"}, + {"get_all_awaited_by", get_all_awaited_by, METH_VARARGS, + "Get all tasks and their awaited_by from a given pid"}, + {NULL, NULL, 0, NULL}, +}; + +static struct PyModuleDef module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "_remotedebugging", + .m_size = -1, + .m_methods = methods, +}; + +PyMODINIT_FUNC +PyInit__remotedebugging(void) +{ + PyObject* mod = PyModule_Create(&module); + if (mod == NULL) { + return NULL; + } +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); +#endif + int rc = PyModule_AddIntConstant( + mod, "PROCESS_VM_READV_SUPPORTED", HAVE_PROCESS_VM_READV); + if (rc < 0) { + Py_DECREF(mod); + return NULL; + } + return mod; +} diff --git a/Modules/_testexternalinspection.c b/Modules/_testexternalinspection.c deleted file mode 100644 index b65c582..0000000 --- a/Modules/_testexternalinspection.c +++ /dev/null @@ -1,1551 +0,0 @@ -#define _GNU_SOURCE - -#include -#include -#include -#include -#include -#include -#include - -#ifndef Py_BUILD_CORE_BUILTIN -# define Py_BUILD_CORE_MODULE 1 -#endif -#include "Python.h" -#include // _Py_DebugOffsets -#include // FRAME_SUSPENDED_YIELD_FROM -#include // FRAME_OWNED_BY_CSTACK -#include // struct llist_node -#include // Py_TAG_BITS -#include "../Python/remote_debug.h" - -#ifndef HAVE_PROCESS_VM_READV -# define HAVE_PROCESS_VM_READV 0 -#endif - -struct _Py_AsyncioModuleDebugOffsets { - struct _asyncio_task_object { - uint64_t size; - uint64_t task_name; - uint64_t task_awaited_by; - uint64_t task_is_task; - uint64_t task_awaited_by_is_set; - uint64_t task_coro; - uint64_t task_node; - } asyncio_task_object; - struct _asyncio_interpreter_state { - uint64_t size; - uint64_t asyncio_tasks_head; - } asyncio_interpreter_state; - struct _asyncio_thread_state { - uint64_t size; - uint64_t asyncio_running_loop; - uint64_t asyncio_running_task; - uint64_t asyncio_tasks_head; - } asyncio_thread_state; -}; - -// Helper to chain exceptions and avoid repetitions -static void -chain_exceptions(PyObject *exception, const char *string) -{ - PyObject *exc = PyErr_GetRaisedException(); - PyErr_SetString(exception, string); - _PyErr_ChainExceptions1(exc); -} - -// Get the PyAsyncioDebug section address for any platform -static uintptr_t -_Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle) -{ - uintptr_t address; - -#ifdef MS_WINDOWS - // On Windows, search for asyncio debug in executable or DLL - address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio"); -#elif defined(__linux__) - // On Linux, search for asyncio debug in executable or DLL - address = search_linux_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython"); -#elif defined(__APPLE__) && TARGET_OS_OSX - // On macOS, try libpython first, then fall back to python - address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython"); - if (address == 0) { - PyErr_Clear(); - address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython"); - } -#else - Py_UNREACHABLE(); -#endif - - return address; -} - -static int -read_string( - proc_handle_t *handle, - _Py_DebugOffsets* debug_offsets, - uintptr_t address, - char* buffer, - Py_ssize_t size -) { - Py_ssize_t len; - int result = _Py_RemoteDebug_ReadRemoteMemory( - handle, - address + debug_offsets->unicode_object.length, - sizeof(Py_ssize_t), - &len - ); - if (result < 0) { - return -1; - } - if (len >= size) { - PyErr_SetString(PyExc_RuntimeError, "Buffer too small"); - return -1; - } - size_t offset = debug_offsets->unicode_object.asciiobject_size; - result = _Py_RemoteDebug_ReadRemoteMemory(handle, address + offset, len, buffer); - if (result < 0) { - return -1; - } - buffer[len] = '\0'; - return 0; -} - -static inline int -read_ptr(proc_handle_t *handle, uintptr_t address, uintptr_t *ptr_addr) -{ - int result = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(void*), ptr_addr); - if (result < 0) { - return -1; - } - return 0; -} - -static inline int -read_Py_ssize_t(proc_handle_t *handle, uintptr_t address, Py_ssize_t *size) -{ - int result = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(Py_ssize_t), size); - if (result < 0) { - return -1; - } - return 0; -} - -static int -read_py_ptr(proc_handle_t *handle, uintptr_t address, uintptr_t *ptr_addr) -{ - if (read_ptr(handle, address, ptr_addr)) { - return -1; - } - *ptr_addr &= ~Py_TAG_BITS; - return 0; -} - -static int -read_char(proc_handle_t *handle, uintptr_t address, char *result) -{ - int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(char), result); - if (res < 0) { - return -1; - } - return 0; -} - -static int -read_int(proc_handle_t *handle, uintptr_t address, int *result) -{ - int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(int), result); - if (res < 0) { - return -1; - } - return 0; -} - -static int -read_unsigned_long(proc_handle_t *handle, uintptr_t address, unsigned long *result) -{ - int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(unsigned long), result); - if (res < 0) { - return -1; - } - return 0; -} - -static int -read_pyobj(proc_handle_t *handle, uintptr_t address, PyObject *ptr_addr) -{ - int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(PyObject), ptr_addr); - if (res < 0) { - return -1; - } - return 0; -} - -static PyObject * -read_py_str( - proc_handle_t *handle, - _Py_DebugOffsets* debug_offsets, - uintptr_t address, - Py_ssize_t max_len -) { - assert(max_len > 0); - - PyObject *result = NULL; - - char *buf = (char *)PyMem_RawMalloc(max_len); - if (buf == NULL) { - PyErr_NoMemory(); - return NULL; - } - if (read_string(handle, debug_offsets, address, buf, max_len)) { - goto err; - } - - result = PyUnicode_FromString(buf); - if (result == NULL) { - goto err; - } - - PyMem_RawFree(buf); - assert(result != NULL); - return result; - -err: - PyMem_RawFree(buf); - return NULL; -} - -static long -read_py_long(proc_handle_t *handle, _Py_DebugOffsets* offsets, uintptr_t address) -{ - unsigned int shift = PYLONG_BITS_IN_DIGIT; - - Py_ssize_t size; - uintptr_t lv_tag; - - int bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, address + offsets->long_object.lv_tag, - sizeof(uintptr_t), - &lv_tag); - if (bytes_read < 0) { - return -1; - } - - int negative = (lv_tag & 3) == 2; - size = lv_tag >> 3; - - if (size == 0) { - return 0; - } - - digit *digits = (digit *)PyMem_RawMalloc(size * sizeof(digit)); - if (!digits) { - PyErr_NoMemory(); - return -1; - } - - bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, - address + offsets->long_object.ob_digit, - sizeof(digit) * size, - digits - ); - if (bytes_read < 0) { - goto error; - } - - long long value = 0; - - // In theory this can overflow, but because of llvm/llvm-project#16778 - // we can't use __builtin_mul_overflow because it fails to link with - // __muloti4 on aarch64. In practice this is fine because all we're - // testing here are task numbers that would fit in a single byte. - for (Py_ssize_t i = 0; i < size; ++i) { - long long factor = digits[i] * (1UL << (Py_ssize_t)(shift * i)); - value += factor; - } - PyMem_RawFree(digits); - if (negative) { - value *= -1; - } - return (long)value; -error: - PyMem_RawFree(digits); - return -1; -} - -static PyObject * -parse_task_name( - proc_handle_t *handle, - _Py_DebugOffsets* offsets, - struct _Py_AsyncioModuleDebugOffsets* async_offsets, - uintptr_t task_address -) { - uintptr_t task_name_addr; - int err = read_py_ptr( - handle, - task_address + async_offsets->asyncio_task_object.task_name, - &task_name_addr); - if (err) { - return NULL; - } - - // The task name can be a long or a string so we need to check the type - - PyObject task_name_obj; - err = read_pyobj( - handle, - task_name_addr, - &task_name_obj); - if (err) { - return NULL; - } - - unsigned long flags; - err = read_unsigned_long( - handle, - (uintptr_t)task_name_obj.ob_type + offsets->type_object.tp_flags, - &flags); - if (err) { - return NULL; - } - - if ((flags & Py_TPFLAGS_LONG_SUBCLASS)) { - long res = read_py_long(handle, offsets, task_name_addr); - if (res == -1) { - chain_exceptions(PyExc_RuntimeError, "Failed to get task name"); - return NULL; - } - return PyUnicode_FromFormat("Task-%d", res); - } - - if(!(flags & Py_TPFLAGS_UNICODE_SUBCLASS)) { - PyErr_SetString(PyExc_RuntimeError, "Invalid task name object"); - return NULL; - } - - return read_py_str( - handle, - offsets, - task_name_addr, - 255 - ); -} - -static int -parse_coro_chain( - proc_handle_t *handle, - struct _Py_DebugOffsets* offsets, - struct _Py_AsyncioModuleDebugOffsets* async_offsets, - uintptr_t coro_address, - PyObject *render_to -) { - assert((void*)coro_address != NULL); - - uintptr_t gen_type_addr; - int err = read_ptr( - handle, - coro_address + sizeof(void*), - &gen_type_addr); - if (err) { - return -1; - } - - uintptr_t gen_name_addr; - err = read_py_ptr( - handle, - coro_address + offsets->gen_object.gi_name, - &gen_name_addr); - if (err) { - return -1; - } - - PyObject *name = read_py_str( - handle, - offsets, - gen_name_addr, - 255 - ); - if (name == NULL) { - return -1; - } - - if (PyList_Append(render_to, name)) { - Py_DECREF(name); - return -1; - } - Py_DECREF(name); - - int gi_frame_state; - err = read_int( - handle, - coro_address + offsets->gen_object.gi_frame_state, - &gi_frame_state); - if (err) { - return -1; - } - - if (gi_frame_state == FRAME_SUSPENDED_YIELD_FROM) { - char owner; - err = read_char( - handle, - coro_address + offsets->gen_object.gi_iframe + - offsets->interpreter_frame.owner, - &owner - ); - if (err) { - return -1; - } - if (owner != FRAME_OWNED_BY_GENERATOR) { - PyErr_SetString( - PyExc_RuntimeError, - "generator doesn't own its frame \\_o_/"); - return -1; - } - - uintptr_t stackpointer_addr; - err = read_py_ptr( - handle, - coro_address + offsets->gen_object.gi_iframe + - offsets->interpreter_frame.stackpointer, - &stackpointer_addr); - if (err) { - return -1; - } - - if ((void*)stackpointer_addr != NULL) { - uintptr_t gi_await_addr; - err = read_py_ptr( - handle, - stackpointer_addr - sizeof(void*), - &gi_await_addr); - if (err) { - return -1; - } - - if ((void*)gi_await_addr != NULL) { - uintptr_t gi_await_addr_type_addr; - int err = read_ptr( - handle, - gi_await_addr + sizeof(void*), - &gi_await_addr_type_addr); - if (err) { - return -1; - } - - if (gen_type_addr == gi_await_addr_type_addr) { - /* This needs an explanation. We always start with parsing - native coroutine / generator frames. Ultimately they - are awaiting on something. That something can be - a native coroutine frame or... an iterator. - If it's the latter -- we can't continue building - our chain. So the condition to bail out of this is - to do that when the type of the current coroutine - doesn't match the type of whatever it points to - in its cr_await. - */ - err = parse_coro_chain( - handle, - offsets, - async_offsets, - gi_await_addr, - render_to - ); - if (err) { - return -1; - } - } - } - } - - } - - return 0; -} - - -static int -parse_task_awaited_by( - proc_handle_t *handle, - struct _Py_DebugOffsets* offsets, - struct _Py_AsyncioModuleDebugOffsets* async_offsets, - uintptr_t task_address, - PyObject *awaited_by -); - - -static int -parse_task( - proc_handle_t *handle, - struct _Py_DebugOffsets* offsets, - struct _Py_AsyncioModuleDebugOffsets* async_offsets, - uintptr_t task_address, - PyObject *render_to -) { - char is_task; - int err = read_char( - handle, - task_address + async_offsets->asyncio_task_object.task_is_task, - &is_task); - if (err) { - return -1; - } - - PyObject* result = PyList_New(0); - if (result == NULL) { - return -1; - } - - PyObject *call_stack = PyList_New(0); - if (call_stack == NULL) { - goto err; - } - if (PyList_Append(result, call_stack)) { - Py_DECREF(call_stack); - goto err; - } - /* we can operate on a borrowed one to simplify cleanup */ - Py_DECREF(call_stack); - - if (is_task) { - PyObject *tn = parse_task_name( - handle, offsets, async_offsets, task_address); - if (tn == NULL) { - goto err; - } - if (PyList_Append(result, tn)) { - Py_DECREF(tn); - goto err; - } - Py_DECREF(tn); - - uintptr_t coro_addr; - err = read_py_ptr( - handle, - task_address + async_offsets->asyncio_task_object.task_coro, - &coro_addr); - if (err) { - goto err; - } - - if ((void*)coro_addr != NULL) { - err = parse_coro_chain( - handle, - offsets, - async_offsets, - coro_addr, - call_stack - ); - if (err) { - goto err; - } - - if (PyList_Reverse(call_stack)) { - goto err; - } - } - } - - if (PyList_Append(render_to, result)) { - goto err; - } - - 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); - - if (parse_task_awaited_by(handle, offsets, async_offsets, - task_address, awaited_by) - ) { - goto err; - } - Py_DECREF(result); - - return 0; - -err: - Py_DECREF(result); - return -1; -} - -static int -parse_tasks_in_set( - proc_handle_t *handle, - struct _Py_DebugOffsets* offsets, - struct _Py_AsyncioModuleDebugOffsets* async_offsets, - uintptr_t set_addr, - PyObject *awaited_by -) { - uintptr_t set_obj; - if (read_py_ptr( - handle, - set_addr, - &set_obj) - ) { - return -1; - } - - Py_ssize_t num_els; - if (read_Py_ssize_t( - handle, - set_obj + offsets->set_object.used, - &num_els) - ) { - return -1; - } - - Py_ssize_t set_len; - if (read_Py_ssize_t( - handle, - set_obj + offsets->set_object.mask, - &set_len) - ) { - return -1; - } - set_len++; // The set contains the `mask+1` element slots. - - uintptr_t table_ptr; - if (read_ptr( - handle, - set_obj + offsets->set_object.table, - &table_ptr) - ) { - return -1; - } - - Py_ssize_t i = 0; - Py_ssize_t els = 0; - while (i < set_len) { - uintptr_t key_addr; - if (read_py_ptr(handle, table_ptr, &key_addr)) { - return -1; - } - - if ((void*)key_addr != NULL) { - Py_ssize_t ref_cnt; - if (read_Py_ssize_t(handle, table_ptr, &ref_cnt)) { - return -1; - } - - if (ref_cnt) { - // if 'ref_cnt=0' it's a set dummy marker - - if (parse_task( - handle, - offsets, - async_offsets, - key_addr, - awaited_by) - ) { - return -1; - } - - if (++els == num_els) { - break; - } - } - } - - table_ptr += sizeof(void*) * 2; - i++; - } - return 0; -} - - -static int -parse_task_awaited_by( - proc_handle_t *handle, - struct _Py_DebugOffsets* offsets, - struct _Py_AsyncioModuleDebugOffsets* async_offsets, - uintptr_t task_address, - PyObject *awaited_by -) { - uintptr_t task_ab_addr; - int err = read_py_ptr( - handle, - task_address + async_offsets->asyncio_task_object.task_awaited_by, - &task_ab_addr); - if (err) { - return -1; - } - - if ((void*)task_ab_addr == NULL) { - return 0; - } - - char awaited_by_is_a_set; - err = read_char( - handle, - task_address + async_offsets->asyncio_task_object.task_awaited_by_is_set, - &awaited_by_is_a_set); - if (err) { - return -1; - } - - if (awaited_by_is_a_set) { - if (parse_tasks_in_set( - handle, - offsets, - async_offsets, - task_address + async_offsets->asyncio_task_object.task_awaited_by, - awaited_by) - ) { - return -1; - } - } else { - uintptr_t sub_task; - if (read_py_ptr( - handle, - task_address + async_offsets->asyncio_task_object.task_awaited_by, - &sub_task) - ) { - return -1; - } - - if (parse_task( - handle, - offsets, - async_offsets, - sub_task, - awaited_by) - ) { - return -1; - } - } - - return 0; -} - -static int -parse_code_object( - proc_handle_t *handle, - PyObject* result, - struct _Py_DebugOffsets* offsets, - uintptr_t address, - uintptr_t* previous_frame -) { - uintptr_t address_of_function_name; - int bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, - address + offsets->code_object.name, - sizeof(void*), - &address_of_function_name - ); - if (bytes_read < 0) { - return -1; - } - - if ((void*)address_of_function_name == NULL) { - PyErr_SetString(PyExc_RuntimeError, "No function name found"); - return -1; - } - - PyObject* py_function_name = read_py_str( - handle, offsets, address_of_function_name, 256); - if (py_function_name == NULL) { - return -1; - } - - if (PyList_Append(result, py_function_name) == -1) { - Py_DECREF(py_function_name); - return -1; - } - Py_DECREF(py_function_name); - - return 0; -} - -static int -parse_frame_object( - proc_handle_t *handle, - PyObject* result, - struct _Py_DebugOffsets* offsets, - uintptr_t address, - uintptr_t* previous_frame -) { - int err; - - Py_ssize_t bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, - address + offsets->interpreter_frame.previous, - sizeof(void*), - previous_frame - ); - if (bytes_read < 0) { - return -1; - } - - char owner; - if (read_char(handle, address + offsets->interpreter_frame.owner, &owner)) { - return -1; - } - - if (owner >= FRAME_OWNED_BY_INTERPRETER) { - return 0; - } - - uintptr_t address_of_code_object; - err = read_py_ptr( - handle, - address + offsets->interpreter_frame.executable, - &address_of_code_object - ); - if (err) { - return -1; - } - - if ((void*)address_of_code_object == NULL) { - return 0; - } - - return parse_code_object( - handle, result, offsets, address_of_code_object, previous_frame); -} - -static int -parse_async_frame_object( - proc_handle_t *handle, - PyObject* result, - struct _Py_DebugOffsets* offsets, - uintptr_t address, - uintptr_t* previous_frame, - uintptr_t* code_object -) { - int err; - - Py_ssize_t bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, - address + offsets->interpreter_frame.previous, - sizeof(void*), - previous_frame - ); - if (bytes_read < 0) { - return -1; - } - - char owner; - bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, address + offsets->interpreter_frame.owner, sizeof(char), &owner); - if (bytes_read < 0) { - return -1; - } - - if (owner == FRAME_OWNED_BY_CSTACK || owner == FRAME_OWNED_BY_INTERPRETER) { - return 0; // C frame - } - - if (owner != FRAME_OWNED_BY_GENERATOR - && owner != FRAME_OWNED_BY_THREAD) { - PyErr_Format(PyExc_RuntimeError, "Unhandled frame owner %d.\n", owner); - return -1; - } - - err = read_py_ptr( - handle, - address + offsets->interpreter_frame.executable, - code_object - ); - if (err) { - return -1; - } - - assert(code_object != NULL); - if ((void*)*code_object == NULL) { - return 0; - } - - if (parse_code_object( - handle, result, offsets, *code_object, previous_frame)) { - return -1; - } - - return 1; -} - -static int -read_async_debug( - proc_handle_t *handle, - struct _Py_AsyncioModuleDebugOffsets* async_debug -) { - uintptr_t async_debug_addr = _Py_RemoteDebug_GetAsyncioDebugAddress(handle); - if (!async_debug_addr) { - return -1; - } - - size_t size = sizeof(struct _Py_AsyncioModuleDebugOffsets); - int result = _Py_RemoteDebug_ReadRemoteMemory(handle, async_debug_addr, size, async_debug); - return result; -} - -static int -find_running_frame( - proc_handle_t *handle, - uintptr_t runtime_start_address, - _Py_DebugOffsets* local_debug_offsets, - uintptr_t *frame -) { - uint64_t interpreter_state_list_head = - local_debug_offsets->runtime_state.interpreters_head; - - uintptr_t address_of_interpreter_state; - int bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, - runtime_start_address + interpreter_state_list_head, - sizeof(void*), - &address_of_interpreter_state); - if (bytes_read < 0) { - return -1; - } - - if (address_of_interpreter_state == 0) { - PyErr_SetString(PyExc_RuntimeError, "No interpreter state found"); - return -1; - } - - uintptr_t address_of_thread; - bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, - address_of_interpreter_state + - local_debug_offsets->interpreter_state.threads_main, - sizeof(void*), - &address_of_thread); - if (bytes_read < 0) { - return -1; - } - - // No Python frames are available for us (can happen at tear-down). - if ((void*)address_of_thread != NULL) { - int err = read_ptr( - handle, - address_of_thread + local_debug_offsets->thread_state.current_frame, - frame); - if (err) { - return -1; - } - return 0; - } - - *frame = (uintptr_t)NULL; - return 0; -} - -static int -find_running_task( - proc_handle_t *handle, - uintptr_t runtime_start_address, - _Py_DebugOffsets *local_debug_offsets, - struct _Py_AsyncioModuleDebugOffsets *async_offsets, - uintptr_t *running_task_addr -) { - *running_task_addr = (uintptr_t)NULL; - - uint64_t interpreter_state_list_head = - local_debug_offsets->runtime_state.interpreters_head; - - uintptr_t address_of_interpreter_state; - int bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, - runtime_start_address + interpreter_state_list_head, - sizeof(void*), - &address_of_interpreter_state); - if (bytes_read < 0) { - return -1; - } - - if (address_of_interpreter_state == 0) { - PyErr_SetString(PyExc_RuntimeError, "No interpreter state found"); - return -1; - } - - uintptr_t address_of_thread; - bytes_read = _Py_RemoteDebug_ReadRemoteMemory( - handle, - address_of_interpreter_state + - local_debug_offsets->interpreter_state.threads_head, - sizeof(void*), - &address_of_thread); - if (bytes_read < 0) { - return -1; - } - - uintptr_t address_of_running_loop; - // No Python frames are available for us (can happen at tear-down). - if ((void*)address_of_thread == NULL) { - return 0; - } - - bytes_read = read_py_ptr( - handle, - address_of_thread - + async_offsets->asyncio_thread_state.asyncio_running_loop, - &address_of_running_loop); - if (bytes_read == -1) { - return -1; - } - - // no asyncio loop is now running - if ((void*)address_of_running_loop == NULL) { - return 0; - } - - int err = read_ptr( - handle, - address_of_thread - + async_offsets->asyncio_thread_state.asyncio_running_task, - running_task_addr); - if (err) { - return -1; - } - - return 0; -} - -static int -append_awaited_by_for_thread( - proc_handle_t *handle, - uintptr_t head_addr, - struct _Py_DebugOffsets *debug_offsets, - struct _Py_AsyncioModuleDebugOffsets *async_offsets, - PyObject *result -) { - struct llist_node task_node; - - if (0 > _Py_RemoteDebug_ReadRemoteMemory( - handle, - head_addr, - sizeof(task_node), - &task_node)) - { - return -1; - } - - size_t iteration_count = 0; - const size_t MAX_ITERATIONS = 2 << 15; // A reasonable upper bound - while ((uintptr_t)task_node.next != head_addr) { - if (++iteration_count > MAX_ITERATIONS) { - PyErr_SetString(PyExc_RuntimeError, "Task list appears corrupted"); - return -1; - } - - if (task_node.next == NULL) { - PyErr_SetString( - PyExc_RuntimeError, - "Invalid linked list structure reading remote memory"); - return -1; - } - - uintptr_t task_addr = (uintptr_t)task_node.next - - async_offsets->asyncio_task_object.task_node; - - PyObject *tn = parse_task_name( - handle, - debug_offsets, - async_offsets, - task_addr); - if (tn == NULL) { - return -1; - } - - PyObject *current_awaited_by = PyList_New(0); - if (current_awaited_by == NULL) { - Py_DECREF(tn); - return -1; - } - - PyObject *result_item = PyTuple_New(2); - if (result_item == NULL) { - Py_DECREF(tn); - Py_DECREF(current_awaited_by); - return -1; - } - - PyTuple_SET_ITEM(result_item, 0, tn); // steals ref - PyTuple_SET_ITEM(result_item, 1, current_awaited_by); // steals ref - if (PyList_Append(result, result_item)) { - Py_DECREF(result_item); - return -1; - } - Py_DECREF(result_item); - - if (parse_task_awaited_by(handle, debug_offsets, async_offsets, - task_addr, current_awaited_by)) - { - return -1; - } - - // onto the next one... - if (0 > _Py_RemoteDebug_ReadRemoteMemory( - handle, - (uintptr_t)task_node.next, - sizeof(task_node), - &task_node)) - { - return -1; - } - } - - return 0; -} - -static int -append_awaited_by( - proc_handle_t *handle, - unsigned long tid, - uintptr_t head_addr, - struct _Py_DebugOffsets *debug_offsets, - struct _Py_AsyncioModuleDebugOffsets *async_offsets, - PyObject *result) -{ - PyObject *tid_py = PyLong_FromUnsignedLong(tid); - if (tid_py == NULL) { - return -1; - } - - PyObject *result_item = PyTuple_New(2); - if (result_item == NULL) { - Py_DECREF(tid_py); - return -1; - } - - PyObject* awaited_by_for_thread = PyList_New(0); - if (awaited_by_for_thread == NULL) { - Py_DECREF(tid_py); - Py_DECREF(result_item); - return -1; - } - - PyTuple_SET_ITEM(result_item, 0, tid_py); // steals ref - PyTuple_SET_ITEM(result_item, 1, awaited_by_for_thread); // steals ref - if (PyList_Append(result, result_item)) { - Py_DECREF(result_item); - return -1; - } - Py_DECREF(result_item); - - if (append_awaited_by_for_thread( - handle, - head_addr, - debug_offsets, - async_offsets, - awaited_by_for_thread)) - { - return -1; - } - - return 0; -} - -static PyObject* -get_all_awaited_by(PyObject* self, PyObject* args) -{ -#if (!defined(__linux__) && !defined(__APPLE__)) && !defined(MS_WINDOWS) || \ - (defined(__linux__) && !HAVE_PROCESS_VM_READV) - PyErr_SetString( - PyExc_RuntimeError, - "get_all_awaited_by is not implemented on this platform"); - return NULL; -#endif - - int pid; - if (!PyArg_ParseTuple(args, "i", &pid)) { - return NULL; - } - - proc_handle_t the_handle; - proc_handle_t *handle = &the_handle; - if (_Py_RemoteDebug_InitProcHandle(handle, pid) < 0) { - return 0; - } - - PyObject *result = NULL; - - uintptr_t runtime_start_addr = _Py_RemoteDebug_GetPyRuntimeAddress(handle); - if (runtime_start_addr == 0) { - if (!PyErr_Occurred()) { - PyErr_SetString( - PyExc_RuntimeError, "Failed to get .PyRuntime address"); - } - goto result_err; - } - struct _Py_DebugOffsets local_debug_offsets; - - if (_Py_RemoteDebug_ReadDebugOffsets(handle, &runtime_start_addr, &local_debug_offsets)) { - chain_exceptions(PyExc_RuntimeError, "Failed to read debug offsets"); - goto result_err; - } - - struct _Py_AsyncioModuleDebugOffsets local_async_debug; - if (read_async_debug(handle, &local_async_debug)) { - chain_exceptions(PyExc_RuntimeError, "Failed to read asyncio debug offsets"); - goto result_err; - } - - result = PyList_New(0); - if (result == NULL) { - goto result_err; - } - - uint64_t interpreter_state_list_head = - local_debug_offsets.runtime_state.interpreters_head; - - uintptr_t interpreter_state_addr; - if (0 > _Py_RemoteDebug_ReadRemoteMemory( - handle, - runtime_start_addr + interpreter_state_list_head, - sizeof(void*), - &interpreter_state_addr)) - { - goto result_err; - } - - uintptr_t thread_state_addr; - unsigned long tid = 0; - if (0 > _Py_RemoteDebug_ReadRemoteMemory( - handle, - interpreter_state_addr - + local_debug_offsets.interpreter_state.threads_head, - sizeof(void*), - &thread_state_addr)) - { - goto result_err; - } - - uintptr_t head_addr; - while (thread_state_addr != 0) { - if (0 > _Py_RemoteDebug_ReadRemoteMemory( - handle, - thread_state_addr - + local_debug_offsets.thread_state.native_thread_id, - sizeof(tid), - &tid)) - { - goto result_err; - } - - head_addr = thread_state_addr - + local_async_debug.asyncio_thread_state.asyncio_tasks_head; - - if (append_awaited_by(handle, tid, head_addr, &local_debug_offsets, - &local_async_debug, result)) - { - goto result_err; - } - - if (0 > _Py_RemoteDebug_ReadRemoteMemory( - handle, - thread_state_addr + local_debug_offsets.thread_state.next, - sizeof(void*), - &thread_state_addr)) - { - goto result_err; - } - } - - head_addr = interpreter_state_addr - + local_async_debug.asyncio_interpreter_state.asyncio_tasks_head; - - // On top of a per-thread task lists used by default by asyncio to avoid - // contention, there is also a fallback per-interpreter list of tasks; - // any tasks still pending when a thread is destroyed will be moved to the - // per-interpreter task list. It's unlikely we'll find anything here, but - // interesting for debugging. - if (append_awaited_by(handle, 0, head_addr, &local_debug_offsets, - &local_async_debug, result)) - { - goto result_err; - } - - _Py_RemoteDebug_CleanupProcHandle(handle); - return result; - -result_err: - Py_XDECREF(result); - _Py_RemoteDebug_CleanupProcHandle(handle); - return NULL; -} - -static PyObject* -get_stack_trace(PyObject* self, PyObject* args) -{ -#if (!defined(__linux__) && !defined(__APPLE__)) && !defined(MS_WINDOWS) || \ - (defined(__linux__) && !HAVE_PROCESS_VM_READV) - PyErr_SetString( - PyExc_RuntimeError, - "get_stack_trace is not supported on this platform"); - return NULL; -#endif - - int pid; - if (!PyArg_ParseTuple(args, "i", &pid)) { - return NULL; - } - - proc_handle_t the_handle; - proc_handle_t *handle = &the_handle; - if (_Py_RemoteDebug_InitProcHandle(handle, pid) < 0) { - return 0; - } - - PyObject* result = NULL; - - uintptr_t runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(handle); - if (runtime_start_address == 0) { - if (!PyErr_Occurred()) { - PyErr_SetString( - PyExc_RuntimeError, "Failed to get .PyRuntime address"); - } - goto result_err; - } - struct _Py_DebugOffsets local_debug_offsets; - - if (_Py_RemoteDebug_ReadDebugOffsets(handle, &runtime_start_address, &local_debug_offsets)) { - chain_exceptions(PyExc_RuntimeError, "Failed to read debug offsets"); - goto result_err; - } - - uintptr_t address_of_current_frame; - if (find_running_frame( - handle, runtime_start_address, &local_debug_offsets, - &address_of_current_frame) - ) { - goto result_err; - } - - result = PyList_New(0); - if (result == NULL) { - goto result_err; - } - - while ((void*)address_of_current_frame != NULL) { - if (parse_frame_object( - handle, - result, - &local_debug_offsets, - address_of_current_frame, - &address_of_current_frame) - < 0) - { - Py_DECREF(result); - goto result_err; - } - } - -result_err: - _Py_RemoteDebug_CleanupProcHandle(handle); - return result; -} - -static PyObject* -get_async_stack_trace(PyObject* self, PyObject* args) -{ -#if (!defined(__linux__) && !defined(__APPLE__)) && !defined(MS_WINDOWS) || \ - (defined(__linux__) && !HAVE_PROCESS_VM_READV) - PyErr_SetString( - PyExc_RuntimeError, - "get_stack_trace is not supported on this platform"); - return NULL; -#endif - int pid; - - if (!PyArg_ParseTuple(args, "i", &pid)) { - return NULL; - } - - proc_handle_t the_handle; - proc_handle_t *handle = &the_handle; - if (_Py_RemoteDebug_InitProcHandle(handle, pid) < 0) { - return 0; - } - - PyObject *result = NULL; - - uintptr_t runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(handle); - if (runtime_start_address == 0) { - if (!PyErr_Occurred()) { - PyErr_SetString( - PyExc_RuntimeError, "Failed to get .PyRuntime address"); - } - goto result_err; - } - struct _Py_DebugOffsets local_debug_offsets; - - if (_Py_RemoteDebug_ReadDebugOffsets(handle, &runtime_start_address, &local_debug_offsets)) { - chain_exceptions(PyExc_RuntimeError, "Failed to read debug offsets"); - goto result_err; - } - - struct _Py_AsyncioModuleDebugOffsets local_async_debug; - if (read_async_debug(handle, &local_async_debug)) { - chain_exceptions(PyExc_RuntimeError, "Failed to read asyncio debug offsets"); - goto result_err; - } - - result = PyList_New(1); - if (result == NULL) { - goto result_err; - } - PyObject* calls = PyList_New(0); - if (calls == NULL) { - goto result_err; - } - if (PyList_SetItem(result, 0, calls)) { /* steals ref to 'calls' */ - Py_DECREF(calls); - goto result_err; - } - - uintptr_t running_task_addr = (uintptr_t)NULL; - if (find_running_task( - handle, runtime_start_address, &local_debug_offsets, &local_async_debug, - &running_task_addr) - ) { - chain_exceptions(PyExc_RuntimeError, "Failed to find running task"); - goto result_err; - } - - if ((void*)running_task_addr == NULL) { - PyErr_SetString(PyExc_RuntimeError, "No running task found"); - goto result_err; - } - - uintptr_t running_coro_addr; - if (read_py_ptr( - handle, - running_task_addr + local_async_debug.asyncio_task_object.task_coro, - &running_coro_addr - )) { - chain_exceptions(PyExc_RuntimeError, "Failed to read running task coro"); - goto result_err; - } - - if ((void*)running_coro_addr == NULL) { - PyErr_SetString(PyExc_RuntimeError, "Running task coro is NULL"); - goto result_err; - } - - // note: genobject's gi_iframe is an embedded struct so the address to - // the offset leads directly to its first field: f_executable - uintptr_t address_of_running_task_code_obj; - if (read_py_ptr( - handle, - running_coro_addr + local_debug_offsets.gen_object.gi_iframe, - &address_of_running_task_code_obj - )) { - goto result_err; - } - - if ((void*)address_of_running_task_code_obj == NULL) { - PyErr_SetString(PyExc_RuntimeError, "Running task code object is NULL"); - goto result_err; - } - - uintptr_t address_of_current_frame; - if (find_running_frame( - handle, runtime_start_address, &local_debug_offsets, - &address_of_current_frame) - ) { - chain_exceptions(PyExc_RuntimeError, "Failed to find running frame"); - goto result_err; - } - - uintptr_t address_of_code_object; - while ((void*)address_of_current_frame != NULL) { - int res = parse_async_frame_object( - handle, - calls, - &local_debug_offsets, - address_of_current_frame, - &address_of_current_frame, - &address_of_code_object - ); - - if (res < 0) { - chain_exceptions(PyExc_RuntimeError, "Failed to parse async frame object"); - goto result_err; - } - - if (address_of_code_object == address_of_running_task_code_obj) { - break; - } - } - - PyObject *tn = parse_task_name( - handle, &local_debug_offsets, &local_async_debug, running_task_addr); - if (tn == NULL) { - goto result_err; - } - if (PyList_Append(result, tn)) { - Py_DECREF(tn); - goto result_err; - } - Py_DECREF(tn); - - PyObject* awaited_by = PyList_New(0); - if (awaited_by == NULL) { - goto result_err; - } - if (PyList_Append(result, awaited_by)) { - Py_DECREF(awaited_by); - goto result_err; - } - Py_DECREF(awaited_by); - - if (parse_task_awaited_by( - handle, &local_debug_offsets, &local_async_debug, - running_task_addr, awaited_by) - ) { - goto result_err; - } - - _Py_RemoteDebug_CleanupProcHandle(handle); - return result; - -result_err: - _Py_RemoteDebug_CleanupProcHandle(handle); - Py_XDECREF(result); - return NULL; -} - - -static PyMethodDef methods[] = { - {"get_stack_trace", get_stack_trace, METH_VARARGS, - "Get the Python stack from a given pod"}, - {"get_async_stack_trace", get_async_stack_trace, METH_VARARGS, - "Get the asyncio stack from a given pid"}, - {"get_all_awaited_by", get_all_awaited_by, METH_VARARGS, - "Get all tasks and their awaited_by from a given pid"}, - {NULL, NULL, 0, NULL}, -}; - -static struct PyModuleDef module = { - .m_base = PyModuleDef_HEAD_INIT, - .m_name = "_testexternalinspection", - .m_size = -1, - .m_methods = methods, -}; - -PyMODINIT_FUNC -PyInit__testexternalinspection(void) -{ - PyObject* mod = PyModule_Create(&module); - if (mod == NULL) { - return NULL; - } -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); -#endif - int rc = PyModule_AddIntConstant( - mod, "PROCESS_VM_READV_SUPPORTED", HAVE_PROCESS_VM_READV); - if (rc < 0) { - Py_DECREF(mod); - return NULL; - } - return mod; -} diff --git a/PCbuild/_remotedebugging.vcxproj b/PCbuild/_remotedebugging.vcxproj new file mode 100644 index 0000000..a16079f --- /dev/null +++ b/PCbuild/_remotedebugging.vcxproj @@ -0,0 +1,114 @@ +ο»Ώ + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + PGInstrument + ARM + + + PGInstrument + ARM64 + + + PGInstrument + Win32 + + + PGInstrument + x64 + + + PGUpdate + ARM + + + PGUpdate + ARM64 + + + PGUpdate + Win32 + + + PGUpdate + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {4D7C112F-3083-4D9E-9754-9341C14D9B39} + _remotedebugging + Win32Proj + false + + + + + DynamicLibrary + NotSet + + + + $(PyStdlibPydExt) + + + + + + + + + + <_ProjectFileVersion>10.0.30319.1 + + + + + + + + + + {cf7ac3d1-e2df-41d2-bea6-1e2556cdea26} + false + + + {885d4898-d08d-4091-9c40-c700cfe3fc5a} + false + + + + + + diff --git a/PCbuild/_remotedebugging.vcxproj.filters b/PCbuild/_remotedebugging.vcxproj.filters new file mode 100644 index 0000000..888e2cd --- /dev/null +++ b/PCbuild/_remotedebugging.vcxproj.filters @@ -0,0 +1,20 @@ +ο»Ώ + + + + {6d101329-41df-49a0-8639-f35408ad7c6d} + + + {711941d1-269c-49cb-a733-759b2b91fc61} + + + + + + + + Resource Files + + + + diff --git a/PCbuild/_testexternalinspection.vcxproj b/PCbuild/_testexternalinspection.vcxproj deleted file mode 100644 index d5f347e..0000000 --- a/PCbuild/_testexternalinspection.vcxproj +++ /dev/null @@ -1,114 +0,0 @@ -ο»Ώ - - - - Debug - ARM - - - Debug - ARM64 - - - Debug - Win32 - - - Debug - x64 - - - PGInstrument - ARM - - - PGInstrument - ARM64 - - - PGInstrument - Win32 - - - PGInstrument - x64 - - - PGUpdate - ARM - - - PGUpdate - ARM64 - - - PGUpdate - Win32 - - - PGUpdate - x64 - - - Release - ARM - - - Release - ARM64 - - - Release - Win32 - - - Release - x64 - - - - {4D7C112F-3083-4D9E-9754-9341C14D9B39} - _testexternalinspection - Win32Proj - false - - - - - DynamicLibrary - NotSet - - - - $(PyStdlibPydExt) - - - - - - - - - - <_ProjectFileVersion>10.0.30319.1 - - - - - - - - - - {cf7ac3d1-e2df-41d2-bea6-1e2556cdea26} - false - - - {885d4898-d08d-4091-9c40-c700cfe3fc5a} - false - - - - - - diff --git a/PCbuild/_testexternalinspection.vcxproj.filters b/PCbuild/_testexternalinspection.vcxproj.filters deleted file mode 100644 index feb4343..0000000 --- a/PCbuild/_testexternalinspection.vcxproj.filters +++ /dev/null @@ -1,20 +0,0 @@ -ο»Ώ - - - - {6d101329-41df-49a0-8639-f35408ad7c6d} - - - {711941d1-269c-49cb-a733-759b2b91fc61} - - - - - - - - Resource Files - - - - diff --git a/PCbuild/pcbuild.proj b/PCbuild/pcbuild.proj index 1bf430e..eec213d 100644 --- a/PCbuild/pcbuild.proj +++ b/PCbuild/pcbuild.proj @@ -66,7 +66,7 @@ - + @@ -79,7 +79,7 @@ - + diff --git a/PCbuild/pcbuild.sln b/PCbuild/pcbuild.sln index 803bb14..d2bfb94 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 9873890..761eecb 100644 --- a/Tools/build/generate_stdlib_module_names.py +++ b/Tools/build/generate_stdlib_module_names.py @@ -34,7 +34,7 @@ IGNORE = { '_testlimitedcapi', '_testmultiphase', '_testsinglephase', - '_testexternalinspection', + '_remotedebugging', '_xxtestfuzz', 'idlelib.idle_test', 'test', diff --git a/configure b/configure index 7dbb35f..3b74554 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 65f2650..ed5c65e 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], -- cgit v0.12