agentmux_srv/backend/remote/
host.rs1use std::path::PathBuf;
10use std::time::Duration;
11
12use tokio::io::AsyncWriteExt;
13
14use super::conn::SshDest;
15use super::ConnTarget;
16
17#[derive(Debug, Clone)]
18pub struct HostSsh {
19 pub dest: SshDest,
20 pub ssh_path: PathBuf,
21 pub control_dir: Option<PathBuf>,
23 pub env: Vec<(String, String)>,
25}
26
27#[derive(Debug)]
29pub struct HostOutput {
30 pub code: Option<i32>,
31 pub stdout: String,
32 pub stderr: String,
33}
34
35impl HostOutput {
36 pub fn ok(self) -> Result<String, String> {
39 if self.code == Some(0) {
40 return Ok(self.stdout);
41 }
42 Err(self
43 .stderr
44 .lines()
45 .rev()
46 .find(|l| !l.trim().is_empty())
47 .unwrap_or("ssh failed")
48 .trim()
49 .to_string())
50 }
51}
52
53impl HostSsh {
54 pub fn for_connection(conn: &str) -> Result<Self, String> {
56 let dest = match ConnTarget::parse(conn)? {
57 ConnTarget::Ssh(d) => d,
58 _ => return Err(format!("{conn:?} is not an SSH connection")),
59 };
60 let ssh_path = super::ssh::binary().ok_or_else(super::ssh::missing_binary_message)?;
61 Ok(Self {
62 dest,
63 ssh_path,
64 control_dir: super::ssh::shared_control_dir(),
65 env: Vec::new(),
66 })
67 }
68
69 pub fn ask_user_in(
75 &mut self,
76 block_id: &str,
77 conn: &str,
78 auth_key: &str,
79 ) -> Option<super::askpass::Revoke> {
80 use super::askpass;
81 let program = askpass::program()?;
82 let secret = askpass::grant(askpass::AskpassGrant {
83 agent_block_id: block_id.to_string(),
84 agent: String::new(),
85 connection: conn.to_string(),
86 user_pane: true,
87 });
88 let local_url = std::env::var("AGENTMUX_LOCAL_URL").unwrap_or_default();
89 self.env = askpass::ssh_env(&secret, &program, &local_url, auth_key);
90 Some(askpass::Revoke(secret))
91 }
92
93 pub fn command(&self, remote: &str) -> tokio::process::Command {
97 let mut args = super::ssh::launch_exec(&self.dest, remote, "", self.control_dir.as_deref());
98 if self.env.is_empty() {
102 args.insert(1, "-oBatchMode=yes".to_string());
103 }
104 self.ssh(&args)
105 }
106
107 fn ssh(&self, args: &[String]) -> tokio::process::Command {
111 let mut cmd = tokio::process::Command::new(&self.ssh_path);
112 cmd.args(args).kill_on_drop(true);
113 #[cfg(windows)]
114 {
115 use agentmux_common::win32::NoWindow;
116 cmd.no_window();
117 }
118 crate::backend::pane_env::sanitize_process_command(&mut cmd);
119 cmd.envs(self.env.iter().map(|(k, v)| (k, v)));
120 cmd
121 }
122
123 pub async fn shared_connection_up(&self) -> Option<bool> {
127 let dir = self.control_dir.as_deref()?;
128 let args = super::ssh::control(&self.dest, dir, super::ssh::Control::Check);
129 let out = self.run_control(&args).await?;
130 Some(out.status.success())
131 }
132
133 pub async fn close_shared_connection(&self) {
137 if let Some(dir) = self.control_dir.as_deref() {
138 let args = super::ssh::control(&self.dest, dir, super::ssh::Control::Exit);
139 let _ = self.run_control(&args).await;
140 }
141 }
142
143 async fn run_control(&self, args: &[String]) -> Option<std::process::Output> {
146 let mut cmd = self.ssh(args);
147 cmd.stdin(std::process::Stdio::null())
148 .stdout(std::process::Stdio::piped())
149 .stderr(std::process::Stdio::piped());
150 tokio::time::timeout(Duration::from_secs(5), cmd.output())
151 .await
152 .ok()?
153 .ok()
154 }
155
156 pub async fn run(
159 &self,
160 remote: &str,
161 input: Option<Vec<u8>>,
162 limit: Duration,
163 ) -> Result<HostOutput, String> {
164 let mut cmd = self.command(remote);
165 cmd.stdin(if input.is_some() {
166 std::process::Stdio::piped()
167 } else {
168 std::process::Stdio::null()
169 })
170 .stdout(std::process::Stdio::piped())
171 .stderr(std::process::Stdio::piped());
172 let mut child = cmd.spawn().map_err(|e| format!("could not run ssh: {e}"))?;
173 if let (Some(bytes), Some(mut stdin)) = (input, child.stdin.take()) {
174 stdin
175 .write_all(&bytes)
176 .await
177 .map_err(|e| format!("upload: {e}"))?;
178 drop(stdin);
179 }
180 let out = tokio::time::timeout(limit, child.wait_with_output())
181 .await
182 .map_err(|_| "ssh took too long".to_string())?
183 .map_err(|e| format!("ssh: {e}"))?;
184 Ok(HostOutput {
185 code: out.status.code(),
186 stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
187 stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
188 })
189 }
190}