| # Copyright 2026 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import annotations |
| |
| from typing import TYPE_CHECKING |
| |
| from pydap.client import DapError |
| from shared.protocol import Response |
| from shared.protocol.async_backtrace import ( |
| COMMAND_NAME, |
| AsyncBacktraceRequest, |
| AsyncBacktraceResponse, |
| ) |
| from zxdb_dap import AsyncTaskNode, ZxdbAsyncBacktraceArguments |
| |
| __all__ = ["COMMAND_NAME", "handle"] |
| |
| if TYPE_CHECKING: |
| from daemon.daemon import Daemon |
| |
| |
| async def handle( |
| daemon: Daemon, req: AsyncBacktraceRequest |
| ) -> Response[AsyncBacktraceResponse]: |
| if not daemon.zxdb_writer: |
| return Response( |
| success=False, message="Not connected to zxdb DAP server" |
| ) |
| |
| try: |
| target_proc = None |
| if req.pid is not None: |
| target_proc = daemon.processes.get(req.pid) |
| if not target_proc: |
| return Response( |
| success=False, |
| message=f"Process {req.pid} not found", |
| ) |
| else: |
| if len(daemon.processes) == 1: |
| target_proc = next(iter(daemon.processes.values())) |
| elif len(daemon.processes) == 0: |
| return Response( |
| success=False, |
| message="No active processes found", |
| ) |
| else: |
| return Response( |
| success=False, |
| message="Multiple processes active; please specify a PID with --pid / -p", |
| ) |
| |
| target_pid = target_proc.id |
| await daemon.ensure_process_stopped(target_pid) |
| |
| target_proc = daemon.processes.get(target_pid) |
| if not target_proc: |
| return Response( |
| success=False, |
| message=f"Process {target_pid} not found after stop", |
| ) |
| |
| if not target_proc.threads: |
| try: |
| threads_resp = await daemon.dap_client.zxdb_threads() |
| if threads_resp.body and threads_resp.body.threads: |
| daemon.update_thread_cache(threads_resp.body.threads) |
| except Exception: |
| pass |
| target_proc = daemon.processes.get(target_pid) |
| |
| if not target_proc or not target_proc.threads: |
| return Response( |
| success=True, |
| body=AsyncBacktraceResponse( |
| process_id=target_pid, |
| tasks=[], |
| ), |
| ) |
| |
| # Note: we assume that any thread that can return async tasks will return the entire global |
| # set of tasks. |
| # |
| # This isn't correct in the general case, but should cover the vast majority of processes |
| # and components in practice which will not be running more than one executor in one |
| # process. It will either be a single threaded executor or a multithreaded program with some worker |
| # threads associated with the executor and possibly other threads that are wholly unrelated |
| # to any executor. In the multithreaded executor case, we assume that all threads |
| # associated with the executor share the same pool of global tasks, and that any worker |
| # thread can access that global state. |
| # |
| # Based on those assumptions, this loop terminates precisely at the first thread that |
| # returns a non-empty list of tasks, which will always correspond to the global tree of |
| # tasks that are known to the executor in both single and multithreaded environments. |
| tasks: list[AsyncTaskNode] = [] |
| for thread in list(target_proc.threads.values()): |
| try: |
| abt_resp = await daemon.dap_client.zxdb_async_backtrace( |
| ZxdbAsyncBacktraceArguments(thread_id=thread.id) |
| ) |
| if ( |
| abt_resp.success |
| and abt_resp.body |
| and abt_resp.body.tasks is not None |
| ): |
| tasks = abt_resp.body.tasks |
| if tasks: |
| break |
| except DapError: |
| continue |
| |
| return Response( |
| success=True, |
| body=AsyncBacktraceResponse( |
| process_id=target_proc.id, |
| tasks=tasks, |
| ), |
| ) |
| except Exception as e: |
| return Response( |
| success=False, |
| message=f"Failed to get async backtrace: {e}", |
| ) |