use std::net::IpAddr;
use std::path::Path;
use std::time::Duration;
use anyhow::{Context, Result};
use framesmith::{FileAuthTokenStore, FrameTv};
pub struct TvConnection {
pub tv: Option<FrameTv>,
host: IpAddr,
token_file: std::path::PathBuf,
timeout: Duration,
pub connected: bool,
backoff_secs: u64,
}
const MIN_BACKOFF: u64 = 4;
const MAX_BACKOFF: u64 = 32;
impl TvConnection {
pub async fn connect(host: IpAddr, token_file: &Path, timeout_secs: u64) -> Result<Self> {
let tv = connect_persistent(host, token_file, timeout_secs).await?;
Ok(Self {
tv: Some(tv),
host,
token_file: token_file.to_owned(),
timeout: Duration::from_secs(timeout_secs),
connected: true,
backoff_secs: MIN_BACKOFF,
})
}
pub fn new_disconnected(host: IpAddr, token_file: &Path, timeout_secs: u64) -> Self {
Self {
tv: None,
host,
token_file: token_file.to_owned(),
timeout: Duration::from_secs(timeout_secs),
connected: false,
backoff_secs: MIN_BACKOFF,
}
}
pub fn mark_disconnected(&mut self) {
self.connected = false;
tracing::warn!("TV connection marked as disconnected, will retry with backoff");
}
pub fn backoff_duration(&self) -> Duration {
Duration::from_secs(self.backoff_secs)
}
pub async fn attempt_reconnect(&mut self) -> bool {
tracing::info!(
"attempting reconnect to {} (backoff {}s)",
self.host,
self.backoff_secs
);
if let Some(ref tv) = self.tv {
match tv.reconnect().await {
Ok(()) => {
tracing::info!("reconnected to {}", self.host);
self.connected = true;
self.backoff_secs = MIN_BACKOFF;
return true;
}
Err(e) => {
tracing::warn!("reconnect failed: {e:#}");
}
}
}
match connect_persistent(self.host, &self.token_file, self.timeout.as_secs()).await {
Ok(tv) => {
tracing::info!("fresh connection established to {}", self.host);
self.tv = Some(tv);
self.connected = true;
self.backoff_secs = MIN_BACKOFF;
true
}
Err(e) => {
tracing::warn!("fresh connection failed: {e:#}");
self.backoff_secs = (self.backoff_secs * 2).min(MAX_BACKOFF);
false
}
}
}
pub async fn send_keepalive(&self) {
if let Some(ref tv) = self.tv {
tv.send_keepalive().await;
}
}
pub fn host(&self) -> IpAddr {
self.host
}
}
async fn connect_persistent(host: IpAddr, token_file: &Path, timeout_secs: u64) -> Result<FrameTv> {
if let Some(parent) = token_file.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory: {}", parent.display()))?;
}
let tv = FrameTv::connection_builder(host)
.auth_token_store(FileAuthTokenStore::new(token_file))
.connection_timeout(Duration::from_secs(timeout_secs))
.persistent()
.connect()
.await
.context("failed to connect to TV")?;
Ok(tv)
}