diff options
Diffstat (limited to 'cmd_windows.go')
| -rw-r--r-- | cmd_windows.go | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/cmd_windows.go b/cmd_windows.go new file mode 100644 index 0000000..b68851a --- /dev/null +++ b/cmd_windows.go @@ -0,0 +1,250 @@ +//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 <source> -socket <socket-path>") + 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 + }) +} |
