use crate::modules::input::Token;
use crate::{
RuntimeError,
constants::{INLINE_PAYLOAD, VNODE_POLL},
futures::{
file::change::{Change, EVERY_NOTE, Snapshot},
net::step::settle,
task::{
Nothing, Task,
sealed::{self, Park, Step},
},
},
modules::{c_path::c_path, fd::Fd, park, retried::retried},
};
use std::{ffi::CString, mem, path::Path, sync::Arc, time::Instant};
const _: () = assert!(mem::size_of::<Result<Change, RuntimeError>>() <= INLINE_PAYLOAD);
#[derive(Debug, Clone)]
#[must_use = "a task does nothing until it is run or spawned"]
pub struct WatchTask {
path: Option<CString>,
notes: u32,
seen: Option<Snapshot>,
fd: Option<Arc<Fd>>,
appear: bool,
parent: Option<Arc<Fd>>,
}
impl WatchTask {
pub(crate) fn new(path: impl AsRef<Path>) -> Self {
Self {
path: c_path(path),
notes: EVERY_NOTE,
seen: None,
fd: None,
appear: false,
parent: None,
}
}
pub fn appear(mut self) -> Self {
self.appear = true;
self
}
pub fn only(mut self, wanted: Change) -> Self {
self.notes = wanted.notes();
self
}
fn watching(&mut self) -> Result<(Arc<Fd>, Snapshot), RuntimeError> {
if let (Some(fd), Some(seen)) = (self.fd.as_ref(), self.seen) {
return Ok((Arc::clone(fd), seen));
}
let (fd, seen) = {
let path = self.path.as_ref().ok_or(RuntimeError::BadPath)?;
let fd = Arc::new(open_watch(path)?);
let seen = Snapshot::take(&fd, path)?;
(fd, seen)
};
self.fd = Some(Arc::clone(&fd));
self.seen = Some(seen);
Ok((fd, seen))
}
fn arrival(&mut self) -> Result<Option<Step<Result<Change, RuntimeError>>>, RuntimeError> {
let path = self.path.clone().ok_or(RuntimeError::BadPath)?;
let waited = self.parent.is_some();
if exists(&path) {
match self.watching() {
Ok(_) if waited => {
self.parent = None;
return Ok(Some(Step::Done(Ok(Change::CREATED))));
}
Ok(_) => return Ok(None),
Err(RuntimeError::CheckError(Some(libc::ENOENT))) => {}
Err(error) => return Err(error),
}
}
let parent = match &self.parent {
Some(parent) => Arc::clone(parent),
None => {
let parent = Arc::new(open_watch(&directory_of(&path)?)?);
self.parent = Some(Arc::clone(&parent));
return self.arrival();
}
};
Ok(Some(Step::Park(Park {
ident: parent.raw(),
filter: libc::EVFILT_VNODE,
notes: libc::NOTE_WRITE,
deadline: Some(Instant::now() + VNODE_POLL),
})))
}
fn advance(&mut self) -> Result<Step<Result<Change, RuntimeError>>, RuntimeError> {
if self.appear && self.fd.is_none() {
if let Some(step) = self.arrival()? {
return Ok(step);
}
}
let (fd, seen) = self.watching()?;
let now = {
let path = self.path.as_ref().ok_or(RuntimeError::BadPath)?;
Snapshot::take(&fd, path)?
};
if let Some(change) = seen.against(&now, self.notes) {
self.seen = Some(now);
return Ok(Step::Done(Ok(change)));
}
let deadline = match seen.gone() {
true => None,
false => Some(Instant::now() + VNODE_POLL),
};
Ok(Step::Park(Park {
ident: fd.raw(),
filter: libc::EVFILT_VNODE,
notes: self.notes,
deadline,
}))
}
}
impl sealed::Sealed for WatchTask {}
impl Task for WatchTask {
type Output = Result<Change, RuntimeError>;
type Input = Nothing;
fn execute(&self, _token: Token, reactor_id: i32, task_id: usize) -> Self::Output {
park::drive(self.clone(), reactor_id, task_id)
}
fn step(&mut self, _token: Token, _reactor_id: i32, _task_id: usize) -> Step<Self::Output> {
settle(self.advance())
}
}
fn exists(path: &CString) -> bool {
let mut raw: libc::stat = unsafe { mem::zeroed() };
retried(|| unsafe { libc::stat(path.as_ptr(), &mut raw) }).is_ok()
}
fn directory_of(path: &CString) -> Result<CString, RuntimeError> {
let bytes = path.as_bytes();
let trimmed = bytes.strip_suffix(b"/").unwrap_or(bytes);
let parent = match trimmed.iter().rposition(|byte| *byte == b'/') {
Some(0) => &b"/"[..],
Some(at) => &trimmed[..at],
None => &b"."[..],
};
CString::new(parent).map_err(|_| RuntimeError::BadPath)
}
fn open_watch(path: &CString) -> Result<Fd, RuntimeError> {
let flags = libc::O_EVTONLY | libc::O_NONBLOCK | libc::O_CLOEXEC;
retried(|| unsafe { libc::open(path.as_ptr(), flags) }).map(Fd::new)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modules::input::token;
#[test]
fn a_watch_does_not_block() {
assert!(
!WatchTask::new("a").blocking(token()),
"a watch parks instead"
);
}
#[test]
fn a_path_with_a_zero_byte_is_a_bad_path() {
let mut task = WatchTask::new("a\0b");
assert!(matches!(task.advance(), Err(RuntimeError::BadPath)));
}
#[test]
fn a_path_that_is_not_there_says_so() {
let mut task = WatchTask::new("/nonexistent-atap-watch-target");
assert_eq!(
task.advance().err(),
Some(RuntimeError::CheckError(Some(libc::ENOENT))),
);
}
#[test]
fn a_narrowed_watch_asks_for_less() {
let task = WatchTask::new("a").only(Change::REMOVED | Change::RENAMED);
assert_eq!(task.notes, (Change::REMOVED | Change::RENAMED).notes());
assert_eq!(WatchTask::new("a").notes, EVERY_NOTE);
}
}