arche / internal/watcher/watcher.go

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