use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticsStrategy {
Version,
TokenMonitor,
ProcessMonitor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActivityState {
Active,
Idle,
Dead,
}
pub trait ProgressMonitor {
fn poll(&mut self) -> ActivityState;
}
pub struct TokenMonitor {
state: Arc<std::sync::atomic::AtomicU8>,
alive: Arc<std::sync::atomic::AtomicBool>,
}
impl TokenMonitor {
pub const fn new(
state: Arc<std::sync::atomic::AtomicU8>,
alive: Arc<std::sync::atomic::AtomicBool>,
) -> Self {
Self { state, alive }
}
}
impl ProgressMonitor for TokenMonitor {
fn poll(&mut self) -> ActivityState {
if !self.alive.load(Ordering::SeqCst) {
return ActivityState::Dead;
}
let state = crate::lsp::state::ServerState::from_u8(self.state.load(Ordering::SeqCst));
match state {
crate::lsp::state::ServerState::Indexing => ActivityState::Active,
crate::lsp::state::ServerState::Dead => ActivityState::Dead,
_ => ActivityState::Idle,
}
}
}
pub struct ProcessMonitor {
pid: u32,
alive: Arc<std::sync::atomic::AtomicBool>,
last_cpu: Option<u64>,
trust_failures: Arc<AtomicU32>,
}
impl ProcessMonitor {
pub const fn new(
pid: u32,
alive: Arc<std::sync::atomic::AtomicBool>,
trust_failures: Arc<AtomicU32>,
) -> Self {
Self {
pid,
alive,
last_cpu: None,
trust_failures,
}
}
pub fn patience(&self) -> std::time::Duration {
match self.trust_failures.load(Ordering::SeqCst) {
0 => std::time::Duration::from_secs(120),
1 => std::time::Duration::from_secs(60),
2 => std::time::Duration::from_secs(30),
_ => std::time::Duration::from_secs(5),
}
}
}
impl ProgressMonitor for ProcessMonitor {
fn poll(&mut self) -> ActivityState {
if !self.alive.load(Ordering::SeqCst) {
return ActivityState::Dead;
}
let Some(current) = process_cpu_ticks(self.pid) else {
return ActivityState::Dead;
};
let result = self.last_cpu.map_or(
ActivityState::Active,
|prev| {
if current > prev {
ActivityState::Active
} else {
ActivityState::Idle
}
},
);
self.last_cpu = Some(current);
result
}
}
#[cfg(target_os = "linux")]
fn process_cpu_ticks(pid: u32) -> Option<u64> {
let path = format!("/proc/{pid}/stat");
let contents = std::fs::read_to_string(path).ok()?;
let after_comm = contents.rfind(')')? + 1;
let fields: Vec<&str> = contents[after_comm..].split_whitespace().collect();
let utime: u64 = fields.get(11)?.parse().ok()?;
let stime: u64 = fields.get(12)?.parse().ok()?;
Some(utime + stime)
}
#[cfg(target_os = "macos")]
fn process_cpu_ticks(pid: u32) -> Option<u64> {
let output = std::process::Command::new("ps")
.args(["-p", &pid.to_string(), "-o", "cputime="])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let time_str = String::from_utf8_lossy(&output.stdout);
parse_cputime_string(time_str.trim())
}
#[cfg(target_os = "macos")]
fn parse_cputime_string(s: &str) -> Option<u64> {
let parts: Vec<&str> = s.split(':').collect();
match parts.len() {
2 => {
let minutes: u64 = parts[0].parse().ok()?;
let seconds: f64 = parts[1].parse().ok()?;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "CPU time is always positive and fits in u64"
)]
Some(minutes * 6000 + (seconds * 100.0) as u64)
}
3 => {
let hours: u64 = parts[0].parse().ok()?;
let minutes: u64 = parts[1].parse().ok()?;
let seconds: f64 = parts[2].parse().ok()?;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "CPU time is always positive and fits in u64"
)]
Some(hours * 360_000 + minutes * 6000 + (seconds * 100.0) as u64)
}
_ => None,
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn process_cpu_ticks(_pid: u32) -> Option<u64> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicU8};
#[test]
fn token_monitor_idle_when_ready() {
let state = Arc::new(AtomicU8::new(crate::lsp::state::ServerState::Ready.as_u8()));
let alive = Arc::new(AtomicBool::new(true));
let mut monitor = TokenMonitor::new(state, alive);
assert_eq!(monitor.poll(), ActivityState::Idle);
}
#[test]
fn token_monitor_active_when_indexing() {
let state = Arc::new(AtomicU8::new(
crate::lsp::state::ServerState::Indexing.as_u8(),
));
let alive = Arc::new(AtomicBool::new(true));
let mut monitor = TokenMonitor::new(state, alive);
assert_eq!(monitor.poll(), ActivityState::Active);
}
#[test]
fn token_monitor_dead_when_not_alive() {
let state = Arc::new(AtomicU8::new(crate::lsp::state::ServerState::Ready.as_u8()));
let alive = Arc::new(AtomicBool::new(false));
let mut monitor = TokenMonitor::new(state, alive);
assert_eq!(monitor.poll(), ActivityState::Dead);
}
#[test]
fn process_monitor_patience_decay() {
let alive = Arc::new(AtomicBool::new(true));
let trust = Arc::new(AtomicU32::new(0));
let monitor = ProcessMonitor::new(1, alive, trust.clone());
assert_eq!(monitor.patience(), std::time::Duration::from_secs(120));
trust.store(1, Ordering::SeqCst);
assert_eq!(monitor.patience(), std::time::Duration::from_secs(60));
trust.store(2, Ordering::SeqCst);
assert_eq!(monitor.patience(), std::time::Duration::from_secs(30));
trust.store(3, Ordering::SeqCst);
assert_eq!(monitor.patience(), std::time::Duration::from_secs(5));
trust.store(10, Ordering::SeqCst);
assert_eq!(monitor.patience(), std::time::Duration::from_secs(5));
}
#[cfg(target_os = "linux")]
#[test]
fn process_cpu_ticks_self() {
let pid = std::process::id();
let ticks = process_cpu_ticks(pid);
assert!(ticks.is_some(), "Should be able to read own CPU time");
}
#[cfg(target_os = "linux")]
#[test]
fn process_cpu_ticks_nonexistent() {
let ticks = process_cpu_ticks(u32::MAX);
assert!(ticks.is_none(), "Nonexistent PID should return None");
}
}