use anyhow::{bail, Result};
use derive_builder::Builder;
use enumflags2::{bitflags, BitFlags};
use fxhash::FxHashSet;
use notify::event::{
AccessKind, CreateKind, DataChange, MetadataKind, ModifyKind, RemoveKind, RenameMode,
};
use notify_debouncer_full::{DebounceEventHandler, DebounceEventResult};
use poolshark::global::GPooled;
use std::{
borrow::Borrow,
hash::Hash,
ops::Deref,
path::{self, Path, PathBuf},
sync::{Arc, LazyLock},
time::Duration,
};
use tokio::{sync::mpsc, task};
use watch_task::MAX_NOTIFY_BATCH;
mod watch_task;
#[cfg(test)]
mod test;
pub const MIN_POLL_INTERVAL: Duration = Duration::from_millis(100);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Id(u64);
impl Id {
fn new() -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
Self(NEXT.fetch_add(1, Ordering::Relaxed))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ArcPath(Arc<PathBuf>);
impl ArcPath {
pub fn get_mut(&mut self) -> Option<&mut PathBuf> {
Arc::get_mut(&mut self.0)
}
pub fn make_mut(&mut self) -> &mut PathBuf {
Arc::make_mut(&mut self.0)
}
pub fn root() -> Self {
static ROOT: LazyLock<ArcPath> =
LazyLock::new(|| ArcPath::from(path::MAIN_SEPARATOR_STR));
ROOT.clone()
}
}
impl AsRef<Path> for ArcPath {
fn as_ref(&self) -> &Path {
&*self.0
}
}
impl Deref for ArcPath {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl Borrow<Path> for ArcPath {
fn borrow(&self) -> &Path {
&*self.0
}
}
impl From<&Path> for ArcPath {
fn from(value: &Path) -> Self {
Self(Arc::new(value.into()))
}
}
impl From<&str> for ArcPath {
fn from(value: &str) -> Self {
Self(Arc::new(PathBuf::from(value)))
}
}
impl From<PathBuf> for ArcPath {
fn from(value: PathBuf) -> Self {
Self(Arc::new(value))
}
}
impl From<&PathBuf> for ArcPath {
fn from(value: &PathBuf) -> Self {
Self(Arc::new(value.clone()))
}
}
impl From<&ArcPath> for ArcPath {
fn from(value: &ArcPath) -> Self {
value.clone()
}
}
impl PartialEq<Path> for ArcPath {
fn eq(&self, other: &Path) -> bool {
&*self.0 == other
}
}
impl PartialOrd<Path> for ArcPath {
fn partial_cmp(&self, other: &Path) -> Option<std::cmp::Ordering> {
(**self.0).partial_cmp(other)
}
}
impl PartialEq<PathBuf> for ArcPath {
fn eq(&self, other: &PathBuf) -> bool {
&*self.0 == other
}
}
impl PartialOrd<PathBuf> for ArcPath {
fn partial_cmp(&self, other: &PathBuf) -> Option<std::cmp::Ordering> {
(**self.0).partial_cmp(other)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[bitflags]
#[repr(u64)]
pub enum Interest {
Established,
Any,
Access,
AccessOpen,
AccessClose,
AccessRead,
AccessOther,
Create,
CreateFile,
CreateFolder,
CreateOther,
Modify,
ModifyData,
ModifyDataSize,
ModifyDataContent,
ModifyDataOther,
ModifyMetadata,
ModifyMetadataAccessTime,
ModifyMetadataWriteTime,
ModifyMetadataPermissions,
ModifyMetadataOwnership,
ModifyMetadataExtended,
ModifyMetadataOther,
ModifyRename,
ModifyRenameTo,
ModifyRenameFrom,
ModifyRenameBoth,
ModifyRenameOther,
ModifyOther,
Delete,
DeleteFile,
DeleteFolder,
DeleteOther,
Other,
}
impl From<¬ify::EventKind> for Interest {
fn from(kind: ¬ify::EventKind) -> Self {
match kind {
notify::EventKind::Any => Self::Any,
notify::EventKind::Access(AccessKind::Any) => Self::Access,
notify::EventKind::Access(AccessKind::Close(_)) => Self::AccessClose,
notify::EventKind::Access(AccessKind::Open(_)) => Self::AccessOpen,
notify::EventKind::Access(AccessKind::Read) => Self::AccessRead,
notify::EventKind::Access(AccessKind::Other) => Self::AccessOther,
notify::EventKind::Create(CreateKind::Any) => Self::Create,
notify::EventKind::Create(CreateKind::File) => Self::CreateFile,
notify::EventKind::Create(CreateKind::Folder) => Self::CreateFolder,
notify::EventKind::Create(CreateKind::Other) => Self::CreateOther,
notify::EventKind::Modify(ModifyKind::Any) => Self::Modify,
notify::EventKind::Modify(ModifyKind::Data(DataChange::Any)) => {
Self::ModifyData
}
notify::EventKind::Modify(ModifyKind::Data(DataChange::Content)) => {
Self::ModifyDataContent
}
notify::EventKind::Modify(ModifyKind::Data(DataChange::Size)) => {
Self::ModifyDataSize
}
notify::EventKind::Modify(ModifyKind::Data(DataChange::Other)) => {
Self::ModifyDataOther
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)) => {
Self::ModifyMetadata
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::AccessTime)) => {
Self::ModifyMetadataAccessTime
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::Extended)) => {
Self::ModifyMetadataExtended
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other)) => {
Self::ModifyMetadataOther
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)) => {
Self::ModifyMetadataOwnership
}
notify::EventKind::Modify(ModifyKind::Metadata(
MetadataKind::Permissions,
)) => Self::ModifyMetadataPermissions,
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => {
Self::ModifyMetadataWriteTime
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::Any)) => {
Self::ModifyRename
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
Self::ModifyRenameBoth
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
Self::ModifyRenameFrom
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
Self::ModifyRenameTo
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::Other)) => {
Self::ModifyRenameOther
}
notify::EventKind::Modify(ModifyKind::Other) => Self::ModifyOther,
notify::EventKind::Remove(RemoveKind::Any) => Self::Delete,
notify::EventKind::Remove(RemoveKind::File) => Self::DeleteFile,
notify::EventKind::Remove(RemoveKind::Folder) => Self::DeleteFolder,
notify::EventKind::Remove(RemoveKind::Other) => Self::DeleteOther,
notify::EventKind::Other => Self::Other,
}
}
}
#[derive(Debug, Clone)]
struct Watch {
path: ArcPath,
id: Id,
interest: BitFlags<Interest>,
}
impl Watch {
fn interested(&self, kind: ¬ify::EventKind) -> bool {
use Interest::*;
if self.interest.contains(Any) {
return true;
}
match kind {
notify::EventKind::Any => !self.interest.is_empty(),
notify::EventKind::Access(AccessKind::Any) => self
.interest
.intersects(Access | AccessClose | AccessOpen | AccessRead | AccessOther),
notify::EventKind::Access(AccessKind::Close(_)) => {
self.interest.intersects(Access | AccessClose)
}
notify::EventKind::Access(AccessKind::Open(_)) => {
self.interest.intersects(Access | AccessOpen)
}
notify::EventKind::Access(AccessKind::Read) => {
self.interest.intersects(Access | AccessRead)
}
notify::EventKind::Access(AccessKind::Other) => {
self.interest.intersects(Access | AccessOther)
}
notify::EventKind::Create(CreateKind::Any) => {
self.interest.intersects(Create | CreateFile | CreateFolder | CreateOther)
}
notify::EventKind::Create(CreateKind::File) => {
self.interest.intersects(Create | CreateFile)
}
notify::EventKind::Create(CreateKind::Folder) => {
self.interest.intersects(Create | CreateFolder)
}
notify::EventKind::Create(CreateKind::Other) => {
self.interest.intersects(Create | CreateOther)
}
notify::EventKind::Modify(ModifyKind::Any) => self.interest.intersects(
Modify
| ModifyData
| ModifyDataSize
| ModifyDataContent
| ModifyDataOther
| ModifyMetadata
| ModifyMetadataAccessTime
| ModifyMetadataWriteTime
| ModifyMetadataPermissions
| ModifyMetadataOwnership
| ModifyMetadataExtended
| ModifyMetadataOther
| ModifyRename
| ModifyRenameTo
| ModifyRenameFrom
| ModifyRenameBoth
| ModifyRenameOther
| ModifyOther,
),
notify::EventKind::Modify(ModifyKind::Data(DataChange::Any)) => {
self.interest.intersects(
Modify
| ModifyData
| ModifyDataSize
| ModifyDataContent
| ModifyDataOther,
)
}
notify::EventKind::Modify(ModifyKind::Data(DataChange::Content)) => {
self.interest.intersects(Modify | ModifyData | ModifyDataContent)
}
notify::EventKind::Modify(ModifyKind::Data(DataChange::Size)) => {
self.interest.intersects(Modify | ModifyData | ModifyDataSize)
}
notify::EventKind::Modify(ModifyKind::Data(DataChange::Other)) => {
self.interest.intersects(Modify | ModifyData | ModifyDataOther)
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)) => {
self.interest.intersects(
Modify
| ModifyMetadata
| ModifyMetadataAccessTime
| ModifyMetadataWriteTime
| ModifyMetadataPermissions
| ModifyMetadataOwnership
| ModifyMetadataExtended
| ModifyMetadataOther,
)
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::AccessTime)) => {
self.interest
.intersects(Modify | ModifyMetadata | ModifyMetadataAccessTime)
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::Extended)) => {
self.interest.intersects(Modify | ModifyMetadata | ModifyMetadataExtended)
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other)) => {
self.interest.intersects(Modify | ModifyMetadata | ModifyMetadataOther)
}
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)) => {
self.interest
.intersects(Modify | ModifyMetadata | ModifyMetadataOwnership)
}
notify::EventKind::Modify(ModifyKind::Metadata(
MetadataKind::Permissions,
)) => self
.interest
.intersects(Modify | ModifyMetadata | ModifyMetadataPermissions),
notify::EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => {
self.interest
.intersects(Modify | ModifyMetadata | ModifyMetadataWriteTime)
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::Any)) => {
self.interest.intersects(
Modify
| ModifyRename
| ModifyRenameTo
| ModifyRenameFrom
| ModifyRenameBoth
| ModifyRenameOther,
)
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
self.interest.intersects(Modify | ModifyRename | ModifyRenameBoth)
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
self.interest.intersects(Modify | ModifyRename | ModifyRenameFrom)
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
self.interest.intersects(Modify | ModifyRename | ModifyRenameTo)
}
notify::EventKind::Modify(ModifyKind::Name(RenameMode::Other)) => {
self.interest.intersects(Modify | ModifyRename | ModifyRenameOther)
}
notify::EventKind::Modify(ModifyKind::Other) => {
self.interest.intersects(Modify | ModifyOther)
}
notify::EventKind::Remove(RemoveKind::Any) => {
self.interest.intersects(Delete | DeleteFile | DeleteFolder | DeleteOther)
}
notify::EventKind::Remove(RemoveKind::File) => {
self.interest.intersects(Delete | DeleteFile)
}
notify::EventKind::Remove(RemoveKind::Folder) => {
self.interest.intersects(Delete | DeleteFolder)
}
notify::EventKind::Remove(RemoveKind::Other) => {
self.interest.intersects(Delete | DeleteOther)
}
notify::EventKind::Other => self.interest.contains(Other),
}
}
}
#[derive(Debug)]
enum Cmd {
Watch(Watch),
Stop(Id),
SetPollInterval(Duration),
SetPollBatch(usize),
}
struct NotifyChan(mpsc::Sender<DebounceEventResult>);
impl DebounceEventHandler for NotifyChan {
fn handle_event(&mut self, event: notify_debouncer_full::DebounceEventResult) {
let _ = self.0.blocking_send(event);
}
}
#[derive(Debug, Clone)]
pub enum EventKind {
Error(Arc<anyhow::Error>),
Event(Interest),
}
#[derive(Debug)]
pub struct Event {
pub paths: GPooled<FxHashSet<ArcPath>>,
pub event: EventKind,
}
pub type EventBatch = GPooled<Vec<(Id, Event)>>;
pub trait EventHandler: Send + 'static {
fn handle_event(
&mut self,
event: EventBatch,
) -> impl Future<Output = Result<()>> + Send;
}
impl EventHandler for mpsc::Sender<EventBatch> {
fn handle_event(
&mut self,
event: EventBatch,
) -> impl Future<Output = Result<()>> + Send {
async { Ok(self.send(event).await?) }
}
}
impl EventHandler for futures::channel::mpsc::Sender<EventBatch> {
fn handle_event(
&mut self,
event: EventBatch,
) -> impl Future<Output = Result<()>> + Send {
use futures::SinkExt;
async { Ok(self.send(event).await?) }
}
}
#[derive(Debug)]
pub struct Watched {
id: Id,
watcher: Watcher,
}
impl Watched {
pub fn id(&self) -> Id {
self.id
}
}
impl Borrow<Id> for Watched {
fn borrow(&self) -> &Id {
&self.id
}
}
impl<'a> Borrow<Id> for &'a Watched {
fn borrow(&self) -> &'a Id {
&self.id
}
}
impl PartialEq for Watched {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Watched {}
impl PartialOrd for Watched {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.id.cmp(&other.id))
}
}
impl Ord for Watched {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id)
}
}
impl Hash for Watched {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state)
}
}
impl Drop for Watched {
fn drop(&mut self) {
let _ = self.watcher.0.send(Cmd::Stop(self.id));
}
}
#[derive(Debug, Clone, Builder)]
pub struct WatcherConfig<T: EventHandler> {
#[builder(default = "Duration::from_millis(250)")]
timeout: Duration,
#[builder(setter(strip_option), default)]
tick_rate: Option<Duration>,
#[builder(default = "Duration::from_secs(1)")]
poll_interval: Duration,
#[builder(default = "100")]
poll_batch: usize,
event_handler: T,
}
impl<T: EventHandler> WatcherConfig<T> {
pub fn start(self) -> Result<Watcher> {
let (notify_tx, notify_rx) = mpsc::channel(MAX_NOTIFY_BATCH);
let watcher = notify_debouncer_full::new_debouncer(
self.timeout,
self.tick_rate,
NotifyChan(notify_tx),
)?;
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
task::spawn(watch_task::watcher_loop(
self.poll_interval,
self.poll_batch,
watcher,
notify_rx,
cmd_rx,
self.event_handler,
));
Ok(Watcher(cmd_tx))
}
}
#[derive(Debug, Clone)]
pub struct Watcher(mpsc::UnboundedSender<Cmd>);
impl Watcher {
pub fn add(&self, path: ArcPath, interest: BitFlags<Interest>) -> Result<Watched> {
let id = Id::new();
self.0.send(Cmd::Watch(Watch { path, interest, id }))?;
Ok(Watched { id, watcher: self.clone() })
}
pub fn set_poll_interval(&self, t: Duration) -> Result<()> {
if t < MIN_POLL_INTERVAL {
bail!("poll interval may not be less than {MIN_POLL_INTERVAL:?}")
}
Ok(self.0.send(Cmd::SetPollInterval(t))?)
}
pub fn set_poll_batch(&self, n: usize) -> Result<()> {
Ok(self.0.send(Cmd::SetPollBatch(n))?)
}
}