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}