diff options
| author | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-07 09:03:31 +0200 |
|---|---|---|
| committer | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-07 09:03:31 +0200 |
| commit | a0d157d45c5b4c63afc2cfc82bde3ae3207bc455 (patch) | |
| tree | c6152cf56c437945359a3b80fc16b76e9ba7332a /watcher_test_helper.go | |
| parent | e4c2f78abe427d9fdbfb46bb205f2210a03704fa (diff) | |
| download | sourcewatch-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.go | 142 |
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 + }) +} |
