arche / internal/watcher/watcher.go

commit a22ffc45
 1package watcher
 2
 3import (
 4	"context"
 5	"fmt"
 6	"os"
 7	"path/filepath"
 8	"strings"
 9
10	"arche/internal/store"
11
12	"github.com/fsnotify/fsnotify"
13	"golang.org/x/sys/unix"
14)
15
16func IsActive(archeDir string, st store.Store) bool {
17	pid, err := st.GetWatcherPID()
18	if err == nil && pid > 0 {
19		return unix.Kill(pid, 0) == nil
20	}
21	data, ferr := os.ReadFile(filepath.Join(archeDir, "watch.pid"))
22	if ferr != nil {
23		return false
24	}
25	var legacyPID int
26	fmt.Sscanf(strings.TrimSpace(string(data)), "%d", &legacyPID) //nolint:errcheck
27	return legacyPID > 0 && unix.Kill(legacyPID, 0) == nil
28}
29
30func Run(ctx context.Context, workRoot, archeDir string, st store.Store) error {
31	pid := os.Getpid()
32	if err := st.SetWatcherPID(pid); err != nil {
33		return fmt.Errorf("store watcher pid: %w", err)
34	}
35	defer st.ClearWatcherPID() //nolint:errcheck
36
37	w, err := fsnotify.NewWatcher()
38	if err != nil {
39		return fmt.Errorf("fsnotify: %w", err)
40	}
41	defer w.Close()
42
43	if err := filepath.Walk(workRoot, func(p string, info os.FileInfo, walkErr error) error {
44		if walkErr != nil || !info.IsDir() {
45			return nil
46		}
47		if filepath.Base(p) == ".arche" {
48			return filepath.SkipDir
49		}
50		return w.Add(p)
51	}); err != nil {
52		return fmt.Errorf("watch setup: %w", err)
53	}
54
55	for {
56		select {
57		case <-ctx.Done():
58			return nil
59
60		case event, ok := <-w.Events:
61			if !ok {
62				return nil
63			}
64			const interesting = fsnotify.Write | fsnotify.Create | fsnotify.Rename | fsnotify.Remove | fsnotify.Chmod
65			if event.Op&interesting == 0 {
66				continue
67			}
68			rel, err := filepath.Rel(workRoot, event.Name)
69			if err != nil || strings.HasPrefix(rel, "..") || strings.HasPrefix(rel, ".arche") {
70				continue
71			}
72			rel = filepath.ToSlash(rel)
73			if markErr := st.MarkWCacheDirty(rel); markErr != nil {
74				fmt.Fprintf(os.Stderr, "arche watch: mark dirty %q: %v\n", rel, markErr)
75			}
76
77			if event.Op&fsnotify.Create != 0 {
78				if info, statErr := os.Lstat(event.Name); statErr == nil && info.IsDir() {
79					_ = w.Add(event.Name)
80				}
81			}
82
83		case _, ok := <-w.Errors:
84			if !ok {
85				return nil
86			}
87		}
88	}
89}