Skip to main content

mail4agent_messenger_shell/cli/
web_client.rs

1//! The web machine client as a process: [`MachineClient::from_env`], then
2//! [`MachineClient::tick`] in a loop, and
3//! [`MachineClient::poll_agent_directory`] on `M4A_AGENT_RESCAN_SECS`.
4//!
5//! Settings come from the environment, then from the env file
6//! (`M4A_ENV_FILE`, default `~/.config/mail4agent/web-client.env`) for
7//! anything unset. The client listens on the local send socket
8//! (`M4A_SEND_SOCK`, default `web-client.sock` under the store root) for
9//! `m4a-send`.
10//!
11//! Environment: everything `from_env` reads, plus `M4A_DRIVE_SECS` (full
12//! drive period, default 15) and `M4A_RUN_SECS` (exit after that many
13//! seconds; unset or 0 runs until killed). Prints nicks, event ids, room
14//! ids, and HTTP statuses only. Never prints URLs, keys, or bearers.
15
16use crate::MachineClient;
17use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
18
19/// Streak at which the log says once more that the daemon is still retrying.
20const HEAL_LOG_STREAK: u32 = 10;
21
22fn env_secs(key: &str, default: u64) -> u64 {
23    std::env::var(key)
24        .ok()
25        .and_then(|value| value.parse().ok())
26        .unwrap_or(default)
27}
28
29fn now_ms() -> i64 {
30    SystemTime::now()
31        .duration_since(UNIX_EPOCH)
32        .map(|elapsed| elapsed.as_millis() as i64)
33        .unwrap_or(0)
34}
35
36pub fn run(_args: Vec<String>) {
37    crate::load_env_file();
38    let mut client = match MachineClient::from_env() {
39        Ok(client) => client,
40        Err(err) => {
41            eprintln!("open failed: {err}");
42            std::process::exit(1);
43        }
44    };
45    match client.listen_for_sends_from_env() {
46        Ok(path) => println!("send socket {}", path.display()),
47        Err(err) => eprintln!("send socket not opened: {err}"),
48    }
49    println!("open ok; waiting on the push socket");
50    let drive_secs = env_secs("M4A_DRIVE_SECS", 15);
51    let run_secs = env_secs("M4A_RUN_SECS", 0);
52    let started = Instant::now();
53    let mut seen: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
54    loop {
55        let report = client.tick(now_ms(), drive_secs);
56        for (nick, event_id) in &report.pushed {
57            println!("push {nick} event={event_id}");
58        }
59        for (nick, text) in &report.alerts {
60            println!("alert {nick}: {text}");
61        }
62        for (nick, room) in &report.joined {
63            println!("joined {nick} room={room}");
64        }
65        for (from, to, reply) in &report.sent {
66            match (&reply.event_id, &reply.error) {
67                (_, Some(err)) => println!("send {from} -> {to} failed: {err}"),
68                (Some(event_id), None) => println!("send {from} -> {to} event={event_id}"),
69                (None, None) => println!("send {from} -> {to} ok"),
70            }
71        }
72        for (nick, err) in &report.errors {
73            eprintln!("drive {nick}: {err}");
74        }
75        // The log is per session, flattened: count what was printed for each nick, not overall.
76        let log = client.wake_log();
77        let mut now_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
78        for (nick, attempt) in &log {
79            let n = now_counts.entry(nick.clone()).or_insert(0);
80            *n += 1;
81            if *n > seen.get(nick).copied().unwrap_or(0) {
82                let status = attempt.status.map(|code| code.to_string()).unwrap_or_else(|| "none".to_string());
83                println!("wake {nick} event={} status={status}", attempt.event_id);
84            }
85        }
86        seen = now_counts;
87        if let Err(err) = client.poll_agent_directory() {
88            eprintln!("agent poll: {err}");
89        }
90        if run_secs > 0 && started.elapsed().as_secs() >= run_secs {
91            println!("run time reached; exiting");
92            break;
93        }
94        // Sessions keep failing (homeserver down or restarting): back off instead of hammering it,
95        // up to 10 s. The client rebuilds its push socket itself and the next clean tick resets this.
96        let streak = client.error_streak();
97        if streak == 1 || streak == HEAL_LOG_STREAK {
98            eprintln!("homeserver unreachable or failing; backing off and retrying on its own");
99        }
100        let pause_ms = if streak == 0 { 250 } else { (250u64 << streak.min(6)).min(10_000) };
101        std::thread::sleep(Duration::from_millis(pause_ms));
102    }
103}