routa_core/acp/
binary_manager.rs1use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use tokio::sync::Mutex;
13
14use super::paths::AcpPaths;
15use super::registry_types::BinaryInfo;
16
17pub struct AcpBinaryManager {
19 paths: AcpPaths,
20 download_locks: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
22}
23
24impl AcpBinaryManager {
25 pub fn new(paths: AcpPaths) -> Self {
27 Self {
28 paths,
29 download_locks: Arc::new(Mutex::new(HashMap::new())),
30 }
31 }
32
33 pub async fn install_binary(
36 &self,
37 agent_id: &str,
38 version: &str,
39 binary_info: &BinaryInfo,
40 ) -> Result<PathBuf, String> {
41 let lock = {
43 let mut locks = self.download_locks.lock().await;
44 locks
45 .entry(agent_id.to_string())
46 .or_insert_with(|| Arc::new(Mutex::new(())))
47 .clone()
48 };
49
50 let _guard = lock.lock().await;
52
53 let install_dir = self.paths.agent_version_dir(agent_id, version);
54 let download_dir = self.paths.agent_download_dir(agent_id, version);
55
56 if install_dir.exists() {
58 if let Some(exe) = self.find_executable(&install_dir, binary_info).await {
59 tracing::info!(
60 "[AcpBinaryManager] Agent {} already installed at {:?}",
61 agent_id,
62 exe
63 );
64 return Ok(exe);
65 }
66 }
67
68 tokio::fs::create_dir_all(&download_dir)
70 .await
71 .map_err(|e| format!("Failed to create download dir: {e}"))?;
72 tokio::fs::create_dir_all(&install_dir)
73 .await
74 .map_err(|e| format!("Failed to create install dir: {e}"))?;
75
76 let archive_path = self
78 .download_archive(&binary_info.archive, &download_dir)
79 .await?;
80
81 self.extract_archive(&archive_path, &install_dir).await?;
83
84 let exe_path = self
86 .find_executable(&install_dir, binary_info)
87 .await
88 .ok_or_else(|| "Could not find executable in extracted archive".to_string())?;
89
90 self.prepare_executable(&exe_path).await?;
92
93 let _ = tokio::fs::remove_dir_all(&download_dir).await;
95
96 tracing::info!(
97 "[AcpBinaryManager] Installed {} v{} at {:?}",
98 agent_id,
99 version,
100 exe_path
101 );
102 Ok(exe_path)
103 }
104
105 async fn download_archive(&self, url: &str, download_dir: &Path) -> Result<PathBuf, String> {
107 tracing::info!("[AcpBinaryManager] Downloading from {}", url);
108
109 let response = reqwest::get(url)
110 .await
111 .map_err(|e| format!("Failed to download: {e}"))?;
112
113 if !response.status().is_success() {
114 return Err(format!(
115 "Download failed with status: {}",
116 response.status()
117 ));
118 }
119
120 let filename = url
122 .split('/')
123 .next_back()
124 .unwrap_or("archive")
125 .split('?')
126 .next()
127 .unwrap_or("archive");
128
129 let archive_path = download_dir.join(filename);
130
131 let bytes = response
132 .bytes()
133 .await
134 .map_err(|e| format!("Failed to read response: {e}"))?;
135
136 tokio::fs::write(&archive_path, &bytes)
137 .await
138 .map_err(|e| format!("Failed to write archive: {e}"))?;
139
140 tracing::info!(
141 "[AcpBinaryManager] Downloaded {} bytes to {:?}",
142 bytes.len(),
143 archive_path
144 );
145 Ok(archive_path)
146 }
147
148 async fn extract_archive(&self, archive_path: &Path, install_dir: &Path) -> Result<(), String> {
150 let archive_str = archive_path.to_string_lossy().to_lowercase();
151 let archive_path = archive_path.to_path_buf();
152 let install_dir = install_dir.to_path_buf();
153
154 tokio::task::spawn_blocking(move || {
156 if archive_str.ends_with(".zip") {
157 Self::extract_zip(&archive_path, &install_dir)
158 } else if archive_str.ends_with(".tar.gz") || archive_str.ends_with(".tgz") {
159 Self::extract_tar_gz(&archive_path, &install_dir)
160 } else if archive_str.ends_with(".tar.bz2") || archive_str.ends_with(".tbz2") {
161 Self::extract_tar_bz2(&archive_path, &install_dir)
162 } else if archive_str.ends_with(".tar") {
163 Self::extract_tar(&archive_path, &install_dir)
164 } else {
165 let filename = archive_path.file_name().unwrap_or_default();
167 let dest = install_dir.join(filename);
168 std::fs::copy(&archive_path, &dest)
169 .map_err(|e| format!("Failed to copy binary: {e}"))?;
170 Ok(())
171 }
172 })
173 .await
174 .map_err(|e| format!("Extract task failed: {e}"))?
175 }
176
177 fn extract_zip(archive: &Path, dest: &Path) -> Result<(), String> {
178 let file = std::fs::File::open(archive).map_err(|e| format!("Failed to open zip: {e}"))?;
179 let mut archive =
180 zip::ZipArchive::new(file).map_err(|e| format!("Failed to read zip: {e}"))?;
181
182 for i in 0..archive.len() {
183 let mut file = archive
184 .by_index(i)
185 .map_err(|e| format!("Failed to read zip entry: {e}"))?;
186 let outpath = dest.join(file.mangled_name());
187
188 if file.name().ends_with('/') {
189 std::fs::create_dir_all(&outpath).ok();
190 } else {
191 if let Some(p) = outpath.parent() {
192 std::fs::create_dir_all(p).ok();
193 }
194 let mut outfile = std::fs::File::create(&outpath)
195 .map_err(|e| format!("Failed to create file: {e}"))?;
196 std::io::copy(&mut file, &mut outfile)
197 .map_err(|e| format!("Failed to extract file: {e}"))?;
198 }
199 }
200 Ok(())
201 }
202
203 fn extract_tar_gz(archive: &Path, dest: &Path) -> Result<(), String> {
204 let file =
205 std::fs::File::open(archive).map_err(|e| format!("Failed to open tar.gz: {e}"))?;
206 let gz = flate2::read::GzDecoder::new(file);
207 let mut tar = tar::Archive::new(gz);
208 tar.unpack(dest)
209 .map_err(|e| format!("Failed to extract tar.gz: {e}"))?;
210 Ok(())
211 }
212
213 fn extract_tar_bz2(archive: &Path, dest: &Path) -> Result<(), String> {
214 let file =
215 std::fs::File::open(archive).map_err(|e| format!("Failed to open tar.bz2: {e}"))?;
216 let bz2 = bzip2::read::BzDecoder::new(file);
217 let mut tar = tar::Archive::new(bz2);
218 tar.unpack(dest)
219 .map_err(|e| format!("Failed to extract tar.bz2: {e}"))?;
220 Ok(())
221 }
222
223 fn extract_tar(archive: &Path, dest: &Path) -> Result<(), String> {
224 let file = std::fs::File::open(archive).map_err(|e| format!("Failed to open tar: {e}"))?;
225 let mut tar = tar::Archive::new(file);
226 tar.unpack(dest)
227 .map_err(|e| format!("Failed to extract tar: {e}"))?;
228 Ok(())
229 }
230
231 async fn find_executable(
233 &self,
234 install_dir: &Path,
235 binary_info: &BinaryInfo,
236 ) -> Option<PathBuf> {
237 if let Some(cmd) = &binary_info.cmd {
239 let exe_name = cmd.strip_prefix("./").unwrap_or(cmd);
241 let direct = install_dir.join(exe_name);
242 if direct.exists() {
243 return Some(direct);
244 }
245 if let Some(found) = self.find_file_recursive(install_dir, exe_name).await {
247 return Some(found);
248 }
249 }
250
251 let mut entries = tokio::fs::read_dir(install_dir).await.ok()?;
253
254 while let Ok(Some(entry)) = entries.next_entry().await {
255 let path = entry.path();
256 if path.is_file() {
257 #[cfg(unix)]
259 {
260 use std::os::unix::fs::PermissionsExt;
261 if let Ok(meta) = path.metadata() {
262 if meta.permissions().mode() & 0o111 != 0 {
263 return Some(path);
264 }
265 }
266 }
267 #[cfg(windows)]
268 {
269 if path.extension().map(|e| e == "exe").unwrap_or(false) {
270 return Some(path);
271 }
272 }
273 }
274 }
275 None
276 }
277
278 async fn find_file_recursive(&self, dir: &Path, name: &str) -> Option<PathBuf> {
279 let mut stack = vec![dir.to_path_buf()];
280 while let Some(current) = stack.pop() {
281 if let Ok(mut entries) = tokio::fs::read_dir(¤t).await {
282 while let Ok(Some(entry)) = entries.next_entry().await {
283 let path = entry.path();
284 if path.is_dir() {
285 stack.push(path);
286 } else if path.file_name().map(|n| n == name).unwrap_or(false) {
287 return Some(path);
288 }
289 }
290 }
291 }
292 None
293 }
294
295 async fn prepare_executable(&self, _exe_path: &Path) -> Result<(), String> {
297 #[cfg(unix)]
298 {
299 use std::os::unix::fs::PermissionsExt;
300 let mut perms = tokio::fs::metadata(_exe_path)
301 .await
302 .map_err(|e| format!("Failed to get metadata: {e}"))?
303 .permissions();
304 perms.set_mode(perms.mode() | 0o755);
305 tokio::fs::set_permissions(_exe_path, perms)
306 .await
307 .map_err(|e| format!("Failed to set permissions: {e}"))?;
308 }
309
310 #[cfg(target_os = "macos")]
312 {
313 let exe_str = _exe_path.to_string_lossy().to_string();
314 let _ = tokio::process::Command::new("xattr")
315 .args(["-d", "com.apple.quarantine", &exe_str])
316 .output()
317 .await;
318 }
319
320 Ok(())
321 }
322
323 pub async fn uninstall(&self, agent_id: &str) -> Result<(), String> {
325 let agent_dir = self.paths.agent_dir(agent_id);
326 if agent_dir.exists() {
327 tokio::fs::remove_dir_all(&agent_dir)
328 .await
329 .map_err(|e| format!("Failed to remove agent directory: {e}"))?;
330 }
331 Ok(())
332 }
333}