Files
mesh-catalog/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socketwatch.go
T

104 lines
3.3 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
// socketAbsent says whether a write failed because nothing answers at the node-engine's write socket: its file is
// gone, or it is there with no one listening. That is the node-engine replacing itself (issue 1165); the write is
// tried again when the socket is back (watchSocket).
func socketAbsent(err error) bool {
return errors.Is(err, syscall.ENOENT) || errors.Is(err, syscall.ECONNREFUSED)
}
// watchSocket tells, on the channel it returns, each time the file at socket is made, moved in or changed in its
// attributes — a node-engine that starts makes its write socket and sets its mode — until ctx ends. It is the
// operating system's own event (inotify), not a timer: the client is on the node-engine's machine. Events that
// come while one is unread are one. When the socket's directory is gone it watches the directory above for its
// return. The channel only wakes: what is owed a write is the client's to read.
func watchSocket(ctx context.Context, socket string) (<-chan struct{}, error) {
fd, err := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if err != nil {
return nil, fmt.Errorf("watching %s: %w", socket, err)
}
f := os.NewFile(uintptr(fd), "inotify")
out := make(chan struct{}, 1)
dir := filepath.Dir(socket)
// The directory's watch and, while the directory is absent, its parent's.
if err := addWatches(fd, dir); err != nil {
_ = f.Close()
return nil, fmt.Errorf("watching %s: %w", socket, err)
}
go func() { <-ctx.Done(); _ = f.Close() }()
go func() {
buf := make([]byte, 16*(unix.SizeofInotifyEvent+unix.NAME_MAX+1))
for {
n, err := f.Read(buf)
if err != nil {
return
}
wake, rewatch := false, false
for off := 0; off+unix.SizeofInotifyEvent <= n; {
ev := (*unix.InotifyEvent)(unsafe.Pointer(&buf[off]))
name := ""
if ev.Len > 0 {
raw := buf[off+unix.SizeofInotifyEvent : off+unix.SizeofInotifyEvent+int(ev.Len)]
for i, b := range raw {
if b == 0 {
raw = raw[:i]
break
}
}
name = string(raw)
}
off += unix.SizeofInotifyEvent + int(ev.Len)
switch {
case ev.Mask&(unix.IN_DELETE_SELF|unix.IN_MOVE_SELF|unix.IN_IGNORED) != 0:
rewatch = true
case name == filepath.Base(socket):
wake = true
case name == filepath.Base(dir):
// The directory came back: watched again, and the socket may already be in it.
rewatch, wake = true, true
}
}
if rewatch {
if err := addWatches(fd, dir); err != nil {
logf("[provisioner-client] watching %s again: %v", socket, err)
}
if _, err := os.Lstat(socket); err == nil {
wake = true
}
}
if wake {
select {
case out <- struct{}{}:
default:
}
}
}
}()
return out, nil
}
// addWatches watches dir for what is made in it, and its parent for dir's own return; a watch already there is kept.
func addWatches(fd int, dir string) error {
if _, err := unix.InotifyAddWatch(fd, filepath.Dir(dir), unix.IN_CREATE|unix.IN_MOVED_TO); err != nil {
return err
}
_, err := unix.InotifyAddWatch(fd, dir, unix.IN_CREATE|unix.IN_MOVED_TO|unix.IN_ATTRIB|unix.IN_DELETE_SELF|
unix.IN_MOVE_SELF)
if errors.Is(err, syscall.ENOENT) {
return nil // absent: its parent's watch tells when it is made
}
return err
}