use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{Mutex, Notify, mpsc};
use crate::app_state::AppState;
use crate::cli::ViewArgs;
use crate::common::config::{AppConfig, EnvConfig};
use crate::network::webhook::{enqueue as enqueue_webhook, spawn_webhook_worker};
use crate::ui::alerts::WebhookPayload;
use crate::ui::notification::NotificationType;
pub use super::data_collection::{
CollectionConfig, DataCollectionStrategy, LocalCollector, RemoteCollectorBuilder, SshStrategy,
};
pub struct DataCollector {
app_state: Arc<Mutex<AppState>>,
data_notify: Option<Arc<Notify>>,
webhook_tx: std::sync::OnceLock<mpsc::Sender<WebhookPayload>>,
}
impl DataCollector {
#[allow(dead_code)] pub fn new(app_state: Arc<Mutex<AppState>>) -> Self {
Self {
app_state,
data_notify: None,
webhook_tx: std::sync::OnceLock::new(),
}
}
pub fn with_notify(app_state: Arc<Mutex<AppState>>, notify: Arc<Notify>) -> Self {
Self {
app_state,
data_notify: Some(notify),
webhook_tx: std::sync::OnceLock::new(),
}
}
fn notify_ui(&self) {
if let Some(ref notify) = self.data_notify {
notify.notify_one();
}
}
async fn evaluate_alerts(&self) {
let transitions;
let webhook_url;
let bell_on_critical;
{
let mut state = self.app_state.lock().await;
bell_on_critical = state.alerter.config().bell_on_critical;
webhook_url = state.alerter.config().webhook_url.clone();
let snapshot = state.gpu_info.clone();
transitions = state.alerter.evaluate(&snapshot);
for t in &transitions {
let notification_type = match t.to {
crate::ui::alerts::AlertLevel::Crit => NotificationType::Error,
crate::ui::alerts::AlertLevel::Warn => NotificationType::Warning,
crate::ui::alerts::AlertLevel::Ok => NotificationType::Status,
};
let _ = state.notifications.show_with_duration(
t.message.clone(),
notification_type,
AppConfig::NOTIFICATION_DURATION_SECS,
);
state.push_alert_transition(t.clone());
}
}
if transitions.is_empty() {
return;
}
if bell_on_critical
&& transitions
.iter()
.any(|t| t.to == crate::ui::alerts::AlertLevel::Crit)
{
tokio::task::spawn_blocking(|| {
use std::io::Write;
let mut out = std::io::stdout();
let _ = out.write_all(b"\x07");
let _ = out.flush();
});
}
if !webhook_url.is_empty() {
let tx = self
.webhook_tx
.get_or_init(|| spawn_webhook_worker(webhook_url.clone()));
for t in &transitions {
let payload = WebhookPayload::from(t);
enqueue_webhook(tx, payload);
}
}
self.notify_ui();
}
pub async fn run_local_mode(&self, args: ViewArgs) {
let mut profiler = crate::utils::StartupProfiler::new();
profiler.checkpoint("Starting local mode data collection");
let collector = LocalCollector::new();
let mut first_iteration = true;
let interval = args.interval.unwrap_or_else(EnvConfig::local_interval);
loop {
let mut config = CollectionConfig {
interval,
first_iteration,
hosts: Vec::new(),
};
let data = if first_iteration {
profiler.checkpoint("Starting first data collection");
match collector
.collect_with_app_state(self.app_state.clone(), &config)
.await
{
Ok(data) => {
profiler.checkpoint("First data collection complete");
profiler.finish();
data
}
Err(e) => {
eprintln!("Error collecting data: {e}");
tokio::time::sleep(Duration::from_secs(config.interval)).await;
continue;
}
}
} else {
match collector.collect(&config).await {
Ok(data) => data,
Err(e) => {
eprintln!("Error collecting data: {e}");
tokio::time::sleep(Duration::from_secs(config.interval)).await;
continue;
}
}
};
collector
.update_state(self.app_state.clone(), data, &config)
.await;
self.notify_ui();
self.evaluate_alerts().await;
if first_iteration {
first_iteration = false;
config.first_iteration = false;
}
tokio::time::sleep(Duration::from_secs(interval)).await;
}
}
pub async fn run_remote_mode(
&self,
args: ViewArgs,
hosts: Vec<String>,
hostfile: Option<String>,
) {
let mut builder = RemoteCollectorBuilder::new().with_hosts(hosts.clone());
if let Some(ref file_path) = hostfile {
match builder.load_hosts_from_file(file_path) {
Ok(b) => builder = b,
Err(e) => {
eprintln!("Error loading hosts from file {file_path}: {e}");
return;
}
}
}
let collector = builder.build();
loop {
let hosts_list = if let Some(file_path) = &hostfile {
let mut hosts_vec = hosts.clone();
match std::fs::metadata(file_path) {
Ok(metadata) => {
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; if metadata.len() > MAX_FILE_SIZE {
eprintln!("Warning: Hostfile too large, skipping reload");
hosts_vec
} else if let Ok(content) = std::fs::read_to_string(file_path) {
const MAX_HOSTS: usize = 1000;
let file_hosts: Vec<String> = content
.lines()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.filter(|s| !s.starts_with('#'))
.take(MAX_HOSTS)
.filter_map(|s| {
crate::common::http_hosts::parse_http_host_url(s)
.ok()
.map(|_| s.to_string())
})
.collect();
hosts_vec.extend(file_hosts);
hosts_vec
} else {
hosts_vec
}
}
Err(e) => {
eprintln!("Warning: Cannot access hostfile: {e}");
hosts_vec
}
}
} else {
hosts.clone()
};
let config = CollectionConfig {
interval: args
.interval
.unwrap_or_else(|| EnvConfig::adaptive_interval(hosts_list.len())),
first_iteration: false,
hosts: hosts_list.clone(),
};
match collector.collect(&config).await {
Ok(data) => {
collector
.update_state(self.app_state.clone(), data, &config)
.await;
self.notify_ui();
self.evaluate_alerts().await;
}
Err(e) => {
eprintln!("Error collecting remote data: {e}");
}
}
let interval = args
.interval
.unwrap_or_else(|| EnvConfig::adaptive_interval(hosts_list.len()));
tokio::time::sleep(Duration::from_secs(interval)).await;
}
}
pub async fn run_ssh_mode(&self, args: ViewArgs, strategy: std::sync::Arc<SshStrategy>) {
let target_count = strategy.target_count();
loop {
let config = CollectionConfig {
interval: args
.interval
.unwrap_or_else(|| EnvConfig::adaptive_interval(target_count.max(1))),
first_iteration: false,
hosts: Vec::new(),
};
match strategy.collect(&config).await {
Ok(data) => {
strategy
.update_state(self.app_state.clone(), data, &config)
.await;
self.notify_ui();
self.evaluate_alerts().await;
}
Err(e) => {
tracing::warn!(error = %e, "ssh collect failed");
}
}
let interval = args
.interval
.unwrap_or_else(|| EnvConfig::adaptive_interval(target_count.max(1)));
tokio::time::sleep(Duration::from_secs(interval)).await;
}
}
}