honker 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +193 -2
- data/ext/honker/extconf.rb +69 -0
- data/ext/honker/honker-core/Cargo.toml +49 -0
- data/ext/honker/honker-core/LICENSE +6 -0
- data/ext/honker/honker-core/LICENSE-APACHE +176 -0
- data/ext/honker/honker-core/LICENSE-MIT +21 -0
- data/ext/honker/honker-core/README.md +30 -0
- data/ext/honker/honker-core/src/cron.rs +473 -0
- data/ext/honker/honker-core/src/honker_ops.rs +1518 -0
- data/ext/honker/honker-core/src/kernel_watcher.rs +434 -0
- data/ext/honker/honker-core/src/lib.rs +3116 -0
- data/ext/honker/honker-core/src/shm_watcher.rs +250 -0
- data/ext/honker/honker-extension/Cargo.toml +36 -0
- data/ext/honker/honker-extension/LICENSE +6 -0
- data/ext/honker/honker-extension/LICENSE-APACHE +176 -0
- data/ext/honker/honker-extension/LICENSE-MIT +21 -0
- data/ext/honker/honker-extension/README.md +41 -0
- data/ext/honker/honker-extension/src/lib.rs +330 -0
- data/honker.gemspec +24 -1
- data/lib/honker/README.md +28 -0
- data/lib/honker/railtie.rb +11 -0
- data/lib/honker/scheduler.rb +59 -0
- data/lib/honker/version.rb +1 -1
- data/lib/honker.rb +111 -3
- metadata +39 -8
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
//! Optional kernel-watch backend (feature = `kernel-watcher`).
|
|
2
|
+
//!
|
|
3
|
+
//! **Experimental.** Weaker correctness contract than the polling
|
|
4
|
+
//! backend, in exchange for lower idle CPU and lower wake latency.
|
|
5
|
+
//!
|
|
6
|
+
//! # Contract
|
|
7
|
+
//!
|
|
8
|
+
//! `on_change()` fires on every relevant filesystem event observed on
|
|
9
|
+
//! the database file, its parent directory, or SQLite sidecar files
|
|
10
|
+
//! (`-wal`, `-shm`, `-journal`). **There is no `PRAGMA data_version`
|
|
11
|
+
//! verification and no safety-net poll.** This means:
|
|
12
|
+
//!
|
|
13
|
+
//! - **Spurious wakes are possible.** Any file change in the directory
|
|
14
|
+
//! (other apps writing nearby files, the OS touching metadata, etc.)
|
|
15
|
+
//! produces a wake. Consumers re-read state on every wake anyway, so
|
|
16
|
+
//! this is wasted work, not incorrect.
|
|
17
|
+
//!
|
|
18
|
+
//! - **Missed wakes are possible.** If the OS drops or coalesces
|
|
19
|
+
//! notifications, or fails to deliver an event for a SQLite commit,
|
|
20
|
+
//! `on_change()` will not fire for that commit. The consumer's
|
|
21
|
+
//! `idle_poll_s` (default 5 s) is the only backstop.
|
|
22
|
+
//!
|
|
23
|
+
//! - **Setup failures raise at `open()`.** [`probe`] runs at
|
|
24
|
+
//! `honker.open()` time and surfaces any init failure as an error
|
|
25
|
+
//! so the user knows immediately. No silent backend disable.
|
|
26
|
+
//!
|
|
27
|
+
//! Tests assert that wakes do fire, with bounded latency, on the
|
|
28
|
+
//! platforms we support. If a test fails, the backend is broken on
|
|
29
|
+
//! that platform — not "fall back to polling and pretend it worked".
|
|
30
|
+
|
|
31
|
+
use crate::stat_identity;
|
|
32
|
+
use std::path::PathBuf;
|
|
33
|
+
use std::sync::Arc;
|
|
34
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
35
|
+
use std::time::{Duration, Instant};
|
|
36
|
+
|
|
37
|
+
#[cfg(target_os = "macos")]
|
|
38
|
+
use macos::{probe_kqueue, run_kqueue_loop};
|
|
39
|
+
#[cfg(not(target_os = "macos"))]
|
|
40
|
+
use notify::{RecursiveMode, Watcher};
|
|
41
|
+
#[cfg(not(target_os = "macos"))]
|
|
42
|
+
use std::collections::HashSet;
|
|
43
|
+
#[cfg(not(target_os = "macos"))]
|
|
44
|
+
use std::path::Path;
|
|
45
|
+
#[cfg(not(target_os = "macos"))]
|
|
46
|
+
use std::sync::mpsc;
|
|
47
|
+
|
|
48
|
+
/// How long `recv_timeout` blocks before sampling the stop flag.
|
|
49
|
+
/// Bounds graceful shutdown latency at this value.
|
|
50
|
+
const RX_POLL_MS: u64 = 50;
|
|
51
|
+
/// Cadence for the dead-man's switch (db-file replacement detection).
|
|
52
|
+
/// Same as the polling backend so file-replacement detection latency
|
|
53
|
+
/// doesn't depend on which backend the user picked.
|
|
54
|
+
const IDENTITY_CHECK_MS: u64 = 100;
|
|
55
|
+
|
|
56
|
+
pub(crate) fn run_kernel_watch_loop<F>(
|
|
57
|
+
db_path: PathBuf,
|
|
58
|
+
on_change: F,
|
|
59
|
+
stop: Arc<AtomicBool>,
|
|
60
|
+
ready: std::sync::mpsc::SyncSender<()>,
|
|
61
|
+
) where
|
|
62
|
+
F: Fn() + Send + 'static,
|
|
63
|
+
{
|
|
64
|
+
#[cfg(target_os = "macos")]
|
|
65
|
+
{
|
|
66
|
+
run_kqueue_loop(db_path, on_change, stop, ready);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[cfg(not(target_os = "macos"))]
|
|
71
|
+
{
|
|
72
|
+
let (tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
|
|
73
|
+
let mut watcher = match notify::recommended_watcher(tx) {
|
|
74
|
+
Ok(w) => w,
|
|
75
|
+
Err(e) => {
|
|
76
|
+
eprintln!("honker: kernel-watcher init failed: {e}. Backend disabled.");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Attach watches: db file (catches in-place writes), parent dir
|
|
82
|
+
// (catches journal/wal/shm create+delete), and sidecars directly
|
|
83
|
+
// when present. SQLite can create the WAL after watcher startup,
|
|
84
|
+
// so retry per-file attaches on parent-dir activity and on the
|
|
85
|
+
// dead-man cadence.
|
|
86
|
+
let watch_dir = db_path
|
|
87
|
+
.parent()
|
|
88
|
+
.unwrap_or(std::path::Path::new("."))
|
|
89
|
+
.to_path_buf();
|
|
90
|
+
let wal = PathBuf::from(format!("{}-wal", db_path.display()));
|
|
91
|
+
let shm = PathBuf::from(format!("{}-shm", db_path.display()));
|
|
92
|
+
let journal = PathBuf::from(format!("{}-journal", db_path.display()));
|
|
93
|
+
|
|
94
|
+
let mut watched = HashSet::new();
|
|
95
|
+
let mut attached = 0;
|
|
96
|
+
for path in [&watch_dir, &db_path, &wal, &shm, &journal] {
|
|
97
|
+
if attach_watch(&mut watcher, &mut watched, path) {
|
|
98
|
+
attached += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if attached == 0 {
|
|
102
|
+
eprintln!(
|
|
103
|
+
"honker: kernel-watcher couldn't attach to db dir or -wal/-shm. Backend disabled."
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Dead-man's switch: snapshot db inode; panic if it changes
|
|
109
|
+
// (atomic rename, litestream restore, NFS remount). Per-file
|
|
110
|
+
// watches would silently sit on the dead inode otherwise.
|
|
111
|
+
let initial_id = match stat_identity(&db_path) {
|
|
112
|
+
Ok(id) => id,
|
|
113
|
+
Err(e) => {
|
|
114
|
+
eprintln!("honker: failed to stat database for identity check: {e}");
|
|
115
|
+
(0, 0)
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
let mut last_id_check = Instant::now();
|
|
119
|
+
let _ = ready.send(());
|
|
120
|
+
drop(ready);
|
|
121
|
+
|
|
122
|
+
while !stop.load(Ordering::Acquire) {
|
|
123
|
+
match rx.recv_timeout(Duration::from_millis(RX_POLL_MS)) {
|
|
124
|
+
Ok(Ok(_event)) => {
|
|
125
|
+
for path in [&db_path, &wal, &shm, &journal] {
|
|
126
|
+
let _ = attach_watch(&mut watcher, &mut watched, path);
|
|
127
|
+
}
|
|
128
|
+
on_change();
|
|
129
|
+
}
|
|
130
|
+
Ok(Err(e)) => {
|
|
131
|
+
eprintln!("honker: kernel-watcher event error: {e}");
|
|
132
|
+
on_change();
|
|
133
|
+
}
|
|
134
|
+
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
|
135
|
+
_ => {}
|
|
136
|
+
}
|
|
137
|
+
if last_id_check.elapsed() >= Duration::from_millis(IDENTITY_CHECK_MS) {
|
|
138
|
+
for path in [&db_path, &wal, &shm, &journal] {
|
|
139
|
+
let _ = attach_watch(&mut watcher, &mut watched, path);
|
|
140
|
+
}
|
|
141
|
+
if check_db_identity(&db_path, initial_id) {
|
|
142
|
+
on_change();
|
|
143
|
+
}
|
|
144
|
+
last_id_check = Instant::now();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[cfg(not(target_os = "macos"))]
|
|
151
|
+
fn attach_watch<W: Watcher>(watcher: &mut W, watched: &mut HashSet<PathBuf>, path: &Path) -> bool {
|
|
152
|
+
if watched.contains(path) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if watcher.watch(path, RecursiveMode::NonRecursive).is_ok() {
|
|
156
|
+
watched.insert(path.to_path_buf());
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
false
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Panics if the db file at `db_path` has been replaced since startup.
|
|
163
|
+
/// Returns `true` on stat error so caller can fire a conservative wake.
|
|
164
|
+
fn check_db_identity(db_path: &std::path::Path, initial: (u64, u64)) -> bool {
|
|
165
|
+
match stat_identity(db_path) {
|
|
166
|
+
Ok(current) => {
|
|
167
|
+
if current != initial {
|
|
168
|
+
panic!(
|
|
169
|
+
"honker: database file replaced: \
|
|
170
|
+
expected (dev={}, ino={}), found (dev={}, ino={}) at {:?}. \
|
|
171
|
+
The watcher cannot recover; \
|
|
172
|
+
close the Database and reopen with honker.open().",
|
|
173
|
+
initial.0, initial.1, current.0, current.1, db_path
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
false
|
|
177
|
+
}
|
|
178
|
+
Err(e) => {
|
|
179
|
+
eprintln!("honker: stat identity check failed: {e}");
|
|
180
|
+
true
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Probe at `honker.open()` so a misconfigured backend errors
|
|
186
|
+
/// immediately instead of silently producing no wakes.
|
|
187
|
+
pub(crate) fn probe(db_path: &std::path::Path) -> Result<(), String> {
|
|
188
|
+
#[cfg(target_os = "macos")]
|
|
189
|
+
{
|
|
190
|
+
return probe_kqueue(db_path);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[cfg(not(target_os = "macos"))]
|
|
194
|
+
{
|
|
195
|
+
let (tx, _rx) = mpsc::channel::<notify::Result<notify::Event>>();
|
|
196
|
+
let mut w =
|
|
197
|
+
notify::recommended_watcher(tx).map_err(|e| format!("notify init failed: {e}"))?;
|
|
198
|
+
let dir = db_path.parent().unwrap_or(std::path::Path::new("."));
|
|
199
|
+
w.watch(dir, RecursiveMode::NonRecursive)
|
|
200
|
+
.map_err(|e| format!("can't watch {dir:?}: {e}"))?;
|
|
201
|
+
Ok(())
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[cfg(target_os = "macos")]
|
|
206
|
+
mod macos {
|
|
207
|
+
use super::*;
|
|
208
|
+
use std::ffi::CString;
|
|
209
|
+
use std::os::unix::ffi::OsStrExt;
|
|
210
|
+
use std::path::{Path, PathBuf};
|
|
211
|
+
use std::ptr;
|
|
212
|
+
|
|
213
|
+
struct Kqueue {
|
|
214
|
+
fd: libc::c_int,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
impl Kqueue {
|
|
218
|
+
fn new() -> Result<Self, String> {
|
|
219
|
+
let fd = unsafe { libc::kqueue() };
|
|
220
|
+
if fd < 0 {
|
|
221
|
+
return Err(format!(
|
|
222
|
+
"kqueue failed: {}",
|
|
223
|
+
std::io::Error::last_os_error()
|
|
224
|
+
));
|
|
225
|
+
}
|
|
226
|
+
Ok(Self { fd })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn add_vnode(&self, fd: libc::c_int) -> Result<(), String> {
|
|
230
|
+
let mut event = libc::kevent {
|
|
231
|
+
ident: fd as libc::uintptr_t,
|
|
232
|
+
filter: libc::EVFILT_VNODE,
|
|
233
|
+
flags: libc::EV_ADD | libc::EV_ENABLE | libc::EV_CLEAR,
|
|
234
|
+
fflags: libc::NOTE_WRITE
|
|
235
|
+
| libc::NOTE_EXTEND
|
|
236
|
+
| libc::NOTE_ATTRIB
|
|
237
|
+
| libc::NOTE_DELETE
|
|
238
|
+
| libc::NOTE_RENAME
|
|
239
|
+
| libc::NOTE_REVOKE,
|
|
240
|
+
data: 0,
|
|
241
|
+
udata: ptr::null_mut(),
|
|
242
|
+
};
|
|
243
|
+
let n =
|
|
244
|
+
unsafe { libc::kevent(self.fd, &mut event, 1, ptr::null_mut(), 0, ptr::null()) };
|
|
245
|
+
if n < 0 {
|
|
246
|
+
Err(format!(
|
|
247
|
+
"kevent add failed: {}",
|
|
248
|
+
std::io::Error::last_os_error()
|
|
249
|
+
))
|
|
250
|
+
} else {
|
|
251
|
+
Ok(())
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn wait_one(&self, timeout: Duration) -> Result<Option<libc::kevent>, String> {
|
|
256
|
+
let mut event = libc::kevent {
|
|
257
|
+
ident: 0,
|
|
258
|
+
filter: 0,
|
|
259
|
+
flags: 0,
|
|
260
|
+
fflags: 0,
|
|
261
|
+
data: 0,
|
|
262
|
+
udata: ptr::null_mut(),
|
|
263
|
+
};
|
|
264
|
+
let ts = libc::timespec {
|
|
265
|
+
tv_sec: timeout.as_secs() as libc::time_t,
|
|
266
|
+
tv_nsec: i64::from(timeout.subsec_nanos()) as libc::c_long,
|
|
267
|
+
};
|
|
268
|
+
let n = unsafe { libc::kevent(self.fd, ptr::null(), 0, &mut event, 1, &ts) };
|
|
269
|
+
if n < 0 {
|
|
270
|
+
Err(format!(
|
|
271
|
+
"kevent wait failed: {}",
|
|
272
|
+
std::io::Error::last_os_error()
|
|
273
|
+
))
|
|
274
|
+
} else if n == 0 {
|
|
275
|
+
Ok(None)
|
|
276
|
+
} else {
|
|
277
|
+
Ok(Some(event))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
impl Drop for Kqueue {
|
|
283
|
+
fn drop(&mut self) {
|
|
284
|
+
unsafe {
|
|
285
|
+
libc::close(self.fd);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
struct WatchedPath {
|
|
291
|
+
path: PathBuf,
|
|
292
|
+
fd: libc::c_int,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
impl Drop for WatchedPath {
|
|
296
|
+
fn drop(&mut self) {
|
|
297
|
+
unsafe {
|
|
298
|
+
libc::close(self.fd);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn open_event_fd(path: &Path) -> Result<libc::c_int, String> {
|
|
304
|
+
let c_path = CString::new(path.as_os_str().as_bytes())
|
|
305
|
+
.map_err(|_| format!("path contains NUL byte: {path:?}"))?;
|
|
306
|
+
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_EVTONLY) };
|
|
307
|
+
if fd < 0 {
|
|
308
|
+
Err(format!(
|
|
309
|
+
"open {path:?} failed: {}",
|
|
310
|
+
std::io::Error::last_os_error()
|
|
311
|
+
))
|
|
312
|
+
} else {
|
|
313
|
+
Ok(fd)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn candidate_paths(db_path: &Path) -> Vec<PathBuf> {
|
|
318
|
+
let mut paths = Vec::with_capacity(5);
|
|
319
|
+
if let Some(parent) = db_path.parent() {
|
|
320
|
+
paths.push(parent.to_path_buf());
|
|
321
|
+
}
|
|
322
|
+
paths.push(db_path.to_path_buf());
|
|
323
|
+
paths.push(PathBuf::from(format!("{}-wal", db_path.display())));
|
|
324
|
+
paths.push(PathBuf::from(format!("{}-shm", db_path.display())));
|
|
325
|
+
paths.push(PathBuf::from(format!("{}-journal", db_path.display())));
|
|
326
|
+
paths
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn attach_path(kq: &Kqueue, path: PathBuf, watched: &mut Vec<WatchedPath>) -> bool {
|
|
330
|
+
if watched.iter().any(|w| w.path == path) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
let fd = match open_event_fd(&path) {
|
|
334
|
+
Ok(fd) => fd,
|
|
335
|
+
Err(_) => return false,
|
|
336
|
+
};
|
|
337
|
+
if let Err(e) = kq.add_vnode(fd) {
|
|
338
|
+
eprintln!("honker: kqueue couldn't watch {path:?}: {e}");
|
|
339
|
+
unsafe {
|
|
340
|
+
libc::close(fd);
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
watched.push(WatchedPath { path, fd });
|
|
345
|
+
true
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn attach_existing(kq: &Kqueue, db_path: &Path, watched: &mut Vec<WatchedPath>) -> usize {
|
|
349
|
+
candidate_paths(db_path)
|
|
350
|
+
.into_iter()
|
|
351
|
+
.filter(|path| path.exists())
|
|
352
|
+
.filter(|path| attach_path(kq, path.clone(), watched))
|
|
353
|
+
.count()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn prune_deleted(event: &libc::kevent, watched: &mut Vec<WatchedPath>) {
|
|
357
|
+
if event.fflags & (libc::NOTE_DELETE | libc::NOTE_RENAME | libc::NOTE_REVOKE) == 0 {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
let ident = event.ident as libc::c_int;
|
|
361
|
+
watched.retain(|w| w.fd != ident);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
pub(super) fn run_kqueue_loop<F>(
|
|
365
|
+
db_path: PathBuf,
|
|
366
|
+
on_change: F,
|
|
367
|
+
stop: Arc<AtomicBool>,
|
|
368
|
+
ready: std::sync::mpsc::SyncSender<()>,
|
|
369
|
+
) where
|
|
370
|
+
F: Fn() + Send + 'static,
|
|
371
|
+
{
|
|
372
|
+
let kq = match Kqueue::new() {
|
|
373
|
+
Ok(kq) => kq,
|
|
374
|
+
Err(e) => {
|
|
375
|
+
eprintln!("honker: kernel-watcher init failed: {e}. Backend disabled.");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
let mut watched = Vec::new();
|
|
380
|
+
let attached = attach_existing(&kq, &db_path, &mut watched);
|
|
381
|
+
if attached == 0 {
|
|
382
|
+
eprintln!(
|
|
383
|
+
"honker: kqueue couldn't attach to db dir or database files. Backend disabled."
|
|
384
|
+
);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let initial_id = match stat_identity(&db_path) {
|
|
389
|
+
Ok(id) => id,
|
|
390
|
+
Err(e) => {
|
|
391
|
+
eprintln!("honker: failed to stat database for identity check: {e}");
|
|
392
|
+
(0, 0)
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
let mut last_id_check = Instant::now();
|
|
396
|
+
let _ = ready.send(());
|
|
397
|
+
drop(ready);
|
|
398
|
+
|
|
399
|
+
while !stop.load(Ordering::Acquire) {
|
|
400
|
+
match kq.wait_one(Duration::from_millis(RX_POLL_MS)) {
|
|
401
|
+
Ok(Some(event)) => {
|
|
402
|
+
prune_deleted(&event, &mut watched);
|
|
403
|
+
let _ = attach_existing(&kq, &db_path, &mut watched);
|
|
404
|
+
on_change();
|
|
405
|
+
}
|
|
406
|
+
Ok(None) => {
|
|
407
|
+
let _ = attach_existing(&kq, &db_path, &mut watched);
|
|
408
|
+
}
|
|
409
|
+
Err(e) => {
|
|
410
|
+
eprintln!("honker: kqueue event error: {e}");
|
|
411
|
+
on_change();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if last_id_check.elapsed() >= Duration::from_millis(IDENTITY_CHECK_MS) {
|
|
416
|
+
if check_db_identity(&db_path, initial_id) {
|
|
417
|
+
on_change();
|
|
418
|
+
}
|
|
419
|
+
last_id_check = Instant::now();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
pub(super) fn probe_kqueue(db_path: &Path) -> Result<(), String> {
|
|
425
|
+
let kq = Kqueue::new()?;
|
|
426
|
+
let dir = db_path.parent().unwrap_or(Path::new("."));
|
|
427
|
+
let dir_fd = open_event_fd(dir)?;
|
|
428
|
+
let result = kq.add_vnode(dir_fd);
|
|
429
|
+
unsafe {
|
|
430
|
+
libc::close(dir_fd);
|
|
431
|
+
}
|
|
432
|
+
result.map_err(|e| format!("can't watch {dir:?}: {e}"))
|
|
433
|
+
}
|
|
434
|
+
}
|