sysroot-cleaner/src/cleaners.rs

218 lines
6.5 KiB
Rust

// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: EUPL-1.2
mod dso;
mod list;
use crate::{
args::Args,
decision::{Action, Decision},
};
use anyhow::Result;
use async_trait::async_trait;
use dso::DsoCleaner;
use list::ListCleaner;
use nix::libc::EXDEV;
use std::{
collections::HashMap,
io,
path::{Path, PathBuf},
};
use tokio::{
sync::{broadcast, mpsc},
task::JoinSet,
};
use walkdir::{DirEntry, WalkDir};
#[async_trait]
pub trait Cleaner {
async fn run(
&mut self,
mut files: broadcast::Receiver<PathBuf>,
decisions: mpsc::Sender<Decision>,
) -> Result<()>;
}
type Cleaners = Vec<Box<dyn Cleaner + Send>>;
type RemovalFn = Box<dyn Fn(&Path) -> io::Result<()>>;
pub struct Runner {
cleaners: Cleaners,
removal_fn: RemovalFn,
}
const CHANNEL_SIZE: usize = 100;
const CHANNEL_MAX_LOAD: usize = CHANNEL_SIZE * 3 / 4;
impl Runner {
pub fn new(args: Args) -> Self {
let removal_fn = Self::new_removal_fn(&args);
let mut cleaners: Cleaners = vec![];
cleaners.push(Box::new(DsoCleaner::new(args.output_dotfile)));
if let Some(wl) = args.allowlist {
cleaners.push(Box::new(ListCleaner::new(Action::Keep, wl)));
}
if let Some(bl) = args.blocklist {
cleaners.push(Box::new(ListCleaner::new(Action::Remove, bl)));
}
Self {
cleaners,
removal_fn,
}
}
pub async fn run(self) -> Result<()> {
let input_tx = broadcast::Sender::new(CHANNEL_SIZE);
let (output_tx, output_rx) = mpsc::channel(CHANNEL_SIZE);
let mut tasks = JoinSet::new();
// Processors
for mut cleaner in self.cleaners {
let input_rx = input_tx.subscribe();
let output_tx_clone = output_tx.clone();
tasks.spawn(async move { cleaner.run(input_rx, output_tx_clone).await });
}
drop(output_tx);
// Producer of inputs (note that this needs to happen
// after all channels have been created)
tasks.spawn(Self::input_producer(input_tx));
// Output consumer
Self::output_consumer(self.removal_fn, output_rx).await;
while let Some(task) = tasks.join_next().await {
if let Err(err) = task? {
log::error!("{}", err);
}
}
Ok(())
}
async fn input_producer(input_tx: broadcast::Sender<PathBuf>) -> Result<()> {
let walker = WalkDir::new(".").follow_links(false);
for entry in walker {
match entry {
Ok(e) if !Self::is_dir(&e) => {
if input_tx.len() >= CHANNEL_MAX_LOAD {
// TODO: FIXME: make this better, this is a quick hack
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
input_tx.send(e.into_path())?;
}
Ok(_) => continue,
Err(err) => log::warn!("unable to access path: {}", err),
}
}
// we handle errors by warning the user, otherwise we always succeed
Ok(())
}
fn is_dir(entry: &DirEntry) -> bool {
let ty = entry.file_type();
if ty.is_dir() {
true
} else if ty.is_file() {
false
} else {
// it is a symlink
match std::fs::metadata(entry.path()) {
Ok(metadata) => metadata.is_dir(),
Err(e) => {
log::debug!(
"unable to resolve symlink {}: {}",
entry.path().display(),
e
);
false
}
}
}
}
async fn output_consumer(removal_fn: RemovalFn, mut output_rx: mpsc::Receiver<Decision>) {
let mut to_remove = HashMap::new();
while let Some(decision) = output_rx.recv().await {
match to_remove.get_mut(&decision.path) {
Some(prev_action) => {
if decision.action == Action::Keep {
*prev_action = Action::Keep;
}
}
None => {
to_remove.insert(decision.path, decision.action);
}
}
}
for (file, action) in to_remove {
if action == Action::Remove {
if let Err(err) = (removal_fn)(&file) {
log::error!("{}: {}", file.display(), err);
}
}
}
}
fn new_removal_fn(args: &Args) -> RemovalFn {
if let Some(dest) = args.split_to.clone() {
if args.dry_run {
Box::new(move |path| {
log::info!(
"(dry-run) would move {} to {}",
path.display(),
dest.display()
);
Ok(())
})
} else {
Box::new(move |path| {
log::info!("moving {} to {}", path.display(), dest.display());
Self::move_preserve(&path, &dest)
})
}
} else {
if args.dry_run {
Box::new(|path| {
let ty = if path.is_symlink() {
"symlink"
} else {
"regular file"
};
log::info!("(dry-run) would remove {} {}", ty, path.display());
Ok(())
})
} else {
Box::new(move |path| {
log::info!("removing {}", path.display());
std::fs::remove_file(&path)
})
}
}
}
fn move_preserve(src: &Path, dest: &Path) -> io::Result<()> {
assert!(src.is_relative());
let abs_dest = dest.join(src);
if let Some(parent) = abs_dest.parent() {
std::fs::create_dir_all(parent)?;
}
match std::fs::rename(&src, &abs_dest) {
Err(err) if err.raw_os_error() == Some(EXDEV) => {
log::trace!(
"different filesystems, falling back to copying {} to {}",
src.display(),
abs_dest.display()
);
std::fs::copy(src, abs_dest).and_then(|_| std::fs::remove_file(src))
}
other => other,
}
}
}