From a4f43ca65377643e96ec91108effe7321a6b0d6c Mon Sep 17 00:00:00 2001 From: Justin Venus Date: Wed, 4 Feb 2026 19:12:43 -0600 Subject: [PATCH] Add support for Rename and Create events in inotify file watching Fixes #204 Previously, the inotify watcher only handled Write and Remove events. This caused issues when editors like vim perform atomic writes (which generate Rename events) or when files are atomically swapped via directory operations. This change: - Adds explicit handling for Rename events by re-adding the watch after rename operations (ensuring continued monitoring when files are replaced) - Adds handling for Create events (which occur with atomic writes from some editors) - Improves error handling for Remove events to be non-fatal (important for Kubernetes configmap/secret mounts) - Maintains watching the specific file path rather than the directory or inode, as preferred by users The solution watches the file path and handles all event types that can occur during atomic file operations, making it robust for both editor-based edits and atomic swap scenarios without requiring directory-level watching. --- integration/reload.bats | 37 +++++++++++++++++++++++++++++++++++++ main.go | 31 +++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/integration/reload.bats b/integration/reload.bats index e8219d7..5110199 100755 --- a/integration/reload.bats +++ b/integration/reload.bats @@ -85,3 +85,40 @@ grep_test_file() { kill -s TERM "$PID" wait } + +@test "if inotify is enabled it handles RENAME events (vim-style atomic writes)" { + echo '* * * * * * * echo a > "$TEST_FILE"' > "$CRONTAB_FILE" + + "${BATS_TEST_DIRNAME}/../supercronic" -inotify "$CRONTAB_FILE" 3>&- & + PID="$!" + + wait_for grep_test_file a + + # Simulate vim-style atomic write: create temp file, write to it, rename over original + TEMP_FILE="${CRONTAB_FILE}.tmp" + echo '* * * * * * * echo b > "$TEST_FILE"' > "$TEMP_FILE" + mv "$TEMP_FILE" "$CRONTAB_FILE" + + wait_for grep_test_file b + + kill -s TERM "$PID" + wait +} + +@test "if inotify is enabled it handles CREATE events (file recreation)" { + echo '* * * * * * * echo a > "$TEST_FILE"' > "$CRONTAB_FILE" + + "${BATS_TEST_DIRNAME}/../supercronic" -inotify "$CRONTAB_FILE" 3>&- & + PID="$!" + + wait_for grep_test_file a + + # Simulate Create event: remove file, then create new one at same path + rm "$CRONTAB_FILE" + echo '* * * * * * * echo b > "$TEST_FILE"' > "$CRONTAB_FILE" + + wait_for grep_test_file b + + kill -s TERM "$PID" + wait +} diff --git a/main.go b/main.go index 8c6de95..094ac1c 100644 --- a/main.go +++ b/main.go @@ -202,19 +202,34 @@ func main() { } logrus.Debugf("event: %v, watch-list: %v", event, watcher.WatchList()) - switch event.Op { - case event.Op & fsnotify.Write: - logrus.Debug("watched file changed") + // Handle any file modification event (Write, Create, Remove, Rename) + // Many editors use atomic writes that generate Rename or Create events + if event.Op&fsnotify.Write != 0 { + logrus.Debug("watched file changed (write)") termChan <- syscall.SIGUSR2 - - // workaround for k8s configmap and secret mounts - case event.Op & fsnotify.Remove: - logrus.Debug("watched file changed") + // workaround for k8s configmap and secret mounts + } else if event.Op&fsnotify.Create != 0 { + logrus.Debug("watched file changed (create)") + termChan <- syscall.SIGUSR2 + } else if event.Op&fsnotify.Rename != 0 { + logrus.Debug("watched file changed (rename)") + // Re-add watch after rename (file might have been replaced) if err := watcher.Add(crontabFileName); err != nil { + logrus.Errorf("failed to re-add watch after rename: %s", err) logrus.Fatal(err) return + } else { + termChan <- syscall.SIGUSR2 + } + } else if event.Op&fsnotify.Remove != 0 { + // workaround for k8s configmap and secret mounts + logrus.Debug("watched file changed (remove)") + if err := watcher.Add(crontabFileName); err != nil { + logrus.Errorf("failed to re-add watch: %s", err) + // Don't return/fatal here - just log and continue + } else { + termChan <- syscall.SIGUSR2 } - termChan <- syscall.SIGUSR2 } case err, ok := <-watcher.Errors: