blob: 911031a5682806a09029ac3b64458d0187ec4b78 [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.
import argparse
import sys
from typing import Any, get_args
from cli.commands.base import BaseCommand
from cli.commands.location_utils import parse_and_resolve_file_line
from shared.protocol.tracepoint import COMMAND_NAME, CaptureItem
from shared.protocol.watch_request import WatchAccessType
def _has_cli_capture_or_watch_flags(args: argparse.Namespace) -> bool:
"""Returns True if any capture, watch, or threshold CLI flag is non-default."""
return (
getattr(args, "collect_locals", False)
or getattr(args, "collect_registers", False)
or getattr(args, "collect_backtrace", False)
or getattr(args, "raw_backtrace", False)
or args.condition is not None
or bool(args.expressions)
or args.size is not None
or args.access_type != "write"
or args.thread_id is not None
or args.frame_index != 0
or args.max_matching_frames != 10
or args.max_hits != 20
)
def _pop_capture_flags(args: argparse.Namespace) -> list[CaptureItem]:
"""Converts CLI `--locals`/`--registers`/`--backtrace` flags into `capture`."""
capture: list[CaptureItem] = ["frame"]
if getattr(args, "collect_backtrace", False) or getattr(
args, "raw_backtrace", False
):
capture.append("backtrace")
if getattr(args, "collect_locals", False):
capture.append("locals")
if getattr(args, "collect_registers", False):
capture.append("registers")
for attr in (
"collect_locals",
"collect_registers",
"collect_backtrace",
):
if hasattr(args, attr):
delattr(args, attr)
return capture
def _apply_explicit_file_line(
args: argparse.Namespace,
target: str | None,
file_flag: str,
line_flag: int,
) -> int | None:
"""Validates and resolves explicit `--file` and `--line` CLI flags."""
if (
target is not None
or args.function is not None
or args.watch is not None
):
print(
"Error: Cannot combine --file/--line with positional target,"
" --function, or --watch.",
file=sys.stderr,
)
return 1
parsed = parse_and_resolve_file_line(f"{file_flag}:{line_flag}")
if parsed is None:
return 1
args.file, args.line = parsed
return None
def _apply_target_flags(
args: argparse.Namespace,
target: str | None,
file_flag: str | None,
line_flag: int | None,
) -> int | None:
"""Resolves positional and flag-based target specs into args.file/line/function."""
if (file_flag is not None) != (line_flag is not None):
print(
"Error: Both --file and --line must be specified together.",
file=sys.stderr,
)
return 1
if file_flag is not None and line_flag is not None:
return _apply_explicit_file_line(args, target, file_flag, line_flag)
if args.function is not None or args.watch is not None:
if target is not None or (
args.function is not None and args.watch is not None
):
print(
"Error: Cannot specify multiple targets (positional target,"
" --function, or --watch).",
file=sys.stderr,
)
return 1
return None
if target is None:
print(
"Error: Must specify target '<file>:<line>', '--file <path> --line"
" <n>', function symbol, '--watch <expr>', '--list', or '--clear'.",
file=sys.stderr,
)
return 1
if ":" in target and "::" not in target:
parsed = parse_and_resolve_file_line(target)
if parsed is None:
return 1
args.file, args.line = parsed
else:
args.function = target
return None
def _validate_management_cli_flags(
args: argparse.Namespace,
target: str | None,
file_flag: str | None,
line_flag: int | None,
has_extra_opts: bool,
) -> tuple[bool, int | None]:
"""Validates `--list`, `--clear`, `--enable`/`--disable`, and `--delete` flags."""
has_target = any(
val is not None
for val in (target, file_flag, line_flag, args.function, args.watch)
)
mode_count = sum(
[args.list_all, args.clear, args.delete, args.enabled is not None]
)
if mode_count > 1:
print(
"Error: Cannot combine --list, --clear, --delete, --enable, or"
" --disable.",
file=sys.stderr,
)
return True, 1
if args.list_all or args.clear:
if args.tracepoint_id is not None or has_target or has_extra_opts:
print(
"Error: --list and --clear cannot be combined with --id,"
" target, or capture options.",
file=sys.stderr,
)
return True, 1
return True, None
if args.enabled is not None:
if args.tracepoint_id is None:
print(
"Error: --id (or positional ID) is required with"
" --enable/--disable.",
file=sys.stderr,
)
return True, 1
if has_target or has_extra_opts:
print(
"Error: --enable/--disable cannot be combined with target or"
" capture options.",
file=sys.stderr,
)
return True, 1
return True, None
if args.delete:
if has_extra_opts:
print(
"Error: --delete cannot be combined with capture or watch"
" options.",
file=sys.stderr,
)
return True, 1
if args.tracepoint_id is not None:
if has_target:
print(
"Error: Cannot combine --id with a target when deleting.",
file=sys.stderr,
)
return True, 1
return True, None
return False, None
def _add_target_arguments(tp_parser: argparse.ArgumentParser) -> None:
"""Registers target and watchpoint arguments on `tp_parser`."""
tp_parser.add_argument(
"target",
nargs="?",
default=None,
type=str,
help="Target '<file>:<line>' or function symbol (e.g. 'foo::Bar::Baz')",
)
tp_parser.add_argument(
"-f",
"--function",
default=None,
type=str,
help="Explicit function symbol name",
)
tp_parser.add_argument(
"--file",
dest="file_flag",
default=None,
type=str,
help="Source file path (requires --line)",
)
tp_parser.add_argument(
"--line",
dest="line_flag",
default=None,
type=int,
help="1-based source line number (requires --file)",
)
tp_parser.add_argument(
"-w",
"--watch",
default=None,
type=str,
help="Expression or hex address for a hardware watchpoint tracepoint",
)
tp_parser.add_argument(
"--size",
default=None,
type=int,
help="Byte size for hardware watchpoint (e.g. 1, 2, 4, 8)",
)
tp_parser.add_argument(
"-a",
"--access-type",
default="write",
choices=list(get_args(WatchAccessType)),
help="Access type for hardware watchpoint (default: write)",
)
tp_parser.add_argument(
"-t",
"--thread-id",
default=None,
type=int,
help="Thread ID for evaluating watchpoint expression",
)
tp_parser.add_argument(
"--frame-index",
default=0,
type=int,
help="Stack frame index for watchpoint expression (default: 0)",
)
def _add_capture_arguments(tp_parser: argparse.ArgumentParser) -> None:
"""Registers condition, capture, and hit-budget arguments on `tp_parser`."""
tp_parser.add_argument(
"-c",
"--condition",
default=None,
type=str,
help="Condition expression required for tracepoint to trigger",
)
tp_parser.add_argument(
"-e",
"--expr",
"--expression",
dest="expressions",
action="append",
default=[],
help="Expression to evaluate on hit (may be passed multiple times)",
)
tp_parser.add_argument(
"--locals",
dest="collect_locals",
action="store_true",
default=False,
help="Capture local variables and arguments on hit",
)
tp_parser.add_argument(
"--registers",
dest="collect_registers",
action="store_true",
default=False,
help="Capture CPU registers on hit",
)
tp_parser.add_argument(
"--backtrace",
dest="collect_backtrace",
action="store_true",
default=False,
help="Capture multi-frame backtrace on hit",
)
tp_parser.add_argument(
"-r",
"--raw-backtrace",
action="store_true",
default=False,
help="Do not filter or elide stack frames when capturing backtrace",
)
tp_parser.add_argument(
"--max-frames",
"--max-matching-frames",
dest="max_matching_frames",
default=10,
type=int,
help="Maximum matching stack frames to capture (default: 10)",
)
tp_parser.add_argument(
"--max-hits",
default=20,
type=int,
help="Auto-disable tracepoint after this many hits (default: 20)",
)
def _add_management_arguments(tp_parser: argparse.ArgumentParser) -> None:
"""Registers delete, clear, enable, disable, ID, and list flags."""
tp_parser.add_argument(
"-d",
"--delete",
action="store_true",
default=False,
help="Delete the matching tracepoint",
)
tp_parser.add_argument(
"--clear",
action="store_true",
default=False,
help="Clear all active tracepoints",
)
tp_parser.add_argument(
"--enable",
dest="enable_flag",
action="store_true",
default=False,
help="Enable tracepoint matching --id",
)
tp_parser.add_argument(
"--disable",
dest="disable_flag",
action="store_true",
default=False,
help="Disable tracepoint matching --id",
)
tp_parser.add_argument(
"--id",
dest="tracepoint_id",
default=None,
type=int,
help="Tracepoint ID to update, delete, enable, or disable",
)
tp_parser.add_argument(
"--list",
dest="list_all",
action="store_true",
default=False,
help="List all configured tracepoints",
)
class Command(BaseCommand):
"""CLI command definition for `tracepoint`."""
COMMAND_NAME = COMMAND_NAME
ALIASES = ["tp", "trace"]
@staticmethod
def register_cli(subparsers: Any) -> None:
"""Registers the `tracepoint` subparser and arguments."""
tp_parser = subparsers.add_parser(
Command.COMMAND_NAME,
aliases=Command.ALIASES,
help="Set, manage, delete, or list non-stopping tracepoints",
description=(
"Configure non-stopping tracepoints at a <file>:<line>,"
" function symbol, or watchpoint. When hit, the backend"
" captures requested state and immediately resumes the thread."
),
)
_add_target_arguments(tp_parser)
_add_capture_arguments(tp_parser)
_add_management_arguments(tp_parser)
@staticmethod
async def execute(args: argparse.Namespace) -> int | None:
"""Validates and normalizes `tracepoint` CLI arguments."""
if not hasattr(args, "target") and not hasattr(args, "collect_locals"):
return None
enable_flag = getattr(args, "enable_flag", False)
disable_flag = getattr(args, "disable_flag", False)
for attr in ("enable_flag", "disable_flag"):
if hasattr(args, attr):
delattr(args, attr)
if enable_flag and disable_flag:
print(
"Error: Cannot specify both --enable and --disable.",
file=sys.stderr,
)
return 1
args.enabled = (
True if enable_flag else (False if disable_flag else None)
)
has_extra_opts = _has_cli_capture_or_watch_flags(args)
args.capture = _pop_capture_flags(args)
target = getattr(args, "target", None)
file_flag = getattr(args, "file_flag", None)
line_flag = getattr(args, "line_flag", None)
for attr in ("target", "file_flag", "line_flag"):
if hasattr(args, attr):
delattr(args, attr)
args.file = None
args.line = None
if (
(args.delete or args.enabled is not None)
and args.tracepoint_id is None
and target is not None
and target.isdigit()
):
args.tracepoint_id = int(target)
target = None
handled, err_code = _validate_management_cli_flags(
args, target, file_flag, line_flag, has_extra_opts
)
if handled:
return err_code
return _apply_target_flags(args, target, file_flag, line_flag)