diff --git a/file_system.go b/file_system.go index d8e7c54..d972428 100644 --- a/file_system.go +++ b/file_system.go @@ -4,6 +4,7 @@ package fuse import ( + "fmt" "os" "time" @@ -121,7 +122,26 @@ type InodeID uint64 // are minted by the file system, the FUSE VFS layer may send a request for // this ID without the file system ever having referenced it in a previous // response. -const RootInodeID InodeID = InodeID(bazilfuse.RootID) +const RootInodeID = 1 + +func init() { + // Make sure the constant above is correct. We do this at runtime rather than + // defining the constant in terms of bazilfuse.RootID for two reasons: + // + // 1. Users can more clearly see that the root ID is low and can therefore + // be used as e.g. an array index, with space reserved up to the root. + // + // 2. The constant can be untyped and can therefore more easily be used as + // an array index. + // + if RootInodeID != bazilfuse.RootID { + panic( + fmt.Sprintf( + "Oops, RootInodeID is wrong: %v vs. %v", + RootInodeID, + bazilfuse.RootID)) + } +} // Attributes for a file or directory inode. Corresponds to struct inode (cf. // http://goo.gl/tvYyQt). diff --git a/samples/memfs/dir.go b/samples/memfs/dir.go index 073c490..a2cc7fb 100644 --- a/samples/memfs/dir.go +++ b/samples/memfs/dir.go @@ -4,18 +4,14 @@ package memfs import ( + "fmt" + "github.com/jacobsa/fuse" "github.com/jacobsa/fuse/fuseutil" "github.com/jacobsa/gcloud/syncutil" ) type memDir struct { - ///////////////////////// - // Constant data - ///////////////////////// - - inode fuse.InodeID - ///////////////////////// // Mutable state ///////////////////////// @@ -28,5 +24,25 @@ type memDir struct { // we use its indices for Dirent.Offset, which is exposed to the user who // might be calling readdir in a loop while concurrently modifying the // directory. Unused entries can, however, be reused. + // + // TODO(jacobsa): Add good tests exercising concurrent modifications while + // doing readdir, seekdir, etc. calls. + // + // INVARIANT: For each i, entries[i].Offset == i+1 entries []fuseutil.Dirent } + +func newDir() (d *memDir) { + d = &memDir{} + d.mu = syncutil.NewInvariantMutex(d.checkInvariants) + + return +} + +func (d *memDir) checkInvariants() { + for i, e := range d.entries { + if e.Offset != fuse.DirOffset(i+1) { + panic(fmt.Sprintf("Unexpected offset in entry: %v", e)) + } + } +} diff --git a/samples/memfs/fs.go b/samples/memfs/fs.go index 0feac15..406828f 100644 --- a/samples/memfs/fs.go +++ b/samples/memfs/fs.go @@ -4,10 +4,13 @@ package memfs import ( + "fmt" + "github.com/jacobsa/fuse" "github.com/jacobsa/fuse/fuseutil" "github.com/jacobsa/gcloud/syncutil" "github.com/jacobsa/gcsfuse/timeutil" + "golang.org/x/net/context" ) type memFS struct { @@ -45,5 +48,122 @@ type memFS struct { // Create a file system that stores data and metadata in memory. func NewMemFS( clock timeutil.Clock) fuse.FileSystem { - panic("TODO(jacobsa): Implement NewMemFS.") + // Set up the basic struct. + fs := &memFS{ + clock: clock, + inodes: make([]inode, fuse.RootInodeID+1), + } + + // Set up the root inode. + fs.inodes[fuse.RootInodeID].impl = newDir() + + // Set up invariant checking. + fs.mu = syncutil.NewInvariantMutex(fs.checkInvariants) + + return fs +} + +func (fs *memFS) checkInvariants() { + // Check general inode invariants. + for i := range fs.inodes { + fs.inodes[i].checkInvariants() + } + + // Check reserved inodes. + for i := 0; i < fuse.RootInodeID; i++ { + var inode *inode = &fs.inodes[i] + if inode.impl != nil { + panic(fmt.Sprintf("Non-nil impl for ID: %v", i)) + } + } + + // Check the root inode. + _ = fs.inodes[fuse.RootInodeID].impl.(*memDir) + + // Check inodes, building our own set of free IDs. + freeIDsEncountered := make(map[fuse.InodeID]struct{}) + for i := fuse.RootInodeID + 1; i < len(fs.inodes); i++ { + var inode *inode = &fs.inodes[i] + if inode.impl == nil { + freeIDsEncountered[fuse.InodeID(i)] = struct{}{} + continue + } + } + + // Check fs.freeInodes. + if len(fs.freeInodes) != len(freeIDsEncountered) { + panic( + fmt.Sprintf( + "Length mismatch: %v vs. %v", + len(fs.freeInodes), + len(freeIDsEncountered))) + } + + for _, id := range fs.freeInodes { + if _, ok := freeIDsEncountered[id]; !ok { + panic(fmt.Sprintf("Unexected free inode ID: %v", id)) + } + } +} + +func (fs *memFS) Init( + ctx context.Context, + req *fuse.InitRequest) (resp *fuse.InitResponse, err error) { + resp = &fuse.InitResponse{} + return +} + +// Panic if not a live dir. +// +// LOCKS_EXCLUDED(fs.mu) +func (fs *memFS) getDirOrDie(inodeID fuse.InodeID) (d *memDir) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + if inodeID >= fuse.InodeID(len(fs.inodes)) { + panic(fmt.Sprintf("Inode out of range: %v vs. %v", inodeID, len(fs.inodes))) + } + + var inode *inode = &fs.inodes[inodeID] + d = inode.impl.(*memDir) + + return +} + +func (fs *memFS) OpenDir( + ctx context.Context, + req *fuse.OpenDirRequest) (resp *fuse.OpenDirResponse, err error) { + resp = &fuse.OpenDirResponse{} + + // We don't mutate spontaneosuly, so if the VFS layer has asked for an + // inode that doesn't exist, something screwed up earlier (a lookup, a + // cache invalidation, etc.). + _ = fs.getDirOrDie(req.Inode) + + return +} + +func (fs *memFS) ReadDir( + ctx context.Context, + req *fuse.ReadDirRequest) (resp *fuse.ReadDirResponse, err error) { + resp = &fuse.ReadDirResponse{} + + // Grab the directory. + d := fs.getDirOrDie(req.Inode) + + d.mu.RLock() + defer d.mu.RUnlock() + + // Return the entries requested. + for i := int(req.Offset); i < len(d.entries); i++ { + resp.Data = fuseutil.AppendDirent(resp.Data, d.entries[i]) + + // Trim and stop early if we've exceeded the requested size. + if len(resp.Data) > req.Size { + resp.Data = resp.Data[:req.Size] + break + } + } + + return } diff --git a/samples/memfs/inode.go b/samples/memfs/inode.go index 70c6f0c..d3ae224 100644 --- a/samples/memfs/inode.go +++ b/samples/memfs/inode.go @@ -3,7 +3,16 @@ package memfs +import ( + "fmt" + "reflect" +) + // Common attributes for files and directories. +// +// TODO(jacobsa): Add tests for interacting with a file/directory after it has +// been unlinked, including creating a new file. Make sure we don't screw up +// and reuse the inode while it is still in use. type inode struct { // The *memFile or *memDir for this inode, or nil if the inode is available // for reuse. @@ -11,3 +20,14 @@ type inode struct { // INVARIANT: impl is nil, or of type *memFile or *memDir impl interface{} } + +func (inode *inode) checkInvariants() { + switch inode.impl.(type) { + case nil: + case *memFile: + case *memDir: + default: + panic( + fmt.Sprintf("Unexpected inode impl type: %v", reflect.TypeOf(inode.impl))) + } +} diff --git a/samples/memfs/memfs_test.go b/samples/memfs/memfs_test.go index 97d2430..40fe749 100644 --- a/samples/memfs/memfs_test.go +++ b/samples/memfs/memfs_test.go @@ -13,6 +13,7 @@ import ( "github.com/jacobsa/fuse" "github.com/jacobsa/fuse/samples/memfs" "github.com/jacobsa/gcsfuse/timeutil" + . "github.com/jacobsa/oglematchers" . "github.com/jacobsa/ogletest" "golang.org/x/net/context" ) @@ -85,7 +86,10 @@ func (t *MemFSTest) TearDown() { //////////////////////////////////////////////////////////////////////// func (t *MemFSTest) ContentsOfEmptyFileSystem() { - AssertTrue(false, "TODO") + entries, err := ioutil.ReadDir(t.mfs.Dir()) + + AssertEq(nil, err) + ExpectThat(entries, ElementsAre()) } func (t *MemFSTest) DoesFoo() {