rust_expect/session/
lifecycle.rs

1//! Session lifecycle management.
2//!
3//! This module provides utilities for managing session lifecycle,
4//! including graceful shutdown, signal handling, and cleanup.
5
6use std::time::Duration;
7
8use crate::types::{ControlChar, ProcessExitStatus, SessionState};
9
10/// Shutdown strategy for closing a session.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ShutdownStrategy {
13    /// Send exit command and wait for graceful shutdown.
14    Graceful,
15    /// Send SIGTERM (or equivalent) and wait.
16    Terminate,
17    /// Send SIGKILL (or equivalent) immediately.
18    Kill,
19    /// Try graceful, then terminate, then kill.
20    #[default]
21    Escalating,
22}
23
24/// Configuration for session shutdown.
25#[derive(Debug, Clone)]
26pub struct ShutdownConfig {
27    /// The shutdown strategy to use.
28    pub strategy: ShutdownStrategy,
29    /// Timeout for graceful shutdown.
30    pub graceful_timeout: Duration,
31    /// Timeout for terminate signal.
32    pub terminate_timeout: Duration,
33    /// Exit command to send for graceful shutdown.
34    pub exit_command: Option<String>,
35    /// Whether to wait for process to exit.
36    pub wait_for_exit: bool,
37}
38
39impl Default for ShutdownConfig {
40    fn default() -> Self {
41        Self {
42            strategy: ShutdownStrategy::Escalating,
43            graceful_timeout: Duration::from_secs(5),
44            terminate_timeout: Duration::from_secs(3),
45            exit_command: Some("exit".to_string()),
46            wait_for_exit: true,
47        }
48    }
49}
50
51impl ShutdownConfig {
52    /// Create a new shutdown config with graceful strategy.
53    #[must_use]
54    pub fn graceful() -> Self {
55        Self {
56            strategy: ShutdownStrategy::Graceful,
57            ..Default::default()
58        }
59    }
60
61    /// Create a new shutdown config with kill strategy.
62    #[must_use]
63    pub fn kill() -> Self {
64        Self {
65            strategy: ShutdownStrategy::Kill,
66            wait_for_exit: false,
67            ..Default::default()
68        }
69    }
70
71    /// Create a new shutdown config with custom exit command.
72    #[must_use]
73    pub fn with_exit_command(mut self, command: impl Into<String>) -> Self {
74        self.exit_command = Some(command.into());
75        self
76    }
77
78    /// Set the graceful timeout.
79    #[must_use]
80    pub const fn with_graceful_timeout(mut self, timeout: Duration) -> Self {
81        self.graceful_timeout = timeout;
82        self
83    }
84}
85
86/// Lifecycle events that can occur during a session.
87#[derive(Debug, Clone)]
88pub enum LifecycleEvent {
89    /// Session started.
90    Started,
91    /// Session became ready (e.g., shell prompt appeared).
92    Ready,
93    /// Session state changed.
94    StateChanged(SessionState),
95    /// Session is shutting down.
96    ShuttingDown,
97    /// Session closed normally.
98    Closed,
99    /// Session exited with status.
100    Exited(ProcessExitStatus),
101    /// Session encountered an error.
102    Error(String),
103}
104
105/// Callback type for lifecycle events.
106pub type LifecycleCallback = Box<dyn Fn(LifecycleEvent) + Send + Sync>;
107
108/// Manager for session lifecycle events.
109pub struct LifecycleManager {
110    /// Registered callbacks.
111    callbacks: Vec<LifecycleCallback>,
112    /// Current state.
113    state: SessionState,
114    /// Shutdown configuration.
115    shutdown_config: ShutdownConfig,
116}
117
118impl LifecycleManager {
119    /// Create a new lifecycle manager.
120    #[must_use]
121    pub fn new() -> Self {
122        Self {
123            callbacks: Vec::new(),
124            state: SessionState::Starting,
125            shutdown_config: ShutdownConfig::default(),
126        }
127    }
128
129    /// Set the shutdown configuration.
130    pub fn set_shutdown_config(&mut self, config: ShutdownConfig) {
131        self.shutdown_config = config;
132    }
133
134    /// Get the shutdown configuration.
135    #[must_use]
136    pub const fn shutdown_config(&self) -> &ShutdownConfig {
137        &self.shutdown_config
138    }
139
140    /// Register a lifecycle callback.
141    pub fn on_event(&mut self, callback: LifecycleCallback) {
142        self.callbacks.push(callback);
143    }
144
145    /// Emit a lifecycle event.
146    pub fn emit(&self, event: &LifecycleEvent) {
147        for callback in &self.callbacks {
148            callback(event.clone());
149        }
150    }
151
152    /// Update the session state and emit event.
153    pub fn set_state(&mut self, state: SessionState) {
154        self.state = state;
155        self.emit(&LifecycleEvent::StateChanged(state));
156    }
157
158    /// Get the current state.
159    #[must_use]
160    pub const fn state(&self) -> &SessionState {
161        &self.state
162    }
163
164    /// Signal that the session has started.
165    pub fn started(&mut self) {
166        self.set_state(SessionState::Running);
167        self.emit(&LifecycleEvent::Started);
168    }
169
170    /// Signal that the session is ready.
171    pub fn ready(&mut self) {
172        self.emit(&LifecycleEvent::Ready);
173    }
174
175    /// Signal that the session is shutting down.
176    pub fn shutting_down(&mut self) {
177        self.set_state(SessionState::Closing);
178        self.emit(&LifecycleEvent::ShuttingDown);
179    }
180
181    /// Signal that the session has closed.
182    pub fn closed(&mut self) {
183        self.set_state(SessionState::Closed);
184        self.emit(&LifecycleEvent::Closed);
185    }
186
187    /// Signal that the session has exited.
188    pub fn exited(&mut self, status: ProcessExitStatus) {
189        self.set_state(SessionState::Exited(status));
190        self.emit(&LifecycleEvent::Exited(status));
191    }
192
193    /// Signal an error.
194    pub fn error(&mut self, message: impl Into<String>) {
195        self.emit(&LifecycleEvent::Error(message.into()));
196    }
197}
198
199impl Default for LifecycleManager {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205impl std::fmt::Debug for LifecycleManager {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        f.debug_struct("LifecycleManager")
208            .field("state", &self.state)
209            .field("callbacks", &self.callbacks.len())
210            .finish()
211    }
212}
213
214/// Signals that can be sent to a process.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum Signal {
217    /// Interrupt (Ctrl+C).
218    Interrupt,
219    /// Quit (Ctrl+\).
220    Quit,
221    /// Terminate.
222    Terminate,
223    /// Kill (non-catchable).
224    Kill,
225    /// Hangup.
226    Hangup,
227    /// User defined signal 1.
228    User1,
229    /// User defined signal 2.
230    User2,
231}
232
233impl Signal {
234    /// Get the control character for this signal, if applicable.
235    #[must_use]
236    pub const fn as_control_char(&self) -> Option<ControlChar> {
237        match self {
238            Self::Interrupt => Some(ControlChar::CtrlC),
239            Self::Quit => Some(ControlChar::CtrlBackslash),
240            _ => None,
241        }
242    }
243
244    /// Get the Unix signal number for this signal.
245    #[cfg(unix)]
246    #[must_use]
247    pub const fn as_signal_number(&self) -> i32 {
248        match self {
249            Self::Interrupt => 2,  // SIGINT
250            Self::Quit => 3,       // SIGQUIT
251            Self::Terminate => 15, // SIGTERM
252            Self::Kill => 9,       // SIGKILL
253            Self::Hangup => 1,     // SIGHUP
254            Self::User1 => 10,     // SIGUSR1
255            Self::User2 => 12,     // SIGUSR2
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn shutdown_config_default() {
266        let config = ShutdownConfig::default();
267        assert_eq!(config.strategy, ShutdownStrategy::Escalating);
268        assert!(config.exit_command.is_some());
269    }
270
271    #[test]
272    fn lifecycle_manager_state_transitions() {
273        let mut manager = LifecycleManager::new();
274
275        assert!(matches!(manager.state(), SessionState::Starting));
276
277        manager.started();
278        assert!(matches!(manager.state(), SessionState::Running));
279
280        manager.shutting_down();
281        assert!(matches!(manager.state(), SessionState::Closing));
282
283        manager.closed();
284        assert!(matches!(manager.state(), SessionState::Closed));
285    }
286
287    #[test]
288    fn signal_control_char() {
289        assert_eq!(
290            Signal::Interrupt.as_control_char(),
291            Some(ControlChar::CtrlC)
292        );
293        assert_eq!(Signal::Terminate.as_control_char(), None);
294    }
295}