Skip to main content

autocore_std/fb/
shutdown.rs

1/// System Shutdown Controller
2///
3/// A function block for initiating and cancelling a full system shutdown
4/// via IPC. The shutdown has a 15-second delay on the server side, during
5/// which it can be cancelled.
6///
7/// This block is **non-blocking**: calling [`initiate()`](Self::initiate) or
8/// [`cancel()`](Self::cancel) sends the IPC command immediately and returns.
9/// Call [`call()`](Self::call) every scan cycle to poll for the server's
10/// response and update the output flags.
11///
12/// # Example
13///
14/// ```ignore
15/// use autocore_std::fb::Shutdown;
16///
17/// struct MyProgram {
18///     shutdown: Shutdown,
19/// }
20///
21/// fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
22///     self.shutdown.call(ctx.client);
23///
24///     // Initiate shutdown on rising edge of a button
25///     if self.start_trigger.call(ctx.gm.shutdown_button) {
26///         self.shutdown.initiate(ctx.client);
27///     }
28///
29///     // Cancel if abort button is pressed
30///     if self.abort_trigger.call(ctx.gm.abort_button) {
31///         self.shutdown.cancel(ctx.client);
32///     }
33///
34///     // Check status
35///     if self.shutdown.done {
36///         // Server accepted the command
37///     }
38///     if self.shutdown.error {
39///         // Something went wrong — check self.shutdown.error_message
40///     }
41/// }
42/// ```
43///
44/// # States
45///
46/// | State | `busy` | `done` | `error` | Meaning |
47/// |-------|--------|--------|---------|---------|
48/// | Idle | false | false | false | No operation in progress |
49/// | Initiating | true | false | false | `full_shutdown` sent, awaiting response |
50/// | Cancelling | true | false | false | `cancel_full_shutdown` sent, awaiting response |
51/// | Done | false | true | false | Server confirmed the command |
52/// | Error | false | false | true | Server returned an error |
53use crate::command_client::CommandClient;
54
55/// Operation currently in flight.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57enum PendingOp {
58    Initiate,
59    Cancel,
60}
61
62#[derive(Debug, Clone)]
63pub struct Shutdown {
64    /// `true` while waiting for a response from the server.
65    pub busy: bool,
66    /// `true` for one cycle after the server confirms the command.
67    pub done: bool,
68    /// `true` for one cycle after the server returns an error.
69    pub error: bool,
70    /// Error description from the server (empty when no error).
71    pub error_message: String,
72
73    pending_tid: Option<u32>,
74    pending_op: Option<PendingOp>,
75}
76
77impl Shutdown {
78    /// Creates a new shutdown controller in the idle state.
79    pub fn new() -> Self {
80        Self {
81            busy: false,
82            done: false,
83            error: false,
84            error_message: String::new(),
85            pending_tid: None,
86            pending_op: None,
87        }
88    }
89
90    /// Send a `system.full_shutdown` command to the server.
91    ///
92    /// Does nothing if an operation is already in progress (`busy` is `true`).
93    /// The server will schedule a shutdown with a 15-second delay.
94    pub fn initiate(&mut self, client: &mut CommandClient) {
95        if self.busy {
96            return;
97        }
98        let tid = client.send("system.full_shutdown", serde_json::json!({}));
99        self.pending_tid = Some(tid);
100        self.pending_op = Some(PendingOp::Initiate);
101        self.busy = true;
102        self.done = false;
103        self.error = false;
104        self.error_message.clear();
105    }
106
107    /// Send a `system.cancel_full_shutdown` command to the server.
108    ///
109    /// Does nothing if an operation is already in progress (`busy` is `true`).
110    pub fn cancel(&mut self, client: &mut CommandClient) {
111        if self.busy {
112            return;
113        }
114        let tid = client.send("system.cancel_full_shutdown", serde_json::json!({}));
115        self.pending_tid = Some(tid);
116        self.pending_op = Some(PendingOp::Cancel);
117        self.busy = true;
118        self.done = false;
119        self.error = false;
120        self.error_message.clear();
121    }
122
123    /// Poll for the server response. Call this **once per scan cycle**.
124    ///
125    /// Updates `busy`, `done`, `error`, and `error_message` based on
126    /// whether a response has arrived.
127    pub fn call(&mut self, client: &mut CommandClient) {
128        // Clear one-shot outputs from previous cycle
129        if self.done || self.error {
130            self.done = false;
131            self.error = false;
132            self.error_message.clear();
133        }
134
135        let tid = match self.pending_tid {
136            Some(tid) => tid,
137            None => return,
138        };
139
140        if let Some(response) = client.take_response(tid) {
141            self.busy = false;
142            self.pending_tid = None;
143            self.pending_op = None;
144
145            if response.success {
146                self.done = true;
147            } else {
148                self.error = true;
149                self.error_message = response.error_message;
150            }
151        }
152    }
153
154    /// Returns `true` if a shutdown initiation is currently pending.
155    pub fn is_initiating(&self) -> bool {
156        self.pending_op == Some(PendingOp::Initiate)
157    }
158
159    /// Returns `true` if a shutdown cancellation is currently pending.
160    pub fn is_cancelling(&self) -> bool {
161        self.pending_op == Some(PendingOp::Cancel)
162    }
163}
164
165impl Default for Shutdown {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use mechutil::ipc::CommandMessage;
175    use serde_json::json;
176    use tokio::sync::mpsc;
177
178    fn make_client() -> (CommandClient, mpsc::UnboundedSender<CommandMessage>) {
179        let (write_tx, _write_rx) = mpsc::unbounded_channel();
180        let (response_tx, response_rx) = mpsc::unbounded_channel();
181        (CommandClient::new(write_tx, response_rx), response_tx)
182    }
183
184    #[test]
185    fn test_initiate_sends_command() {
186        let (mut client, _response_tx) = make_client();
187        let mut shutdown = Shutdown::new();
188
189        assert!(!shutdown.busy);
190        shutdown.initiate(&mut client);
191
192        assert!(shutdown.busy);
193        assert!(shutdown.is_initiating());
194        assert!(!shutdown.is_cancelling());
195        assert_eq!(client.pending_count(), 1);
196    }
197
198    #[test]
199    fn test_cancel_sends_command() {
200        let (mut client, _response_tx) = make_client();
201        let mut shutdown = Shutdown::new();
202
203        shutdown.cancel(&mut client);
204
205        assert!(shutdown.busy);
206        assert!(shutdown.is_cancelling());
207        assert!(!shutdown.is_initiating());
208        assert_eq!(client.pending_count(), 1);
209    }
210
211    #[test]
212    fn test_ignores_while_busy() {
213        let (mut client, _response_tx) = make_client();
214        let mut shutdown = Shutdown::new();
215
216        shutdown.initiate(&mut client);
217        assert_eq!(client.pending_count(), 1);
218
219        // Second initiate should be ignored
220        shutdown.initiate(&mut client);
221        assert_eq!(client.pending_count(), 1);
222
223        // Cancel should also be ignored while busy
224        shutdown.cancel(&mut client);
225        assert_eq!(client.pending_count(), 1);
226    }
227
228    #[test]
229    fn test_success_response() {
230        let (mut client, response_tx) = make_client();
231        let mut shutdown = Shutdown::new();
232
233        shutdown.initiate(&mut client);
234        let tid = shutdown.pending_tid.unwrap();
235
236        // Simulate server response
237        response_tx.send(CommandMessage::response(tid, json!({"status": "shutdown_scheduled"}))).unwrap();
238        client.poll();
239
240        shutdown.call(&mut client);
241
242        assert!(!shutdown.busy);
243        assert!(shutdown.done);
244        assert!(!shutdown.error);
245    }
246
247    #[test]
248    fn test_error_response() {
249        let (mut client, response_tx) = make_client();
250        let mut shutdown = Shutdown::new();
251
252        shutdown.initiate(&mut client);
253        let tid = shutdown.pending_tid.unwrap();
254
255        // Simulate error response
256        let mut resp = CommandMessage::response(tid, json!(null));
257        resp.success = false;
258        resp.error_message = "Shutdown already scheduled".to_string();
259        response_tx.send(resp).unwrap();
260        client.poll();
261
262        shutdown.call(&mut client);
263
264        assert!(!shutdown.busy);
265        assert!(!shutdown.done);
266        assert!(shutdown.error);
267        assert_eq!(shutdown.error_message, "Shutdown already scheduled");
268    }
269
270    #[test]
271    fn test_done_clears_after_one_cycle() {
272        let (mut client, response_tx) = make_client();
273        let mut shutdown = Shutdown::new();
274
275        shutdown.initiate(&mut client);
276        let tid = shutdown.pending_tid.unwrap();
277
278        response_tx.send(CommandMessage::response(tid, json!({"status": "shutdown_scheduled"}))).unwrap();
279        client.poll();
280
281        // First call: done = true
282        shutdown.call(&mut client);
283        assert!(shutdown.done);
284
285        // Second call: done cleared
286        shutdown.call(&mut client);
287        assert!(!shutdown.done);
288    }
289
290    #[test]
291    fn test_cancel_after_initiate_completes() {
292        let (mut client, response_tx) = make_client();
293        let mut shutdown = Shutdown::new();
294
295        // Initiate
296        shutdown.initiate(&mut client);
297        let tid1 = shutdown.pending_tid.unwrap();
298
299        response_tx.send(CommandMessage::response(tid1, json!({"status": "shutdown_scheduled"}))).unwrap();
300        client.poll();
301        shutdown.call(&mut client);
302        assert!(shutdown.done);
303
304        // Clear one-shot
305        shutdown.call(&mut client);
306
307        // Now cancel
308        shutdown.cancel(&mut client);
309        assert!(shutdown.busy);
310        assert!(shutdown.is_cancelling());
311
312        let tid2 = shutdown.pending_tid.unwrap();
313        response_tx.send(CommandMessage::response(tid2, json!({"status": "shutdown_cancelled"}))).unwrap();
314        client.poll();
315        shutdown.call(&mut client);
316
317        assert!(shutdown.done);
318        assert!(!shutdown.busy);
319    }
320}