use std::time::{Duration, Instant};
use super::keepalive::{KeepaliveConfig, KeepaliveManager};
use super::retry::{RetryPolicy, RetryState, RetryStrategy};
use super::session::{SshConfig, SshSession};
#[derive(Debug, Clone)]
pub struct ResilientConfig {
pub ssh: SshConfig,
pub retry: RetryPolicy,
pub keepalive: KeepaliveConfig,
pub auto_reconnect: bool,
pub max_reconnect_attempts: u32,
pub reconnect_delay: Duration,
}
impl ResilientConfig {
#[must_use]
pub fn new(ssh: SshConfig) -> Self {
Self {
ssh,
retry: RetryPolicy::default(),
keepalive: KeepaliveConfig::default(),
auto_reconnect: true,
max_reconnect_attempts: 5,
reconnect_delay: Duration::from_secs(5),
}
}
#[must_use]
pub fn with_retry(mut self, policy: RetryPolicy) -> Self {
self.retry = policy;
self
}
#[must_use]
pub const fn with_keepalive(mut self, config: KeepaliveConfig) -> Self {
self.keepalive = config;
self
}
#[must_use]
pub const fn no_auto_reconnect(mut self) -> Self {
self.auto_reconnect = false;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResilientState {
Disconnected,
Connected,
Reconnecting,
Failed,
}
#[derive(Debug)]
pub struct ResilientSession {
config: ResilientConfig,
session: Option<SshSession>,
state: ResilientState,
reconnect_state: RetryState,
keepalive: KeepaliveManager,
last_activity: Instant,
reconnect_count: u32,
}
impl ResilientSession {
#[must_use]
pub fn new(config: ResilientConfig) -> Self {
let keepalive = KeepaliveManager::new(config.keepalive.clone());
let reconnect_strategy =
RetryStrategy::exponential(config.reconnect_delay, config.max_reconnect_attempts);
Self {
config,
session: None,
state: ResilientState::Disconnected,
reconnect_state: RetryState::new(reconnect_strategy),
keepalive,
last_activity: Instant::now(),
reconnect_count: 0,
}
}
#[must_use]
pub const fn state(&self) -> ResilientState {
self.state
}
#[must_use]
pub fn is_connected(&self) -> bool {
self.state == ResilientState::Connected
&& self
.session
.as_ref()
.is_some_and(super::session::SshSession::is_connected)
}
#[must_use]
pub const fn reconnect_count(&self) -> u32 {
self.reconnect_count
}
pub fn connect(&mut self) -> crate::error::Result<()> {
let mut session = SshSession::new(self.config.ssh.clone());
session.connect()?;
self.session = Some(session);
self.state = ResilientState::Connected;
self.last_activity = Instant::now();
self.reconnect_state.reset();
Ok(())
}
pub fn disconnect(&mut self) {
if let Some(ref mut session) = self.session {
session.disconnect();
}
self.session = None;
self.state = ResilientState::Disconnected;
}
pub fn reconnect(&mut self) -> crate::error::Result<()> {
if !self.config.auto_reconnect {
return Err(crate::error::ExpectError::SessionClosed);
}
if !self.reconnect_state.should_retry() {
self.state = ResilientState::Failed;
return Err(crate::error::ExpectError::SessionClosed);
}
self.state = ResilientState::Reconnecting;
if let Some(delay) = self.reconnect_state.next_delay() {
std::thread::sleep(delay);
}
self.reconnect_state.record_attempt();
match self.connect() {
Ok(()) => {
self.reconnect_count += 1;
Ok(())
}
Err(e) => {
if !self.reconnect_state.should_retry() {
self.state = ResilientState::Failed;
}
Err(e)
}
}
}
pub fn handle_disconnect(&mut self) -> crate::error::Result<()> {
self.session = None;
if self.config.auto_reconnect {
self.reconnect()
} else {
self.state = ResilientState::Disconnected;
Err(crate::error::ExpectError::SessionClosed)
}
}
pub fn keepalive_tick(&mut self) -> bool {
use super::keepalive::KeepaliveAction;
if !self.is_connected() {
return false;
}
match self.keepalive.tick() {
KeepaliveAction::None | KeepaliveAction::SendKeepalive => true,
KeepaliveAction::Timeout | KeepaliveAction::Disconnect => false,
}
}
#[must_use]
pub fn is_healthy(&self) -> bool {
self.is_connected() && self.keepalive.state().is_alive()
}
#[must_use]
pub const fn session(&self) -> Option<&SshSession> {
self.session.as_ref()
}
pub const fn session_mut(&mut self) -> Option<&mut SshSession> {
self.session.as_mut()
}
pub fn record_activity(&mut self) {
self.last_activity = Instant::now();
self.keepalive.handle_response();
}
#[must_use]
pub fn idle_time(&self) -> Duration {
self.last_activity.elapsed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resilient_config() {
let ssh_config = SshConfig::new("example.com");
let config = ResilientConfig::new(ssh_config)
.with_keepalive(KeepaliveConfig::new().interval(Duration::from_secs(10)));
assert!(config.auto_reconnect);
assert_eq!(config.keepalive.interval, Duration::from_secs(10));
}
#[test]
fn resilient_session_state() {
let ssh_config = SshConfig::new("example.com");
let config = ResilientConfig::new(ssh_config);
let session = ResilientSession::new(config);
assert_eq!(session.state(), ResilientState::Disconnected);
assert!(!session.is_connected());
}
}