autocore-std 3.3.34

Standard library for AutoCore control programs - shared memory, IPC, and logging utilities
Documentation
/// System Shutdown Controller
///
/// A function block for initiating and cancelling a full system shutdown
/// via IPC. The shutdown has a 15-second delay on the server side, during
/// which it can be cancelled.
///
/// This block is **non-blocking**: calling [`initiate()`](Self::initiate) or
/// [`cancel()`](Self::cancel) sends the IPC command immediately and returns.
/// Call [`call()`](Self::call) every scan cycle to poll for the server's
/// response and update the output flags.
///
/// # Example
///
/// ```ignore
/// use autocore_std::fb::Shutdown;
///
/// struct MyProgram {
///     shutdown: Shutdown,
/// }
///
/// fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
///     self.shutdown.call(ctx.client);
///
///     // Initiate shutdown on rising edge of a button
///     if self.start_trigger.call(ctx.gm.shutdown_button) {
///         self.shutdown.initiate(ctx.client);
///     }
///
///     // Cancel if abort button is pressed
///     if self.abort_trigger.call(ctx.gm.abort_button) {
///         self.shutdown.cancel(ctx.client);
///     }
///
///     // Check status
///     if self.shutdown.done {
///         // Server accepted the command
///     }
///     if self.shutdown.error {
///         // Something went wrong — check self.shutdown.error_message
///     }
/// }
/// ```
///
/// # States
///
/// | State | `busy` | `done` | `error` | Meaning |
/// |-------|--------|--------|---------|---------|
/// | Idle | false | false | false | No operation in progress |
/// | Initiating | true | false | false | `full_shutdown` sent, awaiting response |
/// | Cancelling | true | false | false | `cancel_full_shutdown` sent, awaiting response |
/// | Done | false | true | false | Server confirmed the command |
/// | Error | false | false | true | Server returned an error |
use crate::command_client::CommandClient;

/// Operation currently in flight.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PendingOp {
    Initiate,
    Cancel,
}

#[derive(Debug, Clone)]
pub struct Shutdown {
    /// `true` while waiting for a response from the server.
    pub busy: bool,
    /// `true` for one cycle after the server confirms the command.
    pub done: bool,
    /// `true` for one cycle after the server returns an error.
    pub error: bool,
    /// Error description from the server (empty when no error).
    pub error_message: String,

    pending_tid: Option<u32>,
    pending_op: Option<PendingOp>,
}

impl Shutdown {
    /// Creates a new shutdown controller in the idle state.
    pub fn new() -> Self {
        Self {
            busy: false,
            done: false,
            error: false,
            error_message: String::new(),
            pending_tid: None,
            pending_op: None,
        }
    }

    /// Send a `system.full_shutdown` command to the server.
    ///
    /// Does nothing if an operation is already in progress (`busy` is `true`).
    /// The server will schedule a shutdown with a 15-second delay.
    pub fn initiate(&mut self, client: &mut CommandClient) {
        if self.busy {
            return;
        }
        let tid = client.send("system.full_shutdown", serde_json::json!({}));
        self.pending_tid = Some(tid);
        self.pending_op = Some(PendingOp::Initiate);
        self.busy = true;
        self.done = false;
        self.error = false;
        self.error_message.clear();
    }

    /// Send a `system.cancel_full_shutdown` command to the server.
    ///
    /// Does nothing if an operation is already in progress (`busy` is `true`).
    pub fn cancel(&mut self, client: &mut CommandClient) {
        if self.busy {
            return;
        }
        let tid = client.send("system.cancel_full_shutdown", serde_json::json!({}));
        self.pending_tid = Some(tid);
        self.pending_op = Some(PendingOp::Cancel);
        self.busy = true;
        self.done = false;
        self.error = false;
        self.error_message.clear();
    }

    /// Poll for the server response. Call this **once per scan cycle**.
    ///
    /// Updates `busy`, `done`, `error`, and `error_message` based on
    /// whether a response has arrived.
    pub fn call(&mut self, client: &mut CommandClient) {
        // Clear one-shot outputs from previous cycle
        if self.done || self.error {
            self.done = false;
            self.error = false;
            self.error_message.clear();
        }

        let tid = match self.pending_tid {
            Some(tid) => tid,
            None => return,
        };

        if let Some(response) = client.take_response(tid) {
            self.busy = false;
            self.pending_tid = None;
            self.pending_op = None;

            if response.success {
                self.done = true;
            } else {
                self.error = true;
                self.error_message = response.error_message;
            }
        }
    }

    /// Returns `true` if a shutdown initiation is currently pending.
    pub fn is_initiating(&self) -> bool {
        self.pending_op == Some(PendingOp::Initiate)
    }

