use std::sync::atomic::Ordering;
use crate::actions::{switch_off, switch_profile};
use crate::logline::logline;
use crate::profile::{app_state_mtime, load_config};
use crate::usage::{collect_third_party_entries, collect_tokens, is_idle, now_ms};
use super::{SwitchBackoff, active_diverged_unsaved, switch_backoff_ms};
const SWITCH_RETRY_TTL_MS: u64 = 120_000;
impl super::Daemon {
pub(super) fn tick(&mut self) {
self.reload_if_changed();
self.drain_pending_switch();
self.drain_pending_switch_off();
self.write_status();
}
fn rebuild_tokens(&self) {
#[allow(clippy::expect_used, reason = "mutex poisoning is unrecoverable")]
{
let tokens = collect_tokens(&self.config.lock().expect("config poisoned"));
let third_party =
collect_third_party_entries(&self.config.lock().expect("config poisoned").profiles);
*self.usage_tokens.lock().expect("usage_tokens poisoned") = tokens;
*self
.third_party_tokens
.lock()
.expect("third_party_tokens poisoned") = third_party;
}
}
pub(super) fn reload_if_changed(&mut self) {
let current = app_state_mtime();
if current == self.last_state_mtime {
return;
}
self.last_state_mtime = current;
match load_config() {
Ok(new_config) => {
self.refresh_interval
.store(new_config.state.refresh_interval_ms, Ordering::Relaxed);
if let Ok(mut c) = self.config.lock() {
*c = new_config;
}
self.rebuild_tokens();
logline!("clauth daemon: reloaded config after an external change");
}
Err(e) => logline!("clauth daemon: config reload failed: {e}"),
}
}
pub(super) fn drain_pending_switch(&mut self) {
let target: Option<String> = self
.pending_switch
.lock()
.map(|mut g| {
let t = g.iter().min().cloned();
g.clear();
t
})
.unwrap_or_default();
let Some(target) = target else {
return;
};
let now = now_ms();
let target_exists = self
.config
.lock()
.map(|c| c.find(&target).is_some())
.unwrap_or(false);
if !target_exists {
logline!(
"clauth daemon: dropping queued switch to '{target}': profile no longer exists (deleted?)"
);
if self
.switch_backoff
.as_ref()
.is_some_and(|b| b.target == target)
{
self.switch_backoff = None;
}
return;
}
if let Some(b) = &self.switch_backoff
&& b.target == target
{
if now >= b.retry_until {
logline!(
"clauth daemon: gave up switching to '{target}': {}",
b.reason
);
self.switch_backoff = None;
return;
}
if now < b.not_before {
self.requeue_quiet(target);
return;
}
}
if !is_idle(&self.activity, &target) {
self.fail_switch(target, now, "target is mid-fetch");
return;
}
let outgoing = self
.config
.lock()
.ok()
.and_then(|c| c.state.active_profile.as_deref().map(str::to_string));
if let Some(active) = &outgoing
&& active != &target
&& active_diverged_unsaved(active)
{
self.fail_switch(
target,
now,
&format!("active '{active}' has unsaved credentials — resolve in the TUI"),
);
return;
}
match crate::oauth::ensure_installable(&self.config, &target, crate::oauth::refresh_result)
{
crate::oauth::AuthGate::Ready | crate::oauth::AuthGate::Refreshed => {}
crate::oauth::AuthGate::Broken => {
self.last_state_mtime = app_state_mtime();
logline!(
"clauth daemon: login for '{0}' revoked — run: clauth login {0}",
target
);
self.switch_backoff = None;
return;
}
crate::oauth::AuthGate::Transient(e) => {
self.fail_switch(target, now, &format!("refresh failed transiently ({e})"));
return;
}
}
let result = {
#[allow(
clippy::expect_used,
reason = "config mutex poisoning is unrecoverable"
)]
let mut cfg = self.config.lock().expect("config poisoned");
crate::lock::with_state_lock(|| {
switch_profile(&mut cfg, &target)?;
Ok(app_state_mtime())
})
};
match result {
Ok(mtime) => {
self.rebuild_tokens();
self.last_state_mtime = mtime;
self.switch_backoff = None;
logline!("clauth daemon: switched to '{target}'");
}
Err(e) => {
self.fail_switch(target, now, &format!("switch failed: {e}"));
}
}
}
fn fail_switch(&mut self, target: String, now: u64, reason: &str) {
let (attempts, retry_until) = match &self.switch_backoff {
Some(b) if b.target == target => (b.attempts + 1, b.retry_until),
_ => (1, now.saturating_add(SWITCH_RETRY_TTL_MS)),
};
let changed = self
.switch_backoff
.as_ref()
.is_none_or(|b| b.target != target || b.reason != reason);
if changed {
logline!("clauth daemon: deferring switch to '{target}': {reason}");
self.switch_failure_logs += 1;
}
if now < retry_until {
self.switch_backoff = Some(SwitchBackoff {
target: target.clone(),
attempts,
not_before: now.saturating_add(switch_backoff_ms(attempts)),
reason: reason.to_string(),
retry_until,
});
self.requeue_quiet(target);
} else {
logline!("clauth daemon: gave up switching to '{target}': {reason}");
self.switch_backoff = None;
}
}
fn requeue_quiet(&mut self, target: String) {
if let Ok(mut q) = self.pending_switch.lock()
&& q.is_empty()
{
q.insert(target);
}
}
pub(super) fn drain_pending_switch_off(&mut self) {
let off = self
.pending_switch_off
.lock()
.map(|mut g| std::mem::replace(&mut *g, false))
.unwrap_or(false);
if !off {
return;
}
let active = self
.config
.lock()
.ok()
.and_then(|c| c.state.active_profile.as_deref().map(str::to_string));
let Some(active) = active else {
return; };
if active_diverged_unsaved(&active) {
logline!(
"clauth daemon: skipping switch-off — active '{active}' has unsaved credentials"
);
return;
}
let result = {
#[allow(
clippy::expect_used,
reason = "config mutex poisoning is unrecoverable"
)]
let mut cfg = self.config.lock().expect("config poisoned");
crate::lock::with_state_lock(|| {
switch_off(&mut cfg)?;
Ok(app_state_mtime())
})
};
match result {
Ok(mtime) => {
self.rebuild_tokens();
self.last_state_mtime = mtime;
logline!("clauth daemon: switched off — all accounts spent");
}
Err(e) => logline!("clauth daemon: switch-off failed: {e}"),
}
}
}