From 98eb69b5e1112a190cbffa55156211711022a4cc Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 21 Jul 2013 12:26:05 -0700 Subject: [PATCH] fs: bunch of read-write FUSE work, debugging, and integration tests. Change-Id: I74807f693720effb7ae8405259797331f79f59fd --- cmd/cammount/cammount.go | 2 +- pkg/fs/fs_test.go | 221 +++++++++++++++++++++++++++ pkg/fs/mut.go | 207 +++++++++++++++++++++---- pkg/test/testdata/server-config.json | Bin 1469 -> 1708 bytes pkg/test/world.go | 1 + 5 files changed, 397 insertions(+), 34 deletions(-) create mode 100644 pkg/fs/fs_test.go diff --git a/cmd/cammount/cammount.go b/cmd/cammount/cammount.go index 9e7267a0a..e899bc936 100644 --- a/cmd/cammount/cammount.go +++ b/cmd/cammount/cammount.go @@ -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) { diff --git a/pkg/fs/fs_test.go b/pkg/fs/fs_test.go new file mode 100644 index 000000000..2f3f4d4c8 --- /dev/null +++ b/pkg/fs/fs_test.go @@ -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 + } +} diff --git a/pkg/fs/mut.go b/pkg/fs/mut.go index 37979f5e2..23e2b0002 100644 --- a/pkg/fs/mut.go +++ b/pkg/fs/mut.go @@ -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 diff --git a/pkg/test/testdata/server-config.json b/pkg/test/testdata/server-config.json index f495e6c88ced1c7b67899a05d1f2aee95756145a..c18b4ca8a4535e0b273897b25bc38cbbe9e787e2 100644 GIT binary patch delta 184 zcmdnXy@q!~2#c_Sl74Y!dPZtaL28k{l9fU=m%_vgq8wSp`FTL`yvYhoy7k4W$wjFp zL792!Kn2lC@u_)bN;(QkD%H-8zCNDu!LH6ht|9S3o__ANO0haz3MfD+JGIg?1*lQU z$jr^u(aFV82}4OiX;MyRvUh5wOKNcmNU45OvAz<_pvkM4xhCIblF0}FGCVPc delta 12 TcmZ3(yO(=I2+L+?)=WkK9l-