use std::io;
use std::time::{Duration, Instant};
use usb_gadget::function::custom::Event;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinkState {
Offline,
Ready,
Online,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinkCommand {
Fatal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinkOfflineReason {
Ep0Disable,
Ep0Unbind,
IoError,
LivenessTimeout,
}
pub struct LinkController {
state: LinkState,
last_status: Option<Instant>,
last_offline_reason: Option<LinkOfflineReason>,
liveness_timeout: Duration,
reopen_backoff: Duration,
reopen_backoff_max: Duration,
reopen_not_before: Option<Instant>,
pending_drop: bool,
}
impl LinkController {
pub fn new(liveness_timeout: Duration) -> Self {
Self {
state: LinkState::Offline,
last_status: None,
last_offline_reason: None,
liveness_timeout,
reopen_backoff: Duration::from_secs(1),
reopen_backoff_max: Duration::from_secs(30),
reopen_not_before: None,
pending_drop: false,
}
}
pub fn state(&self) -> LinkState {
self.state
}
pub fn last_offline_reason(&self) -> Option<LinkOfflineReason> {
self.last_offline_reason
}
pub fn on_ep0_event(&mut self, event: Event) {
match event {
Event::Bind | Event::Enable | Event::Resume => {
self.reset_reopen_backoff();
self.enter_ready();
}
Event::Disable => {
self.enter_offline(LinkOfflineReason::Ep0Disable);
}
Event::Unbind => {
self.enter_offline(LinkOfflineReason::Ep0Unbind);
}
Event::Suspend => {
self.state = LinkState::Ready;
self.pending_drop = false;
}
Event::SetupDeviceToHost(_) | Event::SetupHostToDevice(_) => { }
Event::Unknown(_) => {}
_ => {}
}
}
pub fn on_status_ping(&mut self) {
let now = Instant::now();
self.last_status = Some(now);
if matches!(self.state, LinkState::Offline) {
if let Some(not_before) = self.reopen_not_before {
if now < not_before {
return;
}
}
self.enter_ready();
}
if matches!(self.state, LinkState::Ready | LinkState::Online) {
self.state = LinkState::Online;
self.reset_reopen_backoff();
}
}
pub fn on_io_error(&mut self, _err: &io::Error) {
if matches!(self.state, LinkState::Offline) {
return;
}
self.reopen_not_before = Some(Instant::now() + self.reopen_backoff);
self.reopen_backoff = self
.reopen_backoff
.saturating_mul(2)
.min(self.reopen_backoff_max);
self.enter_offline(LinkOfflineReason::IoError);
}
pub fn tick(&mut self, now: Instant) {
if let Some(last) = self.last_status {
if now.saturating_duration_since(last) > self.liveness_timeout {
self.enter_offline(LinkOfflineReason::LivenessTimeout);
}
}
}
pub fn take_command(&mut self) -> Option<LinkCommand> {
if self.pending_drop {
self.pending_drop = false;
return Some(LinkCommand::Fatal);
}
None
}
fn enter_ready(&mut self) {
self.state = LinkState::Ready;
self.reopen_not_before = None;
self.last_offline_reason = None;
self.pending_drop = false;
}
fn enter_offline(&mut self, reason: LinkOfflineReason) {
self.state = LinkState::Offline;
self.last_status = None;
self.last_offline_reason = Some(reason);
self.pending_drop = true;
}
fn reset_reopen_backoff(&mut self) {
self.reopen_backoff = Duration::from_secs(1);
self.reopen_not_before = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transitions_on_events_and_status() {
let mut ctrl = LinkController::new(Duration::from_secs(5));
assert_eq!(ctrl.state(), LinkState::Offline);
ctrl.on_ep0_event(Event::Enable);
assert_eq!(ctrl.state(), LinkState::Ready);
assert_eq!(ctrl.take_command(), None);
ctrl.on_status_ping();
assert_eq!(ctrl.state(), LinkState::Online);
let err = io::Error::from_raw_os_error(libc::EPIPE);
ctrl.on_io_error(&err);
assert_eq!(ctrl.state(), LinkState::Offline);
assert_eq!(ctrl.last_offline_reason(), Some(LinkOfflineReason::IoError));
assert_eq!(ctrl.take_command(), Some(LinkCommand::Fatal));
}
#[test]
fn liveness_timeout_forces_offline() {
let mut ctrl = LinkController::new(Duration::from_millis(100));
ctrl.on_ep0_event(Event::Enable);
ctrl.take_command();
ctrl.on_status_ping();
let now = Instant::now() + Duration::from_millis(250);
ctrl.tick(now);
assert_eq!(ctrl.state(), LinkState::Offline);
assert_eq!(
ctrl.last_offline_reason(),
Some(LinkOfflineReason::LivenessTimeout)
);
assert_eq!(ctrl.take_command(), Some(LinkCommand::Fatal));
}
}