    /// Returns `true` if a shutdown cancellation is currently pending.
    pub fn is_cancelling(&self) -> bool {
        self.pending_op == Some(PendingOp::Cancel)
    }
}

impl Default for Shutdown {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use mechutil::ipc::CommandMessage;
    use serde_json::json;
    use tokio::sync::mpsc;

    fn make_client() -> (CommandClient, mpsc::UnboundedSender<CommandMessage>) {
        let (write_tx, _write_rx) = mpsc::unbounded_channel();
        let (response_tx, response_rx) = mpsc::unbounded_channel();
        (CommandClient::new(write_tx, response_rx), response_tx)
    }

    #[test]
    fn test_initiate_sends_command() {
        let (mut client, _response_tx) = make_client();
        let mut shutdown = Shutdown::new();

        assert!(!shutdown.busy);
        shutdown.initiate(&mut client);

        assert!(shutdown.busy);
        assert!(shutdown.is_initiating());
        assert!(!shutdown.is_cancelling());
        assert_eq!(client.pending_count(), 1);
    }

    #[test]
    fn test_cancel_sends_command() {
        let (mut client, _response_tx) = make_client();
        let mut shutdown = Shutdown::new();

        shutdown.cancel(&mut client);

        assert!(shutdown.busy);
        assert!(shutdown.is_cancelling());
        assert!(!shutdown.is_initiating());
        assert_eq!(client.pending_count(), 1);
    }

    #[test]
    fn test_ignores_while_busy() {
        let (mut client, _response_tx) = make_client();
        let mut shutdown = Shutdown::new();

        shutdown.initiate(&mut client);
        assert_eq!(client.pending_count(), 1);

        // Second initiate should be ignored
        shutdown.initiate(&mut client);
        assert_eq!(client.pending_count(), 1);

        // Cancel should also be ignored while busy
        shutdown.cancel(&mut client);
        assert_eq!(client.pending_count(), 1);
    }

    #[test]
    fn test_success_response() {
        let (mut client, response_tx) = make_client();
        let mut shutdown = Shutdown::new();

        shutdown.initiate(&mut client);
        let tid = shutdown.pending_tid.unwrap();

        // Simulate server response
        response_tx.send(CommandMessage::response(tid, json!({"status": "shutdown_scheduled"}))).unwrap();
        client.poll();

        shutdown.call(&mut client);

        assert!(!shutdown.busy);
        assert!(shutdown.done);
        assert!(!shutdown.error);
    }

    #[test]
    fn test_error_response() {
        let (mut client, response_tx) = make_client();
        let mut shutdown = Shutdown::new();

        shutdown.initiate(&mut client);
        let tid = shutdown.pending_tid.unwrap();

        // Simulate error response
        let mut resp = CommandMessage::response(tid, json!(null));
        resp.success = false;
        resp.error_message = "Shutdown already scheduled".to_string();
        response_tx.send(resp).unwrap();
        client.poll();

        shutdown.call(&mut client);

        assert!(!shutdown.busy);
        assert!(!shutdown.done);
        assert!(shutdown.error);
        assert_eq!(shutdown.error_message, "Shutdown already scheduled");
    }

    #[test]
    fn test_done_clears_after_one_cycle() {
        let (mut client, response_tx) = make_client();
        let mut shutdown = Shutdown::new();

        shutdown.initiate(&mut client);
        let tid = shutdown.pending_tid.unwrap();

        response_tx.send(CommandMessage::response(tid, json!({"status": "shutdown_scheduled"}))).unwrap();
        client.poll();

        // First call: done = true
        shutdown.call(&mut client);
        assert!(shutdown.done);

        // Second call: done cleared
        shutdown.call(&mut client);
        assert!(!shutdown.done);
    }

    #[test]
    fn test_cancel_after_initiate_completes() {
        let (mut client, response_tx) = make_client();
        let mut shutdown = Shutdown::new();

        // Initiate
        shutdown.initiate(&mut client);
        let tid1 = shutdown.pending_tid.unwrap();

        response_tx.send(CommandMessage::response(tid1, json!({"status": "shutdown_scheduled"}))).unwrap();
        client.poll();
        shutdown.call(&mut client);
        assert!(shutdown.done);

        // Clear one-shot
        shutdown.call(&mut client);

        // Now cancel
        shutdown.cancel(&mut client);
        assert!(shutdown.busy);
        assert!(shutdown.is_cancelling());

        let tid2 = shutdown.pending_tid.unwrap();
        response_tx.send(CommandMessage::response(tid2, json!({"status": "shutdown_cancelled"}))).unwrap();
        client.poll();
        shutdown.call(&mut client);

        assert!(shutdown.done);
        assert!(!shutdown.busy);
    }
}