//go:build unix package hostexec import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "io" "os" "path/filepath" "time" "golang.org/x/sys/unix" ) type localDir struct { fd int sync func(int) error } func openDir(path string) (int, error) { fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return -1, err } var st unix.Stat_t if err = unix.Fstat(fd, &st); err != nil { unix.Close(fd) return -1, err } if st.Mode&unix.S_IFMT != unix.S_IFDIR { unix.Close(fd) return -1, ErrState } return fd, nil } func openLocal(dataDir string) (*localDir, error) { fd, err := openDir(filepath.Join(dataDir, ".hostexec")) if err != nil { return nil, err } return &localDir{fd: fd, sync: unix.Fsync}, nil } func (d *localDir) close() { _ = unix.Close(d.fd) } func (d *localDir) read(host string) (State, error) { var s State if _, err := Key(host, "check"); err != nil { return s, err } fd, err := unix.Openat(d.fd, host+".json", unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) if err != nil { return s, err } f := os.NewFile(uintptr(fd), host) defer f.Close() var st unix.Stat_t if err = unix.Fstat(fd, &st); err != nil { return s, err } if st.Mode&unix.S_IFMT != unix.S_IFREG { return s, ErrState } b, err := io.ReadAll(io.LimitReader(f, 65537)) if err != nil { return s, err } if len(b) > 65536 { return s, ErrState } if err = decode(b, &s); err != nil { return s, err } if err = s.validate(); err != nil { return s, err } if s.Host != host { return s, ErrState } return s, nil } func (d *localDir) write(s State) error { if err := s.validate(); err != nil { return err } if _, err := d.read(s.Host); err != nil && !errors.Is(err, os.ErrNotExist) { return err } var nonce [16]byte if _, err := rand.Read(nonce[:]); err != nil { return err } tmp := ".tmp-" + hex.EncodeToString(nonce[:]) fd, err := unix.Openat(d.fd, tmp, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0660) if err != nil { return err } defer unix.Unlinkat(d.fd, tmp, 0) f := os.NewFile(uintptr(fd), tmp) defer f.Close() if err = unix.Fchmod(fd, 0660); err != nil { return err } b, err := json.Marshal(s) if err != nil { return err } if _, err = f.Write(append(b, '\n')); err != nil { return err } if err = d.sync(fd); err != nil { return err } if err = f.Close(); err != nil { return err } if err = unix.Renameat(d.fd, tmp, d.fd, s.Host+".json"); err != nil { return err } return d.sync(d.fd) } func (d *localDir) remove(host string) error { if _, err := d.read(host); err != nil { return err } if err := unix.Unlinkat(d.fd, host+".json", 0); err != nil { return err } return d.sync(d.fd) } func tryAcquire(dir, host string) (func(), error) { return tryAcquireNamed(dir, host, ".lock") } func tryAcquireLive(dir, host string) (func(), error) { return tryAcquireNamed(dir, host, ".live") } func tryAcquireNamed(dir, host, suffix string) (func(), error) { if _, err := Key(host, "check"); err != nil { return nil, err } fd, err := openDir(dir) if err != nil { return nil, err } defer unix.Close(fd) flags := unix.O_RDWR | unix.O_CREAT | unix.O_NOFOLLOW | unix.O_CLOEXEC | unix.O_NONBLOCK lock, err := unix.Openat(fd, host+suffix, flags|unix.O_EXCL, 0660) created := err == nil if errors.Is(err, unix.EEXIST) { lock, err = unix.Openat(fd, host+suffix, flags, 0660) } if err != nil { return nil, err } fail := func(e error) (func(), error) { unix.Close(lock); return nil, e } var st unix.Stat_t if err = unix.Fstat(lock, &st); err != nil { return fail(err) } if st.Mode&unix.S_IFMT != unix.S_IFREG { return fail(ErrState) } if created || st.Mode&07777 != 0660 { if err = unix.Fchmod(lock, 0660); err != nil { return fail(err) } } err = unix.Flock(lock, unix.LOCK_EX|unix.LOCK_NB) if err == nil { return func() { _ = unix.Flock(lock, unix.LOCK_UN); _ = unix.Close(lock) }, nil } if errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, unix.EAGAIN) { return fail(ErrControllerBusy) } return fail(err) } func acquire(ctx context.Context, dir, host string) (func(), error) { for { if err := ctx.Err(); err != nil { return nil, err } unlock, err := tryAcquire(dir, host) if !errors.Is(err, ErrControllerBusy) { return unlock, err } select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(10 * time.Millisecond): } } }