| # 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 shared.protocol.watch_request import COMMAND_NAME, WatchAccessType |
| |
| |
| class Command(BaseCommand): |
| COMMAND_NAME = COMMAND_NAME |
| ALIASES = [ |
| "watchpoint", |
| "data-breakpoint", |
| "setDataBreakpoints", |
| "set-data-breakpoints", |
| "set_data_breakpoints", |
| ] |
| |
| @staticmethod |
| def register_cli(subparsers: Any) -> None: |
| watch_parser = subparsers.add_parser( |
| Command.COMMAND_NAME, |
| aliases=Command.ALIASES, |
| help="Set or delete a data breakpoint (watchpoint)", |
| description=( |
| "Set or delete a hardware watchpoint on a variable or expression. " |
| "Passing -t/--thread-id and -a/--access-type (and optionally " |
| "--frame-index) is required when setting a watchpoint. To watch a " |
| "memory address, pass a typed dereference expression " |
| "(e.g. '*(uint32_t*)0x1234'). The expression is evaluated once " |
| "and the watchpoint stays on that fixed address for the life of " |
| "the process; a watchpoint on a local variable keeps firing after " |
| "its function returns and the stack slot is reused. Remove it " |
| "with 'watch -d <data_id>'; 'get-state' lists each active " |
| "watchpoint's data_id and expression." |
| ), |
| ) |
| watch_parser.add_argument( |
| "-t", |
| "--thread-id", |
| type=int, |
| default=None, |
| help="Thread ID whose stack frame and process should be used (required when setting a watchpoint)", |
| ) |
| watch_parser.add_argument( |
| "--frame-index", |
| type=int, |
| default=0, |
| help="Frame index within the stacktrace (defaults to 0)", |
| ) |
| watch_parser.add_argument( |
| "-a", |
| "--access-type", |
| choices=list(get_args(WatchAccessType)), |
| default=None, |
| help="Access type to break on ('write' or 'readWrite'; required when setting a watchpoint)", |
| ) |
| watch_parser.add_argument( |
| "-c", |
| "--condition", |
| type=str, |
| default=None, |
| help="Optional conditional expression for the watchpoint", |
| ) |
| watch_parser.add_argument( |
| "--hit-condition", |
| type=str, |
| default=None, |
| help="Optional hit count condition for the watchpoint", |
| ) |
| watch_parser.add_argument( |
| "-d", |
| "--delete", |
| action="store_true", |
| help="Delete the watchpoint matching the given data_id instead of adding it", |
| ) |
| watch_parser.add_argument( |
| "--clear", |
| action="store_true", |
| help="Clear all active watchpoints", |
| ) |
| watch_parser.add_argument( |
| "name", |
| nargs="*", |
| default=[], |
| help=( |
| "Variable or expression to watch; with -d/--delete, the " |
| "data_id of the watchpoint to remove" |
| ), |
| ) |
| |
| @staticmethod |
| async def execute(args: argparse.Namespace) -> int | None: |
| if isinstance(args.name, list): |
| args.name = " ".join(args.name).strip() |
| if args.clear: |
| return None |
| if args.delete: |
| if not args.name: |
| print( |
| "Error: data_id is required when deleting a watchpoint.", |
| file=sys.stderr, |
| ) |
| return 2 |
| return None |
| if not args.name: |
| print( |
| "Error: expression is required unless --clear is specified.", |
| file=sys.stderr, |
| ) |
| return 2 |
| missing = [] |
| if args.thread_id is None: |
| missing.append("-t/--thread-id") |
| if args.access_type is None: |
| missing.append("-a/--access-type") |
| if missing: |
| print( |
| f"Error: {' and '.join(missing)} required when setting a watchpoint.", |
| file=sys.stderr, |
| ) |
| return 2 |
| return None |