Commit Diff


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<PathBuf>) {
+    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<PathBuf>) {
     let entries = match fs::read_dir(dir) {
         Ok(e) => e,
@@ -419,3 +440,191 @@ fn generate_post_list(posts: &[Post]) -> String {
     html.push_str("</ul>");
     html
 }
+
+//////////////////////////////////////////////////////////////////////////////
+// Watcher
+//////////////////////////////////////////////////////////////////////////////
+
+fn watch_paths() -> Vec<PathBuf> {
+    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<i32> = 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<KEvent> = 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();
+    }
+}