1#![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}