//go:build windows package main import ( "context" "encoding/json" "flag" "fmt" "net" "os" "os/signal" "path/filepath" "sync" "syscall" "time" "github.com/fsnotify/fsnotify" ) var ( socketPath = flag.String("socket", "", "Unix socket path to serve events on (required)") sourceDir = flag.String("dir", "", "Source directory to watch (required)") ) func run() { flag.Parse() if *socketPath == "" || *sourceDir == "" { fmt.Fprintln(os.Stderr, "Usage: sourcewatch.exe -dir -socket ") os.Exit(1) } level, err := ParseLogLevel(*logLevel) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } SetLogLevel(level) logInfo("watcher: starting on %s", *sourceDir) logInfo("watcher: serving on %s", *socketPath) stack := NewGitignoreStack(*sourceDir) watcher, err := fsnotify.NewWatcher() if err != nil { logErrorf("watcher: %v", err) os.Exit(1) } defer watcher.Close() if err := addWatchersRecursively(watcher, *sourceDir, stack); err != nil { logErrorf("watcher: %v", err) os.Exit(1) } os.Remove(*socketPath) ln, err := net.Listen("unix", *socketPath) if err != nil { logErrorf("socket: %v", err) os.Exit(1) } defer func() { ln.Close() os.Remove(*socketPath) }() var mu sync.Mutex debounceTimers := make(map[string]*time.Timer) debounceOps := make(map[string]fsnotify.Op) clients := make(map[net.Conn]bool) var clientsMu sync.Mutex go func() { for { conn, err := ln.Accept() if err != nil { return } clientsMu.Lock() clients[conn] = true clientsMu.Unlock() logDebug("client connected: %s", conn.RemoteAddr()) go func(c net.Conn) { defer func() { clientsMu.Lock() delete(clients, c) clientsMu.Unlock() c.Close() }() buf := make([]byte, 1) for { if _, err := c.Read(buf); err != nil { return } } }(conn) } }() broadcast := func(evt FileEvent) { data, err := json.Marshal(evt) if err != nil { return } data = append(data, '\n') clientsMu.Lock() defer clientsMu.Unlock() for c := range clients { if _, err := c.Write(data); err != nil { go func() { clientsMu.Lock() delete(clients, c) clientsMu.Unlock() c.Close() }() } } } ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() go func() { <-ctx.Done() ln.Close() }() for { select { case <-ctx.Done(): return case event, ok := <-watcher.Events: if !ok { return } relPath, err := filepath.Rel(*sourceDir, event.Name) if err != nil { continue } if isGitDir(relPath) { continue } if stack.IsIgnored(relPath) { continue } logDebug("event: %s %s", event.Op, relPath) if event.Op&fsnotify.Create != 0 { srcPath := filepath.Join(*sourceDir, relPath) info, err := os.Lstat(srcPath) if err == nil && info.IsDir() { if hasGitignore(srcPath) { stack.Push(srcPath) } if err := watcher.Add(srcPath); err != nil { logErrorf("watch %s: %v", relPath, err) } 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() evt := FileEvent{Path: path} switch { case op&fsnotify.Create != 0: evt.Op = "create" case op&fsnotify.Write != 0: evt.Op = "write" case op&fsnotify.Remove != 0: evt.Op = "remove" case op&fsnotify.Rename != 0: evt.Op = "remove" default: return } broadcast(evt) }) mu.Unlock() case err, ok := <-watcher.Errors: if !ok { return } logErrorf("watcher: %v", err) } } } func addWatchersRecursively(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 }) }