/// Local context: execute commands locally /// Context driver: Does nothing use super::api::{ContextDriver, LineSink, Stream}; use super::capture::pump; use std::io; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::Arc; use std::time::SystemTime; pub struct LocalDriver; impl ContextDriver for LocalDriver { fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result { let dest_root_path = Path::new(dest_root); let dest = dest_root_path.join(src.file_name().unwrap_or(src.as_os_str())); if src != dest { // Copy src inside dest_root self.copy_path(src, &dest)?; } dest.canonicalize() } fn create_temp_dir(&self) -> io::Result { // Sub-second precision and an atomic create: two concurrent // contexts racing on the same name must never share a directory, // so the loser of a create falls through to the next attempt // instead of probing for existence first (a probe-then-create // window loses exactly when two callers arrive together). let base_timestamp = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_millis(); let mut attempt = 0; loop { let work_dir_name = if attempt == 0 { format!("pkh-{base_timestamp}") } else { format!("pkh-{base_timestamp}-{attempt}") }; let temp_dir_path = std::env::temp_dir().join(&work_dir_name); match std::fs::create_dir(&temp_dir_path) { Ok(()) => return Ok(temp_dir_path.to_string_lossy().to_string()), Err(e) if e.kind() == io::ErrorKind::AlreadyExists => attempt += 1, Err(e) => return Err(e), } } } fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> { self.copy_path(src, dest) } fn list_files(&self, path: &Path) -> io::Result> { let mut entries = Vec::new(); for entry in std::fs::read_dir(path)? { let entry = entry?; entries.push(entry.path()); } Ok(entries) } fn run( &self, program: &str, args: &[String], env: &[(String, String)], cwd: Option<&str>, ) -> io::Result { let mut cmd = Command::new(program); cmd.args(args).envs(env.iter().map(|(k, v)| (k, v))); if let Some(dir) = cwd { cmd.current_dir(dir); } cmd.status() } fn run_captured( &self, program: &str, args: &[String], env: &[(String, String)], cwd: Option<&str>, sink: Arc, ) -> io::Result { let mut cmd = Command::new(program); cmd.args(args).envs(env.iter().map(|(k, v)| (k, v))); // Best-effort: ask children not to emit ANSI colors; captured lines are // stripped anyway. cmd.env("NO_COLOR", "1"); if let Some(dir) = cwd { cmd.current_dir(dir); } cmd.stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(Stdio::null()); let mut child = cmd.spawn()?; let stdout = child.stdout.take(); let stderr = child.stderr.take(); // One reader thread per stream; lines are forwarded to the sink as // they arrive so the UI stays live. let out_sink = sink.clone(); let t_out = stdout.map(|r| std::thread::spawn(move || pump(r, Stream::Stdout, out_sink.as_ref()))); let err_sink = sink.clone(); let t_err = stderr.map(|r| std::thread::spawn(move || pump(r, Stream::Stderr, err_sink.as_ref()))); let status = child.wait(); if let Some(t) = t_out { let _ = t.join(); } if let Some(t) = t_err { let _ = t.join(); } status } fn run_output( &self, program: &str, args: &[String], env: &[(String, String)], cwd: Option<&str>, ) -> io::Result { let mut cmd = Command::new(program); cmd.args(args).envs(env.iter().map(|(k, v)| (k, v))); if let Some(dir) = cwd { cmd.current_dir(dir); } cmd.output() } fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()> { copy_dir_recursive(src, dest) } fn read_file(&self, path: &Path) -> io::Result { std::fs::read_to_string(path) } fn write_file(&self, path: &Path, content: &str) -> io::Result<()> { std::fs::write(path, content) } fn exists(&self, path: &Path) -> io::Result { Ok(path.exists()) } fn is_dir(&self, path: &Path) -> io::Result { Ok(path.is_dir()) } } fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> { // Reproduce symlinks as symlinks rather than following them, so that // dangling/absolute symlinks do not abort the copy. if std::fs::symlink_metadata(src)?.file_type().is_symlink() { let target = std::fs::read_link(src)?; let _ = std::fs::remove_file(dest); return symlink(&target, dest); } if src.is_dir() { std::fs::create_dir_all(dest)?; for entry in std::fs::read_dir(src)? { let entry = entry?; let path = entry.path(); // Never ship VCS metadata into the build tree: its presence // flips autotools 'building from VCS' detection (see // is_vcs_dir_name) and activates maintainer-only regeneration // rules requiring undeclared tools (e.g. help2man). if path.symlink_metadata().map(|m| m.is_dir()).unwrap_or(false) && super::is_vcs_dir_name(&entry.file_name()) { continue; } let dest_path = dest.join(entry.file_name()); copy_dir_recursive(&path, &dest_path)?; } } else { std::fs::copy(src, dest)?; } Ok(()) } #[cfg(test)] mod tests { use super::*; /// Concurrent callers must never share a temporary directory: the /// create is atomic, so a lost race falls through to the next name /// instead of both callers probing the same free name and unpacking /// into the same directory. #[test] fn create_temp_dir_is_unique_under_concurrency() { const CALLERS: usize = 8; let (tx, rx) = std::sync::mpsc::channel(); let handles: Vec<_> = (0..CALLERS) .map(|_| { let tx = tx.clone(); std::thread::spawn(move || { let dir = LocalDriver.create_temp_dir().unwrap(); tx.send(dir).unwrap(); }) }) .collect(); for handle in handles { handle.join().unwrap(); } drop(tx); let mut names: Vec = rx.iter().collect(); names.sort(); let unique: std::collections::BTreeSet<&String> = names.iter().collect(); assert_eq!(names.len(), unique.len(), "duplicate temp dirs: {names:?}"); for name in &unique { std::fs::remove_dir(name).unwrap(); } } }