diff --git a/fsutil/fsutil.go b/fsutil/fsutil.go new file mode 100644 index 0000000..152ef85 --- /dev/null +++ b/fsutil/fsutil.go @@ -0,0 +1,50 @@ +// 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 fsutil + +import ( + "fmt" + "io/ioutil" + "os" + "path" +) + +// Create a temporary file with the same semantics as ioutil.TempFile, but +// ensure that it is unlinked before returning so that it does not persist +// after the process exits. +// +// Warning: this is not production-quality code, and should only be used for +// testing purposes. In particular, there is a race between creating and +// unlinking by name. +func AnonymousFile(dir string) (f *os.File, err error) { + // Choose a prefix based on the binary name. + prefix := path.Base(os.Args[0]) + + // Create the file. + f, err = ioutil.TempFile(dir, prefix) + if err != nil { + err = fmt.Errorf("TempFile: %v", err) + return + } + + // Unlink it. + err = os.Remove(f.Name()) + if err != nil { + err = fmt.Errorf("Remove: %v", err) + return + } + + return +} diff --git a/samples/flushfs/flush_fs_test.go b/samples/flushfs/flush_fs_test.go index 2421ede..a5ffeb2 100644 --- a/samples/flushfs/flush_fs_test.go +++ b/samples/flushfs/flush_fs_test.go @@ -15,18 +15,19 @@ package flushfs_test import ( + "bufio" + "encoding/hex" + "fmt" "io" - "io/ioutil" "os" "path" "runtime" - "sync" "syscall" "testing" - "github.com/jacobsa/fuse" + "github.com/jacobsa/bazilfuse" + "github.com/jacobsa/fuse/fsutil" "github.com/jacobsa/fuse/samples" - "github.com/jacobsa/fuse/samples/flushfs" . "github.com/jacobsa/oglematchers" . "github.com/jacobsa/ogletest" ) @@ -37,54 +38,59 @@ func TestFlushFS(t *testing.T) { RunTests(t) } // Boilerplate //////////////////////////////////////////////////////////////////////// -type FlushFSTest struct { - samples.SampleTest +type flushFSTest struct { + samples.SubprocessTest + + // Files to which mount_sample is writing reported flushes and fsyncs. + flushes *os.File + fsyncs *os.File // File handles that are closed in TearDown if non-nil. f1 *os.File f2 *os.File - - mu sync.Mutex - - // GUARDED_BY(mu) - flushes []string - flushErr error - - // GUARDED_BY(mu) - fsyncs []string - fsyncErr error } -func init() { RegisterTestSuite(&FlushFSTest{}) } - -func (t *FlushFSTest) SetUp(ti *TestInfo) { +func (t *flushFSTest) setUp( + ti *TestInfo, + flushErr bazilfuse.Errno, + fsyncErr bazilfuse.Errno) { var err error - // Set up a file system. - reportTo := func(slice *[]string, err *error) func(string) error { - return func(s string) error { - t.mu.Lock() - defer t.mu.Unlock() + // Set up files to receive flush and fsync reports. + t.flushes, err = fsutil.AnonymousFile("") + AssertEq(nil, err) - *slice = append(*slice, s) - return *err - } + t.fsyncs, err = fsutil.AnonymousFile("") + AssertEq(nil, err) + + // Set up test config. + t.MountType = "flushfs" + t.MountFlags = []string{ + "--flushfs.flush_error", + fmt.Sprintf("%d", int(flushErr)), + + "--flushfs.fsync_error", + fmt.Sprintf("%d", int(fsyncErr)), } - t.FileSystem, err = flushfs.NewFileSystem( - reportTo(&t.flushes, &t.flushErr), - reportTo(&t.fsyncs, &t.fsyncErr)) - - if err != nil { - panic(err) + t.MountFiles = map[string]*os.File{ + "flushfs.flushes_file": t.flushes, + "flushfs.fsyncs_file": t.fsyncs, } - // Mount it. - t.SampleTest.SetUp(ti) + t.SubprocessTest.SetUp(ti) } -func (t *FlushFSTest) TearDown() { - // Close files if non-nil. +func (t *flushFSTest) TearDown() { + // Unlink reporting files. + os.Remove(t.flushes.Name()) + os.Remove(t.fsyncs.Name()) + + // Close reporting files. + t.flushes.Close() + t.fsyncs.Close() + + // Close test files if non-nil. if t.f1 != nil { ExpectEq(nil, t.f1.Close()) } @@ -94,53 +100,66 @@ func (t *FlushFSTest) TearDown() { } // Finish tearing down. - t.SampleTest.TearDown() + t.SubprocessTest.TearDown() } //////////////////////////////////////////////////////////////////////// // Helpers //////////////////////////////////////////////////////////////////////// -// Return a copy of the current contents of t.flushes. -// -// LOCKS_EXCLUDED(t.mu) -func (t *FlushFSTest) getFlushes() (p []string) { - t.mu.Lock() - defer t.mu.Unlock() +func readReports(f *os.File) (reports []string, err error) { + // Seek the file to the start. + _, err = f.Seek(0, 0) + if err != nil { + err = fmt.Errorf("Seek: %v", err) + return + } + + // We expect reports to end in a newline (including the final one). + reader := bufio.NewReader(f) + for { + var record []byte + record, err = reader.ReadBytes('\n') + if err == io.EOF { + if len(record) != 0 { + err = fmt.Errorf("Unexpected record:\n%s", hex.Dump(record)) + return + } + + err = nil + return + } + + if err != nil { + err = fmt.Errorf("ReadBytes: %v", err) + return + } + + // Strip the newline. + reports = append(reports, string(record[:len(record)-1])) + } +} + +// Return a copy of the current contents of t.flushes. +func (t *flushFSTest) getFlushes() (p []string) { + var err error + if p, err = readReports(t.flushes); err != nil { + panic(err) + } - p = make([]string, len(t.flushes)) - copy(p, t.flushes) return } // Return a copy of the current contents of t.fsyncs. -// -// LOCKS_EXCLUDED(t.mu) -func (t *FlushFSTest) getFsyncs() (p []string) { - t.mu.Lock() - defer t.mu.Unlock() +func (t *flushFSTest) getFsyncs() (p []string) { + var err error + if p, err = readReports(t.fsyncs); err != nil { + panic(err) + } - p = make([]string, len(t.fsyncs)) - copy(p, t.fsyncs) return } -// LOCKS_EXCLUDED(t.mu) -func (t *FlushFSTest) setFlushError(err error) { - t.mu.Lock() - defer t.mu.Unlock() - - t.flushErr = err -} - -// LOCKS_EXCLUDED(t.mu) -func (t *FlushFSTest) setFsyncError(err error) { - t.mu.Lock() - defer t.mu.Unlock() - - t.fsyncErr = err -} - // Like syscall.Dup2, but correctly annotates the syscall as blocking. See here // for more info: https://github.com/golang/go/issues/10202 func dup2(oldfd int, newfd int) (err error) { @@ -155,10 +174,21 @@ func dup2(oldfd int, newfd int) (err error) { } //////////////////////////////////////////////////////////////////////// -// Tests +// No errors //////////////////////////////////////////////////////////////////////// -func (t *FlushFSTest) CloseReports_ReadWrite() { +type NoErrorsTest struct { + flushFSTest +} + +func init() { RegisterTestSuite(&NoErrorsTest{}) } + +func (t *NoErrorsTest) SetUp(ti *TestInfo) { + const noErr = 0 + t.flushFSTest.setUp(ti, noErr, noErr) +} + +func (t *NoErrorsTest) Close_ReadWrite() { var n int var off int64 var err error @@ -196,7 +226,7 @@ func (t *FlushFSTest) CloseReports_ReadWrite() { ExpectThat(t.getFsyncs(), ElementsAre()) } -func (t *FlushFSTest) CloseReports_ReadOnly() { +func (t *NoErrorsTest) Close_ReadOnly() { var err error // Open the file. @@ -217,7 +247,7 @@ func (t *FlushFSTest) CloseReports_ReadOnly() { ExpectThat(t.getFsyncs(), ElementsAre()) } -func (t *FlushFSTest) CloseReports_WriteOnly() { +func (t *NoErrorsTest) Close_WriteOnly() { var n int var err error @@ -244,7 +274,7 @@ func (t *FlushFSTest) CloseReports_WriteOnly() { ExpectThat(t.getFsyncs(), ElementsAre()) } -func (t *FlushFSTest) CloseReports_MultipleTimes_NonOverlappingFileHandles() { +func (t *NoErrorsTest) Close_MultipleTimes_NonOverlappingFileHandles() { var n int var err error @@ -291,7 +321,7 @@ func (t *FlushFSTest) CloseReports_MultipleTimes_NonOverlappingFileHandles() { AssertThat(t.getFsyncs(), ElementsAre()) } -func (t *FlushFSTest) CloseReports_MultipleTimes_OverlappingFileHandles() { +func (t *NoErrorsTest) Close_MultipleTimes_OverlappingFileHandles() { var n int var err error @@ -340,25 +370,7 @@ func (t *FlushFSTest) CloseReports_MultipleTimes_OverlappingFileHandles() { AssertThat(t.getFsyncs(), ElementsAre()) } -func (t *FlushFSTest) CloseError() { - var err error - - // Open the file. - t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_RDWR, 0) - AssertEq(nil, err) - - // Configure a flush error. - t.setFlushError(fuse.ENOENT) - - // Close the file. - err = t.f1.Close() - t.f1 = nil - - AssertNe(nil, err) - ExpectThat(err, Error(HasSubstr("no such file"))) -} - -func (t *FlushFSTest) FsyncReports() { +func (t *NoErrorsTest) Fsync() { var n int var err error @@ -397,24 +409,7 @@ func (t *FlushFSTest) FsyncReports() { AssertThat(t.getFsyncs(), ElementsAre("taco", "tacos")) } -func (t *FlushFSTest) FsyncError() { - var err error - - // Open the file. - t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_RDWR, 0) - AssertEq(nil, err) - - // Configure an fsync error. - t.setFsyncError(fuse.ENOENT) - - // Fsync. - err = t.f1.Sync() - - AssertNe(nil, err) - ExpectThat(err, Error(HasSubstr("no such file"))) -} - -func (t *FlushFSTest) Dup() { +func (t *NoErrorsTest) Dup() { var n int var err error @@ -478,46 +473,7 @@ func (t *FlushFSTest) Dup() { ExpectThat(t.getFsyncs(), ElementsAre()) } -func (t *FlushFSTest) Dup_FlushError() { - var err error - - // Open the file. - t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_WRONLY, 0) - AssertEq(nil, err) - - fd1 := t.f1.Fd() - - // Use dup(2) to get another copy. - fd2, err := syscall.Dup(int(fd1)) - AssertEq(nil, err) - - t.f2 = os.NewFile(uintptr(fd2), t.f1.Name()) - - // Configure a flush error. - t.setFlushError(fuse.ENOENT) - - // Close by the first handle. On OS X, where the semantics of file handles - // are different (cf. https://github.com/osxfuse/osxfuse/issues/199), this - // does not result in an error. - err = t.f1.Close() - t.f1 = nil - - if runtime.GOOS == "darwin" { - AssertEq(nil, err) - } else { - AssertNe(nil, err) - ExpectThat(err, Error(HasSubstr("no such file"))) - } - - // Close by the second handle. - err = t.f2.Close() - t.f2 = nil - - AssertNe(nil, err) - ExpectThat(err, Error(HasSubstr("no such file"))) -} - -func (t *FlushFSTest) Dup2() { +func (t *NoErrorsTest) Dup2() { var n int var err error @@ -530,11 +486,8 @@ func (t *FlushFSTest) Dup2() { AssertEq(nil, err) AssertEq(4, n) - // Open and unlink some temporary file. - t.f2, err = ioutil.TempFile("", "") - AssertEq(nil, err) - - err = os.Remove(t.f2.Name()) + // Create some anonymous temporary file. + t.f2, err = fsutil.AnonymousFile("") AssertEq(nil, err) // Duplicate the temporary file descriptor on top of the file from our file @@ -546,30 +499,7 @@ func (t *FlushFSTest) Dup2() { ExpectThat(t.getFsyncs(), ElementsAre()) } -func (t *FlushFSTest) Dup2_FlushError() { - var err error - - // Open the file. - t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_WRONLY, 0) - AssertEq(nil, err) - - // Open and unlink some temporary file. - t.f2, err = ioutil.TempFile("", "") - AssertEq(nil, err) - - err = os.Remove(t.f2.Name()) - AssertEq(nil, err) - - // Configure a flush error. - t.setFlushError(fuse.ENOENT) - - // Duplicate the temporary file descriptor on top of the file from our file - // system. We shouldn't see the flush error. - err = dup2(int(t.f2.Fd()), int(t.f1.Fd())) - ExpectEq(nil, err) -} - -func (t *FlushFSTest) Mmap_MunmapBeforeClose() { +func (t *NoErrorsTest) Mmap_MunmapBeforeClose() { var n int var err error @@ -629,7 +559,7 @@ func (t *FlushFSTest) Mmap_MunmapBeforeClose() { } } -func (t *FlushFSTest) Mmap_CloseBeforeMunmap() { +func (t *NoErrorsTest) Mmap_CloseBeforeMunmap() { var n int var err error @@ -683,6 +613,118 @@ func (t *FlushFSTest) Mmap_CloseBeforeMunmap() { ExpectThat(t.getFsyncs(), ElementsAre()) } -func (t *FlushFSTest) Directory() { +func (t *NoErrorsTest) Directory() { AssertTrue(false, "TODO") } + +//////////////////////////////////////////////////////////////////////// +// Flush error +//////////////////////////////////////////////////////////////////////// + +type FlushErrorTest struct { + flushFSTest +} + +func init() { RegisterTestSuite(&FlushErrorTest{}) } + +func (t *FlushErrorTest) SetUp(ti *TestInfo) { + const noErr = 0 + t.flushFSTest.setUp(ti, bazilfuse.ENOENT, noErr) +} + +func (t *FlushErrorTest) Close() { + var err error + + // Open the file. + t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_RDWR, 0) + AssertEq(nil, err) + + // Close the file. + err = t.f1.Close() + t.f1 = nil + + AssertNe(nil, err) + ExpectThat(err, Error(HasSubstr("no such file"))) +} + +func (t *FlushErrorTest) Dup() { + var err error + + // Open the file. + t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_WRONLY, 0) + AssertEq(nil, err) + + fd1 := t.f1.Fd() + + // Use dup(2) to get another copy. + fd2, err := syscall.Dup(int(fd1)) + AssertEq(nil, err) + + t.f2 = os.NewFile(uintptr(fd2), t.f1.Name()) + + // Close by the first handle. On OS X, where the semantics of file handles + // are different (cf. https://github.com/osxfuse/osxfuse/issues/199), this + // does not result in an error. + err = t.f1.Close() + t.f1 = nil + + if runtime.GOOS == "darwin" { + AssertEq(nil, err) + } else { + AssertNe(nil, err) + ExpectThat(err, Error(HasSubstr("no such file"))) + } + + // Close by the second handle. + err = t.f2.Close() + t.f2 = nil + + AssertNe(nil, err) + ExpectThat(err, Error(HasSubstr("no such file"))) +} + +func (t *FlushErrorTest) Dup2() { + var err error + + // Open the file. + t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_WRONLY, 0) + AssertEq(nil, err) + + // Create some anonymous temporary file. + t.f2, err = fsutil.AnonymousFile("") + AssertEq(nil, err) + + // Duplicate the temporary file descriptor on top of the file from our file + // system. We shouldn't see the flush error. + err = dup2(int(t.f2.Fd()), int(t.f1.Fd())) + ExpectEq(nil, err) +} + +//////////////////////////////////////////////////////////////////////// +// Fsync error +//////////////////////////////////////////////////////////////////////// + +type FsyncErrorTest struct { + flushFSTest +} + +func init() { RegisterTestSuite(&FsyncErrorTest{}) } + +func (t *FsyncErrorTest) SetUp(ti *TestInfo) { + const noErr = 0 + t.flushFSTest.setUp(ti, noErr, bazilfuse.ENOENT) +} + +func (t *FsyncErrorTest) Fsync() { + var err error + + // Open the file. + t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_RDWR, 0) + AssertEq(nil, err) + + // Fsync. + err = t.f1.Sync() + + AssertNe(nil, err) + ExpectThat(err, Error(HasSubstr("no such file"))) +} diff --git a/samples/testing.go b/samples/in_process.go similarity index 81% rename from samples/testing.go rename to samples/in_process.go index bdb2b57..c0fb1da 100644 --- a/samples/testing.go +++ b/samples/in_process.go @@ -18,8 +18,7 @@ import ( "fmt" "io" "io/ioutil" - "log" - "strings" + "os" "time" "github.com/googlecloudplatform/gcsfuse/timeutil" @@ -55,8 +54,8 @@ type SampleTest struct { mfs *fuse.MountedFileSystem } -// Mount the supplied file system and initialize the other exported fields of -// the struct. Panics on error. +// Mount t.FileSystem and initialize the other exported fields of the struct. +// Panics on error. // // REQUIRES: t.FileSystem has been set. func (t *SampleTest) SetUp(ti *ogletest.TestInfo) { @@ -66,7 +65,7 @@ func (t *SampleTest) SetUp(ti *ogletest.TestInfo) { } } -// Like Initialize, but doens't panic. +// Like SetUp, but doens't panic. func (t *SampleTest) initialize( fs fuse.FileSystem, config *fuse.MountConfig) (err error) { @@ -90,7 +89,7 @@ func (t *SampleTest) initialize( return } - // Wait for it to be read. + // Wait for it to be ready. err = t.mfs.WaitForReady(t.Ctx) if err != nil { err = fmt.Errorf("WaitForReady: %v", err) @@ -124,27 +123,16 @@ func (t *SampleTest) destroy() (err error) { return } - // Unmount the file system. Try again on "resource busy" errors. - delay := 10 * time.Millisecond - for { - err = t.mfs.Unmount() - if err == nil { - break - } - - if strings.Contains(err.Error(), "resource busy") { - log.Println("Resource busy error while unmounting; trying again") - time.Sleep(delay) - delay = time.Duration(1.3 * float64(delay)) - continue - } - - err = fmt.Errorf("MountedFileSystem.Unmount: %v", err) + // Unmount the file system. + err = unmount(t.Dir) + if err != nil { + err = fmt.Errorf("unmount: %v", err) return } - if err = t.mfs.Join(t.Ctx); err != nil { - err = fmt.Errorf("MountedFileSystem.Join: %v", err) + // Unlink the mount point. + if err = os.Remove(t.Dir); err != nil { + err = fmt.Errorf("Unlinking mount point: %v", err) return } diff --git a/samples/mount_sample/mount.go b/samples/mount_sample/mount.go new file mode 100644 index 0000000..e7d0def --- /dev/null +++ b/samples/mount_sample/mount.go @@ -0,0 +1,152 @@ +// 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. + +// A simple tool for mounting sample file systems, used by the tests in +// samples/. +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "os" + + "github.com/jacobsa/bazilfuse" + "github.com/jacobsa/fuse" + "github.com/jacobsa/fuse/samples/flushfs" + "golang.org/x/net/context" +) + +var fType = flag.String("type", "", "The name of the samples/ sub-dir.") +var fMountPoint = flag.String("mount_point", "", "Path to mount point.") +var fReadyFile = flag.Uint64("ready_file", 0, "FD to signal when ready.") + +var fFlushesFile = flag.Uint64("flushfs.flushes_file", 0, "") +var fFsyncsFile = flag.Uint64("flushfs.fsyncs_file", 0, "") +var fFlushError = flag.Int("flushfs.flush_error", 0, "") +var fFsyncError = flag.Int("flushfs.fsync_error", 0, "") + +func makeFlushFS() (fs fuse.FileSystem, err error) { + // Check the flags. + if *fFlushesFile == 0 || *fFsyncsFile == 0 { + err = fmt.Errorf("You must set the flushfs flags.") + return + } + + // Set up the files. + flushes := os.NewFile(uintptr(*fFlushesFile), "(flushes file)") + fsyncs := os.NewFile(uintptr(*fFsyncsFile), "(fsyncs file)") + + // Set up errors. + var flushErr error + var fsyncErr error + + if *fFlushError != 0 { + flushErr = bazilfuse.Errno(*fFlushError) + } + + if *fFsyncError != 0 { + fsyncErr = bazilfuse.Errno(*fFsyncError) + } + + // Report flushes and fsyncs by writing the contents followed by a newline. + report := func(f *os.File, outErr error) func(string) error { + return func(s string) (err error) { + buf := []byte(s) + buf = append(buf, '\n') + + _, err = f.Write(buf) + if err != nil { + err = fmt.Errorf("Write: %v", err) + return + } + + err = outErr + return + } + } + + reportFlush := report(flushes, flushErr) + reportFsync := report(fsyncs, fsyncErr) + + // Create the file system. + fs, err = flushfs.NewFileSystem(reportFlush, reportFsync) + + return +} + +func makeFS() (fs fuse.FileSystem, err error) { + switch *fType { + default: + err = fmt.Errorf("Unknown FS type: %v", *fType) + + case "flushfs": + fs, err = makeFlushFS() + } + + return +} + +func getReadyFile() (f *os.File, err error) { + if *fReadyFile == 0 { + err = errors.New("You must set --ready_file.") + return + } + + f = os.NewFile(uintptr(*fReadyFile), "(ready file)") + return +} + +func main() { + flag.Parse() + + // Grab the file to signal when ready. + readyFile, err := getReadyFile() + if err != nil { + log.Fatalf("getReadyFile:", err) + } + + // Create an appropriate file system. + fs, err := makeFS() + if err != nil { + log.Fatalf("makeFS: %v", err) + } + + // Mount the file system. + if *fMountPoint == "" { + log.Fatalf("You must set --mount_point.") + } + + mfs, err := fuse.Mount(*fMountPoint, fs, &fuse.MountConfig{}) + if err != nil { + log.Fatalf("Mount: %v", err) + } + + // Wait for it to be ready. + if err = mfs.WaitForReady(context.Background()); err != nil { + log.Fatalf("WaitForReady: %v", err) + } + + // Signal that it is ready. + _, err = readyFile.Write([]byte("x")) + if err != nil { + log.Fatalf("readyFile.Write: %v", err) + } + + // Wait for it to be unmounted. + if err = mfs.Join(context.Background()); err != nil { + log.Fatalf("Join: %v", err) + } +} diff --git a/samples/subprocess.go b/samples/subprocess.go new file mode 100644 index 0000000..38733bf --- /dev/null +++ b/samples/subprocess.go @@ -0,0 +1,360 @@ +// 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 samples + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path" + "sync" + + "github.com/jacobsa/ogletest" + "golang.org/x/net/context" +) + +var fToolPath = flag.String( + "mount_sample", + "", + "Path to the mount_sample tool. If unset, we will compile it.") + +// A struct that implements common behavior needed by tests in the samples/ +// directory where the file system is mounted by a subprocess. Use it as an +// embedded field in your test fixture, calling its SetUp method from your +// SetUp method after setting the MountType and MountFlags fields. +type SubprocessTest struct { + // The type of the file system to mount. Must be recognized by mount_sample. + MountType string + + // Additional flags to be passed to the mount_sample tool. + MountFlags []string + + // A list of files to pass to mount_sample. The given string flag will be + // used to pass the file descriptor number. + MountFiles map[string]*os.File + + // A context object that can be used for long-running operations. + Ctx context.Context + + // The directory at which the file system is mounted. + Dir string + + // Anothing non-nil in this slice will be closed by TearDown. The test will + // fail if closing fails. + ToClose []io.Closer + + mountSampleErr <-chan error +} + +// Mount the file system and initialize the other exported fields of the +// struct. Panics on error. +// +// REQUIRES: t.FileSystem has been set. +func (t *SubprocessTest) SetUp(ti *ogletest.TestInfo) { + err := t.initialize() + if err != nil { + panic(err) + } +} + +// Private state for getToolPath. +var getToolContents_Contents []byte +var getToolContents_Err error +var getToolContents_Once sync.Once + +// Implementation detail of getToolPath. +func getToolContentsImpl() (contents []byte, err error) { + // Fast path: has the user set the flag? + if *fToolPath != "" { + contents, err = ioutil.ReadFile(*fToolPath) + if err != nil { + err = fmt.Errorf("Reading mount_sample contents: %v", err) + return + } + + return + } + + // Create a temporary directory into which we will compile the tool. + tempDir, err := ioutil.TempDir("", "sample_test") + if err != nil { + err = fmt.Errorf("TempDir: %v", err) + return + } + + toolPath := path.Join(tempDir, "mount_sample") + + // Ensure that we kill the temporary directory when we're finished here. + defer os.RemoveAll(tempDir) + + // Run "go build". + cmd := exec.Command( + "go", + "build", + "-o", + toolPath, + "github.com/jacobsa/fuse/samples/mount_sample") + + output, err := cmd.CombinedOutput() + if err != nil { + err = fmt.Errorf( + "mount_sample exited with %v, output:\n%s", + err, + string(output)) + + return + } + + // Slurp the tool contents. + contents, err = ioutil.ReadFile(toolPath) + if err != nil { + err = fmt.Errorf("ReadFile: %v", err) + return + } + + return +} + +// Build the mount_sample tool if it has not yet been built for this process. +// Return its contents. +func getToolContents() (contents []byte, err error) { + // Get hold of the binary contents, if we haven't yet. + getToolContents_Once.Do(func() { + getToolContents_Contents, getToolContents_Err = getToolContentsImpl() + }) + + contents, err = getToolContents_Contents, getToolContents_Err + return +} + +func waitForMountSample( + cmd *exec.Cmd, + errChan chan<- error, + stderr *bytes.Buffer) { + // However we exit, write the error to the channel. + var err error + defer func() { + errChan <- err + }() + + // Wait for the command. + err = cmd.Wait() + if err == nil { + return + } + + // Make exit errors nicer. + if exitErr, ok := err.(*exec.ExitError); ok { + err = fmt.Errorf( + "mount_sample exited with %v. Stderr:\n%s", + exitErr, + stderr.String()) + + return + } + + err = fmt.Errorf("Waiting for mount_sample: %v", err) +} + +func waitForReady(readyReader *os.File, c chan<- struct{}) { + _, err := readyReader.Read(make([]byte, 1)) + if err != nil { + log.Printf("Readying from ready pipe: %v", err) + return + } + + c <- struct{}{} +} + +// Like SetUp, but doens't panic. +func (t *SubprocessTest) initialize() (err error) { + // Initialize the context. + t.Ctx = context.Background() + + // Set up a temporary directory. + t.Dir, err = ioutil.TempDir("", "sample_test") + if err != nil { + err = fmt.Errorf("TempDir: %v", err) + return + } + + // Build/read the mount_sample tool. + toolContents, err := getToolContents() + if err != nil { + err = fmt.Errorf("getTooltoolContents: %v", err) + return + } + + // Create a temporary file to hold the contents of the tool. + toolFile, err := ioutil.TempFile("", "sample_test") + if err != nil { + err = fmt.Errorf("TempFile: %v", err) + return + } + + defer toolFile.Close() + + // Ensure that it is deleted when we leave. + toolPath := toolFile.Name() + defer os.Remove(toolPath) + + // Write out the tool contents and make them executable. + if _, err = toolFile.Write(toolContents); err != nil { + err = fmt.Errorf("toolFile.Write: %v", err) + return + } + + if err = toolFile.Chmod(0500); err != nil { + err = fmt.Errorf("toolFile.Chmod: %v", err) + return + } + + // Close the tool file to prevent "text file busy" errors below. + err = toolFile.Close() + toolFile = nil + if err != nil { + err = fmt.Errorf("toolFile.Close: %v", err) + return + } + + // Set up basic args for the subprocess. + args := []string{ + "--type", + t.MountType, + "--mount_point", + t.Dir, + } + + args = append(args, t.MountFlags...) + + // Set up a pipe for the "ready" status. + readyReader, readyWriter, err := os.Pipe() + if err != nil { + err = fmt.Errorf("Pipe: %v", err) + return + } + + defer readyReader.Close() + defer readyWriter.Close() + + t.MountFiles["ready_file"] = readyWriter + + // Set up inherited files and appropriate flags. + var extraFiles []*os.File + for flag, file := range t.MountFiles { + // Cf. os/exec.Cmd.ExtraFiles + fd := 3 + len(extraFiles) + + extraFiles = append(extraFiles, file) + args = append(args, "--"+flag) + args = append(args, fmt.Sprintf("%d", fd)) + } + + // Set up a command. + var stderr bytes.Buffer + mountCmd := exec.Command(toolPath, args...) + mountCmd.Stderr = &stderr + mountCmd.ExtraFiles = extraFiles + + // Start it. + if err = mountCmd.Start(); err != nil { + err = fmt.Errorf("mountCmd.Start: %v", err) + return + } + + // Launch a goroutine that waits for it and returns its status. + mountSampleErr := make(chan error, 1) + go waitForMountSample(mountCmd, mountSampleErr, &stderr) + + // Wait for the tool to say the file system is ready. In parallel, watch for + // the tool to fail. + readyChan := make(chan struct{}, 1) + go waitForReady(readyReader, readyChan) + + select { + case <-readyChan: + case err = <-mountSampleErr: + return + } + + // TearDown is no responsible for joining. + t.mountSampleErr = mountSampleErr + + return +} + +// Unmount the file system and clean up. Panics on error. +func (t *SubprocessTest) TearDown() { + err := t.destroy() + if err != nil { + panic(err) + } +} + +// Like TearDown, but doesn't panic. +func (t *SubprocessTest) destroy() (err error) { + // Make sure we clean up after ourselves after everything else below. + + // Close what is necessary. + for _, c := range t.ToClose { + if c == nil { + continue + } + + ogletest.ExpectEq(nil, c.Close()) + } + + // If we didn't try to mount the file system, there's nothing further to do. + if t.mountSampleErr == nil { + return + } + + // In the background, initiate an unmount. + unmountErrChan := make(chan error) + go func() { + unmountErrChan <- unmount(t.Dir) + }() + + // Make sure we wait for the unmount, even if we've already returned early in + // error. Return its error if we haven't seen any other error. + defer func() { + // Wait. + unmountErr := <-unmountErrChan + if unmountErr != nil { + if err != nil { + log.Println("unmount:", unmountErr) + return + } + + err = fmt.Errorf("unmount: %v", unmountErr) + return + } + + // Clean up. + ogletest.ExpectEq(nil, os.Remove(t.Dir)) + }() + + // Wait for the subprocess. + if err = <-t.mountSampleErr; err != nil { + return + } + + return +} diff --git a/samples/unmount.go b/samples/unmount.go new file mode 100644 index 0000000..37652e3 --- /dev/null +++ b/samples/unmount.go @@ -0,0 +1,48 @@ +// 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 samples + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/jacobsa/bazilfuse" +) + +// Unmount the file system mounted at the supplied directory. Try again on +// "resource busy" errors, which happen from time to time on OS X (due to weird +// requests from the Finder) and when tests don't or can't synchronize all +// events. +func unmount(dir string) (err error) { + delay := 10 * time.Millisecond + for { + err = bazilfuse.Unmount(dir) + if err == nil { + return + } + + if strings.Contains(err.Error(), "resource busy") { + log.Println("Resource busy error while unmounting; trying again") + time.Sleep(delay) + delay = time.Duration(1.3 * float64(delay)) + continue + } + + err = fmt.Errorf("Unmount: %v", err) + return + } +}