use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use crate::clean;
use crate::project::{Artifact, GitStatus, Project, ProjectId};
use crate::scan::{self, ScanOptions};
use crate::size::{self, DirStat};
use crate::{config::Settings, gitignore};
const PROGRESS_EVERY: u64 = 16;
#[derive(Debug)]
pub enum Event {
Found(Box<Project>),
Progress {
dirs: u64,
current: PathBuf,
},
ScanFinished {
dirs: u64,
},
Artifacts {
id: ProjectId,
artifacts: Vec<Artifact>,
git: GitStatus,
skipped: Vec<PathBuf>,
},
Sized {
id: ProjectId,
index: usize,
stat: DirStat,
},
CleanStarted {
id: ProjectId,
},
CleanFinished {
id: ProjectId,
freed: u64,
errors: Vec<String>,
},
}
enum Job {
Resolve {
id: ProjectId,
root: PathBuf,
candidates: Vec<(String, PathBuf)>,
},
Size {
id: ProjectId,
index: usize,
path: PathBuf,
},
Clean {
id: ProjectId,
root: PathBuf,
paths: Vec<PathBuf>,
},
}
struct Queue {
state: Mutex<QueueState>,
signal: Condvar,
}
struct QueueState {
jobs: VecDeque<Job>,
pending: usize,
scanning: bool,
closed: bool,
}
impl Queue {
fn new() -> Self {
Self {
state: Mutex::new(QueueState {
jobs: VecDeque::new(),
pending: 0,
scanning: true,
closed: false,
}),
signal: Condvar::new(),
}
}
fn push(&self, job: Job, front: bool) {
let mut state = self.state.lock().unwrap();
if state.closed {
return;
}
state.pending += 1;
if front {
state.jobs.push_front(job);
} else {
state.jobs.push_back(job);
}
self.signal.notify_one();
}
fn pop(&self) -> Option<Job> {
let mut state = self.state.lock().unwrap();
loop {
if let Some(job) = state.jobs.pop_front() {
return Some(job);
}
if state.closed {
return None;
}
state = self.signal.wait(state).unwrap();
}
}
fn finish_job(&self) {
let mut state = self.state.lock().unwrap();
state.pending = state.pending.saturating_sub(1);
self.signal.notify_all();
}
fn scan_finished(&self) {
let mut state = self.state.lock().unwrap();
state.scanning = false;
self.signal.notify_all();
}
fn wait_idle(&self) {
let mut state = self.state.lock().unwrap();
while !state.closed && (state.scanning || state.pending > 0) {
state = self.signal.wait(state).unwrap();
}
}
fn close(&self) {
let mut state = self.state.lock().unwrap();
state.closed = true;
state.jobs.clear();
self.signal.notify_all();
}
}
pub struct Engine {
events: Receiver<Event>,
queue: Arc<Queue>,
cancel: Arc<AtomicBool>,
threads: Vec<JoinHandle<()>>,
}
impl Engine {
pub fn start(options: ScanOptions, settings: &Settings) -> Self {
let (tx, events) = channel();
let queue = Arc::new(Queue::new());
let cancel = Arc::new(AtomicBool::new(false));
let next_id = Arc::new(AtomicUsize::new(0));
let require_git_ignored = settings.require_git_ignored;
let mut threads = Vec::new();
for _ in 0..worker_count(settings.workers) {
let queue = Arc::clone(&queue);
let cancel = Arc::clone(&cancel);
let tx = tx.clone();
threads.push(thread::spawn(move || {
worker(&queue, &cancel, &tx, require_git_ignored)
}));
}
{
let queue = Arc::clone(&queue);
let cancel = Arc::clone(&cancel);
let tx = tx.clone();
threads.push(thread::spawn(move || {
scanner(options, &queue, &cancel, &next_id, &tx)
}));
}
drop(tx);
Self {
events,
queue,
cancel,
threads,
}
}
pub fn drain(&self) -> Vec<Event> {
let mut out = Vec::new();
loop {
match self.events.try_recv() {
Ok(event) => out.push(event),
Err(TryRecvError::Empty | TryRecvError::Disconnected) => return out,
}
}
}
pub fn run_to_completion<F: FnMut(Event)>(&self, mut sink: F) {
let queue = Arc::clone(&self.queue);
let waiter = thread::spawn(move || queue.wait_idle());
while !waiter.is_finished() {
for event in self.drain() {
sink(event);
}
thread::sleep(std::time::Duration::from_millis(5));
}
let _ = waiter.join();
for event in self.drain() {
sink(event);
}
}
pub fn submit_clean(&self, id: ProjectId, root: PathBuf, paths: Vec<PathBuf>) {
self.queue.push(Job::Clean { id, root, paths }, true);
}
pub fn shutdown(&mut self) {
self.cancel.store(true, Ordering::Relaxed);
self.queue.close();
for handle in self.threads.drain(..) {
let _ = handle.join();
}
}
}
impl Drop for Engine {
fn drop(&mut self) {
self.shutdown();
}
}
fn worker_count(configured: usize) -> usize {
if configured > 0 {
return configured;
}
thread::available_parallelism()
.map(|n| n.get().clamp(2, 8))
.unwrap_or(4)
}
fn scanner(
options: ScanOptions,
queue: &Arc<Queue>,
cancel: &Arc<AtomicBool>,
next_id: &Arc<AtomicUsize>,
tx: &Sender<Event>,
) {
let mut dirs = 0u64;
scan::walk(
&options,
cancel,
|candidate| {
let id = next_id.fetch_add(1, Ordering::SeqCst);
let mut project = Project::new(
id,
candidate.root.clone(),
candidate.scan_root,
candidate.types,
);
project.last_activity = candidate.last_activity;
project.source_files = candidate.source_files;
if tx.send(Event::Found(Box::new(project))).is_err() {
return;
}
queue.push(
Job::Resolve {
id,
root: candidate.root,
candidates: candidate.artifacts,
},
false,
);
},
|count, current| {
dirs = count;
if count % PROGRESS_EVERY == 0 {
let _ = tx.send(Event::Progress {
dirs: count,
current: current.to_path_buf(),
});
}
},
);
let _ = tx.send(Event::ScanFinished { dirs });
queue.scan_finished();
}
fn worker(
queue: &Arc<Queue>,
cancel: &Arc<AtomicBool>,
tx: &Sender<Event>,
require_git_ignored: bool,
) {
while let Some(job) = queue.pop() {
if cancel.load(Ordering::Relaxed) {
queue.finish_job();
break;
}
match job {
Job::Resolve {
id,
root,
candidates,
} => {
let (artifacts, git, skipped) = resolve(&root, candidates, require_git_ignored);
let _ = tx.send(Event::Artifacts {
id,
artifacts: artifacts.clone(),
git,
skipped,
});
for (index, artifact) in artifacts.iter().enumerate() {
queue.push(
Job::Size {
id,
index,
path: artifact.path.clone(),
},
false,
);
}
}
Job::Size { id, index, path } => {
let stat = size::measure(&path, cancel);
let _ = tx.send(Event::Sized { id, index, stat });
}
Job::Clean { id, root, paths } => {
let _ = tx.send(Event::CleanStarted { id });
let outcome = clean::remove_all(&root, &paths);
let _ = tx.send(Event::CleanFinished {
id,
freed: outcome.freed,
errors: outcome.errors,
});
}
}
queue.finish_job();
}
}
fn resolve(
root: &Path,
candidates: Vec<(String, PathBuf)>,
require_git_ignored: bool,
) -> (Vec<Artifact>, GitStatus, Vec<PathBuf>) {
let mut patterns: HashMap<PathBuf, String> = HashMap::new();
let mut paths = Vec::with_capacity(candidates.len());
for (pattern, path) in candidates {
patterns.insert(path.clone(), pattern);
paths.push(path);
}
let (kept, skipped, git) = if require_git_ignored {
gitignore::partition_ignored(root, paths)
} else {
(paths, Vec::new(), GitStatus::Disabled)
};
let artifacts = kept
.into_iter()
.map(|path| Artifact {
pattern: patterns.remove(&path).unwrap_or_default(),
path,
stat: None,
})
.collect();
(artifacts, git, skipped)
}