fs: bunch of read-write FUSE work, debugging, and integration tests.

Change-Id: I74807f693720effb7ae8405259797331f79f59fd
This commit is contained in:
Brad Fitzpatrick 2013-07-21 12:26:05 -07:00
parent 62b0f66f05
commit 98eb69b5e1
5 changed files with 397 additions and 34 deletions

View File

@ -172,7 +172,7 @@ func main() {
err = fs.Unmount(mountPoint)
log.Printf("Unmount = %v", err)
log.Printf("cammount FUSE processending.")
log.Printf("cammount FUSE process ending.")
}
func awaitQuitKey(done chan<- bool) {

221
pkg/fs/fs_test.go Normal file
View File

@ -0,0 +1,221 @@
/*
Copyright 2013 Google Inc.
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 fs
import (
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"sync"
"testing"
"time"
"camlistore.org/pkg/test"
)
var (
errmu sync.Mutex
lasterr error
)
func condSkip(t *testing.T) {
errmu.Lock()
defer errmu.Unlock()
if lasterr != nil {
t.Skipf("Skipping test; some other test already failed.")
}
if runtime.GOOS != "darwin" {
t.Skipf("Skipping test on OS %q", runtime.GOOS)
}
if runtime.GOOS == "darwin" {
_, err := os.Stat("/Library/Filesystems/osxfusefs.fs/Support/mount_osxfusefs")
if os.IsNotExist(err) {
test.DependencyErrorOrSkip(t)
} else if err != nil {
t.Fatal(err)
}
}
}
func cammountTest(t *testing.T, fn func(mountPoint string)) {
w := test.GetWorld(t)
mountPoint, err := ioutil.TempDir("", "fs-test-mount")
if err != nil {
t.Fatal(err)
}
verbose := "false"
if os.Getenv("VERBOSE_FUSE") != "" {
verbose = "true"
}
mount := w.Cmd("cammount", "--debug="+verbose, mountPoint)
mount.Stderr = os.Stderr
stdin, err := mount.StdinPipe()
if err != nil {
t.Fatal(err)
}
if err := mount.Start(); err != nil {
t.Fatal(err)
}
waitc := make(chan error, 1)
go func() { waitc <- mount.Wait() }()
defer func() {
log.Printf("Sending quit")
stdin.Write([]byte("q\n"))
select {
case <-time.After(5 * time.Second):
log.Printf("timeout waiting for cammount to finish")
mount.Process.Kill()
Unmount(mountPoint)
case err := <-waitc:
log.Printf("cammount exited: %v", err)
}
if !waitFor(not(dirToBeFUSE(mountPoint)), 5*time.Second, 1*time.Second) {
// It didn't unmount. Try again.
Unmount(mountPoint)
}
}()
if !waitFor(dirToBeFUSE(mountPoint), 5*time.Second, 100*time.Millisecond) {
t.Fatalf("error waiting for %s to be mounted", mountPoint)
}
fn(mountPoint)
}
func TestRoot(t *testing.T) {
condSkip(t)
cammountTest(t, func(mountPoint string) {
f, err := os.Open(mountPoint)
if err != nil {
t.Fatal(err)
}
defer f.Close()
names, err := f.Readdirnames(-1)
if err != nil {
t.Fatal(err)
}
sort.Strings(names)
want := []string{"WELCOME.txt", "date", "recent", "roots", "sha1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "tag"}
if !reflect.DeepEqual(names, want) {
t.Errorf("root directory = %q; want %q", names, want)
}
})
}
func TestMutable(t *testing.T) {
condSkip(t)
cammountTest(t, func(mountPoint string) {
rootDir := filepath.Join(mountPoint, "roots", "r")
if err := os.Mkdir(rootDir, 0700); err != nil {
t.Fatalf("Failed to make roots/r dir: %v", err)
}
fi, err := os.Stat(rootDir)
if err != nil || !fi.IsDir() {
t.Fatalf("Stat of roots/r dir = %v, %v; want a directory", fi, err)
}
filename := filepath.Join(rootDir, "x")
f, err := os.Create(filename)
if err != nil {
t.Fatalf("Create: %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
fi, err = os.Stat(filename)
if err != nil || !fi.Mode().IsRegular() || fi.Size() != 0 {
t.Fatalf("Stat of roots/r/x = %v, %v; want a 0-byte regular file", fi, err)
}
if false { // broken
for _, str := range []string{"foo, ", "bar\n", "another line.\n"} {
f, err = os.OpenFile(filename, os.O_APPEND, 0644)
if err != nil {
t.Fatalf("OpenFile: %v", err)
}
if _, err := f.Write([]byte(str)); err != nil {
t.Fatalf("Error appending %q to %s: %v", str, filename, err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
}
slurp, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
const want = "foo, bar\nanother line.\n"
fi, err = os.Stat(filename)
if err != nil || !fi.Mode().IsRegular() || fi.Size() != int64(len(want)) {
t.Errorf("Stat of roots/r/x = %v, %v; want a %d byte regular file", fi, len(want), err)
}
if got := string(slurp); got != want {
t.Fatalf("contents = %q; want %q", got, want)
}
}
// Delete it.
if err := os.Remove(filename); err != nil {
t.Fatal(err)
}
// Gone?
if _, err := os.Stat(filename); !os.IsNotExist(err) {
t.Fatalf("expected file to be gone; got stat err = %v instead", err)
}
})
}
func waitFor(condition func() bool, maxWait, checkInterval time.Duration) bool {
t0 := time.Now()
tmax := t0.Add(maxWait)
for time.Now().Before(tmax) {
if condition() {
return true
}
time.Sleep(checkInterval)
}
return false
}
func not(cond func() bool) func() bool {
return func() bool {
return !cond()
}
}
func dirToBeFUSE(dir string) func() bool {
return func() bool {
out, err := exec.Command("df", dir).CombinedOutput()
if err != nil {
return false
}
if runtime.GOOS == "darwin" {
if strings.Contains(string(out), "mount_osxfusefs@") {
log.Printf("fs %s is mounted on fuse.", dir)
return true
}
return false
}
return false
}
}

View File

@ -24,6 +24,7 @@ import (
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
@ -44,14 +45,22 @@ const populateInterval = 30 * time.Second
type mutDir struct {
fs *CamliFileSystem
permanode *blobref.BlobRef
parent *mutDir
name string // ent name (base name within parent)
parent *mutDir // or nil, if the root within its roots.go root.
name string // ent name (base name within parent)
mu sync.Mutex
lastPop time.Time
children map[string]fuse.Node
}
// for debugging
func (n *mutDir) fullPath() string {
if n == nil {
return ""
}
return filepath.Join(n.parent.fullPath(), n.name)
}
func (n *mutDir) Attr() fuse.Attr {
return fuse.Attr{
Mode: os.ModeDir | 0700,
@ -141,15 +150,33 @@ func (n *mutDir) ReadDir(intr fuse.Intr) ([]fuse.Dirent, fuse.Error) {
n.mu.Lock()
defer n.mu.Unlock()
var ents []fuse.Dirent
for name := range n.children {
ents = append(ents, fuse.Dirent{
for name, childNode := range n.children {
var ino uint64
switch v := childNode.(type) {
case *mutDir:
ino = v.permanode.AsUint64()
case *mutFile:
ino = v.permanode.AsUint64()
default:
log.Printf("mutDir.ReadDir: unknown child type %T", childNode)
}
// TODO: figure out what Dirent.Type means.
// fuse.go says "Type uint32 // ?"
dirent := fuse.Dirent{
Name: name,
})
Inode: ino,
}
log.Printf("mutDir(%q) appending inode %x, %+v", n.fullPath(), dirent.Inode, dirent)
ents = append(ents, dirent)
}
return ents, nil
}
func (n *mutDir) Lookup(name string, intr fuse.Intr) (fuse.Node, fuse.Error) {
func (n *mutDir) Lookup(name string, intr fuse.Intr) (ret fuse.Node, err fuse.Error) {
defer func() {
log.Printf("mutDir(%q).Lookup(%q) = %#v, %v", n.fullPath(), name, ret, err)
}()
if err := n.populate(); err != nil {
log.Println("populate:", err)
return nil, fuse.EIO
@ -162,10 +189,16 @@ func (n *mutDir) Lookup(name string, intr fuse.Intr) (fuse.Node, fuse.Error) {
return nil, fuse.ENOENT
}
// Create of regular file. (not a dir)
//
// TOOD(bradfitz): what are the two bits in fl=514? what are CreateRequest.Flags?
//
// 2013/07/21 05:26:35 <- &{Create [ID=0x3 Node=0x8 Uid=61652 Gid=5000 Pid=13115] "x" fl=514 mode=-rw-r--r-- fuse.Intr}
// 2013/07/21 05:26:36 -> 0x3 Create {LookupResponse:{Node:23 Generation:0 EntryValid:1m0s AttrValid:1m0s Attr:{Inode:15976986887557313215 Size:0 Blocks:0 Atime:2013-07-21 05:23:51.537251251 +1200 NZST Mtime:2013-07-21 05:23:51.537251251 +1200 NZST Ctime:2013-07-21 05:23:51.537251251 +1200 NZST Crtime:2013-07-21 05:23:51.537251251 +1200 NZST Mode:-rw------- Nlink:1 Uid:61652 Gid:5000 Rdev:0 Flags:0}} OpenResponse:{Handle:1 Flags:OpenDirectIO}}
func (n *mutDir) Create(req *fuse.CreateRequest, res *fuse.CreateResponse, intr fuse.Intr) (fuse.Node, fuse.Handle, fuse.Error) {
child, err := n.creat(req.Name, false)
if err != nil {
log.Printf("mutDir.Mkdir(%q): %v", req.Name, err)
log.Printf("mutDir.Create(%q): %v", req.Name, err)
return nil, nil, fuse.EIO
}
@ -251,24 +284,65 @@ type mutFile struct {
parent *mutDir
name string // ent name (base name within parent)
mu sync.Mutex
content *blobref.BlobRef
size int64
mu sync.Mutex
content *blobref.BlobRef
size int64
mtime, atime time.Time // if zero, use serverStart
}
// for debugging
func (n *mutFile) fullPath() string {
if n == nil {
return ""
}
return filepath.Join(n.parent.fullPath(), n.name)
}
func (n *mutFile) Attr() fuse.Attr {
// TODO: don't grab n.mu three+ times in here.
n.mu.Lock()
size := n.size
var blocks uint64
if size > 0 {
blocks = uint64(size)/512 + 1
}
inode := n.permanode.AsUint64()
n.mu.Unlock()
return fuse.Attr{
Mode: 0600, // writable!
Uid: uint32(os.Getuid()),
Gid: uint32(os.Getgid()),
Size: uint64(n.size),
// TODO(adg): use the real stuff here
Mtime: serverStart,
Inode: inode,
Mode: 0600, // writable!
Uid: uint32(os.Getuid()),
Gid: uint32(os.Getgid()),
Size: uint64(size),
Blocks: blocks,
Mtime: n.modTime(),
Atime: n.accessTime(),
Ctime: serverStart,
Crtime: serverStart,
}
}
func (n *mutFile) accessTime() time.Time {
n.mu.Lock()
if !n.atime.IsZero() {
defer n.mu.Unlock()
return n.atime
}
n.mu.Unlock()
return n.modTime()
}
func (n *mutFile) modTime() time.Time {
n.mu.Lock()
defer n.mu.Unlock()
if !n.mtime.IsZero() {
return n.mtime
}
return serverStart
}
func (n *mutFile) setContent(br *blobref.BlobRef, size int64) error {
n.mu.Lock()
defer n.mu.Unlock()
@ -279,13 +353,37 @@ func (n *mutFile) setContent(br *blobref.BlobRef, size int64) error {
return err
}
// Empirically:
// open for read: req.Flags == 0
// open for append: req.Flags == 1
// open for write: req.Flags == 1
// open for read/write (+<) == 2 (bitmask? of?)
// TODO(bradfitz): look this up, once I have connectivity.
func (n *mutFile) Open(req *fuse.OpenRequest, res *fuse.OpenResponse, intr fuse.Intr) (fuse.Handle, fuse.Error) {
log.Printf("mutFile.Open: %v: content: %v", n.permanode, n.content)
log.Printf("mutFile.Open: %v: content: %v dir=%v flags=%v mode=%v", n.permanode, n.content, req.Dir, req.Flags, req.Mode)
r, err := schema.NewFileReader(n.fs.fetcher, n.content)
if err != nil {
log.Printf("mutFile.Open: %v", err)
return nil, fuse.EIO
}
// Read-only.
if res.Flags == 0 {
log.Printf("mutFile.Open returning read-only file")
n := &node{
fs: n.fs,
blobref: n.content,
}
return &nodeReader{n: n, fr: r}, nil
}
log.Printf("mutFile.Open returning read-write filehandle")
// Turn off the OpenDirectIO bit (on by default in rsc fuse server.go),
// else append operations don't work for some reason.
// TODO(bradfitz): also do tihs in Create? CreateResponse.OpenResponse.Flags.
res.Flags &= ^fuse.OpenDirectIO
defer r.Close()
return n.newHandle(r)
}
@ -293,6 +391,29 @@ func (n *mutFile) Open(req *fuse.OpenRequest, res *fuse.OpenResponse, intr fuse.
func (n *mutFile) Fsync(r *fuse.FsyncRequest, intr fuse.Intr) fuse.Error {
// TODO(adg): in the fuse package, plumb through fsync to mutFileHandle
// in the same way we did Truncate.
log.Printf("mutFile.Fsync: TODO")
return nil
}
func (n *mutFile) Setattr(req *fuse.SetattrRequest, res *fuse.SetattrResponse, intr fuse.Intr) fuse.Error {
log.Printf("mutFile.Setattr on %q: %#v", n.fullPath(), req)
// 2013/07/17 19:43:41 mutFile.Setattr on "foo": &fuse.SetattrRequest{Header:fuse.Header{Conn:(*fuse.Conn)(0xc210047180), ID:0x3, Node:0x3d, Uid:0xf0d4, Gid:0x1388, Pid:0x75e8}, Valid:0x30, Handle:0x0, Size:0x0, Atime:time.Time{sec:63509651021, nsec:0x4aec6b8, loc:(*time.Location)(0x47f7600)}, Mtime:time.Time{sec:63509651021, nsec:0x4aec6b8, loc:(*time.Location)(0x47f7600)}, Mode:0x4000000, Uid:0x0, Gid:0x0, Bkuptime:time.Time{sec:62135596800, nsec:0x0, loc:(*time.Location)(0x47f7600)}, Chgtime:time.Time{sec:62135596800, nsec:0x0, loc:(*time.Location)(0x47f7600)}, Crtime:time.Time{sec:0, nsec:0x0, loc:(*time.Location)(nil)}, Flags:0x0}
n.mu.Lock()
if req.Valid&fuse.SetattrMtime != 0 {
n.mtime = req.Mtime
}
if req.Valid&fuse.SetattrAtime != 0 {
n.atime = req.Atime
}
if req.Valid&fuse.SetattrSize != 0 {
// TODO(bradfitz): truncate?
n.size = int64(req.Size)
}
n.mu.Unlock()
res.AttrValid = 1 * time.Minute
res.Attr = n.Attr()
return nil
}
@ -321,10 +442,14 @@ func (n *mutFile) newHandle(body io.Reader) (fuse.Handle, fuse.Error) {
type mutFileHandle struct {
f *mutFile
tmp *os.File
written bool
}
func (h *mutFileHandle) Read(req *fuse.ReadRequest, res *fuse.ReadResponse, intr fuse.Intr) fuse.Error {
if h.tmp == nil {
log.Printf("Read called on camli mutFileHandle without a tempfile set")
return fuse.EIO
}
buf := make([]byte, req.Size)
n, err := h.tmp.ReadAt(buf, req.Offset)
if err == io.EOF {
@ -339,8 +464,13 @@ func (h *mutFileHandle) Read(req *fuse.ReadRequest, res *fuse.ReadResponse, intr
}
func (h *mutFileHandle) Write(req *fuse.WriteRequest, res *fuse.WriteResponse, intr fuse.Intr) fuse.Error {
h.written = true
if h.tmp == nil {
log.Printf("Write called on camli mutFileHandle without a tempfile set")
return fuse.EIO
}
n, err := h.tmp.WriteAt(req.Data, req.Offset)
log.Printf("mutFileHandle.Write(%q, at %d, flags %v, %q) = %d, %v", h.f.fullPath(), req.Offset, req.Flags, req.Data, n, err)
if err != nil {
log.Println("mutFileHandle.Write:", err)
return fuse.EIO
@ -350,27 +480,38 @@ func (h *mutFileHandle) Write(req *fuse.WriteRequest, res *fuse.WriteResponse, i
}
func (h *mutFileHandle) Release(req *fuse.ReleaseRequest, intr fuse.Intr) fuse.Error {
if h.written {
_, err := h.tmp.Seek(0, 0)
if err != nil {
log.Println("mutFileHandle.Release:", err)
return fuse.EIO
}
var n int64
br, err := schema.WriteFileFromReader(h.f.fs.client, h.f.name, readerutil.CountingReader{Reader: h.tmp, N: &n})
if err != nil {
log.Println("mutFileHandle.Release:", err)
return fuse.EIO
}
h.f.setContent(br, n)
if h.tmp == nil {
log.Printf("Release called on camli mutFileHandle without a tempfile set")
return fuse.EIO
}
log.Printf("mutFileHandle release.")
_, err := h.tmp.Seek(0, 0)
if err != nil {
log.Println("mutFileHandle.Release:", err)
return fuse.EIO
}
var n int64
br, err := schema.WriteFileFromReader(h.f.fs.client, h.f.name, readerutil.CountingReader{Reader: h.tmp, N: &n})
if err != nil {
log.Println("mutFileHandle.Release:", err)
return fuse.EIO
}
h.f.setContent(br, n)
h.tmp.Close()
os.Remove(h.tmp.Name())
h.tmp = nil
return nil
}
func (h *mutFileHandle) Truncate(size uint64, intr fuse.Intr) fuse.Error {
h.written = true
if h.tmp == nil {
log.Printf("Truncate called on camli mutFileHandle without a tempfile set")
return fuse.EIO
}
log.Printf("mutFileHandle.Truncate(%q) to size %d", h.f.fullPath(), size)
if err := h.tmp.Truncate(int64(size)); err != nil {
log.Println("mutFileHandle.Truncate:", err)
return fuse.EIO

Binary file not shown.

View File

@ -106,6 +106,7 @@ func (w *World) Start() error {
w.server.Dir = w.tempDir
w.server.Env = append(os.Environ(),
"CAMLI_ROOT="+w.tempDir,
"CAMLI_SECRET_RING="+filepath.Join(w.camRoot, filepath.FromSlash("pkg/jsonsign/testdata/test-secring.gpg")),
"CAMLI_BASE_URL=http://127.0.0.1:"+strconv.Itoa(w.port),
)
listenerFD, err := w.listener.(*net.TCPListener).File()