blob: e3d4c1c43028a73cb1cf79c8d1a9fbab15835720 [file] [edit]
# 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.models import StackTraceArguments
from shared.protocol.base import Response
from shared.protocol.stack_trace import (
COMMAND_NAME,
ProcessStackTraceResponse,
StackFrame,
StackTraceRequest,
ThreadStackTraceResponse,
)
__all__ = ["COMMAND_NAME", "handle"]
if TYPE_CHECKING:
from daemon.daemon import Daemon
def _get_frame_origin(frame: StackFrame) -> str | None:
return frame.source.origin if frame.source is not None else None
def collapse_elided_frames(
frames: list[StackFrame],
) -> list[StackFrame]:
result = []
i = 0
n = len(frames)
while i < n:
frame = frames[i]
if frame.presentation_hint == "subtle":
start_i = i
group_origin = _get_frame_origin(frame)
while (
i + 1 < n
and frames[i + 1].presentation_hint == "subtle"
and _get_frame_origin(frames[i + 1]) == group_origin
):
i += 1
end_i = i
if start_i == end_i:
result.append(frame)
else:
desc = group_origin or "subtle frames"
collapsed_frame = frame.model_copy(
update={
"frame_index": start_i,
"name": f"{start_i}…{end_i} «{desc}» (-r expands)",
}
)
result.append(collapsed_frame)
else:
result.append(frame)
i += 1
return result
async def _fetch_thread_stack_trace(
daemon: Daemon,
thread_id: int,
raw: bool = False,
) -> ThreadStackTraceResponse:
await daemon.ensure_stopped(thread_id)
stack_resp = await daemon.dap_client.stack_trace(
StackTraceArguments(
thread_id=thread_id,
),
)
frames: list[StackFrame] = []
total_frames = 0
if stack_resp.body:
raw_frames = stack_resp.body.stack_frames
total_frames = (
stack_resp.body.total_frames
if stack_resp.body.total_frames is not None
else len(raw_frames)
)
for i, f in enumerate(raw_frames):
frames.append(
StackFrame(
frame_index=i,
name=f.name,
line=f.line,
column=f.column,
source=f.source,
presentation_hint=f.presentation_hint,
)
)
if not raw:
frames = collapse_elided_frames(frames)
return ThreadStackTraceResponse(
thread_id=thread_id,
stack_frames=frames,
total_frames=total_frames,
)
async def handle(
daemon: Daemon, req: StackTraceRequest
) -> Response[ThreadStackTraceResponse | ProcessStackTraceResponse]:
if not daemon.zxdb_writer:
return Response(
success=False, message="Not connected to zxdb DAP server"
)
try:
if req.thread_id is not None:
thread_trace = await _fetch_thread_stack_trace(
daemon, req.thread_id, raw=req.raw
)
return Response(
success=True,
body=thread_trace,
)
if req.pid is None:
return Response(
success=False,
message="Must specify either thread_id or pid",
)
await daemon.ensure_process_stopped(req.pid)
process = daemon.processes.get(req.pid)
if not process or not process.threads:
return Response(
success=False,
message=f"No threads found for process {req.pid}",
)
threads_traces: list[ThreadStackTraceResponse] = []
for t in process.threads.values():
try:
t_trace = await _fetch_thread_stack_trace(
daemon, t.id, raw=req.raw
)
threads_traces.append(t_trace)
except Exception:
threads_traces.append(
ThreadStackTraceResponse(
thread_id=t.id,
stack_frames=[],
total_frames=0,
)
)
return Response(
success=True,
body=ProcessStackTraceResponse(
process_id=req.pid,
stacks=threads_traces,
),
)
except Exception as e:
return Response(
success=False, message=f"Failed to get stack trace: {e}"
)