mail4agent_messenger_shell/cli/
web_client.rs1use crate::MachineClient;
17use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
18
19const 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 let stale_secs = env_secs("M4A_STALE_SECS", 180);
55 let mut failing_since: Option<Instant> = None;
56 let mut last_probe = Instant::now();
57 loop {
58 let report = client.tick(now_ms(), drive_secs);
59 for (nick, event_id) in &report.pushed {
60 println!("push {nick} event={event_id}");
61 }
62 for (nick, text) in &report.alerts {
63 println!("alert {nick}: {text}");
64 }
65 for (nick, room) in &report.joined {
66 println!("joined {nick} room={room}");
67 }
68 for (from, to, reply) in &report.sent {
69 match (&reply.event_id, &reply.error) {
70 (_, Some(err)) => println!("send {from} -> {to} failed: {err}"),
71 (Some(event_id), None) => println!("send {from} -> {to} event={event_id}"),
72 (None, None) => println!("send {from} -> {to} ok"),
73 }
74 }
75 for (nick, err) in &report.errors {
76 eprintln!("drive {nick}: {err}");
77 }
78 let log = client.wake_log();
80 let mut now_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
81 for (nick, attempt) in &log {
82 let n = now_counts.entry(nick.clone()).or_insert(0);
83 *n += 1;
84 if *n > seen.get(nick).copied().unwrap_or(0) {
85 let status = attempt.status.map(|code| code.to_string()).unwrap_or_else(|| "queued (live deliver retried with backoff)".to_string());
86 println!("wake {nick} event={} status={status}", attempt.event_id);
87 }
88 }
89 seen = now_counts;
90 if let Err(err) = client.poll_agent_directory() {
91 eprintln!("agent poll: {err}");
92 }
93 if run_secs > 0 && started.elapsed().as_secs() >= run_secs {
94 println!("run time reached; exiting");
95 break;
96 }
97 let streak = client.error_streak();
100 if streak == 1 || streak == HEAL_LOG_STREAK {
101 eprintln!("homeserver unreachable or failing; backing off and retrying on its own");
102 }
103 if streak == 0 { failing_since = None; } else if failing_since.is_none() { failing_since = Some(Instant::now()); }
107 if let Some(since) = failing_since {
108 if stale_secs > 0 && since.elapsed().as_secs() >= stale_secs && last_probe.elapsed().as_secs() >= 60 {
109 last_probe = Instant::now();
110 if let Some(code) = fresh_probe() {
111 eprintln!("stale client: failing {}s while a fresh connection gets HTTP {code}; exiting for a restart", since.elapsed().as_secs());
112 std::process::exit(75);
113 }
114 }
115 }
116 let pause_ms = if streak == 0 { 250 } else { (250u64 << streak.min(6)).min(10_000) };
117 std::thread::sleep(Duration::from_millis(pause_ms));
118 }
119}
120
121fn fresh_probe() -> Option<u16> {
124 let base = std::env::var("M4A_HOMESERVER_URL").or_else(|_| std::env::var("M4A_PRODUCT_URL")).ok()?;
125 let url = format!("{}/_matrix/client/versions", base.trim_end_matches('/'));
126 let client = reqwest::blocking::Client::builder().timeout(Duration::from_secs(10)).build().ok()?;
127 client.get(url).send().ok().map(|r| r.status().as_u16())
128}