diff --git a/src/backend/KQueue.zig b/src/backend/KQueue.zig index aeee5fd..b7e38b6 100644 --- a/src/backend/KQueue.zig +++ b/src/backend/KQueue.zig @@ -189,7 +189,7 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; defer dir.close(); - // Arena for all temporaries — freed in one shot at the end. + // Arena for all temporaries - freed in one shot at the end. var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const tmp = arena.allocator(); diff --git a/src/backend/KQueueDir.zig b/src/backend/KQueueDir.zig index 0cf816f..c1a8139 100644 --- a/src/backend/KQueueDir.zig +++ b/src/backend/KQueueDir.zig @@ -173,7 +173,7 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; defer dir.close(); - // Arena for all temporaries — freed in one shot at the end. + // Arena for all temporaries - freed in one shot at the end. var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const tmp = arena.allocator(); @@ -242,13 +242,13 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) var cit = current_files.iterator(); while (cit.next()) |entry| { if (snapshot.getPtr(entry.key_ptr.*)) |stored_mtime| { - // File exists in both — check for modification via mtime change. + // File exists in both - check for modification via mtime change. if (stored_mtime.* != entry.value_ptr.*) { stored_mtime.* = entry.value_ptr.*; try to_modify.append(tmp, entry.key_ptr.*); // from current_files (tmp) } } else { - // New file — add to snapshot and to_create list. + // New file - add to snapshot and to_create list. const owned = allocator.dupe(u8, entry.key_ptr.*) catch |e| { return e; }; diff --git a/src/types.zig b/src/types.zig index b3627ba..5af8794 100644 --- a/src/types.zig +++ b/src/types.zig @@ -18,22 +18,25 @@ pub const EventType = enum { /// /// Delivery varies by backend: /// - /// - **INotify**: renames within the watched tree are delivered as a - /// single atomic `rename` callback with both source and destination - /// paths. A move out of the tree appears as `deleted`; a move into - /// the tree appears as `created`. + /// - **INotify**: all watches share a single inotify file descriptor, so + /// moves are paired by cookie across all watched roots. Renames between + /// two watched directories - even separate watch roots on the same + /// watcher instance - are delivered as a single atomic `rename` + /// callback. A move out of all watched paths appears as `deleted`; a + /// move in from an unwatched path appears as `created`. + /// + /// - **Windows**: renames within a single watched root are delivered as a + /// single atomic `rename` callback. However, each root uses an + /// independent `ReadDirectoryChangesW` handle with no shared cookie, so + /// a move between two separately watched roots cannot be paired: it + /// appears as `deleted` on the source side and `created` on the + /// destination side. /// /// - **kqueue / kqueuedir**: when a watched *directory* is itself /// renamed, a `renamed` change event is emitted for the old directory /// path (the new path is not known). Renames of *files inside* a /// watched directory are detected indirectly via directory-level - /// `NOTE_WRITE` events and appear as a `deleted` event for the old - /// name followed by a `created` event for the new name. - /// - /// - **Windows**: renames within the watched tree are delivered as a - /// single atomic `rename` callback, matching INotify behaviour. A - /// move out of the tree appears as `deleted`; a move into the tree - /// appears as `created`. + /// `NOTE_WRITE` events and appear as `deleted` + `created`. /// /// - **FSEvents**: each path involved in a rename receives its own /// `renamed` change event; the two sides are not paired. @@ -61,9 +64,9 @@ pub const Error = error{ /// Selects how the watcher delivers events to the caller. /// -/// - `.threaded` — the backend spawns an internal thread that calls the +/// - `.threaded` - the backend spawns an internal thread that calls the /// handler directly. The caller just needs to keep the `Watcher` alive. -/// - `.polling` — no internal thread is created. The caller must poll +/// - `.polling` - no internal thread is created. The caller must poll /// `poll_fd()` for readability and call `handle_read_ready()` whenever /// data is available. Currently only supported on Linux (inotify). pub const InterfaceType = enum { polling, threaded }; diff --git a/test_manual.ps1 b/test_manual.ps1 index cee98fe..672bc6b 100644 --- a/test_manual.ps1 +++ b/test_manual.ps1 @@ -14,15 +14,17 @@ if (-not (Test-Path $NW)) { exit 1 } -$TESTDIR = Join-Path $env:TEMP "nightwatch_manual_$PID" -New-Item -ItemType Directory -Path $TESTDIR | Out-Null +$TESTDIR = Join-Path $env:TEMP "nightwatch_manual_$PID" +$TESTDIR2 = Join-Path $env:TEMP "nightwatch_manual2_$PID" +New-Item -ItemType Directory -Path $TESTDIR | Out-Null +New-Item -ItemType Directory -Path $TESTDIR2 | Out-Null -Write-Host "--- watching $TESTDIR ---" +Write-Host "--- watching $TESTDIR and $TESTDIR2 ---" Write-Host "--- starting nightwatch (Ctrl-C to stop early) ---" Write-Host "" -# Start nightwatch in background, events go to stdout -$proc = Start-Process -FilePath $NW -ArgumentList $TESTDIR -NoNewWindow -PassThru +# Start nightwatch in background watching both dirs, events go to stdout +$proc = Start-Process -FilePath $NW -ArgumentList $TESTDIR, $TESTDIR2 -NoNewWindow -PassThru Start-Sleep -Milliseconds 500 Write-Host "[op] touch file1.txt" @@ -73,8 +75,38 @@ Write-Host "[op] rmdir dirB (and contents)" Remove-Item -Recurse -Force -Path "$TESTDIR\dirB" Start-Sleep -Milliseconds 500 +Write-Host "" +Write-Host "# cross-root renames (both dirs watched)" +Write-Host "" + +Write-Host "[op] mkdir subA in both roots" +New-Item -ItemType Directory -Path "$TESTDIR\subA" | Out-Null +New-Item -ItemType Directory -Path "$TESTDIR2\subA" | Out-Null +Start-Sleep -Milliseconds 400 + +Write-Host "[op] touch crossfile.txt in dir1" +New-Item -ItemType File -Path "$TESTDIR\crossfile.txt" | Out-Null +Start-Sleep -Milliseconds 400 + +Write-Host "[op] rename crossfile.txt: dir1 -> dir2 (root to root)" +Move-Item -Path "$TESTDIR\crossfile.txt" -Destination "$TESTDIR2\crossfile.txt" +Start-Sleep -Milliseconds 400 + +Write-Host "[op] touch subA\crosssub.txt in dir1" +New-Item -ItemType File -Path "$TESTDIR\subA\crosssub.txt" | Out-Null +Start-Sleep -Milliseconds 400 + +Write-Host "[op] rename subA\crosssub.txt: dir1\subA -> dir2\subA (subdir to subdir)" +Move-Item -Path "$TESTDIR\subA\crosssub.txt" -Destination "$TESTDIR2\subA\crosssub.txt" +Start-Sleep -Milliseconds 400 + +Write-Host "[op] rename subA: dir1 -> dir2 (subdir across roots)" +Move-Item -Path "$TESTDIR\subA" -Destination "$TESTDIR2\subA2" +Start-Sleep -Milliseconds 500 + Write-Host "" Write-Host "--- done, stopping nightwatch ---" Stop-Process -Id $proc.Id -ErrorAction SilentlyContinue $proc.WaitForExit() Remove-Item -Recurse -Force -Path $TESTDIR +Remove-Item -Recurse -Force -Path $TESTDIR2 diff --git a/test_manual.sh b/test_manual.sh index 12db2ac..3680d14 100755 --- a/test_manual.sh +++ b/test_manual.sh @@ -14,12 +14,13 @@ if [ ! -x "$NW" ]; then fi TESTDIR=$(mktemp -d) -echo "--- watching $TESTDIR ---" +TESTDIR2=$(mktemp -d) +echo "--- watching $TESTDIR and $TESTDIR2 ---" echo "--- starting nightwatch (Ctrl-C to stop early) ---" echo "" -# Start nightwatch in background, events go to stdout -"$NW" "$TESTDIR" & +# Start nightwatch in background watching both dirs, events go to stdout +"$NW" "$TESTDIR" "$TESTDIR2" & NW_PID=$! sleep 0.5 @@ -71,8 +72,37 @@ echo "[op] rmdir dirB (and contents)" rm -rf "$TESTDIR/dirB" sleep 0.5 +echo "" +echo "# cross-root renames (both dirs watched)" +echo "" + +echo "[op] mkdir subA in both roots" +mkdir "$TESTDIR/subA" +mkdir "$TESTDIR2/subA" +sleep 0.4 + +echo "[op] touch crossfile.txt in dir1" +touch "$TESTDIR/crossfile.txt" +sleep 0.4 + +echo "[op] rename crossfile.txt: dir1 -> dir2 (root to root)" +mv "$TESTDIR/crossfile.txt" "$TESTDIR2/crossfile.txt" +sleep 0.4 + +echo "[op] touch subA/crosssub.txt in dir1" +touch "$TESTDIR/subA/crosssub.txt" +sleep 0.4 + +echo "[op] rename subA/crosssub.txt: dir1/subA -> dir2/subA (subdir to subdir)" +mv "$TESTDIR/subA/crosssub.txt" "$TESTDIR2/subA/crosssub.txt" +sleep 0.4 + +echo "[op] rename subA: dir1 -> dir2 (subdir across roots)" +mv "$TESTDIR/subA" "$TESTDIR2/subA2" +sleep 0.5 + echo "" echo "--- done, stopping nightwatch ---" kill $NW_PID 2>/dev/null wait $NW_PID 2>/dev/null -rm -rf "$TESTDIR" +rm -rf "$TESTDIR" "$TESTDIR2"