sqzass.git

sqzass

~ A static site generator written in Rust
git clone https://github.com/debuman/sqzass.git
commit 65ca7b048230b63b3986fe4223ff27e274069b1b
parent 32e7cae7ca2812ab0261996fd03cce8383b54f80
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Tue, 28 Jul 2026 21:26:34 +0900

serve 리빌드 사유에서 읽기 이벤트를 거른다

첫 저장 이후 리빌드가 영원히 돌았다. inotify의 감시 마스크에 IN_OPEN이
들어 있어 읽기도 이벤트를 만드는데, 감시 스레드는 경로 노이즈만 거르고
이벤트 종류를 보지 않았다. 리빌드가 content/와 templates/를 읽는 것
자체가 다음 리빌드를 예약한다 — 1페이지 사이트 실측에서 touch 한 번에
10초간 리빌드 66회, 이후 무한 증가.

서빙과 라이브 리로드는 루프 중에도 멀쩡해서 증상이 가려져 있었다.
Access(_)를 리빌드 사유에서 빼면 touch 한 번은 리빌드 한 번이다.

Diffstat:
Msrc/serve.rs | 24++++++++++++++++++++++++
1 file changed, 24 insertion(+), 0 deletion(-)

diff --git a/src/serve.rs b/src/serve.rs
@@ -142,6 +142,7 @@ fn spawn_watcher(
             let Ok(events) = res else { continue };
             let paths: Vec<PathBuf> = events
                 .iter()
+                .filter(|e| is_change(&e.kind))
                 .flat_map(|e| e.paths.clone())
                 .filter(|p| !is_noise(p))
                 .collect();
@@ -155,6 +156,13 @@ fn spawn_watcher(
     Ok(debouncer)
 }
 
+/// 파일을 *바꾼* 이벤트만 리빌드 사유다. inotify의 감시 마스크에는 IN_OPEN이
+/// 들어 있어서 읽기도 이벤트를 만든다 — 이걸 안 거르면 리빌드가 content/를
+/// 읽는 것 자체가 다음 리빌드를 예약해, 첫 저장 이후 영원히 도는 루프가 된다.
+fn is_change(kind: &notify::EventKind) -> bool {
+    !matches!(kind, notify::EventKind::Access(_))
+}
+
 /// 편집기 임시 파일과 VCS 내부 파일은 무시한다. 이걸 안 걸러내면 저장 한 번에
 /// 리빌드가 여러 번 돈다.
 fn is_noise(path: &Path) -> bool {
@@ -463,6 +471,22 @@ mod tests {
         assert_eq!(resolve_key("/.nojekyll"), ".nojekyll");
     }
 
+    #[test]
+    fn reads_do_not_count_as_changes() {
+        // 리빌드 자신이 content/를 읽으며 내는 Access(Open)이 리빌드 사유가
+        // 되면 첫 저장 이후 영원히 도는 루프다. 변경 부류는 전부 통과해야 한다.
+        use notify::EventKind;
+        use notify::event::{AccessKind, CreateKind, ModifyKind, RemoveKind};
+        assert!(!is_change(&EventKind::Access(AccessKind::Open(
+            notify::event::AccessMode::Any
+        ))));
+        assert!(!is_change(&EventKind::Access(AccessKind::Any)));
+        assert!(is_change(&EventKind::Modify(ModifyKind::Any)));
+        assert!(is_change(&EventKind::Create(CreateKind::Any)));
+        assert!(is_change(&EventKind::Remove(RemoveKind::Any)));
+        assert!(is_change(&EventKind::Any));
+    }
+
     #[test]
     fn origin_must_be_present_and_parseable() {
         // fail closed: 판단할 수 없으면 거절