From a0d157d45c5b4c63afc2cfc82bde3ae3207bc455 Mon Sep 17 00:00:00 2001 From: Bernhard Guillon Date: Tue, 7 Jul 2026 09:03:31 +0200 Subject: 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 --- cmd_linux.go | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 cmd_linux.go (limited to 'cmd_linux.go') diff --git a/cmd_linux.go b/cmd_linux.go new file mode 100644 index 0000000..9e7e237 --- /dev/null +++ b/cmd_linux.go @@ -0,0 +1,131 @@ +//go:build !windows + +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "net" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" +) + +var ( + hostDir = flag.String("host", "/host", "Host source directory (read-only)") + dataDir = flag.String("data", "/data", "Data destination directory") + verbose = flag.Bool("verbose", false, "Enable verbose logging (shorthand for -log-level debug)") + logLevel = flag.String("log-level", "info", "Log level: debug, info, warn, error, none") + dryRun = flag.Bool("dry-run", false, "Preview what would be synced without copying") + socketPath = flag.String("socket", "", "Unix socket to receive events from (Windows watcher)") +) + +func run() { + flag.Parse() + + if *verbose { + *logLevel = "debug" + } + + level, err := ParseLogLevel(*logLevel) + if err != nil { + logErrorf("%v", err) + os.Exit(1) + } + SetLogLevel(level) + + stack := NewGitignoreStack(*hostDir) + + manifest, err := initialSync(*hostDir, *dataDir, stack, *dryRun) + if err != nil { + logErrorf("initial sync failed: %v", err) + os.Exit(1) + } + + if *dryRun { + logInfo("dry-run complete, exiting") + return + } + + if *socketPath == "" { + logInfo("no -socket specified, running in standalone mode (no file watching)") + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + <-ctx.Done() + return + } + + logInfo("watcher: connecting to %s ...", *socketPath) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + if err := listenSocket(ctx, *socketPath, manifest, stack); err != nil { + logErrorf("socket listener failed: %v", err) + os.Exit(1) + } +} + +func listenSocket(ctx context.Context, socketPath string, manifest *Manifest, stack *GitignoreStack) error { + var conn net.Conn + var err error + + for { + if ctx.Err() != nil { + return ctx.Err() + } + conn, err = net.Dial("unix", socketPath) + if err == nil { + break + } + logDebug("connect failed: %v, retrying...", err) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1 * time.Second): + } + } + defer conn.Close() + + logInfo("watcher: connected, monitoring for changes ...") + + scanner := bufio.NewScanner(conn) + for scanner.Scan() { + var evt FileEvent + if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil { + logDebug("bad event: %v", err) + continue + } + + logDebug("event: %s %s", evt.Op, evt.Path) + + if isGitDir(evt.Path) { + continue + } + + if stack.IsIgnored(evt.Path) { + continue + } + + destPath := filepath.Join(*dataDir, evt.Path) + srcPath := filepath.Join(*hostDir, evt.Path) + + switch evt.Op { + case "create": + syncCreate(srcPath, destPath, evt.Path, manifest, stack, *dryRun, nil) + case "write": + syncWrite(srcPath, destPath, evt.Path, manifest, *dryRun) + case "remove": + syncRemove(destPath, evt.Path, manifest, *dryRun) + } + } + + if err := scanner.Err(); err != nil { + return err + } + + return nil +} -- cgit v1.2.3