Run the flushfs test in a subprocess to eliminate page fault deadlocks.

For #4.
geesefs-0-30-9
Aaron Jacobs 2015-03-24 11:42:01 +11:00
commit 53bae01f57
6 changed files with 847 additions and 207 deletions

50
fsutil/fsutil.go Normal file
View File

@ -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
}

View File

@ -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")))
}

View File

@ -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
}

View File

@ -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)
}
}

360
samples/subprocess.go Normal file
View File

@ -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
}

48
samples/unmount.go Normal file
View File

@ -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
}
}