Skip to main content

zccache_watcher/
lib.rs

1//! PyO3 cdylib for the polling watcher.
2//!
3//! The Rust polling watcher itself lives in `zccache::watcher`
4//! (issue #365). This crate exists solely to host the `_native` Python
5//! extension module so the polished `zccache.watcher` Python package can
6//! `import zccache.watcher._native`.
7
8#![cfg(feature = "python")]
9
10use std::path::PathBuf;
11use std::time::Duration;
12
13use pyo3::exceptions::{PyOSError, PyRuntimeError};
14use pyo3::prelude::*;
15
16use zccache::watcher::{PollWatchBatch, PollingWatcher, PollingWatcherConfig};
17
18fn io_to_py_err(e: std::io::Error) -> PyErr {
19    PyErr::new::<PyOSError, _>(e.to_string())
20}
21
22fn runtime_to_py_err(message: impl Into<String>) -> PyErr {
23    PyErr::new::<PyRuntimeError, _>(message.into())
24}
25
26#[pyclass(module = "zccache.watcher._native")]
27#[derive(Clone, Debug)]
28pub struct WatchBatch {
29    #[pyo3(get)]
30    changed: Vec<String>,
31    #[pyo3(get)]
32    removed: Vec<String>,
33    #[pyo3(get)]
34    overflow: bool,
35}
36
37impl From<PollWatchBatch> for WatchBatch {
38    fn from(value: PollWatchBatch) -> Self {
39        Self {
40            changed: value
41                .changed
42                .into_iter()
43                .map(|path| path.to_string_lossy().into_owned())
44                .collect(),
45            removed: value
46                .removed
47                .into_iter()
48                .map(|path| path.to_string_lossy().into_owned())
49                .collect(),
50            overflow: value.overflow,
51        }
52    }
53}
54
55#[pyclass(module = "zccache.watcher._native")]
56pub struct NativeWatcher {
57    watcher: PollingWatcher,
58}
59
60unsafe impl Send for NativeWatcher {}
61
62#[pymethods]
63impl NativeWatcher {
64    #[new]
65    #[pyo3(signature = (
66        root,
67        include_folders=vec![],
68        include_globs=vec![],
69        excluded_patterns=vec![],
70        poll_interval_ms=100,
71        debounce_ms=200
72    ))]
73    fn new(
74        root: String,
75        include_folders: Vec<String>,
76        include_globs: Vec<String>,
77        excluded_patterns: Vec<String>,
78        poll_interval_ms: u64,
79        debounce_ms: u64,
80    ) -> PyResult<Self> {
81        let mut config = PollingWatcherConfig::new(PathBuf::from(root));
82        config.include_folders = include_folders.into_iter().map(Into::into).collect();
83        config.include_globs = include_globs;
84        config.excluded_patterns = excluded_patterns;
85        config.poll_interval = Duration::from_millis(poll_interval_ms.max(1));
86        config.debounce = Duration::from_millis(debounce_ms);
87
88        let watcher = PollingWatcher::new(config).map_err(io_to_py_err)?;
89        Ok(Self { watcher })
90    }
91
92    fn start(&self) -> PyResult<()> {
93        self.watcher.start().map_err(io_to_py_err)
94    }
95
96    fn stop(&self) -> PyResult<()> {
97        self.watcher.stop().map_err(io_to_py_err)
98    }
99
100    fn resume(&self) -> PyResult<()> {
101        self.watcher.resume().map_err(io_to_py_err)
102    }
103
104    fn is_running(&self) -> bool {
105        self.watcher.is_running()
106    }
107
108    #[pyo3(signature = (timeout_ms=0))]
109    fn poll_batch(&self, timeout_ms: u64) -> PyResult<Option<WatchBatch>> {
110        let batch = self
111            .watcher
112            .poll_timeout(Duration::from_millis(timeout_ms))
113            .map_err(|_| runtime_to_py_err("watcher polling failed"))?;
114        Ok(batch.map(WatchBatch::from))
115    }
116}
117
118#[pymodule]
119fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
120    m.add_class::<WatchBatch>()?;
121    m.add_class::<NativeWatcher>()?;
122    Ok(())
123}