use crate::hashing::{HashMap, HashSet};
use crate::refresh::{self, Entry, Observer, Selection, Sources};
use crate::store::{self, FileStat, RevisionId, StoredFile};
use std::borrow::Cow;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
mod inotify;
#[cfg(target_os = "linux")]
use inotify::Backend;
#[cfg(not(any(target_os = "linux", windows)))]
mod unwatched;
#[cfg(not(any(target_os = "linux", windows)))]
use unwatched::Backend;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
use windows::Backend;
#[cfg(test)]
mod tests;
#[derive(Debug)]
enum Happened {
Lost,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
Directory,
#[cfg_attr(not(windows), allow(dead_code))]
Failed(String),
Entry {
path: PathBuf,
kind: Kind,
directory: Option<bool>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Kind {
Added,
Removed,
Changed,
}
#[derive(Debug, PartialEq, Eq)]
enum Cover {
Watched(bool),
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
Above(bool),
Unwatched,
}
enum Watching {
Watched(bool),
Unwatched(bool),
}
pub(crate) struct Watch {
index: Vec<OsString>,
covered: Option<Covered>,
changed: bool,
refreshing: bool,
blind: Option<String>,
told: bool,
known: Option<Known>,
}
enum Known {
Left(u64),
Read(RevisionId),
}
struct Covered {
rule: Rule,
backend: Option<Backend>,
names: HashSet<Vec<u8>>,
directories: HashSet<Vec<u8>>,
parents: HashSet<Vec<u8>>,
unwatched: HashSet<Vec<u8>>,
tried: HashSet<Vec<u8>>,
polling: Polling,
looking: Option<std::thread::JoinHandle<Vec<Vec<u8>>>>,
recorded: Option<[u8; 32]>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Rule {
root: PathBuf,
discovery: Option<bool>,
list: Option<Vec<u8>>,
}
impl Rule {
fn of(selection: &Selection) -> Self {
let (discovery, list) = match &selection.sources {
Sources::Files(_) => (None, None),
Sources::List(list) => (None, Some(list.clone())),
Sources::Automatic { recursive } => {
(Some(*recursive), Some(refresh::AUTOMATIC_LIST.to_vec()))
}
};
Self {
root: selection.root.clone(),
discovery,
list,
}
}
}
#[derive(Default)]
struct Polling {
directories: Vec<PolledDirectory>,
places: HashMap<Vec<u8>, usize>,
root_identity: Option<Option<Vec<u8>>>,
}
struct PolledDirectory {
spelled: Vec<u8>,
root: bool,
itself: Polled,
by: By,
names: Vec<Polled>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum By {
Times,
Above,
Names,
}
const MISSING_FOR_TIMES: usize = 8;
impl Polling {
fn add(&mut self, root: &Path, directory: &[u8], name: &[u8]) {
let at = match self.places.get(directory) {
Some(&at) => at,
None => {
let itself = Polled::new(place(root, directory));
let root = self.is_root(root, directory, &itself.seen);
self.places
.insert(directory.to_vec(), self.directories.len());
self.directories.push(PolledDirectory {
spelled: directory.to_vec(),
root,
itself,
by: By::Names,
names: Vec::new(),
});
self.directories.len() - 1
}
};
let name = Polled::new(refresh::below(root, name));
self.directories[at].names.push(name);
self.directories[at].by = self.by(&self.directories[at]);
}
fn is_root(&mut self, root: &Path, directory: &[u8], seen: &Seen) -> bool {
if spells_root(root, directory) {
return true;
}
let identity = self
.root_identity
.get_or_insert_with(|| match refresh::entry_of(root) {
Ok(Entry::Directory(stat)) if !stat.identity.is_empty() => Some(stat.identity),
_ => None,
});
match (identity, seen) {
(None, _) => true,
(Some(identity), Seen::Directory(stat)) => stat.identity == *identity,
(Some(_), _) => false,
}
}
fn by(&self, directory: &PolledDirectory) -> By {
if directory.root {
return By::Names;
}
let above = parent_of(&directory.spelled);
let covered = directory.itself.seen == Seen::Missing
&& above != directory.spelled
&& self.places.get(above).is_some_and(|&at| {
let above = &self.directories[at];
above.by == By::Times && matches!(above.itself.seen, Seen::Directory(_))
});
if covered {
return By::Above;
}
let timed = matches!(directory.itself.seen, Seen::Directory(_) | Seen::Missing);
let missing = directory.names.iter().filter(|name| !name.found()).count();
if timed && missing >= MISSING_FOR_TIMES {
By::Times
} else {
By::Names
}
}
fn again(&mut self) {
let mut order: Vec<usize> = (0..self.directories.len()).collect();
order.sort_by_key(|&at| self.directories[at].spelled.len());
for at in order {
let directory = &mut self.directories[at];
directory.itself.again();
for name in &mut directory.names {
name.again();
}
self.directories[at].by = self.by(&self.directories[at]);
}
}
fn changed(&self) -> bool {
self.directories.iter().any(|directory| {
(directory.by == By::Times && directory.itself.changed())
|| directory
.names
.iter()
.any(|name| directory.looks_at(name) && name.changed())
})
}
#[cfg_attr(not(any(test, feature = "session-profile")), allow(dead_code))]
fn looks(&self) -> usize {
self.directories
.iter()
.map(|directory| {
usize::from(directory.by == By::Times)
+ directory
.names
.iter()
.filter(|name| directory.looks_at(name))
.count()
})
.sum()
}
}
impl PolledDirectory {
fn looks_at(&self, name: &Polled) -> bool {
self.by == By::Names || name.found()
}
}
struct Polled {
path: PathBuf,
seen: Seen,
at: i64,
}
#[derive(Debug, PartialEq, Eq)]
enum Seen {
File(FileStat),
Directory(FileStat),
Other,
Missing,
Failed(std::io::ErrorKind),
}
impl Seen {
fn of(path: &Path) -> Self {
match refresh::entry_of(path) {
Ok(Entry::File(stat, _)) => Self::File(stat),
Ok(Entry::Directory(stat)) => Self::Directory(stat),
Ok(Entry::Other) => Self::Other,
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
) =>
{
Self::Missing
}
Err(error) => Self::Failed(error.kind()),
}
}
}
const RACY_NANOSECONDS: i64 = 2_000_000_000;
impl Polled {
fn new(path: PathBuf) -> Self {
let at = now();
let seen = Seen::of(&path);
Self { path, seen, at }
}
fn again(&mut self) {
self.at = now();
self.seen = Seen::of(&self.path);
}
fn found(&self) -> bool {
self.seen != Seen::Missing
}
fn changed(&self) -> bool {
if let Seen::File(stat) | Seen::Directory(stat) = &self.seen
&& stat.modified.max(stat.changed) >= self.at - RACY_NANOSECONDS
{
return true;
}
Seen::of(&self.path) != self.seen
}
}
impl Watch {
pub(crate) fn new(index: &Path) -> Self {
let name = index.file_name().map(OsString::from).unwrap_or_default();
let index = std::iter::once("")
.chain(store::LOGS)
.map(|suffix| {
let mut companion = name.clone();
companion.push(suffix);
companion
})
.collect();
Self {
index,
covered: None,
changed: true,
refreshing: false,
blind: None,
told: false,
known: None,
}
}
pub(crate) fn changed(&mut self) -> bool {
profile_span!("watch_changed");
if std::mem::take(&mut self.refreshing) {
self.changed = true;
}
if self.blind.is_some() {
return true;
}
let Some(covered) = &mut self.covered else {
return true;
};
let Some(backend) = &mut covered.backend else {
return true;
};
for happened in backend.events() {
match happened {
Happened::Lost => {
covered.backend = None;
self.changed = true;
return true;
}
Happened::Directory => self.changed = true,
Happened::Failed(reason) => {
self.blind = Some(reason);
return true;
}
Happened::Entry {
path,
kind,
directory,
} => {
if covered.replaced(&path, kind) {
covered.backend = None;
self.changed = true;
return true;
}
if covered.relevant(&self.index, &path, kind, directory) {
self.changed = true;
}
if let Err(reason) = covered.discovered(&path, kind, directory) {
self.blind = Some(reason);
return true;
}
}
}
}
match covered.links() {
Ok(anew) => self.changed |= anew,
Err(reason) => {
self.blind = Some(reason);
return true;
}
}
if !self.changed {
profile_span!("watch_polled");
profile_count!("watch_polled", covered.polling.looks());
self.changed = covered.polling.changed();
}
self.changed
}
pub(crate) fn read(&mut self, revision: Option<&RevisionId>) {
let Some(revision) = revision else {
return;
};
let another = match &self.known {
Some(Known::Left(number)) => *number != revision.number,
Some(Known::Read(read)) => read != revision,
None => false,
};
if another {
self.changed = true;
if let Some(covered) = &mut self.covered {
covered.recorded = None;
}
}
self.known = Some(Known::Read(revision.clone()));
}
pub(crate) fn tidy(&mut self) {
if let Some(backend) = self.covered.as_mut().and_then(|c| c.backend.as_mut()) {
backend.tidy();
}
}
pub(crate) fn note(&mut self) -> Option<String> {
if self.told {
return None;
}
let reason = self.blind.as_ref()?;
self.told = true;
Some(format!(
"cannot watch the sources for changes ({reason}); each query updates the index first"
))
}
}
impl Observer for Watch {
fn starting(&mut self, selection: &Selection, files: &[StoredFile], observed: Option<&[u8]>) {
profile_span!("watch_starting");
if self.blind.is_some() {
return;
}
self.changed = false;
self.refreshing = true;
let rule = Rule::of(selection);
let started = match self.covered.take() {
Some(covered) if covered.rule == rule && covered.backend.is_some() => Ok(covered),
_ => Covered::start(rule),
};
let covered = match started {
Ok(covered) => self.covered.insert(covered),
Err(reason) => {
self.blind = Some(reason);
return;
}
};
covered.polling.again();
if covered.recorded.is_some() {
return;
}
match covered.record(files, observed) {
Ok(_) => covered.recorded = Some(digest(files, observed)),
Err(reason) => self.blind = Some(reason),
}
}
fn finished(
&mut self,
revision: u64,
files: &[StoredFile],
observed: &[u8],
missing: &[Vec<u8>],
) {
profile_span!("watch_finished");
self.known = Some(Known::Left(revision));
self.refreshing = false;
if self.blind.is_some() {
return;
}
let Some(covered) = self.covered.as_mut().filter(|c| c.backend.is_some()) else {
return;
};
let names = digest(files, Some(observed));
let covering = if covered.recorded == Some(names) {
covered.links()
} else {
covered.recorded = Some(names);
covered.record(files, Some(observed))
};
let covering = covering.and_then(|anew| {
missing
.iter()
.try_fold(anew, |anew, name| Ok(covered.know(name)? | anew))
});
match covering {
Ok(anew) => self.changed |= anew,
Err(reason) => self.blind = Some(reason),
}
}
}
impl Observer for Option<Watch> {
fn starting(&mut self, selection: &Selection, files: &[StoredFile], observed: Option<&[u8]>) {
if let Some(watch) = self {
watch.starting(selection, files, observed);
}
}
fn finished(
&mut self,
revision: u64,
files: &[StoredFile],
observed: &[u8],
missing: &[Vec<u8>],
) {
if let Some(watch) = self {
watch.finished(revision, files, observed, missing);
}
}
}
impl Covered {
fn start(rule: Rule) -> Result<Self, String> {
let mut backend = Backend::new(&rule.root)?;
if let Some(recursive) = rule.discovery {
backend.discover(&rule.root, recursive)?;
}
let mut covered = Self::with(rule, Some(backend));
if Backend::FOLLOWS_DIRECTORY {
covered.watch(b"")?;
}
Ok(covered)
}
fn with(rule: Rule, backend: Option<Backend>) -> Self {
Self {
rule,
backend,
names: HashSet::default(),
directories: HashSet::default(),
parents: HashSet::default(),
unwatched: HashSet::default(),
tried: HashSet::default(),
polling: Polling::default(),
looking: None,
recorded: None,
}
}
fn record(&mut self, files: &[StoredFile], observed: Option<&[u8]>) -> Result<bool, String> {
self.tried.clear();
let mut anew = self.links()?;
if let Some(list) = self.rule.list.clone() {
anew |= self.know(&list)?;
}
let mut unlooked = Vec::new();
for file in files {
if Backend::FOLLOWS_LINKS
&& !self
.names
.contains(name_key(&self.rule.root, &file.path).as_ref())
{
unlooked.push(file.path.clone());
}
anew |= self.know(&file.path)?;
}
if let Some(questions) = observed.and_then(refresh::questions) {
for name in questions {
anew |= self.know(name)?;
}
}
if !unlooked.is_empty() {
let root = self.rule.root.clone();
self.looking = Some(std::thread::spawn(move || targets(&root, &unlooked)));
}
Ok(anew)
}
fn links(&mut self) -> Result<bool, String> {
let Some(looking) = self.looking.take() else {
return Ok(false);
};
let mut anew = false;
for target in looking.join().unwrap_or_default() {
anew |= self.know(&target)?;
}
Ok(anew)
}
fn know(&mut self, name: &[u8]) -> Result<bool, String> {
let key = name_key(&self.rule.root, name);
let unknown = !self.names.contains(key.as_ref());
let mut anew = false;
if unknown {
let mut above = key.as_ref();
let mut farther = Vec::new();
while let Some(at) = above.iter().rposition(|byte| *byte == b'/') {
above = &above[..at.max(1)];
if !self.directories.insert(above.to_vec()) {
break;
}
farther.push(above.to_vec());
if above == b"/" {
break;
}
}
self.names.insert(key.into_owned());
if Backend::FOLLOWS_DIRECTORY {
for directory in farther.iter().skip(1) {
anew |= matches!(self.watch(directory)?, Watching::Watched(true));
}
}
}
let parent = parent_of(name);
Ok(anew
| match self.watch(parent)? {
Watching::Watched(watched) => watched,
Watching::Unwatched(newly) => {
let polled = unknown || newly;
if polled {
self.polling.add(&self.rule.root, parent, name);
}
polled
}
})
}
fn watch(&mut self, directory: &[u8]) -> Result<Watching, String> {
if self.unwatched.contains(directory) {
return Ok(Watching::Unwatched(false));
}
if self.parents.contains(directory) || self.tried.contains(directory) {
return Ok(Watching::Watched(false));
}
let path = place(&self.rule.root, directory);
let backend = self.backend.as_mut().expect("a started watch");
Ok(match backend.cover(&path)? {
Cover::Watched(anew) => {
self.parents.insert(directory.to_vec());
Watching::Watched(anew)
}
Cover::Above(anew) => {
self.tried.insert(directory.to_vec());
Watching::Watched(anew)
}
Cover::Unwatched => {
self.unwatched.insert(directory.to_vec());
Watching::Unwatched(true)
}
})
}
fn replaced(&self, path: &Path, kind: Kind) -> bool {
kind != Kind::Changed && self.directories.contains(&key(&self.rule.root, path))
}
fn relevant(
&self,
index: &[OsString],
path: &Path,
kind: Kind,
directory: Option<bool>,
) -> bool {
let Some(name) = path.file_name() else {
return true;
};
if index.iter().any(|own| own == name) {
return false;
}
let key = key(&self.rule.root, path);
if self.names.contains(&key) {
return true;
}
if self.directories.contains(&key) {
return !(Backend::ECHOES_ENTRIES && kind == Kind::Changed);
}
if !self.discovers(&key) {
return false;
}
refresh::is_source_name(name.as_encoded_bytes())
|| match directory {
Some(directory) => directory,
None => kind == Kind::Added && path.is_dir(),
}
}
fn discovers(&self, key: &[u8]) -> bool {
let Some(recursive) = self.rule.discovery else {
return false;
};
let inside = !key.is_empty() && !refresh::outside(key);
inside && (recursive || !key.contains(&b'/'))
}
fn discovered(
&mut self,
path: &Path,
kind: Kind,
directory: Option<bool>,
) -> Result<(), String> {
if kind == Kind::Removed
|| directory != Some(true)
|| self.rule.discovery != Some(true)
|| !self.discovers(&key(&self.rule.root, path))
{
return Ok(());
}
match &mut self.backend {
Some(backend) => backend.discover(path, true),
None => Ok(()),
}
}
}
fn targets(root: &Path, names: &[Vec<u8>]) -> Vec<Vec<u8>> {
let threads = refresh::checking_threads(names.len());
refresh::each_on_threads(names, threads, |name| {
let path = refresh::below(root, name);
let link = std::fs::symlink_metadata(&path).is_ok_and(|m| m.file_type().is_symlink());
link.then(|| std::fs::canonicalize(&path).ok()).flatten()
})
.into_iter()
.flatten()
.map(|target| target.into_os_string().into_encoded_bytes())
.collect()
}
fn digest(files: &[StoredFile], observed: Option<&[u8]>) -> [u8; 32] {
let mut digest = blake3::Hasher::new();
for file in files {
digest.update(&(file.path.len() as u64).to_le_bytes());
digest.update(&file.path);
}
digest.update(observed.unwrap_or_default());
digest.finalize().into()
}
fn parent_of(name: &[u8]) -> &[u8] {
match name.iter().rposition(|byte| *byte == b'/') {
Some(0) => b"/",
Some(at) => &name[..at],
None => b"",
}
}
fn place(root: &Path, directory: &[u8]) -> PathBuf {
match directory {
b"" => root.to_path_buf(),
directory => refresh::below(root, directory),
}
}
fn spells_root(root: &Path, directory: &[u8]) -> bool {
let mut lexical = PathBuf::new();
for component in place(root, directory).components() {
match component {
std::path::Component::ParentDir => {
lexical.pop();
}
std::path::Component::CurDir => {}
component => lexical.push(component),
}
}
key(root, &lexical) == key(root, root)
}
fn key(root: &Path, path: &Path) -> Vec<u8> {
#[cfg(windows)]
{
let fold = |path: &Path| {
let path = path.to_string_lossy().replace('\\', "/").to_lowercase();
if let Some(share) = path.strip_prefix("//?/unc/") {
format!("//{share}")
} else if let Some(local) = path.strip_prefix("//?/") {
local.to_owned()
} else {
path
}
};
let (root, path) = (fold(root), fold(path));
let root = root.trim_end_matches('/');
match path
.strip_prefix(root)
.and_then(|rest| rest.strip_prefix('/'))
{
Some(rest) => rest.as_bytes().to_vec(),
None => path.into_bytes(),
}
}
#[cfg(not(windows))]
{
let root = root.as_os_str().as_encoded_bytes();
let path = path.as_os_str().as_encoded_bytes();
let root = root.strip_suffix(b"/").unwrap_or(root);
match path
.strip_prefix(root)
.and_then(|rest| rest.strip_prefix(b"/"))
{
Some(rest) => rest.to_vec(),
None => path.to_vec(),
}
}
}
fn name_key<'n>(root: &Path, name: &'n [u8]) -> Cow<'n, [u8]> {
if cfg!(windows) || name.starts_with(b"/") {
return Cow::Owned(key(root, &refresh::below(root, name)));
}
Cow::Borrowed(name)
}
fn now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |duration| {
i64::try_from(duration.as_nanos()).unwrap_or(i64::MAX)
})
}