aboutsummaryrefslogtreecommitdiffstats
path: root/watcher_test_helper.go
diff options
context:
space:
mode:
authorBernhard Guillon <Bernhard.Guillon@begu.org>2026-07-07 09:03:31 +0200
committerBernhard Guillon <Bernhard.Guillon@begu.org>2026-07-07 09:03:31 +0200
commita0d157d45c5b4c63afc2cfc82bde3ae3207bc455 (patch)
treec6152cf56c437945359a3b80fc16b76e9ba7332a /watcher_test_helper.go
parente4c2f78abe427d9fdbfb46bb205f2210a03704fa (diff)
downloadsourcewatch-a0d157d45c5b4c63afc2cfc82bde3ae3207bc455.tar.gz
sourcewatch-a0d157d45c5b4c63afc2cfc82bde3ae3207bc455.zip
Add Windows native watcher with Unix socket IPC
Architecture: - Windows binary: watches files via ReadDirectoryChangesW (fsnotify), sends events as newline-delimited JSON over Unix domain socket - Container: performs initial sync, then listens on socket for events from Windows watcher New files: - cmd_windows.go: Windows serve command (watcher + socket server) - cmd_linux.go: Linux socket client + event listener - types.go: shared FileEvent, FileMeta, Manifest types - watcher_test_helper.go: inotify-based watcher for integration tests Changes: - main.go: simplified to just call run() - watcher.go: removed fsnotify dependency, extracted Watcher interface - sync.go: removed duplicate type definitions - watcher_test.go: uses watchForTests() helper Usage: sourcewatch.exe -dir C:\myproject -socket D:\sourcewatch.sock docker run -v D:\sourcewatch.sock:/run/sourcewatch.sock \ -v C:\myproject:/host:ro -v mydata:/data sourcewatch
Diffstat (limited to 'watcher_test_helper.go')
-rw-r--r--watcher_test_helper.go142
1 files changed, 142 insertions, 0 deletions
diff --git a/watcher_test_helper.go b/watcher_test_helper.go
new file mode 100644
index 0000000..519d3f6
--- /dev/null
+++ b/watcher_test_helper.go
@@ -0,0 +1,142 @@
+//go:build !windows
+
+package main
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// watchForTests uses inotify directly for integration testing.
+// This is only used in tests - production uses socket-based events from Windows watcher.
+func watchForTests(ctx context.Context, hostDir, dataDir string, manifest *Manifest, stack *GitignoreStack, dryRun bool) error {
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ return err
+ }
+ defer watcher.Close()
+
+ var mu sync.Mutex
+ debounceTimers := make(map[string]*time.Timer)
+ debounceOps := make(map[string]fsnotify.Op)
+
+ if err := addWatchersForTests(watcher, hostDir, stack); err != nil {
+ return err
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ return nil
+
+ case event, ok := <-watcher.Events:
+ if !ok {
+ return nil
+ }
+
+ relPath, err := filepath.Rel(hostDir, event.Name)
+ if err != nil {
+ continue
+ }
+
+ if isGitDir(relPath) {
+ continue
+ }
+
+ if stack.IsIgnored(relPath) {
+ continue
+ }
+
+ if event.Op&fsnotify.Create != 0 {
+ srcPath := filepath.Join(hostDir, relPath)
+ info, err := os.Lstat(srcPath)
+ if err == nil && info.IsDir() {
+ if hasGitignore(srcPath) {
+ stack.Push(srcPath)
+ }
+ watcher.Add(srcPath)
+ filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil || !info.IsDir() || path == srcPath {
+ return nil
+ }
+ watcher.Add(path)
+ return nil
+ })
+ }
+ }
+
+ mu.Lock()
+ if existing, exists := debounceOps[relPath]; exists {
+ debounceTimers[relPath].Stop()
+ debounceOps[relPath] = existing | event.Op
+ } else {
+ debounceOps[relPath] = event.Op
+ }
+ path := relPath
+ debounceTimers[relPath] = time.AfterFunc(200*time.Millisecond, func() {
+ mu.Lock()
+ op := debounceOps[path]
+ delete(debounceOps, path)
+ delete(debounceTimers, path)
+ mu.Unlock()
+
+ destPath := filepath.Join(dataDir, path)
+ srcPath := filepath.Join(hostDir, path)
+
+ switch {
+ case op&fsnotify.Create != 0:
+ syncCreate(srcPath, destPath, path, manifest, stack, dryRun, watcher)
+ case op&fsnotify.Write != 0:
+ syncWrite(srcPath, destPath, path, manifest, dryRun)
+ case op&fsnotify.Remove != 0:
+ syncRemove(destPath, path, manifest, dryRun)
+ case op&fsnotify.Rename != 0:
+ syncRemove(destPath, path, manifest, dryRun)
+ }
+ })
+ mu.Unlock()
+
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ return nil
+ }
+ _ = err
+ }
+ }
+}
+
+func addWatchersForTests(watcher *fsnotify.Watcher, root string, stack *GitignoreStack) error {
+ return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ relPath, err := filepath.Rel(root, path)
+ if err != nil {
+ return err
+ }
+
+ if isGitDir(relPath) {
+ if info.IsDir() {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ if info.IsDir() {
+ if hasGitignore(path) {
+ stack.Push(path)
+ }
+ if !stack.IsIgnored(relPath) {
+ return watcher.Add(path)
+ }
+ }
+
+ return nil
+ })
+}