Skip to main content

tag_stop/
tag-stop.rs

1//! `tag-stop`: stop a running tag and print the resulting status.
2//!
3//! Port of `host/commandline/tag-stop.cc`. Waits for the tag to report a
4//! definite state after the attach reset, sends the stop, then polls until
5//! the tag reaches FINISHED or ABORTED: a stop is posted and acknowledged
6//! before the state machine acts on it, so an immediate status read can
7//! still say RUNNING.
8//!
9//! Exits 0 once the tag is stopped, with its status printed; 1 if the
10//! attach, a status read or the stop fails, the tag never settles, or it
11//! does not stop within `--stop-timeout`.
12//!
13//! Usage: `tag-stop [-d] [-b BUS:DEVICE] [--stop-timeout S]
14//! [--settle-timeout S]` (defaults 30 and 10 s). Status is polled every 2 s,
15//! `ceil(stop-timeout / 2)` times; with `--stop-timeout 0` it is not polled
16//! at all and the tool fails. Option errors exit 2 (clap).
17//!
18//! # Hardware side effects
19//!
20//! The attach resets the tag, and the stop ends its run, leaving the stored
21//! data in place for a download. A tag that is not acquiring (IDLE, for
22//! example) refuses the stop and the tool exits 1. The status is printed
23//! even when the tag fails to stop, for diagnosis.
24
25use std::process::ExitCode;
26use std::thread::sleep;
27use std::time::Duration;
28
29use clap::Parser;
30use tag_tools::{BaseArgs, exit_on_interrupt, init_logging, select_base, state_name};
31use tagcore::Tag;
32use tagcore::proto::{self, Status, TagState};
33
34/// stop a running tag and print the resulting status
35#[derive(Parser)]
36#[command(name = "tag-stop")]
37struct Cli {
38    /// The shared `--debug` and `--base` options.
39    #[command(flatten)]
40    base: BaseArgs,
41
42    /// Seconds to wait for the tag to actually reach a stopped state. The stop
43    /// request is posted and acknowledged before the state machine acts on it
44    #[arg(long, default_value_t = 30, value_name = "SECONDS")]
45    stop_timeout: u64,
46
47    /// Seconds to wait after attach for the tag to report a definite state.
48    /// Attach connects under reset, so the first status can legitimately be
49    /// STATE_UNSPECIFIED while the tag boots
50    #[arg(long, default_value_t = 10, value_name = "SECONDS")]
51    settle_timeout: u64,
52}
53
54/// Prints `message` to stderr and returns exit status 1.
55fn fail(message: impl std::fmt::Display) -> ExitCode {
56    eprintln!("{message}");
57    ExitCode::FAILURE
58}
59
60/// Writes `message` to stderr, adding a newline unless it ends with one.
61fn eprint_line(message: &str) {
62    if message.ends_with('\n') {
63        eprint!("{message}");
64    } else {
65        eprintln!("{message}");
66    }
67}
68
69/// Polls status a second apart until the tag reports a state other than
70/// STATE_UNSPECIFIED. A booting tag refuses a stop ("Monitor request not
71/// permitted in current tag state"), so the stop waits for this.
72fn settle(tag: &Tag, timeout_s: u64) -> Result<(), String> {
73    const SETTLE: Duration = Duration::from_secs(1);
74    for _ in 0..timeout_s {
75        let status = tag
76            .status()
77            .map_err(|_| format!("GetStatus failed: {}", tag.debug_message()))?;
78        if status.state != TagState::StateUnspecified as i32 {
79            return Ok(());
80        }
81        sleep(SETTLE);
82    }
83    Err(format!(
84        "tag still reports STATE_UNSPECIFIED after {timeout_s} s; it is not merely settling"
85    ))
86}
87
88/// Attaches, settles, stops the tag and polls for FINISHED or ABORTED.
89fn main() -> ExitCode {
90    let cli = Cli::parse();
91    init_logging(&cli.base);
92
93    let tag = Tag::new();
94    let dev = select_base::<Cli>(&tag, &cli.base);
95    if !tag.attach(&dev) {
96        return fail("Attach failed");
97    }
98    exit_on_interrupt();
99
100    if let Err(message) = settle(&tag, cli.settle_timeout) {
101        return fail(message);
102    }
103
104    if tag.stop().is_err() {
105        let message = tag.debug_message();
106        if !message.is_empty() {
107            eprint_line(&message);
108        }
109        return fail("Stop failed");
110    }
111
112    // Poll for a terminal state, a few well-spaced tries rather than many
113    // rapid ones: each poll is a request the tag must service while it is
114    // doing the work being waited on.
115    const POLL: Duration = Duration::from_secs(2);
116    let tries = cli.stop_timeout.div_ceil(2);
117    let mut status = Status::default();
118    let mut stopped = false;
119    for _ in 0..tries {
120        status = match tag.status() {
121            Ok(status) => status,
122            Err(_) => {
123                return fail(format!(
124                    "Could not read tag status: {}",
125                    tag.debug_message()
126                ));
127            }
128        };
129        if status.state == TagState::Finished as i32 || status.state == TagState::Aborted as i32 {
130            stopped = true;
131            break;
132        }
133        sleep(POLL);
134    }
135    if !stopped {
136        eprintln!(
137            "Tag did not reach a stopped state within {} s; last state {}",
138            cli.stop_timeout,
139            state_name(status.state)
140        );
141        println!("{}", proto::debug_string(&status));
142        return ExitCode::FAILURE;
143    }
144
145    if !status.debug_message.is_empty() {
146        eprint_line(&status.debug_message);
147    }
148    println!("{}", proto::debug_string(&status));
149    ExitCode::SUCCESS
150}