From cfbfa9920f08af0041366b59e9f6b719e8716327 Mon Sep 17 00:00:00 2001 From: Ben Dischinger Date: Fri, 24 Apr 2026 16:40:06 -0500 Subject: [PATCH] Support for bulk change sequence interface --- Dockerfile.godefs | 20 +++ c_defs_linux.go | 24 ++++ examples/rawinodeinfo/main.go | 181 ++++++++++++++++++++++++ generate-defs.sh | 44 ++++++ scoutfs.go | 257 ++++++++++++++++++++++++++++++++++ scoutfsdefs.go | 58 ++++++++ 6 files changed, 584 insertions(+) create mode 100644 Dockerfile.godefs create mode 100644 examples/rawinodeinfo/main.go create mode 100755 generate-defs.sh diff --git a/Dockerfile.godefs b/Dockerfile.godefs new file mode 100644 index 0000000..5619c5c --- /dev/null +++ b/Dockerfile.godefs @@ -0,0 +1,20 @@ +FROM golang:1.24-bookworm + +ARG SCOUTFS_BRANCH=main + +# Clone scoutfs and checkout the specified branch +RUN git clone https://github.com/versity/scoutfs.git /tmp/scoutfs && \ + cd /tmp/scoutfs && \ + git checkout ${SCOUTFS_BRANCH} + +# Install the scoutfs headers where c_defs_linux.go expects them +RUN mkdir -p /usr/include/scoutfs && \ + cp /tmp/scoutfs/kmod/src/ioctl.h /usr/include/scoutfs/ioctl.h && \ + cp /tmp/scoutfs/kmod/src/format.h /usr/include/scoutfs/format.h + +WORKDIR /work +COPY go.mod ./ +COPY c_defs_linux.go ./ + +# Generate scoutfsdefs.go +CMD ["sh", "-c", "go tool cgo -godefs c_defs_linux.go > /output/scoutfsdefs.go"] diff --git a/c_defs_linux.go b/c_defs_linux.go index 561616c..5028383 100644 --- a/c_defs_linux.go +++ b/c_defs_linux.go @@ -34,7 +34,14 @@ package scoutfs // typedef int32_t __s32; // typedef int64_t __s64; // #define __packed +// #ifndef DIV_ROUND_UP +// #define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) +// #endif +// #ifndef BITS_PER_LONG +// #define BITS_PER_LONG (sizeof(long) * 8) +// #endif // #include "/usr/include/scoutfs/ioctl.h" +// #include "/usr/include/scoutfs/format.h" // typedef struct scoutfs_ioctl_walk_inodes_entry scoutfs_ioctl_walk_inodes_entry_t; // typedef struct scoutfs_ioctl_walk_inodes scoutfs_ioctl_walk_inodes_t; // typedef struct scoutfs_ioctl_ino_path scoutfs_ioctl_ino_path_t; @@ -73,6 +80,12 @@ package scoutfs // typedef struct scoutfs_ioctl_read_xattr_index scoutfs_ioctl_read_xattr_index_t; // typedef struct scoutfs_ioctl_inode_attr_x scoutfs_ioctl_inode_attr_x_t; // typedef struct scoutfs_ioctl_punch_offline scoutfs_ioctl_punch_offline_t; +// typedef struct scoutfs_ioctl_meta_seq scoutfs_ioctl_meta_seq_t; +// typedef struct scoutfs_ioctl_raw_read_meta_seq scoutfs_ioctl_raw_read_meta_seq_t; +// typedef struct scoutfs_ioctl_raw_read_inode_info scoutfs_ioctl_raw_read_inode_info_t; +// typedef struct scoutfs_ioctl_raw_read_result scoutfs_ioctl_raw_read_result_t; +// typedef struct scoutfs_inode scoutfs_inode_t; +// typedef struct scoutfs_timespec scoutfs_timespec_t; import "C" const IOCQUERYINODES = C.SCOUTFS_IOC_WALK_INODES @@ -124,6 +137,11 @@ const IOCIAXBITS = C.SCOUTFS_IOC_IAX__BITS const IOCGETATTRX = C.SCOUTFS_IOC_GET_ATTR_X const IOCSETATTRX = C.SCOUTFS_IOC_SET_ATTR_X const IOCPUNCHOFFLINE = C.SCOUTFS_IOC_PUNCH_OFFLINE +const IOCRAWREADMETASEQ = C.SCOUTFS_IOC_RAW_READ_META_SEQ +const IOCRAWREADINODEINFO = C.SCOUTFS_IOC_RAW_READ_INODE_INFO + +const RAWREADRESULTINODE = C.SCOUTFS_IOC_RAW_READ_RESULT_INODE +const RAWREADRESULTXATTR = C.SCOUTFS_IOC_RAW_READ_RESULT_XATTR type InodesEntry C.scoutfs_ioctl_walk_inodes_entry_t type queryInodes C.scoutfs_ioctl_walk_inodes_t @@ -151,6 +169,12 @@ type indexEntry C.scoutfs_ioctl_xattr_index_entry_t type readXattrIndex C.scoutfs_ioctl_read_xattr_index_t type inodeAttrX C.scoutfs_ioctl_inode_attr_x_t type punchOffline C.scoutfs_ioctl_punch_offline_t +type MetaSeqEntry C.scoutfs_ioctl_meta_seq_t +type rawReadMetaSeq C.scoutfs_ioctl_raw_read_meta_seq_t +type rawReadInodeInfo C.scoutfs_ioctl_raw_read_inode_info_t +type RawReadResult C.scoutfs_ioctl_raw_read_result_t +type ScoutfsInode C.scoutfs_inode_t +type ScoutfsTimespec C.scoutfs_timespec_t const sizeofstatfsMore = C.sizeof_scoutfs_ioctl_statfs_more_t const sizeofxattrTotal = C.sizeof_scoutfs_ioctl_xattr_total_t diff --git a/examples/rawinodeinfo/main.go b/examples/rawinodeinfo/main.go new file mode 100644 index 0000000..cae9648 --- /dev/null +++ b/examples/rawinodeinfo/main.go @@ -0,0 +1,181 @@ +package main + +import ( + "flag" + "fmt" + "os" + "sort" + "time" + "unsafe" + + scoutfs "github.com/versity/scoutfs-go" +) + +type inoState struct { + metaSeq uint64 +} + +func main() { + path := flag.String("path", "", "path to scoutfs mount point") + debug := flag.Bool("debug", false, "enable verbose debugging output") + + var xattrNames stringSlice + flag.Var(&xattrNames, "xattr", "xattr name to read with inodes (can be repeated)") + + flag.Parse() + + if *path == "" { + fmt.Fprintf(os.Stderr, "error: -path is required\n") + flag.Usage() + os.Exit(1) + } + + f, err := os.Open(*path) + if err != nil { + fmt.Fprintf(os.Stderr, "error opening %s: %v\n", *path, err) + os.Exit(1) + } + defer f.Close() + + // Track known inodes and their meta_seq + known := make(map[uint64]*inoState) + lines := 0 + + // Allocate a reusable results buffer for RawReadInodeInfo + resultsBuf := make([]byte, 1024*1024) + + for { + reader := scoutfs.NewRawMetaSeqReader(f) + + var allMS []scoutfs.MetaSeqEntry + for { + items, err := reader.Next() + if err != nil { + fmt.Fprintf(os.Stderr, "error reading meta_seq: %v\n", err) + os.Exit(1) + } + if items == nil { + break + } + allMS = append(allMS, items...) + } + + // Find inodes that differ from our known state + var diffInos []uint64 + seen := make(map[uint64]uint64) // ino -> meta_seq from current scan + + for _, ms := range allMS { + seen[ms.Ino] = ms.Seq + } + + // Check for new or changed inodes + for ino, metaSeq := range seen { + st, ok := known[ino] + if !ok || st.metaSeq != metaSeq { + diffInos = append(diffInos, ino) + } + } + + // Check for removed inodes + for ino := range known { + if _, ok := seen[ino]; !ok { + diffInos = append(diffInos, ino) + } + } + + if len(diffInos) > 0 { + // Sort and deduplicate (inos must be sorted for the ioctl) + sort.Slice(diffInos, func(i, j int) bool { + return diffInos[i] < diffInos[j] + }) + + results, err := scoutfs.RawReadInodeInfo(f, diffInos, xattrNames, resultsBuf) + if err != nil { + fmt.Fprintf(os.Stderr, "error reading inode info: %v\n", err) + os.Exit(1) + } + + // Build set of returned inos + returnedInos := make(map[uint64]bool) + for _, r := range results { + returnedInos[r.Ino] = true + } + + // Remove inodes that weren't returned (nlink==0 or deleted) + for _, ino := range diffInos { + if !returnedInos[ino] { + if *debug { + fmt.Printf(" remove ino %d\n", ino) + } + delete(known, ino) + } + } + + // Print header periodically + if lines%16 == 0 { + fmt.Printf("%8s %12s %12s %12s %6s %6s %6s %6s", + "ino", "meta_seq", "data_seq", "data_ver", + "nlink", "uid", "gid", "mode") + if len(xattrNames) > 0 { + fmt.Printf(" xattrs") + } + fmt.Println() + } + + for _, r := range results { + action := "update" + if _, ok := known[r.Ino]; !ok { + action = "add" + } + + known[r.Ino] = &inoState{metaSeq: r.Inode.Meta_seq} + + fmt.Printf("%8d %12d %12d %12d %6d %6d %6d %6o", + r.Ino, + r.Inode.Meta_seq, + r.Inode.Data_seq, + r.Inode.Data_version, + r.Inode.Nlink, + r.Inode.Uid, + r.Inode.Gid, + r.Inode.Mode) + + if len(r.Xattrs) > 0 { + for _, x := range r.Xattrs { + fmt.Printf(" %s=%q", x.Name, x.Value) + } + } + + if *debug { + fmt.Printf(" [%s]", action) + } + + fmt.Println() + lines++ + } + } + + // Update known state from current scan + for ino, metaSeq := range seen { + if st, ok := known[ino]; ok { + st.metaSeq = metaSeq + } + } + + if *debug { + fmt.Printf("tracking %d inodes, inode result size %d\n", + len(known), unsafe.Sizeof(scoutfs.ScoutfsInode{})) + } + + time.Sleep(1 * time.Second) + } +} + +// stringSlice implements flag.Value for repeated string flags +type stringSlice []string + +func (s *stringSlice) String() string { return fmt.Sprintf("%v", *s) } +func (s *stringSlice) Set(val string) error { + *s = append(*s, val) + return nil +} diff --git a/generate-defs.sh b/generate-defs.sh new file mode 100755 index 0000000..e701293 --- /dev/null +++ b/generate-defs.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# +# generate-defs.sh - Regenerate scoutfsdefs.go from scoutfs kernel headers +# +# This script uses Docker to build a Linux container with the scoutfs +# kernel headers installed, then runs "go tool cgo -godefs" to generate +# the Go type definitions from c_defs_linux.go. +# +# Usage: +# ./generate-defs.sh [BRANCH] +# +# Arguments: +# BRANCH The scoutfs git branch to use for headers (default: main) +# +# Examples: +# ./generate-defs.sh # use main branch +# ./generate-defs.sh zab/get_changed_inos # use a feature branch +# + +set -euo pipefail + +BRANCH="${1:-main}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +IMAGE_NAME="scoutfs-godefs" + +echo "==> Building Docker image with scoutfs branch: ${BRANCH}" +docker build \ + --build-arg "SCOUTFS_BRANCH=${BRANCH}" \ + --no-cache \ + -f "${SCRIPT_DIR}/Dockerfile.godefs" \ + -t "${IMAGE_NAME}" \ + "${SCRIPT_DIR}" + +echo "==> Generating scoutfsdefs.go" +docker run --rm \ + -v "${SCRIPT_DIR}:/output" \ + "${IMAGE_NAME}" + +echo "==> Verifying build" +cd "${SCRIPT_DIR}" +GOOS=linux go vet ./... +GOOS=linux go build ./... + +echo "==> Done. scoutfsdefs.go has been regenerated from branch: ${BRANCH}" diff --git a/scoutfs.go b/scoutfs.go index 74ba189..e824e80 100644 --- a/scoutfs.go +++ b/scoutfs.go @@ -1830,3 +1830,260 @@ func PunchHole(f *os.File, length, offset, version uint64) error { _, err := scoutfsctl(f, IOCPUNCHOFFLINE, unsafe.Pointer(&po)) return err } + +// RawMetaSeqReader iterates over meta_seq items without cluster locking. +// Use NewRawMetaSeqReader to create a new reader. +type RawMetaSeqReader struct { + fsfd *os.File + start MetaSeqEntry + end MetaSeqEntry + last MetaSeqEntry + batch uint32 + buf []byte + done bool +} + +// RMSOption sets various options for NewRawMetaSeqReader +type RMSOption func(*RawMetaSeqReader) + +// WithRMSBatchSize sets the max number of meta_seq items to be returned at a time +func WithRMSBatchSize(size uint32) RMSOption { + return func(r *RawMetaSeqReader) { + r.batch = size + } +} + +// WithRMSRange sets the start and end range for the meta_seq reader +func WithRMSRange(start, end MetaSeqEntry) RMSOption { + return func(r *RawMetaSeqReader) { + r.start = start + r.end = end + } +} + +// NewRawMetaSeqReader creates a new reader for raw meta_seq items. +// An open file within scoutfs is supplied for ioctls +// (usually just the base mount point directory). +func NewRawMetaSeqReader(f *os.File, opts ...RMSOption) *RawMetaSeqReader { + r := &RawMetaSeqReader{ + fsfd: f, + batch: 1024, + end: MetaSeqEntry{Seq: max64, Ino: max64}, + } + + for _, opt := range opts { + opt(r) + } + + r.buf = make([]byte, int(unsafe.Sizeof(MetaSeqEntry{}))*int(r.batch)) + + return r +} + +// Next gets the next batch of meta_seq items. +// Returns nil, nil when iteration is complete. +func (r *RawMetaSeqReader) Next() ([]MetaSeqEntry, error) { + if r.done { + return nil, nil + } + + rms := rawReadMetaSeq{ + Start: r.start, + End: r.end, + Ptr: uint64(uintptr(unsafe.Pointer(&r.buf[0]))), + Size: r.batch, + } + + n, err := scoutfsctl(r.fsfd, IOCRAWREADMETASEQ, unsafe.Pointer(&rms)) + if err != nil { + return nil, err + } + + r.last = rms.Last + + if n == 0 { + r.done = true + return nil, nil + } + + rbuf := bytes.NewReader(r.buf) + items := make([]MetaSeqEntry, n) + + var e MetaSeqEntry + for i := 0; i < n; i++ { + err := binary.Read(rbuf, binary.LittleEndian, &e) + if err != nil { + return nil, err + } + items[i] = e + } + + // Advance start past last for the next call + r.start = r.last + r.start.Ino++ + if r.start.Ino == 0 { + r.start.Seq++ + } + + // If last reached the end of the range, we're done + if r.last.Seq == r.end.Seq && r.last.Ino == r.end.Ino { + r.done = true + } + + return items, nil +} + +// Last returns the last meta_seq position from the most recent Next() call. +// This is the last item that could have been returned by the kernel. +func (r *RawMetaSeqReader) Last() MetaSeqEntry { + return r.last +} + +// Reset resets the reader to start from the beginning of the range +func (r *RawMetaSeqReader) Reset() { + r.start = MetaSeqEntry{} + r.last = MetaSeqEntry{} + r.done = false +} + +// RawInodeResult contains the inode metadata and optional xattr values +// returned by RawReadInodeInfo for a single inode +type RawInodeResult struct { + Ino uint64 + Inode ScoutfsInode + Xattrs []RawXattrResult +} + +// RawXattrResult contains a single xattr name and value +type RawXattrResult struct { + Name string + Value []byte +} + +// RawReadInodeInfo reads inode metadata (and optionally xattr values) for +// the given inode numbers without cluster locking. +// +// inos must be a sorted slice of unique non-zero inode numbers. +// xattrNames optionally specifies xattr names to return with each inode. +// resultsBuf is a caller-provided buffer for the ioctl results, allowing +// reuse across calls to avoid repeated allocations. +// +// An open file within scoutfs is supplied for ioctls +// (usually just the base mount point directory). +func RawReadInodeInfo(f *os.File, inos []uint64, xattrNames []string, resultsBuf []byte) ([]RawInodeResult, error) { + if len(inos) == 0 { + return nil, nil + } + + // Build the null-terminated xattr names buffer + var namesBuf []byte + for _, name := range xattrNames { + namesBuf = append(namesBuf, []byte(name)...) + namesBuf = append(namesBuf, 0) + } + + rii := rawReadInodeInfo{ + Inos_ptr: uint64(uintptr(unsafe.Pointer(&inos[0]))), + Inos_count: uint32(len(inos)), + Results_ptr: uint64(uintptr(unsafe.Pointer(&resultsBuf[0]))), + Results_size: uint32(len(resultsBuf)), + } + + if len(namesBuf) > 0 { + rii.Names_ptr = uint64(uintptr(unsafe.Pointer(&namesBuf[0]))) + rii.Names_count = uint32(len(xattrNames)) + } + + n, err := scoutfsctl(f, IOCRAWREADINODEINFO, unsafe.Pointer(&rii)) + if err != nil { + return nil, err + } + + if n == 0 { + return nil, nil + } + + return parseRawReadResults(resultsBuf[:n]) +} + +func parseRawReadResults(buf []byte) ([]RawInodeResult, error) { + var results []RawInodeResult + resultHdrSize := int(unsafe.Sizeof(RawReadResult{})) + off := 0 + + for off < len(buf) { + if off+resultHdrSize > len(buf) { + break + } + + // Read result header - copy to aligned struct + var hdr RawReadResult + copy((*[12]byte)(unsafe.Pointer(&hdr))[:], buf[off:off+resultHdrSize]) + off += resultHdrSize + + if int(hdr.Size) > len(buf)-off { + return results, fmt.Errorf("result payload size %d exceeds remaining buffer %d", hdr.Size, len(buf)-off) + } + + payload := buf[off : off+int(hdr.Size)] + off += int(hdr.Size) + + switch hdr.Type { + case RAWREADRESULTINODE: + if len(payload) < 8 { + return results, fmt.Errorf("inode result too small: %d bytes", len(payload)) + } + + ino := binary.LittleEndian.Uint64(payload[:8]) + inodeBytes := payload[8:] + + var inode ScoutfsInode + inodeSize := int(unsafe.Sizeof(inode)) + + // Handle potentially smaller inode structs from older format versions + readSize := len(inodeBytes) + if readSize > inodeSize { + readSize = inodeSize + } + err := binary.Read( + bytes.NewReader(inodeBytes[:readSize]), + binary.LittleEndian, + (*[168]byte)(unsafe.Pointer(&inode))[:readSize], + ) + if err != nil { + return results, fmt.Errorf("parse inode %d: %v", ino, err) + } + + results = append(results, RawInodeResult{ + Ino: ino, + Inode: inode, + }) + + case RAWREADRESULTXATTR: + if len(results) == 0 { + return results, fmt.Errorf("xattr result without preceding inode result") + } + + // Find null terminator separating name from value + nullIdx := bytes.IndexByte(payload, 0) + if nullIdx < 0 { + return results, fmt.Errorf("xattr result missing null terminator") + } + + name := string(payload[:nullIdx]) + value := make([]byte, len(payload)-nullIdx-1) + copy(value, payload[nullIdx+1:]) + + last := &results[len(results)-1] + last.Xattrs = append(last.Xattrs, RawXattrResult{ + Name: name, + Value: value, + }) + + default: + // Skip unknown result types for forward compatibility + } + } + + return results, nil +} diff --git a/scoutfsdefs.go b/scoutfsdefs.go index 552edb7..0360c46 100644 --- a/scoutfsdefs.go +++ b/scoutfsdefs.go @@ -52,6 +52,11 @@ const IOCIAXBITS = 0x100 const IOCGETATTRX = 0x4068e812 const IOCSETATTRX = 0x4068e813 const IOCPUNCHOFFLINE = 0x4020e818 +const IOCRAWREADMETASEQ = 0x8040e819 +const IOCRAWREADINODEINFO = 0x8028e819 + +const RAWREADRESULTINODE = 0x1 +const RAWREADRESULTXATTR = 0x2 type InodesEntry struct { Major uint64 @@ -248,6 +253,59 @@ type punchOffline struct { Version uint64 Flags uint64 } +type MetaSeqEntry struct { + Seq uint64 + Ino uint64 +} +type rawReadMetaSeq struct { + Start MetaSeqEntry + End MetaSeqEntry + Last MetaSeqEntry + Ptr uint64 + Size uint32 + X_pad uint32 +} +type rawReadInodeInfo struct { + Inos_ptr uint64 + Inos_count uint32 + Names_count uint32 + Names_ptr uint64 + Results_ptr uint64 + Results_size uint32 + X_pad [4]uint8 +} +type RawReadResult struct { + Size uint32 + X_pad [7]uint8 + Type uint8 +} +type ScoutfsInode struct { + Size uint64 + Meta_seq uint64 + Data_seq uint64 + Data_version uint64 + Online_blocks uint64 + Offline_blocks uint64 + Next_readdir_pos uint64 + Next_xattr_id uint64 + Version uint64 + Nlink uint32 + Uid uint32 + Gid uint32 + Mode uint32 + Rdev uint32 + Flags uint32 + Atime ScoutfsTimespec + Ctime ScoutfsTimespec + Mtime ScoutfsTimespec + Crtime ScoutfsTimespec + Proj uint64 +} +type ScoutfsTimespec struct { + Sec uint64 + Nsec uint32 + X__pad [4]uint8 +} const sizeofstatfsMore = 0x30 const sizeofxattrTotal = 0x28