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 =
179 std::fs::File::open(archive).map_err(|e| format!("Failed to open zip: {}", e))?;
180 let mut archive =
181 zip::ZipArchive::new(file).map_err(|e| format!("Failed to read zip: {}", e))?;
182
183 for i in 0..archive.len() {
184 let mut file = archive
185 .by_index(i)
186 .map_err(|e| format!("Failed to read zip entry: {}", e))?;
187 let outpath = dest.join(file.mangled_name());
188
189 if file.name().ends_with('/') {
190 std::fs::create_dir_all(&outpath).ok();
191 } else {
192 if let Some(p) = outpath.parent() {
193 std::fs::create_dir_all(p).ok();
194 }
195 let mut outfile = std::fs::File::create(&outpath)
196 .map_err(|e| format!("Failed to create file: {}", e))?;
197 std::io::copy(&mut file, &mut outfile)
198 .map_err(|e| format!("Failed to extract file: {}", e))?;
199 }
200 }
201 Ok(())
202 }
203
204 fn extract_tar_gz(archive: &Path, dest: &Path) -> Result<(), String> {
205 let file =
206 std::fs::File::open(archive).map_err(|e| format!("Failed to open tar.gz: {}", e))?;
207 let gz = flate2::read::GzDecoder::new(file);
208 let mut tar = tar::Archive::new(gz);
209 tar.unpack(dest)
210 .map_err(|e| format!("Failed to extract tar.gz: {}", e))?;
211 Ok(())
212 }
213
214 fn extract_tar_bz2(archive: &Path, dest: &Path) -> Result<(), String> {
215 let file =
216 std::fs::File::open(archive).map_err(|e| format!("Failed to open tar.bz2: {}", e))?;
217 let bz2 = bzip2::read::BzDecoder::new(file);
218 let mut tar = tar::Archive::new(bz2);
219 tar.unpack(dest)
220 .map_err(|e| format!("Failed to extract tar.bz2: {}", e))?;
221 Ok(())
222 }
223
224 fn extract_tar(archive: &Path, dest: &Path) -> Result<(), String> {
225 let file =
226 std::fs::File::open(archive).map_err(|e| format!("Failed to open tar: {}", e))?;
227 let mut tar = tar::Archive::new(file);
228 tar.unpack(dest)
229 .map_err(|e| format!("Failed to extract tar: {}", e))?;
230 Ok(())
231 }
232
233 async fn find_executable(
235 &self,
236 install_dir: &Path,
237 binary_info: &BinaryInfo,
238 ) -> Option<PathBuf> {
239 if let Some(cmd) = &binary_info.cmd {
241 let exe_name = cmd.strip_prefix("./").unwrap_or(cmd);
243 let direct = install_dir.join(exe_name);
244 if direct.exists() {
245 return Some(direct);
246 }
247 if let Some(found) = self.find_file_recursive(install_dir, exe_name).await {
249 return Some(found);
250 }
251 }
252
253 let mut entries = tokio::fs::read_dir(install_dir).await.ok()?;
255
256 while let Ok(Some(entry)) = entries.next_entry().await {
257 let path = entry.path();
258 if path.is_file() {
259 #[cfg(unix)]
261 {
262 use std::os::unix::fs::PermissionsExt;
263 if let Ok(meta) = path.metadata() {
264 if meta.permissions().mode() & 0o111 != 0 {
265 return Some(path);
266 }
267 }
268 }
269 #[cfg(windows)]
270 {
271 if path.extension().map(|e| e == "exe").unwrap_or(false) {
272 return Some(path);
273 }
274 }
275 }
276 }
277 None
278 }
279
280 async fn find_file_recursive(&self, dir: &Path, name: &str) -> Option<PathBuf> {
281 let mut stack = vec![dir.to_path_buf()];
282 while let Some(current) = stack.pop() {
283 if let Ok(mut entries) = tokio::fs::read_dir(¤t).await {
284 while let Ok(Some(entry)) = entries.next_entry().await {
285 let path = entry.path();
286 if path.is_dir() {
287 stack.push(path);
288 } else if path.file_name().map(|n| n == name).unwrap_or(false) {
289 return Some(path);
290 }
291 }
292 }
293 }
294 None
295 }
296
297 async fn prepare_executable(&self, _exe_path: &Path) -> Result<(), String> {
299 #[cfg(unix)]
300 {
301 use std::os::unix::fs::PermissionsExt;
302 let mut perms = tokio::fs::metadata(_exe_path)
303 .await
304 .map_err(|e| format!("Failed to get metadata: {}", e))?
305 .permissions();
306 perms.set_mode(perms.mode() | 0o755);
307 tokio::fs::set_permissions(_exe_path, perms)
308 .await
309 .map_err(|e| format!("Failed to set permissions: {}", e))?;
310 }
311
312 #[cfg(target_os = "macos")]
314 {
315 let exe_str = _exe_path.to_string_lossy().to_string();
316 let _ = tokio::process::Command::new("xattr")
317 .args(["-d", "com.apple.quarantine", &exe_str])
318 .output()
319 .await;
320 }
321
322 Ok(())
323 }
324
325 pub async fn uninstall(&self, agent_id: &str) -> Result<(), String> {
327 let agent_dir = self.paths.agent_dir(agent_id);
328 if agent_dir.exists() {
329 tokio::fs::remove_dir_all(&agent_dir)
330 .await
331 .map_err(|e| format!("Failed to remove agent directory: {}", e))?;
332 }
333 Ok(())
334 }
335}