use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, MutexGuard, PoisonError};
use rustix::fd::OwnedFd;
use rustix::fs::{Stat, fstat};
use super::{Found, Scan, Visited, Walk, io};
use crate::ignore::Ignore;
use crate::status::types::check;
use crate::status::{Error, UntrackedKind, UntrackedPath};
pub(super) struct Task {
pub(super) open: Open,
pub(super) path: Vec<u8>,
pub(super) depth: usize,
pub(super) walk: Walk,
pub(super) rules: Option<Arc<Ignore>>,
pub(super) listed: bool,
}
pub(super) enum Open {
Root(Arc<OwnedFd>),
Child {
parent: Arc<OwnedFd>,
name: Vec<u8>,
stat: Stat,
},
}
pub(super) struct Queue {
pub(super) tasks: Vec<Task>,
pub(super) active: usize,
pub(super) failure: Option<Error>,
}
pub(super) struct Budget(AtomicUsize);
impl Budget {
pub(super) fn new(remaining: usize) -> Self {
Self(AtomicUsize::new(remaining))
}
pub(super) fn take(&self, count: usize) -> bool {
let mut remaining = self.0.load(Ordering::Relaxed);
loop {
let Some(left) = remaining.checked_sub(count) else {
return false;
};
match self.0.compare_exchange_weak(
remaining,
left,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return true,
Err(current) => remaining = current,
}
}
}
pub(super) fn charge(&self, count: usize, label: &'static str) -> Result<(), Error> {
if self.take(count) {
Ok(())
} else {
Err(Error::Limit(label))
}
}
pub(super) fn remaining(&self) -> usize {
self.0.load(Ordering::Relaxed)
}
}
struct Taken<'s, 'a>(&'s Scan<'a>);
impl Drop for Taken<'_, '_> {
fn drop(&mut self) {
let mut queue = self.0.lock();
queue.active -= 1;
if std::thread::panicking() {
self.0.stopped.store(true, Ordering::Relaxed);
}
if queue.active == 0 || self.0.stopped.load(Ordering::Relaxed) {
self.0.ready.notify_all();
}
}
}
impl Scan<'_> {
pub(super) fn run(&self, threads: usize) -> Result<Found, Error> {
let threads = std::thread::available_parallelism()
.map_or(1, std::num::NonZero::get)
.min(threads)
.max(1);
let mut found = Found::default();
std::thread::scope(|scope| {
let workers: Vec<_> = (1..threads)
.filter_map(|_| {
std::thread::Builder::new()
.spawn_scoped(scope, || {
let mut found = Found::default();
self.work(&mut found);
found
})
.ok()
})
.collect();
self.work(&mut found);
for worker in workers {
match worker.join() {
Ok(other) => found.extend(other),
Err(panic) => std::panic::resume_unwind(panic),
}
}
});
match self.lock().failure.take() {
Some(error) => Err(error),
None => Ok(found),
}
}
fn lock(&self) -> MutexGuard<'_, Queue> {
self.queue.lock().unwrap_or_else(PoisonError::into_inner)
}
fn work(&self, found: &mut Found) {
while let Some(task) = self.next() {
let taken = Taken(self);
if let Err(error) = self.task(found, task) {
let mut queue = self.lock();
if !self.stopped.swap(true, Ordering::Relaxed) {
queue.failure = Some(error);
}
}
drop(taken);
}
}
fn next(&self) -> Option<Task> {
let mut queue = self.lock();
loop {
if self.stopped.load(Ordering::Relaxed) {
return None;
}
if let Some(task) = queue.tasks.pop() {
queue.active += 1;
return Some(task);
}
if queue.active == 0 {
return None;
}
queue = self
.ready
.wait(queue)
.unwrap_or_else(PoisonError::into_inner);
}
}
pub(super) fn push(&self, task: Task) {
self.lock().tasks.push(task);
self.ready.notify_one();
}
pub(super) fn check(&self) -> Result<(), Error> {
if self.stopped.load(Ordering::Relaxed) {
return Err(Error::Cancelled);
}
check(self.cancel)
}
fn task(&self, found: &mut Found, task: Task) -> Result<(), Error> {
let Task {
open,
path,
depth,
walk,
rules,
listed,
} = task;
match open {
Open::Root(fd) => {
let before = fstat(&*fd).map_err(|source| io(&path, source))?;
self.directory(found, &fd, &before, &path, depth, walk, rules)?;
}
Open::Child { parent, name, stat } => {
let visited =
self.child_directory(found, &parent, &name, &path, &stat, depth, walk, rules)?;
if listed && visited == Visited::Repository {
found.listed.push(UntrackedPath {
path,
kind: UntrackedKind::Repository,
});
}
}
}
Ok(())
}
}