agentmux_srv/backend/remote/
sessions.rs1use std::time::Duration;
9
10use serde::Serialize;
11
12use super::host::HostSsh;
13use crate::backend::blockcontroller::durable_ssh::{
14 helper_path, valid_session_id, META_KEY_SESSION_ID,
15};
16use crate::backend::obj::{self, Block};
17use crate::backend::storage::store::Store;
18
19const EXIT_COMMAND_NOT_FOUND: i32 = 127;
21
22const LIMIT: Duration = Duration::from_secs(150);
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, ts_rs::TS)]
29#[ts(export, export_to = "../../../frontend/types/rpc/")]
30pub struct HostSession {
31 pub id: String,
33 #[ts(type = "number")]
35 pub bytes: u64,
36 #[serde(skip_serializing_if = "Option::is_none")]
39 #[ts(optional)]
40 pub exited: Option<i32>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 #[ts(optional)]
44 pub blockid: Option<String>,
45}
46
47pub fn parse_list(text: &str) -> Vec<HostSession> {
50 text.lines()
51 .filter_map(|line| {
52 let mut words = line.split_whitespace();
53 let (id, end, exited) = (words.next()?, words.next()?, words.next()?);
54 if words.next().is_some() || !valid_session_id(id) {
55 return None;
56 }
57 let exited = match exited {
58 "-" => None,
59 code => Some(code.parse().ok()?),
60 };
61 Some(HostSession {
62 id: id.to_string(),
63 bytes: end.parse().ok()?,
64 exited,
65 blockid: None,
66 })
67 })
68 .collect()
69}
70
71pub fn attribute(sessions: &mut [HostSession], blocks: &[Block], conn: &str) {
73 for s in sessions.iter_mut() {
74 s.blockid = blocks
75 .iter()
76 .find(|b| {
77 obj::meta_get_string(&b.meta, META_KEY_SESSION_ID, "") == s.id
78 && super::conn::same_connection(
79 &obj::meta_get_string(
80 &b.meta,
81 crate::backend::blockcontroller::META_KEY_CONNECTION,
82 "",
83 ),
84 conn,
85 )
86 })
87 .map(|b| b.oid.clone());
88 }
89}
90
91#[derive(Debug, Clone, Copy)]
94pub struct AskIn<'a> {
95 pub block_id: &'a str,
96 pub auth_key: &'a str,
97}
98
99fn host_for(
100 conn: &str,
101 ask: Option<AskIn<'_>>,
102) -> Result<(HostSsh, Option<super::askpass::Revoke>), String> {
103 let mut host = HostSsh::for_connection(conn)?;
104 let grant = ask.and_then(|a| host.ask_user_in(a.block_id, conn, a.auth_key));
105 Ok((host, grant))
106}
107
108pub async fn list(
111 store: &Store,
112 conn: &str,
113 ask: Option<AskIn<'_>>,
114) -> Result<Vec<HostSession>, String> {
115 let (host, _grant) = host_for(conn, ask)?;
116 let out = host
117 .run(&format!("{} list", helper_path()), None, LIMIT)
118 .await?;
119 if out.code == Some(EXIT_COMMAND_NOT_FOUND) {
120 return Ok(Vec::new());
121 }
122 let mut sessions = parse_list(&out.ok()?);
123 let blocks = store.get_all::<Block>().map_err(|e| e.to_string())?;
124 attribute(&mut sessions, &blocks, conn);
125 Ok(sessions)
126}
127
128pub async fn end(conn: &str, id: &str, ask: Option<AskIn<'_>>) -> Result<bool, String> {
131 if !valid_session_id(id) {
132 return Err(format!("{id:?} is not a session id"));
133 }
134 let (host, _grant) = host_for(conn, ask)?;
135 let out = host
136 .run(
137 &format!("{} end --session {id}", helper_path()),
138 None,
139 LIMIT,
140 )
141 .await?;
142 if out.code == Some(EXIT_COMMAND_NOT_FOUND) {
143 return Ok(false);
144 }
145 Ok(out.ok()?.trim() == "ok")
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn the_helper_list_is_read_line_by_line() {
154 let got =
155 parse_list("amx-1 1024 -\namx-2 7 0\n\ngarbage\namx-3 x -\na;b 1 -\namx-4 1 - extra\n");
156 assert_eq!(
157 got,
158 vec![
159 HostSession {
160 id: "amx-1".into(),
161 bytes: 1024,
162 exited: None,
163 blockid: None
164 },
165 HostSession {
166 id: "amx-2".into(),
167 bytes: 7,
168 exited: Some(0),
169 blockid: None
170 },
171 ]
172 );
173 }
174
175 #[test]
176 fn a_session_belongs_to_the_pane_on_its_connection() {
177 let block = |oid: &str, conn: &str, sid: &str| {
178 let mut b = Block {
179 oid: oid.into(),
180 ..Default::default()
181 };
182 b.meta.insert("connection".into(), serde_json::json!(conn));
183 b.meta
184 .insert(META_KEY_SESSION_ID.into(), serde_json::json!(sid));
185 b
186 };
187 let blocks = vec![
188 block("b-other-host", "other", "amx-1"),
189 block("b-here", "user@box", "amx-1"),
190 block("b-none", "user@box", "amx-9"),
191 ];
192 let mut sessions = parse_list("amx-1 5 -\namx-2 5 -\n");
193 attribute(&mut sessions, &blocks, "user@box");
194 assert_eq!(sessions[0].blockid.as_deref(), Some("b-here"));
195 assert_eq!(sessions[1].blockid, None);
197 }
198}