rust_expect/session/
lifecycle.rs1use std::time::Duration;
7
8use crate::types::{ControlChar, ProcessExitStatus, SessionState};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ShutdownStrategy {
13 Graceful,
15 Terminate,
17 Kill,
19 #[default]
21 Escalating,
22}
23
24#[derive(Debug, Clone)]
26pub struct ShutdownConfig {
27 pub strategy: ShutdownStrategy,
29 pub graceful_timeout: Duration,
31 pub terminate_timeout: Duration,
33 pub exit_command: Option<String>,
35 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 #[must_use]
54 pub fn graceful() -> Self {
55 Self {
56 strategy: ShutdownStrategy::Graceful,
57 ..Default::default()
58 }
59 }
60
61 #[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 #[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 #[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#[derive(Debug, Clone)]
88pub enum LifecycleEvent {
89 Started,
91 Ready,
93 StateChanged(SessionState),
95 ShuttingDown,
97 Closed,
99 Exited(ProcessExitStatus),
101 Error(String),
103}
104
105pub type LifecycleCallback = Box<dyn Fn(LifecycleEvent) + Send + Sync>;
107
108pub struct LifecycleManager {
110 callbacks: Vec<LifecycleCallback>,
112 state: SessionState,
114 shutdown_config: ShutdownConfig,
116}
117
118impl LifecycleManager {
119 #[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 pub fn set_shutdown_config(&mut self, config: ShutdownConfig) {
131 self.shutdown_config = config;
132 }
133
134 #[must_use]
136 pub const fn shutdown_config(&self) -> &ShutdownConfig {
137 &self.shutdown_config
138 }
139
140 pub fn on_event(&mut self, callback: LifecycleCallback) {
142 self.callbacks.push(callback);
143 }
144
145 pub fn emit(&self, event: &LifecycleEvent) {
147 for callback in &self.callbacks {
148 callback(event.clone());
149 }
150 }
151
152 pub fn set_state(&mut self, state: SessionState) {
154 self.state = state;
155 self.emit(&LifecycleEvent::StateChanged(state));
156 }
157
158 #[must_use]
160 pub const fn state(&self) -> &SessionState {
161 &self.state
162 }
163
164 pub fn started(&mut self) {
166 self.set_state(SessionState::Running);
167 self.emit(&LifecycleEvent::Started);
168 }
169
170 pub fn ready(&mut self) {
172 self.emit(&LifecycleEvent::Ready);
173 }
174
175 pub fn shutting_down(&mut self) {
177 self.set_state(SessionState::Closing);
178 self.emit(&LifecycleEvent::ShuttingDown);
179 }
180
181 pub fn closed(&mut self) {
183 self.set_state(SessionState::Closed);
184 self.emit(&LifecycleEvent::Closed);
185 }
186
187 pub fn exited(&mut self, status: ProcessExitStatus) {
189 self.set_state(SessionState::Exited(status));
190 self.emit(&LifecycleEvent::Exited(status));
191 }
192
193 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum Signal {
217 Interrupt,
219 Quit,
221 Terminate,
223 Kill,
225 Hangup,
227 User1,
229 User2,
231}
232
233impl Signal {
234 #[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 #[cfg(unix)]
246 #[must_use]
247 pub const fn as_signal_number(&self) -> i32 {
248 match self {
249 Self::Interrupt => 2, Self::Quit => 3, Self::Terminate => 15, Self::Kill => 9, Self::Hangup => 1, Self::User1 => 10, Self::User2 => 12, }
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}