github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/file/fsnodefuse/dir.go (about)

     1  package fsnodefuse
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha512"
     6  	"encoding/binary"
     7  	"os"
     8  	"sync"
     9  	"syscall"
    10  	"time"
    11  
    12  	"github.com/Schaudge/grailbase/file/fsnode"
    13  	"github.com/Schaudge/grailbase/log"
    14  	"github.com/Schaudge/grailbase/sync/loadingcache"
    15  	"github.com/Schaudge/grailbase/sync/loadingcache/ctxloadingcache"
    16  	"github.com/Schaudge/grailbase/writehash"
    17  	"github.com/hanwen/go-fuse/v2/fs"
    18  	"github.com/hanwen/go-fuse/v2/fuse"
    19  )
    20  
    21  type dirInode struct {
    22  	fs.Inode
    23  	cache            loadingcache.Map
    24  	readdirplusCache readdirplusCache
    25  
    26  	mu sync.Mutex
    27  	n  fsnode.Parent
    28  }
    29  
    30  var (
    31  	_ fs.InodeEmbedder = (*dirInode)(nil)
    32  
    33  	_ fs.NodeCreater   = (*dirInode)(nil)
    34  	_ fs.NodeGetattrer = (*dirInode)(nil)
    35  	_ fs.NodeLookuper  = (*dirInode)(nil)
    36  	_ fs.NodeReaddirer = (*dirInode)(nil)
    37  	_ fs.NodeSetattrer = (*dirInode)(nil)
    38  	_ fs.NodeUnlinker  = (*dirInode)(nil)
    39  )
    40  
    41  func (n *dirInode) Readdir(ctx context.Context) (_ fs.DirStream, errno syscall.Errno) {
    42  	defer handlePanicErrno(&errno)
    43  	ctx = ctxloadingcache.With(ctx, &n.cache)
    44  	s, err := newDirStream(ctx, n)
    45  	if err != nil {
    46  		return nil, errToErrno(err)
    47  	}
    48  	return s, fs.OK
    49  }
    50  
    51  func (n *dirInode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (_ *fs.Inode, errno syscall.Errno) {
    52  	defer handlePanicErrno(&errno)
    53  	ctx = ctxloadingcache.With(ctx, &n.cache)
    54  	childFSNode := n.readdirplusCache.Get(name)
    55  	if childFSNode == nil {
    56  		var err error
    57  		childFSNode, err = n.n.Child(ctx, name)
    58  		if err != nil {
    59  			return nil, errToErrno(err)
    60  		}
    61  	}
    62  	childInode := n.GetChild(name)
    63  	if childInode == nil || stableAttr(n, childFSNode) != childInode.StableAttr() {
    64  		childInode = n.newInode(ctx, childFSNode)
    65  	}
    66  	setFSNode(childInode, childFSNode)
    67  	setEntryOut(out, childInode.StableAttr().Ino, childFSNode)
    68  	return childInode, fs.OK
    69  }
    70  
    71  func (n *dirInode) Getattr(ctx context.Context, _ fs.FileHandle, a *fuse.AttrOut) (errno syscall.Errno) {
    72  	defer handlePanicErrno(&errno)
    73  	setAttrFromFileInfo(&a.Attr, n.n.Info())
    74  	a.SetTimeout(getCacheTimeout(n.n))
    75  	return fs.OK
    76  }
    77  
    78  func (n *dirInode) Setattr(ctx context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, a *fuse.AttrOut) (errno syscall.Errno) {
    79  	defer handlePanicErrno(&errno)
    80  	n.cache.DeleteAll()
    81  
    82  	// To avoid deadlock we must notify invalidations while not holding certain inode locks.
    83  	// See: https://github.com/libfuse/libfuse/blob/d709c24cbd9e1041264c551c2a4445e654eaf429/include/fuse_lowlevel.h#L1654-L1661
    84  	// We're ok with best-effort execution of the invalidation so a goroutine conveniently avoids locks.
    85  	children := n.Children()
    86  	go func() {
    87  		for childName, child := range children {
    88  			// TODO: Consider merely NotifyEntry instead of NotifyDelete.
    89  			// Both force a Lookup on the next access, as desired. However, NotifyDelete also
    90  			// deletes the child inode immediately which has UX consequences. For example, if a
    91  			// user's shell is currently working in that directory, after NotifyDelete they may
    92  			// see shell operations fail (similar to what they might see if they `git checkout` a
    93  			// branch that doesn't include the current working directory). NotifyEntry avoids those
    94  			// errors but may introduce inconsistency (that shell will remain using the old inode
    95  			// and its stale contents), which may be confusing.
    96  			// TODO: josh@ is not sure about this inconsistency thing.
    97  			if errno := n.NotifyDelete(childName, child); errno != fs.OK {
    98  				log.Error.Printf("dirInode.Setattr %s: error from NotifyDelete %s: %v", n.Path(nil), childName, errno)
    99  			}
   100  		}
   101  	}()
   102  
   103  	setAttrFromFileInfo(&a.Attr, n.n.Info())
   104  	a.SetTimeout(getCacheTimeout(n.n))
   105  	return fs.OK
   106  }
   107  
   108  func (n *dirInode) Create(
   109  	ctx context.Context,
   110  	name string,
   111  	flags uint32,
   112  	mode uint32,
   113  	out *fuse.EntryOut,
   114  ) (_ *fs.Inode, _ fs.FileHandle, _ uint32, errno syscall.Errno) {
   115  	defer handlePanicErrno(&errno)
   116  	if (mode & syscall.S_IFREG) == 0 {
   117  		return nil, nil, 0, syscall.EINVAL
   118  	}
   119  	leaf, f, err := n.n.AddChildLeaf(ctx, name, flags)
   120  	if err != nil {
   121  		return nil, nil, 0, errToErrno(err)
   122  	}
   123  	ino := hashIno(n, leaf.Info().Name())
   124  	embed := &regInode{n: leaf}
   125  	inode := n.NewInode(ctx, embed, fs.StableAttr{Mode: mode, Ino: ino})
   126  	h, err := makeHandle(embed, flags, f)
   127  	return inode, h, 0, errToErrno(err)
   128  }
   129  
   130  func (n *dirInode) Unlink(ctx context.Context, name string) syscall.Errno {
   131  	return errToErrno(n.n.RemoveChild(ctx, name))
   132  }
   133  
   134  func (n *dirInode) Mkdir(
   135  	ctx context.Context,
   136  	name string,
   137  	mode uint32,
   138  	out *fuse.EntryOut,
   139  ) (_ *fs.Inode, errno syscall.Errno) {
   140  	defer handlePanicErrno(&errno)
   141  	p, err := n.n.AddChildParent(ctx, name)
   142  	if err != nil {
   143  		return nil, errToErrno(err)
   144  	}
   145  	embed := &dirInode{n: p}
   146  	mode |= syscall.S_IFDIR
   147  	ino := hashIno(n, name)
   148  	inode := n.NewInode(ctx, embed, fs.StableAttr{Mode: mode, Ino: ino})
   149  	setEntryOut(out, ino, p)
   150  	return inode, fs.OK
   151  }
   152  
   153  // newInode returns an inode that wraps fsNode.  The type of inode (embedder)
   154  // to create is inferred from the type of fsNode.
   155  func (n *dirInode) newInode(ctx context.Context, fsNode fsnode.T) *fs.Inode {
   156  	var embed fs.InodeEmbedder
   157  	// TODO: Set owner/UID?
   158  	switch fsNode.(type) {
   159  	case fsnode.Parent:
   160  		embed = &dirInode{}
   161  	case fsnode.Leaf:
   162  		embed = &regInode{}
   163  	default:
   164  		log.Panicf("invalid node type: %T", fsNode)
   165  	}
   166  	inode := n.NewInode(ctx, embed, stableAttr(n, fsNode))
   167  	// inode may be an existing inode with an existing embedder.  Regardless,
   168  	// update the underlying fsnode.T.
   169  	setFSNode(inode, fsNode)
   170  	return inode
   171  }
   172  
   173  func setEntryOut(out *fuse.EntryOut, ino uint64, n fsnode.T) {
   174  	out.Ino = ino
   175  	setAttrFromFileInfo(&out.Attr, n.Info())
   176  	cacheTimeout := getCacheTimeout(n)
   177  	out.SetEntryTimeout(cacheTimeout)
   178  	out.SetAttrTimeout(cacheTimeout)
   179  }
   180  
   181  func setAttrFromFileInfo(a *fuse.Attr, info os.FileInfo) {
   182  	if info.IsDir() {
   183  		a.Mode |= syscall.S_IFDIR
   184  	} else {
   185  		a.Mode |= syscall.S_IFREG
   186  	}
   187  	a.Mode |= uint32(info.Mode() & os.ModePerm)
   188  	a.Size = uint64(info.Size())
   189  	a.Blocks = a.Size / blockSize
   190  	// We want to encourage large reads to reduce syscall overhead. FUSE has a 128 KiB read
   191  	// size limit anyway.
   192  	// TODO: Is there a better way to set this, in case size limits ever change?
   193  	setBlockSize(a, 128*1024)
   194  	if mod := info.ModTime(); !mod.IsZero() {
   195  		a.SetTimes(nil, &mod, nil)
   196  	}
   197  }
   198  
   199  func getCacheTimeout(any interface{}) time.Duration {
   200  	cacheableFor := fsnode.CacheableFor(any)
   201  	if cacheableFor < 0 {
   202  		return 365 * 24 * time.Hour
   203  	}
   204  	return cacheableFor
   205  }
   206  
   207  func mode(n fsnode.T) uint32 {
   208  	switch n.(type) {
   209  	case fsnode.Parent:
   210  		return syscall.S_IFDIR
   211  	case fsnode.Leaf:
   212  		return syscall.S_IFREG
   213  	default:
   214  		log.Panicf("invalid node type: %T", n)
   215  		panic("unreachable")
   216  	}
   217  }
   218  
   219  // readdirplusCache caches nodes for calls to Lookup that go-fuse issues when
   220  // servicing READDIRPLUS.  To handle READDIRPLUS, go-fuse interleaves LOOKUP
   221  // calls for each directory entry.  dirStream populates this cache with the
   222  // last returned entry so that it can be used in Lookup, saving a possibly
   223  // costly (fsnode.Parent).Child call.
   224  type readdirplusCache struct {
   225  	// mu is used to provide exclusive access to the fields below.
   226  	mu sync.Mutex
   227  	// m maps child node names to the set of cached nodes for each name.  The
   228  	// calls to Lookup do not indicate whether they are for a READDIRPLUS, so
   229  	// if there are two dirStream instances which each cached a node for a
   230  	// given name, Lookup will use an arbitrary node in the cache, as we don't
   231  	// know which Lookup is associated with which dirStream.  This might cause
   232  	// transiently stale information but keeps the implementation simple.
   233  	m map[string][]fsnode.T
   234  }
   235  
   236  // Put puts a node n in the cache.
   237  func (c *readdirplusCache) Put(n fsnode.T) {
   238  	c.mu.Lock()
   239  	defer c.mu.Unlock()
   240  	if c.m == nil {
   241  		c.m = make(map[string][]fsnode.T)
   242  	}
   243  	name := n.Info().Name()
   244  	c.m[name] = append(c.m[name], n)
   245  }
   246  
   247  // Get gets a node in the cache for the given name.  If no node is cached,
   248  // returns nil.
   249  func (c *readdirplusCache) Get(name string) fsnode.T {
   250  	c.mu.Lock()
   251  	defer c.mu.Unlock()
   252  	if c.m == nil {
   253  		return nil
   254  	}
   255  	ns, ok := c.m[name]
   256  	if !ok {
   257  		return nil
   258  	}
   259  	return ns[0]
   260  }
   261  
   262  // Drop drops the node n from the cache.  n must have been previously added as
   263  // an entry for name using Put.
   264  func (c *readdirplusCache) Drop(n fsnode.T) {
   265  	c.mu.Lock()
   266  	defer c.mu.Unlock()
   267  	name := n.Info().Name()
   268  	ns, _ := c.m[name]
   269  	if len(ns) == 1 {
   270  		delete(c.m, name)
   271  		return
   272  	}
   273  	var dropIndex int
   274  	for i := range ns {
   275  		if n == ns[i] {
   276  			dropIndex = i
   277  			break
   278  		}
   279  	}
   280  	last := len(ns) - 1
   281  	ns[dropIndex] = ns[last]
   282  	ns[last] = nil
   283  	ns = ns[:last]
   284  	c.m[name] = ns
   285  }
   286  
   287  func stableAttr(parent fs.InodeEmbedder, n fsnode.T) fs.StableAttr {
   288  	var mode uint32
   289  	switch modeType := n.Info().Mode().Type(); modeType {
   290  	case 0:
   291  		mode |= syscall.S_IFREG
   292  	case os.ModeDir:
   293  		mode |= syscall.S_IFDIR
   294  	case os.ModeSymlink:
   295  		mode |= syscall.S_IFLNK
   296  	default:
   297  		log.Panicf("invalid node mode type: %v", modeType)
   298  	}
   299  	return fs.StableAttr{
   300  		Mode: mode,
   301  		Ino:  hashIno(parent, n.Info().Name()),
   302  	}
   303  }
   304  
   305  func hashParentInoAndName(parentIno uint64, name string) uint64 {
   306  	h := sha512.New()
   307  	writehash.Uint64(h, parentIno)
   308  	writehash.String(h, name)
   309  	return binary.LittleEndian.Uint64(h.Sum(nil)[:8])
   310  }
   311  
   312  func hashIno(parent fs.InodeEmbedder, name string) uint64 {
   313  	return hashParentInoAndName(parent.EmbeddedInode().StableAttr().Ino, name)
   314  }