diff --git a/fuseops/convert.go b/fuseops/convert.go index a77a975..738321d 100644 --- a/fuseops/convert.go +++ b/fuseops/convert.go @@ -82,6 +82,14 @@ func Convert(r bazilfuse.Request, logger *log.Logger) (o Op) { o = to co = &to.commonOp + case *bazilfuse.ForgetRequest: + to := &ForgetInodeOp{ + Inode: InodeID(typed.Header.Node), + N: typed.N, + } + o = to + co = &to.commonOp + case *bazilfuse.MkdirRequest: to := &MkDirOp{ Parent: InodeID(typed.Header.Node), diff --git a/fuseops/ops.go b/fuseops/ops.go index 20664f5..af75d53 100644 --- a/fuseops/ops.go +++ b/fuseops/ops.go @@ -241,7 +241,8 @@ func (o *SetInodeAttributesOp) Respond(err error) { // revalidating. // // In contrast to all other inodes, RootInodeID begins with an implicit -// reference count of one, without a corresponding op to increase it: +// reference count of one, without a corresponding op to increase it. It also +// is never decremented to zero. Code walk: // // * (http://goo.gl/gWAheU) fuse_fill_super calls fuse_get_root_inode. // @@ -254,10 +255,10 @@ type ForgetInodeOp struct { commonOp // The inode whose reference count should be decremented. - ID InodeID + Inode InodeID // The amount to decrement the reference count. - N int + N uint64 } func (o *ForgetInodeOp) Respond(err error) { diff --git a/samples/forgetfs/forget_fs.go b/samples/forgetfs/forget_fs.go new file mode 100644 index 0000000..1a0f6de --- /dev/null +++ b/samples/forgetfs/forget_fs.go @@ -0,0 +1,310 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package forgetfs + +import ( + "fmt" + "os" + + "github.com/jacobsa/fuse" + "github.com/jacobsa/fuse/fuseops" + "github.com/jacobsa/fuse/fuseutil" + "github.com/jacobsa/gcloud/syncutil" +) + +// Create a file system whose sole contents are a file named "foo" and a +// directory named "bar". +// +// The file "foo" may be opened for reading and/or writing, but reads and +// writes aren't supported. Additionally, any non-existent file or directory +// name may be created within any directory, but the resulting inode will +// appear to have been unlinked immediately. +// +// The file system maintains reference counts for the inodes involved. It will +// panic if a reference count becomes negative or if an inode ID is re-used +// after we expect it to be dead. Its Check method may be used to check that +// there are no inodes with unexpected reference counts remaining, after +// unmounting. +func NewFileSystem() (fs *ForgetFS) { + // Set up the actual file system. + impl := &fsImpl{ + inodes: map[fuseops.InodeID]*inode{ + cannedID_Root: &inode{ + lookupCount: 1, + attributes: fuseops.InodeAttributes{ + Nlink: 1, + Mode: 0777 | os.ModeDir, + }, + }, + cannedID_Foo: &inode{ + attributes: fuseops.InodeAttributes{ + Nlink: 1, + Mode: 0777, + }, + }, + cannedID_Bar: &inode{ + attributes: fuseops.InodeAttributes{ + Nlink: 1, + Mode: 0777 | os.ModeDir, + }, + }, + }, + nextInodeID: cannedID_Next, + } + + impl.mu = syncutil.NewInvariantMutex(impl.checkInvariants) + + // Set up a wrapper that exposes only certain methods. + fs = &ForgetFS{ + impl: impl, + server: fuseutil.NewFileSystemServer(impl), + } + + return +} + +//////////////////////////////////////////////////////////////////////// +// ForgetFS +//////////////////////////////////////////////////////////////////////// + +type ForgetFS struct { + impl *fsImpl + server fuse.Server +} + +func (fs *ForgetFS) ServeOps(c *fuse.Connection) { + fs.server.ServeOps(c) +} + +// Panic if there are any inodes that have a non-zero reference count. For use +// after unmounting. +func (fs *ForgetFS) Check() { + fs.impl.Check() +} + +//////////////////////////////////////////////////////////////////////// +// Actual implementation +//////////////////////////////////////////////////////////////////////// + +const ( + cannedID_Root = fuseops.RootInodeID + iota + cannedID_Foo + cannedID_Bar + cannedID_Next +) + +type fsImpl struct { + fuseutil.NotImplementedFileSystem + + ///////////////////////// + // Mutable state + ///////////////////////// + + mu syncutil.InvariantMutex + + // An index of inode by ID, for all IDs we have issued. + // + // GUARDED_BY(mu) + inodes map[fuseops.InodeID]*inode + + // The next ID to issue. + // + // INVARIANT: For each k in inodes, k < nextInodeID + // + // GUARDED_BY(mu) + nextInodeID fuseops.InodeID +} + +//////////////////////////////////////////////////////////////////////// +// inode +//////////////////////////////////////////////////////////////////////// + +type inode struct { + attributes fuseops.InodeAttributes + + // The current lookup count. + lookupCount uint64 + + // true if lookupCount has ever been positive. + lookedUp bool +} + +func (in *inode) Forgotten() bool { + return in.lookedUp && in.lookupCount == 0 +} + +func (in *inode) IncrementLookupCount() { + in.lookupCount++ + in.lookedUp = true +} + +func (in *inode) DecrementLookupCount(n uint64) { + if in.lookupCount < n { + panic(fmt.Sprintf( + "Overly large decrement: %v, %v", + in.lookupCount, + n)) + } + + in.lookupCount -= n +} + +//////////////////////////////////////////////////////////////////////// +// Helpers +//////////////////////////////////////////////////////////////////////// + +// LOCKS_REQUIRED(fs.mu) +func (fs *fsImpl) checkInvariants() { + // INVARIANT: For each k in inodes, k < nextInodeID + for k, _ := range fs.inodes { + if !(k < fs.nextInodeID) { + panic("Unexpectedly large inode ID") + } + } +} + +// LOCKS_EXCLUDED(fs.mu) +func (fs *fsImpl) Check() { + fs.mu.Lock() + defer fs.mu.Unlock() + + for k, v := range fs.inodes { + // Special case: the root inode should have an implicit lookup count of 1. + if k == fuseops.RootInodeID { + if v.lookupCount != 1 { + panic(fmt.Sprintf("Root has lookup count %v", v.lookupCount)) + } + + continue + } + + // Check other inodes. + if v.lookupCount != 0 { + panic(fmt.Sprintf("Inode %v has lookup count %v", k, v.lookupCount)) + } + } +} + +// Look up the inode and verify it hasn't been forgotten. +// +// LOCKS_REQUIRED(fs.mu) +func (fs *fsImpl) findInodeByID(id fuseops.InodeID) (in *inode) { + in = fs.inodes[id] + if in == nil { + panic(fmt.Sprintf("Unknown inode: %v", id)) + } + + if in.Forgotten() { + panic(fmt.Sprintf("Forgotten inode: %v", id)) + } + + return +} + +//////////////////////////////////////////////////////////////////////// +// FileSystem methods +//////////////////////////////////////////////////////////////////////// + +func (fs *fsImpl) Init( + op *fuseops.InitOp) { + var err error + defer fuseutil.RespondToOp(op, &err) + + return +} + +func (fs *fsImpl) LookUpInode( + op *fuseops.LookUpInodeOp) { + var err error + defer fuseutil.RespondToOp(op, &err) + + fs.mu.Lock() + defer fs.mu.Unlock() + + // Make sure the parent exists and has not been forgotten. + _ = fs.findInodeByID(op.Parent) + + // Handle the names we support. + var childID fuseops.InodeID + switch { + case op.Parent == cannedID_Root && op.Name == "foo": + childID = cannedID_Foo + + case op.Parent == cannedID_Root && op.Name == "bar": + childID = cannedID_Bar + + default: + err = fuse.ENOENT + return + } + + // Look up the child. + child := fs.findInodeByID(childID) + child.IncrementLookupCount() + + // Return an appropriate entry. + op.Entry = fuseops.ChildInodeEntry{ + Child: childID, + Attributes: child.attributes, + } + + return +} + +func (fs *fsImpl) GetInodeAttributes( + op *fuseops.GetInodeAttributesOp) { + var err error + defer fuseutil.RespondToOp(op, &err) + + fs.mu.Lock() + defer fs.mu.Unlock() + + // Find the inode, verifying that it has not been forgotten. + in := fs.findInodeByID(op.Inode) + + // Return appropriate attributes. + op.Attributes = in.attributes + + return +} + +func (fs *fsImpl) ForgetInode( + op *fuseops.ForgetInodeOp) { + var err error + defer fuseutil.RespondToOp(op, &err) + + fs.mu.Lock() + defer fs.mu.Unlock() + + // Find the inode and decrement its count. + in := fs.findInodeByID(op.Inode) + in.DecrementLookupCount(op.N) + + return +} + +func (fs *fsImpl) OpenFile( + op *fuseops.OpenFileOp) { + var err error + defer fuseutil.RespondToOp(op, &err) + + fs.mu.Lock() + defer fs.mu.Unlock() + + // Verify that the inode has not been forgotten. + _ = fs.findInodeByID(op.Inode) + + return +} diff --git a/samples/forgetfs/forget_fs_test.go b/samples/forgetfs/forget_fs_test.go new file mode 100644 index 0000000..4245122 --- /dev/null +++ b/samples/forgetfs/forget_fs_test.go @@ -0,0 +1,86 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package forgetfs_test + +import ( + "os" + "path" + "testing" + + "github.com/jacobsa/fuse/samples" + "github.com/jacobsa/fuse/samples/forgetfs" + . "github.com/jacobsa/ogletest" +) + +func TestForgetFS(t *testing.T) { RunTests(t) } + +//////////////////////////////////////////////////////////////////////// +// Boilerplate +//////////////////////////////////////////////////////////////////////// + +type ForgetFSTest struct { + samples.SampleTest + fs *forgetfs.ForgetFS +} + +func init() { RegisterTestSuite(&ForgetFSTest{}) } + +func (t *ForgetFSTest) SetUp(ti *TestInfo) { + t.fs = forgetfs.NewFileSystem() + t.Server = t.fs + t.SampleTest.SetUp(ti) +} + +func (t *ForgetFSTest) TearDown() { + // Unmount. + t.SampleTest.TearDown() + + // Crash if anything is left. + t.fs.Check() +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +func (t *ForgetFSTest) Open_Foo() { + var err error + + f, err := os.Open(path.Join(t.Dir, "foo")) + AssertEq(nil, err) + + err = f.Close() + AssertEq(nil, err) +} + +func (t *ForgetFSTest) Open_Dir() { + AssertTrue(false, "TODO") +} + +func (t *ForgetFSTest) Stat_Foo() { + AssertTrue(false, "TODO") +} + +func (t *ForgetFSTest) Stat_Dir() { + AssertTrue(false, "TODO") +} + +func (t *ForgetFSTest) CreateFile() { + AssertTrue(false, "TODO") +} + +func (t *ForgetFSTest) MkDir() { + AssertTrue(false, "TODO") +}