1use std::collections::{HashMap, HashSet};
25use std::future::Future;
26use std::sync::{Arc, Mutex, OnceLock};
27use std::time::Duration;
28
29use tokio::sync::Notify;
30
31use super::host::HostSsh;
32use super::status::{self, state};
33use crate::backend::mps::Broker;
34
35const WHILE_CONNECTING: Duration = Duration::from_millis(500);
37const WHILE_UP: Duration = Duration::from_secs(15);
39
40static BROKER: OnceLock<Arc<Broker>> = OnceLock::new();
41
42pub fn init(broker: Arc<Broker>) {
45 let _ = BROKER.set(broker);
46}
47
48fn broker() -> Option<&'static Broker> {
49 BROKER.get().map(Arc::as_ref)
50}
51
52fn shared() -> bool {
54 super::ssh::shared_control_dir().is_some()
55}
56
57pub fn pane_started(connection: &str, logged_in: bool) {
62 started(connection, logged_in, shared());
63}
64
65pub fn pane_ended(connection: &str, error: Option<&str>) {
68 let shared = shared();
69 ended(connection, error, !shared, shared);
70}
71
72pub async fn login<T>(
77 connection: &str,
78 attempt: impl Future<Output = Result<T, String>>,
79) -> Result<T, String> {
80 let shared = shared();
81 started(connection, false, shared);
82 let result = attempt.await;
83 login_ended(connection, result.as_ref().map(|_| ()).map_err(String::as_str), shared);
84 result
85}
86
87pub fn used(connection: &str) {
91 used_with(connection, shared());
92}
93
94fn started(connection: &str, logged_in: bool, shared: bool) {
99 release(connection);
100 status::pane_started(broker(), connection, logged_in || !shared);
101 if shared {
102 watch(connection);
103 }
104}
105
106fn ended(connection: &str, error: Option<&str>, settle: bool, shared: bool) {
110 let error = if held(connection) { None } else { error };
111 status::pane_ended(broker(), connection, error, settle);
112 if shared {
113 watch(connection);
114 }
115}
116
117fn login_ended(connection: &str, outcome: Result<(), &str>, shared: bool) {
120 match outcome {
121 Ok(()) => {
122 status::set(broker(), connection, state::CONNECTED, None);
123 ended(connection, None, false, shared);
124 }
125 Err(message) => ended(connection, Some(message), !shared, shared),
126 }
127}
128
129fn used_with(connection: &str, shared: bool) {
130 release(connection);
131 status::set(broker(), connection, state::CONNECTED, None);
132 if shared {
133 watch(connection);
134 }
135}
136
137pub async fn disconnect(connection: &str) {
140 hold(connection);
141 if let Ok(host) = HostSsh::for_connection(connection) {
142 host.close_shared_connection().await;
143 }
144 status::set(broker(), connection, state::DISCONNECTED, None);
145}
146
147fn held_set() -> &'static Mutex<HashSet<String>> {
150 static HELD: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
151 HELD.get_or_init(Default::default)
152}
153
154pub fn held(connection: &str) -> bool {
158 held_set().lock().unwrap_or_else(|e| e.into_inner()).contains(connection)
159}
160
161pub fn disconnected_note(connection: &str) -> String {
163 format!("Disconnected from {connection}. Press Enter to reconnect.")
164}
165
166fn hold(connection: &str) {
167 held_set().lock().unwrap_or_else(|e| e.into_inner()).insert(connection.to_string());
168}
169
170fn release(connection: &str) {
171 held_set().lock().unwrap_or_else(|e| e.into_inner()).remove(connection);
172}
173
174fn watches() -> &'static Mutex<HashMap<String, Arc<Notify>>> {
177 static WATCHES: OnceLock<Mutex<HashMap<String, Arc<Notify>>>> = OnceLock::new();
178 WATCHES.get_or_init(Default::default)
179}
180
181fn watch(connection: &str) {
183 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
184 return;
185 };
186 let mut watches = watches().lock().unwrap_or_else(|e| e.into_inner());
187 if let Some(ask_now) = watches.get(connection) {
188 ask_now.notify_one();
189 return;
190 }
191 let Ok(host) = HostSsh::for_connection(connection) else {
192 return;
193 };
194 let ask_now = Arc::new(Notify::new());
195 watches.insert(connection.to_string(), ask_now.clone());
196 runtime.spawn(run(connection.to_string(), host, ask_now));
197}
198
199async fn run(connection: String, host: HostSsh, ask_now: Arc<Notify>) {
200 loop {
201 let up = host.shared_connection_up().await;
202 let wait = {
203 let mut watches = watches().lock().unwrap_or_else(|e| e.into_inner());
206 let now = status::get(&connection);
207 let step = step(up, now.as_ref().map(|s| (s.status.as_str(), s.activeconnnum)), held(&connection));
208 if let Some(status) = step.status {
209 status::set(broker(), &connection, status, None);
210 }
211 match step.next {
212 Some(wait) => wait,
213 None => {
214 if watches.get(&connection).is_some_and(|w| Arc::ptr_eq(w, &ask_now)) {
215 watches.remove(&connection);
216 }
217 return;
218 }
219 }
220 };
221 tokio::select! {
222 _ = tokio::time::sleep(wait) => {}
223 _ = ask_now.notified() => {}
224 }
225 }
226}
227
228#[derive(Debug, PartialEq, Eq)]
230struct Step {
231 status: Option<&'static str>,
233 next: Option<Duration>,
235}
236
237fn step(up: Option<bool>, now: Option<(&str, i32)>, held: bool) -> Step {
243 let up = if held { up.map(|_| false) } else { up };
244 let (status, panes) = now.unwrap_or((state::DISCONNECTED, 0));
245 let change = |to: &'static str| (status != to).then_some(to);
246 match up {
247 Some(true) => Step { status: change(state::CONNECTED), next: Some(WHILE_UP) },
248 Some(false) if panes > 0 => Step { status: None, next: Some(WHILE_CONNECTING) },
251 Some(false) => Step {
253 status: (status == state::CONNECTED || status == state::CONNECTING).then_some(state::DISCONNECTED),
254 next: None,
255 },
256 None => Step {
258 status: None,
259 next: Some(if status == state::CONNECTED { WHILE_UP } else { WHILE_CONNECTING }),
260 },
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn s(status: Option<&'static str>, next: Option<Duration>) -> Step {
269 Step { status, next }
270 }
271
272 #[test]
276 fn connecting_turns_connected_only_when_the_connection_answers() {
277 assert_eq!(step(Some(false), Some((state::CONNECTING, 1)), false), s(None, Some(WHILE_CONNECTING)));
278 assert_eq!(step(Some(true), Some((state::CONNECTING, 1)), false), s(Some(state::CONNECTED), Some(WHILE_UP)));
279 assert_eq!(step(Some(true), Some((state::CONNECTED, 1)), false), s(None, Some(WHILE_UP)));
280 }
281
282 #[test]
286 fn a_connection_gone_with_nothing_on_it_is_disconnected() {
287 assert_eq!(step(Some(false), Some((state::CONNECTED, 0)), false), s(Some(state::DISCONNECTED), None));
288 assert_eq!(step(Some(false), Some((state::CONNECTING, 0)), false), s(Some(state::DISCONNECTED), None));
289 assert_eq!(step(Some(false), Some((state::DISCONNECTED, 0)), false), s(None, None));
290 assert_eq!(step(Some(false), None, false), s(None, None));
291 }
292
293 #[test]
295 fn a_failure_keeps_its_reason() {
296 assert_eq!(step(Some(false), Some((state::ERROR, 0)), false), s(None, None));
297 }
298
299 #[test]
302 fn a_connection_outliving_its_panes_stays_up_and_watched() {
303 assert_eq!(step(Some(true), Some((state::CONNECTED, 0)), false), s(None, Some(WHILE_UP)));
304 assert_eq!(step(Some(true), Some((state::DISCONNECTED, 0)), false), s(Some(state::CONNECTED), Some(WHILE_UP)));
305 }
306
307 #[test]
313 fn a_connection_the_user_disconnected_is_never_reported_up() {
314 assert_eq!(step(Some(true), Some((state::DISCONNECTED, 0)), true), s(None, None));
315 assert_eq!(step(Some(true), Some((state::DISCONNECTED, 1)), true), s(None, Some(WHILE_CONNECTING)));
316 }
317
318 #[test]
320 fn no_answer_changes_nothing() {
321 assert_eq!(step(None, Some((state::CONNECTED, 1)), false), s(None, Some(WHILE_UP)));
322 assert_eq!(step(None, Some((state::CONNECTING, 1)), false), s(None, Some(WHILE_CONNECTING)));
323 }
324
325 #[test]
330 fn a_login_that_worked_leaves_the_host_connected_without_sharing() {
331 for shared in [false, true] {
332 let name = if shared { "test-link-login-shared" } else { "test-link-login-unshared" };
333 started(name, false, shared);
334 login_ended(name, Ok(()), shared);
335 let s = status::get(name).unwrap();
336 assert_eq!((s.status.as_str(), s.activeconnnum), (state::CONNECTED, 0), "shared: {shared}");
337 started(name, false, shared);
338 login_ended(name, Err("Permission denied"), shared);
339 assert_eq!(status::get(name).unwrap().status, state::ERROR, "shared: {shared}");
340 }
341 let name = "test-link-used-unshared";
342 used_with(name, false);
343 assert_eq!(status::get(name).map(|s| s.status), Some(state::CONNECTED.to_string()));
344 }
345
346 #[test]
349 fn a_disconnect_holds_the_connection_until_a_login() {
350 let name = "test-link-held";
351 hold(name);
352 assert!(held(name));
353 pane_ended(name, Some("Shared connection closed"));
354 assert_ne!(status::get(name).map(|s| s.status), Some(state::ERROR.to_string()));
355 pane_started(name, false);
356 assert!(!held(name), "a login releases it");
357 }
358}