github.com/grailbio/base@v0.0.11/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/grailbio/base/file/fsnode" 13 "github.com/grailbio/base/log" 14 "github.com/grailbio/base/sync/loadingcache" 15 "github.com/grailbio/base/sync/loadingcache/ctxloadingcache" 16 "github.com/grailbio/base/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 := ®Inode{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 = ®Inode{} 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 }