list_files returns context-relative paths (rooted inside the chroot for an unshare context, on the remote for ssh): whether an entry is a directory can only be decided through the context, never with a host-side stat. Give every driver a proper is_dir rather than approximating it with exists, so callers can keep directories and files apart — the deb package-directory search lists directories only.
231 lines
7.3 KiB
Rust
231 lines
7.3 KiB
Rust
/// 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<PathBuf> {
|
|
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<String> {
|
|
// 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<Vec<PathBuf>> {
|
|
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<std::process::ExitStatus> {
|
|
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<dyn LineSink>,
|
|
) -> io::Result<std::process::ExitStatus> {
|
|
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<std::process::Output> {
|
|
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<String> {
|
|
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<bool> {
|
|
Ok(path.exists())
|
|
}
|
|
|
|
fn is_dir(&self, path: &Path) -> io::Result<bool> {
|
|
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<String> = 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();
|
|
}
|
|
}
|
|
}
|