diff options
-rw-r--r-- | Lib/asyncio/tools.py | 2 | ||||
-rw-r--r-- | Lib/test/test_external_inspection.py | 325 | ||||
-rw-r--r-- | Modules/_remotedebuggingmodule.c | 379 |
3 files changed, 545 insertions, 161 deletions
diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py index 16440b594ad..6c1f725e777 100644 --- a/Lib/asyncio/tools.py +++ b/Lib/asyncio/tools.py @@ -27,6 +27,7 @@ def _index(result): for tid, tname, awaited in tasks: id2name[tid] = tname for stack, parent_id in awaited: + stack = [elem[0] if isinstance(elem, tuple) else elem for elem in stack] awaits.append((parent_id, stack, tid)) return id2name, awaits @@ -151,6 +152,7 @@ def build_task_table(result): ] ) for stack, awaiter_id in awaited: + stack = [elem[0] if isinstance(elem, tuple) else elem for elem in stack] coroutine_chain = " -> ".join(stack) awaiter_name = id2name.get(awaiter_id, "Unknown") table.append( diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 0fd704e698b..f787190b1ae 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -4,6 +4,7 @@ import textwrap import importlib import sys import socket +from asyncio import staggered, taskgroups from unittest.mock import ANY from test.support import os_helper, SHORT_TIMEOUT, busy_retry from test.support.script_helper import make_script @@ -19,27 +20,33 @@ try: from _remotedebugging import get_async_stack_trace from _remotedebugging import get_all_awaited_by except ImportError: - raise unittest.SkipTest( - "Test only runs when _remotedebuggingmodule is available") + raise unittest.SkipTest("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) importlib.invalidate_caches() return to_return -skip_if_not_supported = unittest.skipIf((sys.platform != "darwin" - and sys.platform != "linux" - and sys.platform != "win32"), - "Test only runs on Linux, Windows and MacOS") + +skip_if_not_supported = unittest.skipIf( + (sys.platform != "darwin" and sys.platform != "linux" and sys.platform != "win32"), + "Test only runs on Linux, Windows and MacOS", +) + + class TestGetStackTrace(unittest.TestCase): @skip_if_not_supported - @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, - "Test only runs on Linux with process_vm_readv support") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) def test_remote_stack_trace(self): # Spawn a process with some realistic Python code port = find_unused_port() - script = textwrap.dedent(f"""\ + script = textwrap.dedent( + f"""\ import time, sys, socket # Connect to the test process sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -57,7 +64,8 @@ class TestGetStackTrace(unittest.TestCase): time.sleep(1000) bar() - """) + """ + ) stack_trace = None with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") @@ -66,11 +74,11 @@ class TestGetStackTrace(unittest.TestCase): # Create a socket server to communicate with the target process server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(('localhost', port)) + server_socket.bind(("localhost", port)) server_socket.settimeout(SHORT_TIMEOUT) server_socket.listen(1) - script_name = _make_test_script(script_dir, 'script', script) + script_name = _make_test_script(script_dir, "script", script) client_socket = None try: p = subprocess.Popen([sys.executable, script_name]) @@ -88,22 +96,24 @@ class TestGetStackTrace(unittest.TestCase): p.terminate() p.wait(timeout=SHORT_TIMEOUT) - expected_stack_trace = [ - 'foo', - 'baz', - 'bar', - '<module>' + ("foo", script_name, 15), + ("baz", script_name, 11), + ("bar", script_name, 9), + ("<module>", script_name, 17), ] self.assertEqual(stack_trace, expected_stack_trace) @skip_if_not_supported - @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, - "Test only runs on Linux with process_vm_readv support") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) def test_async_remote_stack_trace(self): # Spawn a process with some realistic Python code port = find_unused_port() - script = textwrap.dedent(f"""\ + script = textwrap.dedent( + f"""\ import asyncio import time import sys @@ -143,7 +153,8 @@ class TestGetStackTrace(unittest.TestCase): return loop asyncio.run(main(), loop_factory={{TASK_FACTORY}}) - """) + """ + ) stack_trace = None for task_factory_variant in "asyncio.new_event_loop", "new_eager_loop": with ( @@ -154,25 +165,24 @@ class TestGetStackTrace(unittest.TestCase): os.mkdir(script_dir) server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(('localhost', port)) + server_socket.bind(("localhost", port)) server_socket.settimeout(SHORT_TIMEOUT) server_socket.listen(1) script_name = _make_test_script( - script_dir, 'script', - script.format(TASK_FACTORY=task_factory_variant)) + script_dir, + "script", + script.format(TASK_FACTORY=task_factory_variant), + ) client_socket = None try: - p = subprocess.Popen( - [sys.executable, script_name] - ) + p = subprocess.Popen([sys.executable, script_name]) client_socket, _ = server_socket.accept() server_socket.close() response = client_socket.recv(1024) self.assertEqual(response, b"ready") stack_trace = get_async_stack_trace(p.pid) except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace") + self.skipTest("Insufficient permissions to read the stack trace") finally: if client_socket is not None: client_socket.close() @@ -185,23 +195,91 @@ class TestGetStackTrace(unittest.TestCase): root_task = "Task-1" expected_stack_trace = [ - ['c5', 'c4', 'c3', 'c2'], - 'c2_root', [ - [['_aexit', '__aexit__', 'main'], root_task, []], - [['c1'], 'sub_main_1', [[['_aexit', '__aexit__', 'main'], root_task, []]]], - [['c1'], 'sub_main_2', [[['_aexit', '__aexit__', 'main'], root_task, []]]], - ] + ("c5", script_name, 11), + ("c4", script_name, 15), + ("c3", script_name, 18), + ("c2", script_name, 21), + ], + "c2_root", + [ + [ + [ + ( + "TaskGroup._aexit", + taskgroups.__file__, + ANY, + ), + ( + "TaskGroup.__aexit__", + taskgroups.__file__, + ANY, + ), + ("main", script_name, 27), + ], + "Task-1", + [], + ], + [ + [("c1", script_name, 24)], + "sub_main_1", + [ + [ + [ + ( + "TaskGroup._aexit", + taskgroups.__file__, + ANY, + ), + ( + "TaskGroup.__aexit__", + taskgroups.__file__, + ANY, + ), + ("main", script_name, 27), + ], + "Task-1", + [], + ] + ], + ], + [ + [("c1", script_name, 24)], + "sub_main_2", + [ + [ + [ + ( + "TaskGroup._aexit", + taskgroups.__file__, + ANY, + ), + ( + "TaskGroup.__aexit__", + taskgroups.__file__, + ANY, + ), + ("main", script_name, 27), + ], + "Task-1", + [], + ] + ], + ], + ], ] self.assertEqual(stack_trace, expected_stack_trace) @skip_if_not_supported - @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, - "Test only runs on Linux with process_vm_readv support") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) def test_asyncgen_remote_stack_trace(self): # Spawn a process with some realistic Python code port = find_unused_port() - script = textwrap.dedent(f"""\ + script = textwrap.dedent( + f"""\ import asyncio import time import sys @@ -225,7 +303,8 @@ class TestGetStackTrace(unittest.TestCase): pass asyncio.run(main()) - """) + """ + ) stack_trace = None with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") @@ -233,10 +312,10 @@ class TestGetStackTrace(unittest.TestCase): # Create a socket server to communicate with the target process server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(('localhost', port)) + server_socket.bind(("localhost", port)) server_socket.settimeout(SHORT_TIMEOUT) server_socket.listen(1) - script_name = _make_test_script(script_dir, 'script', script) + script_name = _make_test_script(script_dir, "script", script) client_socket = None try: p = subprocess.Popen([sys.executable, script_name]) @@ -258,17 +337,26 @@ class TestGetStackTrace(unittest.TestCase): stack_trace[2].sort(key=lambda x: x[1]) expected_stack_trace = [ - ['gen_nested_call', 'gen', 'main'], 'Task-1', [] + [ + ("gen_nested_call", script_name, 11), + ("gen", script_name, 17), + ("main", script_name, 20), + ], + "Task-1", + [], ] self.assertEqual(stack_trace, expected_stack_trace) @skip_if_not_supported - @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, - "Test only runs on Linux with process_vm_readv support") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) def test_async_gather_remote_stack_trace(self): # Spawn a process with some realistic Python code port = find_unused_port() - script = textwrap.dedent(f"""\ + script = textwrap.dedent( + f"""\ import asyncio import time import sys @@ -293,7 +381,8 @@ class TestGetStackTrace(unittest.TestCase): await asyncio.gather(c1(), c2()) asyncio.run(main()) - """) + """ + ) stack_trace = None with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") @@ -301,10 +390,10 @@ class TestGetStackTrace(unittest.TestCase): # Create a socket server to communicate with the target process server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(('localhost', port)) + server_socket.bind(("localhost", port)) server_socket.settimeout(SHORT_TIMEOUT) server_socket.listen(1) - script_name = _make_test_script(script_dir, 'script', script) + script_name = _make_test_script(script_dir, "script", script) client_socket = None try: p = subprocess.Popen([sys.executable, script_name]) @@ -314,8 +403,7 @@ class TestGetStackTrace(unittest.TestCase): self.assertEqual(response, b"ready") stack_trace = get_async_stack_trace(p.pid) except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace") + self.skipTest("Insufficient permissions to read the stack trace") finally: if client_socket is not None: client_socket.close() @@ -326,18 +414,23 @@ 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'], 'Task-2', [[['main'], 'Task-1', []]] + expected_stack_trace = [ + [("deep", script_name, ANY), ("c1", script_name, 16)], + "Task-2", + [[[("main", script_name, 22)], "Task-1", []]], ] self.assertEqual(stack_trace, expected_stack_trace) @skip_if_not_supported - @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, - "Test only runs on Linux with process_vm_readv support") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) def test_async_staggered_race_remote_stack_trace(self): # Spawn a process with some realistic Python code port = find_unused_port() - script = textwrap.dedent(f"""\ + script = textwrap.dedent( + f"""\ import asyncio.staggered import time import sys @@ -365,7 +458,8 @@ class TestGetStackTrace(unittest.TestCase): ) asyncio.run(main()) - """) + """ + ) stack_trace = None with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") @@ -373,10 +467,10 @@ class TestGetStackTrace(unittest.TestCase): # Create a socket server to communicate with the target process server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(('localhost', port)) + server_socket.bind(("localhost", port)) server_socket.settimeout(SHORT_TIMEOUT) server_socket.listen(1) - script_name = _make_test_script(script_dir, 'script', script) + script_name = _make_test_script(script_dir, "script", script) client_socket = None try: p = subprocess.Popen([sys.executable, script_name]) @@ -386,8 +480,7 @@ class TestGetStackTrace(unittest.TestCase): self.assertEqual(response, b"ready") stack_trace = get_async_stack_trace(p.pid) except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace") + self.skipTest("Insufficient permissions to read the stack trace") finally: if client_socket is not None: client_socket.close() @@ -397,20 +490,35 @@ 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', - [[['staggered_race', 'main'], 'Task-1', []]] + [ + ("deep", script_name, ANY), + ("c1", script_name, 16), + ("staggered_race.<locals>.run_one_coro", staggered.__file__, ANY), + ], + "Task-2", + [ + [ + [ + ("staggered_race", staggered.__file__, ANY), + ("main", script_name, 22), + ], + "Task-1", + [], + ] + ], ] self.assertEqual(stack_trace, expected_stack_trace) @skip_if_not_supported - @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, - "Test only runs on Linux with process_vm_readv support") + @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): port = find_unused_port() - script = textwrap.dedent(f"""\ + script = textwrap.dedent( + f"""\ import asyncio import os import random @@ -475,7 +583,8 @@ class TestGetStackTrace(unittest.TestCase): tg.create_task(echo_client_spam(server), name="echo client spam") asyncio.run(main()) - """) + """ + ) stack_trace = None with os_helper.temp_dir() as work_dir: script_dir = os.path.join(work_dir, "script_pkg") @@ -483,10 +592,10 @@ class TestGetStackTrace(unittest.TestCase): # Create a socket server to communicate with the target process server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind(('localhost', port)) + server_socket.bind(("localhost", port)) server_socket.settimeout(SHORT_TIMEOUT) server_socket.listen(1) - script_name = _make_test_script(script_dir, 'script', script) + script_name = _make_test_script(script_dir, "script", script) client_socket = None try: p = subprocess.Popen([sys.executable, script_name]) @@ -506,7 +615,9 @@ class TestGetStackTrace(unittest.TestCase): msg = str(re) if msg.startswith("Task list appears corrupted"): continue - elif msg.startswith("Invalid linked list structure reading remote memory"): + elif msg.startswith( + "Invalid linked list structure reading remote memory" + ): continue elif msg.startswith("Unknown error reading memory"): continue @@ -525,22 +636,62 @@ 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((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) + self.assertIn((ANY, "Task-1", []), entries) + main_stack = [ + ( + "TaskGroup._aexit", + taskgroups.__file__, + ANY, + ), + ( + "TaskGroup.__aexit__", + taskgroups.__file__, + ANY, + ), + ("main", script_name, 60), + ] + self.assertIn( + (ANY, "server task", [[main_stack, ANY]]), + entries, + ) + self.assertIn( + (ANY, "echo client spam", [[main_stack, ANY]]), + entries, + ) - expected_stack = [[['_aexit', '__aexit__', 'echo_client_spam'], ANY]] - tasks_with_stack = [task for task in entries if task[2] == expected_stack] + expected_stack = [ + [ + [ + ( + "TaskGroup._aexit", + taskgroups.__file__, + ANY, + ), + ( + "TaskGroup.__aexit__", + taskgroups.__file__, + ANY, + ), + ("echo_client_spam", script_name, 41), + ], + 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([[['_aexit', '__aexit__', 'echo_client_spam'], ANY]], entries[-1][2]) + self.assertEqual( + expected_stack, + entries[-1][2], + ) except PermissionError: - self.skipTest( - "Insufficient permissions to read the stack trace") + self.skipTest("Insufficient permissions to read the stack trace") finally: if client_socket is not None: client_socket.close() @@ -549,11 +700,21 @@ class TestGetStackTrace(unittest.TestCase): p.wait(timeout=SHORT_TIMEOUT) @skip_if_not_supported - @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, - "Test only runs on Linux with process_vm_readv support") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) def test_self_trace(self): stack_trace = get_stack_trace(os.getpid()) - self.assertEqual(stack_trace[0], "test_self_trace") + self.assertEqual( + stack_trace[0], + ( + "TestGetStackTrace.test_self_trace", + __file__, + self.test_self_trace.__code__.co_firstlineno + 6, + ), + ) + if __name__ == "__main__": unittest.main() diff --git a/Modules/_remotedebuggingmodule.c b/Modules/_remotedebuggingmodule.c index 0e055ae1604..cffa9a38331 100644 --- a/Modules/_remotedebuggingmodule.c +++ b/Modules/_remotedebuggingmodule.c @@ -80,37 +80,6 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle) 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) { @@ -188,20 +157,34 @@ read_py_str( uintptr_t address, Py_ssize_t max_len ) { - assert(max_len > 0); - PyObject *result = NULL; + char *buf = NULL; + + Py_ssize_t len; + int res = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address + debug_offsets->unicode_object.length, + sizeof(Py_ssize_t), + &len + ); + if (res < 0) { + goto err; + } - char *buf = (char *)PyMem_RawMalloc(max_len); + buf = (char *)PyMem_RawMalloc(len+1); if (buf == NULL) { PyErr_NoMemory(); return NULL; } - if (read_string(handle, debug_offsets, address, buf, max_len)) { + + size_t offset = debug_offsets->unicode_object.asciiobject_size; + res = _Py_RemoteDebug_ReadRemoteMemory(handle, address + offset, len, buf); + if (res < 0) { goto err; } + buf[len] = '\0'; - result = PyUnicode_FromString(buf); + result = PyUnicode_FromStringAndSize(buf, len); if (result == NULL) { goto err; } @@ -211,10 +194,63 @@ read_py_str( return result; err: + if (buf != NULL) { + PyMem_RawFree(buf); + } + return NULL; +} + +static PyObject * +read_py_bytes( + proc_handle_t *handle, + _Py_DebugOffsets* debug_offsets, + uintptr_t address +) { + PyObject *result = NULL; + char *buf = NULL; + + Py_ssize_t len; + int res = _Py_RemoteDebug_ReadRemoteMemory( + handle, + address + debug_offsets->bytes_object.ob_size, + sizeof(Py_ssize_t), + &len + ); + if (res < 0) { + goto err; + } + + buf = (char *)PyMem_RawMalloc(len+1); + if (buf == NULL) { + PyErr_NoMemory(); + return NULL; + } + + size_t offset = debug_offsets->bytes_object.ob_sval; + res = _Py_RemoteDebug_ReadRemoteMemory(handle, address + offset, len, buf); + if (res < 0) { + goto err; + } + buf[len] = '\0'; + + result = PyBytes_FromStringAndSize(buf, len); + if (result == NULL) { + goto err; + } + PyMem_RawFree(buf); + assert(result != NULL); + return result; + +err: + if (buf != NULL) { + PyMem_RawFree(buf); + } return NULL; } + + static long read_py_long(proc_handle_t *handle, _Py_DebugOffsets* offsets, uintptr_t address) { @@ -333,6 +369,15 @@ parse_task_name( } static int +parse_frame_object( + proc_handle_t *handle, + PyObject** result, + struct _Py_DebugOffsets* offsets, + uintptr_t address, + uintptr_t* previous_frame +); + +static int parse_coro_chain( proc_handle_t *handle, struct _Py_DebugOffsets* offsets, @@ -351,22 +396,16 @@ parse_coro_chain( 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) { + PyObject* name = NULL; + uintptr_t prev_frame; + if (parse_frame_object( + handle, + &name, + offsets, + coro_address + offsets->gen_object.gi_iframe, + &prev_frame) + < 0) + { return -1; } @@ -743,49 +782,204 @@ parse_task_awaited_by( return 0; } +typedef struct +{ + int lineno; + int end_lineno; + int column; + int end_column; +} LocationInfo; + +static int +scan_varint(const uint8_t **ptr) +{ + unsigned int read = **ptr; + *ptr = *ptr + 1; + unsigned int val = read & 63; + unsigned int shift = 0; + while (read & 64) { + read = **ptr; + *ptr = *ptr + 1; + shift += 6; + val |= (read & 63) << shift; + } + return val; +} + 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( +scan_signed_varint(const uint8_t **ptr) +{ + unsigned int uval = scan_varint(ptr); + if (uval & 1) { + return -(int)(uval >> 1); + } + else { + return uval >> 1; + } +} + + +static bool +parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, LocationInfo* info) +{ + const uint8_t* ptr = (const uint8_t*)(linetable); + uint64_t addr = 0; + info->lineno = firstlineno; + + while (*ptr != '\0') { + // See InternalDocs/code_objects.md for where these magic numbers are from + // and for the decoding algorithm. + uint8_t first_byte = *(ptr++); + uint8_t code = (first_byte >> 3) & 15; + size_t length = (first_byte & 7) + 1; + uintptr_t end_addr = addr + length; + switch (code) { + case PY_CODE_LOCATION_INFO_NONE: { + break; + } + case PY_CODE_LOCATION_INFO_LONG: { + int line_delta = scan_signed_varint(&ptr); + info->lineno += line_delta; + info->end_lineno = info->lineno + scan_varint(&ptr); + info->column = scan_varint(&ptr) - 1; + info->end_column = scan_varint(&ptr) - 1; + break; + } + case PY_CODE_LOCATION_INFO_NO_COLUMNS: { + int line_delta = scan_signed_varint(&ptr); + info->lineno += line_delta; + info->column = info->end_column = -1; + break; + } + case PY_CODE_LOCATION_INFO_ONE_LINE0: + case PY_CODE_LOCATION_INFO_ONE_LINE1: + case PY_CODE_LOCATION_INFO_ONE_LINE2: { + int line_delta = code - 10; + info->lineno += line_delta; + info->end_lineno = info->lineno; + info->column = *(ptr++); + info->end_column = *(ptr++); + break; + } + default: { + uint8_t second_byte = *(ptr++); + assert((second_byte & 128) == 0); + info->column = code << 3 | (second_byte >> 4); + info->end_column = info->column + (second_byte & 15); + break; + } + } + if (addr <= addrq && end_addr > addrq) { + return true; + } + addr = end_addr; + } + return false; +} + +static int +read_remote_pointer(proc_handle_t *handle, uintptr_t address, uintptr_t *out_ptr, const char *error_message) +{ + int bytes_read = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(void *), out_ptr); + if (bytes_read < 0) { + return -1; + } + + if ((void *)(*out_ptr) == NULL) { + PyErr_SetString(PyExc_RuntimeError, error_message); + return -1; + } + + return 0; +} + +static int +read_instruction_ptr(proc_handle_t *handle, struct _Py_DebugOffsets *offsets, + uintptr_t current_frame, uintptr_t *instruction_ptr) +{ + return read_remote_pointer( handle, - address + offsets->code_object.name, - sizeof(void*), - &address_of_function_name + current_frame + offsets->interpreter_frame.instr_ptr, + instruction_ptr, + "No instruction ptr found" ); - if (bytes_read < 0) { +} + +static int +parse_code_object(proc_handle_t *handle, + PyObject **result, + struct _Py_DebugOffsets *offsets, + uintptr_t address, + uintptr_t current_frame, + uintptr_t *previous_frame) +{ + uintptr_t addr_func_name, addr_file_name, addr_linetable, instruction_ptr; + + if (read_remote_pointer(handle, address + offsets->code_object.qualname, &addr_func_name, "No function name found") < 0 || + read_remote_pointer(handle, address + offsets->code_object.filename, &addr_file_name, "No file name found") < 0 || + read_remote_pointer(handle, address + offsets->code_object.linetable, &addr_linetable, "No linetable found") < 0 || + read_instruction_ptr(handle, offsets, current_frame, &instruction_ptr) < 0) { return -1; } - if ((void*)address_of_function_name == NULL) { - PyErr_SetString(PyExc_RuntimeError, "No function name found"); + int firstlineno; + if (_Py_RemoteDebug_ReadRemoteMemory(handle, + address + offsets->code_object.firstlineno, + sizeof(int), + &firstlineno) < 0) { return -1; } - PyObject* py_function_name = read_py_str( - handle, offsets, address_of_function_name, 256); - if (py_function_name == NULL) { + PyObject *py_linetable = read_py_bytes(handle, offsets, addr_linetable); + if (!py_linetable) { + return -1; + } + + uintptr_t addr_code_adaptive = address + offsets->code_object.co_code_adaptive; + ptrdiff_t addrq = (uint16_t *)instruction_ptr - (uint16_t *)addr_code_adaptive; + + LocationInfo info; + parse_linetable(addrq, PyBytes_AS_STRING(py_linetable), firstlineno, &info); + Py_DECREF(py_linetable); // Done with linetable + + PyObject *py_line = PyLong_FromLong(info.lineno); + if (!py_line) { return -1; } - if (PyList_Append(result, py_function_name) == -1) { - Py_DECREF(py_function_name); + PyObject *py_func_name = read_py_str(handle, offsets, addr_func_name, 256); + if (!py_func_name) { + Py_DECREF(py_line); return -1; } - Py_DECREF(py_function_name); + PyObject *py_file_name = read_py_str(handle, offsets, addr_file_name, 256); + if (!py_file_name) { + Py_DECREF(py_line); + Py_DECREF(py_func_name); + return -1; + } + + PyObject *result_tuple = PyTuple_New(3); + if (!result_tuple) { + Py_DECREF(py_line); + Py_DECREF(py_func_name); + Py_DECREF(py_file_name); + return -1; + } + + PyTuple_SET_ITEM(result_tuple, 0, py_func_name); // steals ref + PyTuple_SET_ITEM(result_tuple, 1, py_file_name); // steals ref + PyTuple_SET_ITEM(result_tuple, 2, py_line); // steals ref + + *result = result_tuple; return 0; } static int parse_frame_object( proc_handle_t *handle, - PyObject* result, + PyObject** result, struct _Py_DebugOffsets* offsets, uintptr_t address, uintptr_t* previous_frame @@ -826,13 +1020,13 @@ parse_frame_object( } return parse_code_object( - handle, result, offsets, address_of_code_object, previous_frame); + handle, result, offsets, address_of_code_object, address, previous_frame); } static int parse_async_frame_object( proc_handle_t *handle, - PyObject* result, + PyObject** result, struct _Py_DebugOffsets* offsets, uintptr_t address, uintptr_t* previous_frame, @@ -882,7 +1076,7 @@ parse_async_frame_object( } if (parse_code_object( - handle, result, offsets, *code_object, previous_frame)) { + handle, result, offsets, *code_object, address, previous_frame)) { return -1; } @@ -1353,9 +1547,10 @@ get_stack_trace(PyObject* self, PyObject* args) } while ((void*)address_of_current_frame != NULL) { + PyObject* frame_info = NULL; if (parse_frame_object( handle, - result, + &frame_info, &local_debug_offsets, address_of_current_frame, &address_of_current_frame) @@ -1364,6 +1559,19 @@ get_stack_trace(PyObject* self, PyObject* args) Py_DECREF(result); goto result_err; } + + if (!frame_info) { + continue; + } + + if (PyList_Append(result, frame_info) == -1) { + Py_DECREF(result); + goto result_err; + } + + Py_DECREF(frame_info); + frame_info = NULL; + } result_err: @@ -1485,9 +1693,10 @@ get_async_stack_trace(PyObject* self, PyObject* args) uintptr_t address_of_code_object; while ((void*)address_of_current_frame != NULL) { + PyObject* frame_info = NULL; int res = parse_async_frame_object( handle, - calls, + &frame_info, &local_debug_offsets, address_of_current_frame, &address_of_current_frame, @@ -1499,6 +1708,18 @@ get_async_stack_trace(PyObject* self, PyObject* args) goto result_err; } + if (!frame_info) { + continue; + } + + if (PyList_Append(calls, frame_info) == -1) { + Py_DECREF(calls); + goto result_err; + } + + Py_DECREF(frame_info); + frame_info = NULL; + if (address_of_code_object == address_of_running_task_code_obj) { break; } |