104 lines
3.3 KiB
Go
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
|
|
}
|