main b4521f0

watch_linux.go RawHistoryFolder

1package kvconf
2
3import (
4	"errors"
5	"os"
6	"sync/atomic"
7	"syscall"
8	"time"
9	"unsafe"
10)
11
12// inotify watches directories with Linux inotify. Its descriptor goes
13// through the Go runtime poller, so a wait costs no thread and is woken by a
14// read deadline.
15type inotify struct {
16	f     *os.File
17	fd    int
18	dirs  map[int32]string // watch descriptor -> directory
19	wds   map[string]int32
20	buf   []byte
21	woken atomic.Bool
22	// names interns event names: the same few files change again and again.
23	names map[string]string
24}
25
26const movedMask = syscall.IN_CREATE | syscall.IN_DELETE | syscall.IN_MOVED_FROM | syscall.IN_MOVED_TO
27
28const inotifyMask = syscall.IN_CLOSE_WRITE | syscall.IN_MODIFY | syscall.IN_ATTRIB |
29	syscall.IN_CREATE | syscall.IN_DELETE | syscall.IN_MOVED_FROM | syscall.IN_MOVED_TO |
30	syscall.IN_DELETE_SELF | syscall.IN_MOVE_SELF | syscall.IN_ONLYDIR
31
32func newNative() (backend, error) {
33	fd, err := syscall.InotifyInit1(syscall.IN_CLOEXEC | syscall.IN_NONBLOCK)
34	if err != nil {
35		return nil, os.NewSyscallError("inotify_init1", err)
36	}
37	return &inotify{
38		f:     os.NewFile(uintptr(fd), "inotify"),
39		fd:    fd,
40		dirs:  map[int32]string{},
41		wds:   map[string]int32{},
42		buf:   make([]byte, 16<<10),
43		names: map[string]string{},
44	}, nil
45}
46
47func (in *inotify) set(targets []*target) (missing bool, err error) {
48	want := make(map[string]bool, len(targets))
49	for _, t := range targets {
50		want[t.dir] = true
51	}
52	// Keep unchanged watches: removing one queues an IN_IGNORED.
53	for dir, wd := range in.wds {
54		if !want[dir] {
55			_, _ = syscall.InotifyRmWatch(in.fd, uint32(wd))
56			delete(in.wds, dir)
57			delete(in.dirs, wd)
58		}
59	}
60	for dir := range want {
61		// Always add: a directory renamed away and replaced keeps its path
62		// but not its inode, and adding returns the watch of the current
63		// one (the same descriptor when nothing moved).
64		wd, err := syscall.InotifyAddWatch(in.fd, dir, inotifyMask)
65		if old, ok := in.wds[dir]; ok && (err != nil || old != int32(wd)) {
66			_, _ = syscall.InotifyRmWatch(in.fd, uint32(old))
67			delete(in.wds, dir)
68			delete(in.dirs, old)
69		}
70		if errors.Is(err, syscall.ENOENT) || errors.Is(err, syscall.ENOTDIR) {
71			missing = true
72			continue
73		}
74		if err != nil {
75			return missing, os.NewSyscallError("inotify_add_watch", err)
76		}
77		in.wds[dir] = int32(wd)
78		in.dirs[int32(wd)] = dir
79	}
80	return missing, nil
81}
82
83func (in *inotify) wait(timeout time.Duration, evs []event) ([]event, error) {
84	var deadline time.Time
85	if timeout >= 0 {
86		deadline = time.Now().Add(timeout)
87	}
88	if err := in.f.SetReadDeadline(deadline); err != nil {
89		return evs, err
90	}
91	if in.woken.Load() {
92		return evs, nil
93	}
94	n, err := in.f.Read(in.buf)
95	if errors.Is(err, os.ErrDeadlineExceeded) {
96		return evs, nil
97	}
98	if err != nil {
99		return evs, err
100	}
101	for off := 0; off+syscall.SizeofInotifyEvent <= n; {
102		ev := (*syscall.InotifyEvent)(unsafe.Pointer(&in.buf[off]))
103		size := syscall.SizeofInotifyEvent + int(ev.Len)
104		if off+size > n {
105			break
106		}
107		name := in.buf[off+syscall.SizeofInotifyEvent : off+size]
108		off += size
109		if ev.Mask&syscall.IN_Q_OVERFLOW != 0 {
110			evs = append(evs, event{}) // events were lost: check everything
111			continue
112		}
113		dir, ok := in.dirs[ev.Wd]
114		if !ok {
115			continue // a watch we removed
116		}
117		if ev.Mask&(syscall.IN_IGNORED|syscall.IN_DELETE_SELF|syscall.IN_MOVE_SELF) != 0 {
118			// The watch follows the inode, not the path: drop it.
119			if ev.Mask&syscall.IN_IGNORED == 0 {
120				_, _ = syscall.InotifyRmWatch(in.fd, uint32(ev.Wd))
121			}
122			delete(in.dirs, ev.Wd)
123			if in.wds[dir] == ev.Wd {
124				delete(in.wds, dir)
125			}
126			evs = append(evs, event{})
127			continue
128		}
129		for len(name) > 0 && name[len(name)-1] == 0 {
130			name = name[:len(name)-1]
131		}
132		if len(name) > 0 {
133			evs = append(evs, event{dir: dir, name: in.intern(name), moved: ev.Mask&movedMask != 0})
134		}
135	}
136	return evs, nil
137}
138
139// intern returns name as a string without allocating once it was seen.
140func (in *inotify) intern(name []byte) string {
141	if s, ok := in.names[string(name)]; ok { // no allocation for the lookup
142		return s
143	}
144	s := string(name)
145	if len(in.names) < 1024 { // a directory full of churn cannot grow it forever
146		in.names[s] = s
147	}
148	return s
149}
150
151func (in *inotify) wake() {
152	in.woken.Store(true)
153	_ = in.f.SetReadDeadline(time.Now())
154}
155
156func (in *inotify) close() error { return in.f.Close() }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close