Skip to main content

adguardian/
main.rs

1mod fetch;
2mod ui;
3mod welcome;
4mod widgets;
5
6use reqwest::Client;
7use std::{env, time::Duration};
8use tokio::time::{interval, MissedTickBehavior};
9
10use ui::draw_ui;
11
12use fetch::{
13  fetch_filters::{fetch_adguard_filter_list, AdGuardFilteringStatus},
14  fetch_query_log::{fetch_adguard_query_log, Query},
15  fetch_stats::{fetch_adguard_stats, StatsResponse},
16  fetch_status::{fetch_adguard_status, StatusResponse},
17};
18
19/// Fetch the query log, stats and status together, so a failure leaves the UI's
20/// data in sync (all-or-nothing) rather than partially updated.
21async fn fetch_all(
22  client: &Client,
23  hostname: &str,
24  username: &str,
25  password: &str,
26  query_log_limit: u32,
27) -> anyhow::Result<(Vec<Query>, StatsResponse, StatusResponse)> {
28  let queries =
29    fetch_adguard_query_log(client, hostname, username, password, query_log_limit).await?;
30  let stats = fetch_adguard_stats(client, hostname, username, password).await?;
31  let status = fetch_adguard_status(client, hostname, username, password).await?;
32  Ok((queries.data, stats, status))
33}
34
35async fn run() -> anyhow::Result<()> {
36  // Per-request timeout (seconds), clamped to at least 1, so no request can hang
37  let timeout_secs: u64 = env::var("ADGUARD_TIMEOUT")
38    .unwrap_or_else(|_| "5".into())
39    .parse::<u64>()?
40    .max(1);
41  let client = Client::builder()
42    .timeout(Duration::from_secs(timeout_secs))
43    .build()?;
44
45  // AdGuard instance details, from env vars (verified in welcome.rs)
46  let ip = env::var("ADGUARD_IP")?;
47  let port = env::var("ADGUARD_PORT")?;
48  let protocol = env::var("ADGUARD_PROTOCOL").unwrap_or("http".to_string());
49  let hostname = format!("{}://{}:{}", protocol, ip, port);
50  let username = env::var("ADGUARD_USERNAME")?;
51  let password = env::var("ADGUARD_PASSWORD")?;
52
53  // Fetch the filter list, use empty list on failures is fine
54  let filters = welcome::with_retries(
55    3,
56    Duration::from_secs(5),
57    "Fetching AdGuard filters",
58    || fetch_adguard_filter_list(&client, &hostname, &username, &password),
59  )
60  .await
61  .unwrap_or_else(|e| {
62    eprintln!("Could not fetch filter list, starting without it: {}", e);
63    AdGuardFilteringStatus { filters: None }
64  });
65
66  // Open channels for data fetching where updates are required
67  let (queries_tx, queries_rx) = tokio::sync::mpsc::channel(1);
68  let (stats_tx, stats_rx) = tokio::sync::mpsc::channel(1);
69  let (status_tx, status_rx) = tokio::sync::mpsc::channel(1);
70
71  // Shutdown signal, set by the UI when the user quits
72  let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
73
74  // Spawn the UI task, pass data and update channels
75  let draw_ui_task = tokio::spawn(draw_ui(
76    queries_rx,
77    stats_rx,
78    status_rx,
79    filters,
80    shutdown_tx,
81  ));
82
83  // Get update interval (in seconds), clamped to at least 1 (interval() panics on zero)
84  let interval_secs: u64 = env::var("ADGUARD_UPDATE_INTERVAL")
85    .unwrap_or_else(|_| "2".into())
86    .parse::<u64>()?
87    .max(1);
88  let mut interval = interval(Duration::from_secs(interval_secs));
89  interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
90
91  // Max num of query log entries to fetch per update
92  let query_log_limit: u32 = env::var("ADGUARD_QUERYLOG_LIMIT")
93    .unwrap_or_else(|_| "100".into())
94    .parse()?;
95
96  // Open loop for fetching data at the specified interval
97  loop {
98    tokio::select! {
99        _ = interval.tick() => {
100            // Check data is ok, just skip this update on transient error
101            if let Ok((queries, stats, status)) =
102                fetch_all(&client, &hostname, &username, &password, query_log_limit).await
103            {
104                // A send error means the UI has shut down, so stop fetching
105                if queries_tx.send(queries).await.is_err()
106                    || stats_tx.send(stats).await.is_err()
107                    || status_tx.send(status).await.is_err()
108                {
109                    break;
110                }
111            }
112        }
113        // Resolves when the UI sets the shutdown flag, or drops the sender
114        _ = shutdown_rx.changed() => {
115            break;
116        }
117    }
118  }
119
120  draw_ui_task.await??;
121
122  Ok(())
123}
124
125fn main() {
126  let rt = tokio::runtime::Runtime::new().expect("failed to start async runtime");
127  rt.block_on(async {
128    welcome::welcome().await.unwrap_or_else(|e| {
129      eprintln!("Failed to initialize: {}", e);
130      std::process::exit(1);
131    });
132
133    run()
134      .await
135      .map_err(|e| {
136        eprintln!("Failed to run: {}", e);
137        std::io::Error::other(format!("Failed to run: {}", e))
138      })
139      .unwrap_or_else(|e| {
140        eprintln!("Error: {}", e);
141        std::process::exit(1);
142      });
143  });
144}