commit 5bf6d120ef53d18feb4cefb6be97b47a408da166 from: Murilo Ijanc date: Sun Apr 5 21:57:37 2026 UTC add file watcher via inotify and kqueue commit - 7c20cf6f8ff15852c42ea0a124c7072173e41c60 commit + 5bf6d120ef53d18feb4cefb6be97b47a408da166 blob - 2d2064bd5a743e522fa883ea1ed71d33bb7ca18b blob + fc0efeb04981353d47fd06c87543b0644e2da615 --- src/main.rs +++ src/main.rs @@ -64,6 +64,10 @@ fn content_type(path: &Path) -> &'static str { fn serve() { build(); + std::thread::spawn(|| { + watch_and_rebuild(); + }); + let addr = "127.0.0.1:8080"; let listener = TcpListener::bind(addr).expect("bind"); println!("serving at http://{}", addr); @@ -216,6 +220,23 @@ fn parse_frontmatter(content: &str) -> Metadata { // Filesystem ////////////////////////////////////////////////////////////////////////////// +fn walk_dirs(dir: &Path, dirs: &mut Vec) { + dirs.push(dir.to_path_buf()); + let entries = match fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + for entry in entries { + let path = match entry { + Ok(e) => e.path(), + Err(_) => continue, + }; + if path.is_dir() { + walk_dirs(&path, dirs); + } + } +} + fn walk(dir: &Path, files: &mut Vec) { let entries = match fs::read_dir(dir) { Ok(e) => e, @@ -419,3 +440,191 @@ fn generate_post_list(posts: &[Post]) -> String { html.push_str(""); html } + +////////////////////////////////////////////////////////////////////////////// +// Watcher +////////////////////////////////////////////////////////////////////////////// + +fn watch_paths() -> Vec { + let mut dirs = Vec::new(); + for base in &["content", "templates", "static"] { + let path = Path::new(base); + if path.exists() { + walk_dirs(path, &mut dirs); + } + } + dirs +} + +#[cfg(target_os = "linux")] +fn watch_and_rebuild() { + use std::ffi::CString; + + unsafe extern "C" { + fn inotify_init() -> i32; + fn inotify_add_watch( + fd: i32, path: *const i8, mask: u32, + ) -> i32; + fn read(fd: i32, buf: *mut u8, count: usize) -> isize; + } + + const IN_MODIFY: u32 = 0x02; + const IN_CREATE: u32 = 0x100; + const IN_DELETE: u32 = 0x200; + const MASK: u32 = IN_MODIFY | IN_CREATE | IN_DELETE; + + let fd = unsafe { inotify_init() }; + if fd < 0 { + eprintln!("inotify_init failed"); + return; + } + + let dirs = watch_paths(); + for dir in &dirs { + let cstr = CString::new( + dir.to_str().expect("path to str"), + ) + .expect("cstring"); + let wd = unsafe { + inotify_add_watch(fd, cstr.as_ptr(), MASK) + }; + if wd < 0 { + eprintln!("watch failed: {}", dir.display()); + } else { + log!("watching {}", dir.display()); + } + } + + log!("watcher ready"); + + let mut buf = [0u8; 4096]; + loop { + let n = unsafe { + read(fd, buf.as_mut_ptr(), buf.len()) + }; + if n <= 0 { + continue; + } + log!("change detected, rebuilding"); + build(); + } +} + +#[cfg(target_os = "openbsd")] +fn watch_and_rebuild() { + use std::ffi::CString; + + unsafe extern "C" { + fn kqueue() -> i32; + fn open(path: *const i8, flags: i32) -> i32; + fn kevent( + kq: i32, + changelist: *const KEvent, + nchanges: i32, + eventlist: *mut KEvent, + nevents: i32, + timeout: *const Timespec, + ) -> i32; + } + + #[repr(C)] + struct KEvent { + ident: usize, + filter: i16, + flags: u16, + fflags: u32, + data: isize, + udata: *mut u8, + } + + #[repr(C)] + struct Timespec { + tv_sec: i64, + tv_nsec: i64, + } + + const EVFILT_VNODE: i16 = -4; + const EV_ADD: u16 = 0x0001; + const EV_CLEAR: u16 = 0x0020; + const NOTE_WRITE: u32 = 0x0002; + const NOTE_DELETE: u32 = 0x0001; + const NOTE_RENAME: u32 = 0x0020; + const O_RDONLY: i32 = 0; + const FFLAGS: u32 = + NOTE_WRITE | NOTE_DELETE | NOTE_RENAME; + + let kq = unsafe { kqueue() }; + if kq < 0 { + eprintln!("kqueue failed"); + return; + } + + let dirs = watch_paths(); + let mut fds: Vec = Vec::new(); + + for dir in &dirs { + let cstr = CString::new( + dir.to_str().expect("path to str"), + ) + .expect("cstring"); + let fd = unsafe { open(cstr.as_ptr(), O_RDONLY) }; + if fd < 0 { + eprintln!("open failed: {}", dir.display()); + continue; + } + fds.push(fd); + log!("watching {}", dir.display()); + } + + let changes: Vec = fds + .iter() + .map(|&fd| KEvent { + ident: fd as usize, + filter: EVFILT_VNODE, + flags: EV_ADD | EV_CLEAR, + fflags: FFLAGS, + data: 0, + udata: std::ptr::null_mut(), + }) + .collect(); + + unsafe { + kevent( + kq, + changes.as_ptr(), + changes.len() as i32, + std::ptr::null_mut(), + 0, + std::ptr::null(), + ); + } + + log!("watcher ready"); + + let mut event = KEvent { + ident: 0, + filter: 0, + flags: 0, + fflags: 0, + data: 0, + udata: std::ptr::null_mut(), + }; + + loop { + let n = unsafe { + kevent( + kq, + std::ptr::null(), + 0, + &mut event, + 1, + std::ptr::null(), + ) + }; + if n <= 0 { + continue; + } + log!("change detected, rebuilding"); + build(); + } +}