libgocryptfs/tests/test_helpers/mount_unmount.go
Jakob Unterwurzacher 772afa93f9 tests: add fd leak retry logic to UnmountErr, really return error
Give the gocryptfs process one extra millisecond to close
files. Allows us to drop several other sleeps.

UnmountErr now really returns an error when it detects an fd leak
instead of just printing a message.
2019-01-02 01:09:09 +01:00

226 lines
5.6 KiB
Go

package test_helpers
import (
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"syscall"
"testing"
"time"
)
// Indexed by mountpoint. Initialized in doInit().
var MountInfo map[string]mountInfo
type mountInfo struct {
// PID of the running gocryptfs process. Set by Mount().
Pid int
// List of open FDs of the running gocrypts process. Set by Mount().
Fds []string
}
// Mount CIPHERDIR "c" on PLAINDIR "p"
// Creates "p" if it does not exist.
func Mount(c string, p string, showOutput bool, extraArgs ...string) error {
args := []string{"-q", "-wpanic", "-nosyslog", "-fg", fmt.Sprintf("-notifypid=%d", os.Getpid())}
args = append(args, extraArgs...)
//args = append(args, "-fusedebug")
//args = append(args, "-d")
args = append(args, c, p)
if _, err := os.Stat(p); err != nil {
err = os.Mkdir(p, 0777)
if err != nil {
return err
}
}
cmd := exec.Command(GocryptfsBinary, args...)
if showOutput {
// The Go test logic waits for our stdout to close, and when we share
// it with the subprocess, it will wait for it to close it as well.
// Use an intermediate pipe so the tests do not hang when unmouting
// fails.
pr, pw, err := os.Pipe()
if err != nil {
return err
}
// We can close the fd after cmd.Run() has executed
defer pw.Close()
cmd.Stderr = pw
cmd.Stdout = pw
go func() {
io.Copy(os.Stdout, pr)
pr.Close()
}()
}
// Two things can happen:
// 1) The mount fails and the process exits
// 2) The mount succeeds and the process sends us USR1
chanExit := make(chan error, 1)
chanUsr1 := make(chan os.Signal, 1)
signal.Notify(chanUsr1, syscall.SIGUSR1)
// Start the process and save the PID
err := cmd.Start()
if err != nil {
return err
}
pid := cmd.Process.Pid
// Wait for exit or usr1
go func() {
chanExit <- cmd.Wait()
}()
select {
case err := <-chanExit:
return err
case <-chanUsr1:
// noop
case <-time.After(1 * time.Second):
log.Panicf("Timeout waiting for process %d", pid)
}
// Save PID and open FDs
MountInfo[p] = mountInfo{pid, ListFds(pid)}
return nil
}
// MountOrExit calls Mount() and exits on failure.
func MountOrExit(c string, p string, extraArgs ...string) {
err := Mount(c, p, true, extraArgs...)
if err != nil {
fmt.Printf("mount failed: %v\n", err)
os.Exit(1)
}
}
// MountOrFatal calls Mount() and calls t.Fatal() on failure.
func MountOrFatal(t *testing.T, c string, p string, extraArgs ...string) {
err := Mount(c, p, true, extraArgs...)
if err != nil {
t.Fatal(fmt.Errorf("mount failed: %v", err))
}
}
// UnmountPanic tries to umount "dir" and panics on error.
func UnmountPanic(dir string) {
err := UnmountErr(dir)
if err != nil {
fmt.Printf("UnmountPanic: %v. Running lsof %s\n", err, dir)
cmd := exec.Command("lsof", dir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
panic("UnmountPanic: unmount failed: " + err.Error())
}
}
// UnmountErr tries to unmount "dir", retrying 10 times, and returns the
// resulting error.
func UnmountErr(dir string) (err error) {
var fdsNow []string
pid := MountInfo[dir].Pid
fds := MountInfo[dir].Fds
if pid <= 0 {
fmt.Printf("UnmountErr: %q was not found in MountInfo, cannot check for FD leaks\n", dir)
}
max := 10
// When a new filesystem is mounted, Gnome tries to read files like
// .xdg-volume-info, autorun.inf, .Trash.
// If we try to unmount before Gnome is done, the unmount fails with
// "Device or resource busy", causing spurious test failures.
// Retry a few times to hide that problem.
for i := 1; i <= max; i++ {
if pid > 0 {
fdsNow = ListFds(pid)
if len(fdsNow) > len(fds) {
// File close on FUSE is asynchronous, closing a socket
// when testing -ctlsock as well. Wait one extra millisecond
// and hope that all close commands get through to the gocryptfs
// process.
time.Sleep(1 * time.Millisecond)
fdsNow = ListFds(pid)
}
}
cmd := exec.Command(UnmountScript, "-u", dir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err == nil {
if len(fdsNow) > len(fds) {
return fmt.Errorf("FD leak? pid=%d dir=%q, fds:\nold=%v \nnew=%v\n", pid, dir, fds, fdsNow)
}
return nil
}
code := ExtractCmdExitCode(err)
fmt.Printf("UnmountErr: got exit code %d, retrying (%d/%d)\n", code, i, max)
time.Sleep(100 * time.Millisecond)
}
return err
}
// ListFds lists the open file descriptors for process "pid". Pass pid=0 for
// ourselves.
func ListFds(pid int) []string {
// We need /proc to get the list of fds for other processes. Only exists
// on Linux.
if runtime.GOOS != "linux" && pid > 0 {
return nil
}
// Both Linux and MacOS have /dev/fd
dir := "/dev/fd"
if pid > 0 {
dir = fmt.Sprintf("/proc/%d/fd", pid)
}
f, err := os.Open(dir)
if err != nil {
log.Panic(err)
}
defer f.Close()
// Note: Readdirnames filters "." and ".."
names, err := f.Readdirnames(0)
if err != nil {
log.Panic(err)
}
var out []string
hidden := 0
for _, n := range names {
fdPath := dir + "/" + n
fi, err := os.Lstat(fdPath)
if err != nil {
// fd was closed in the meantime
continue
}
if fi.Mode()&0400 > 0 {
n += "r"
}
if fi.Mode()&0200 > 0 {
n += "w"
}
target, err := os.Readlink(fdPath)
if err != nil {
// fd was closed in the meantime
continue
}
if strings.HasPrefix(target, "pipe:") || strings.HasPrefix(target, "anon_inode:[eventpoll]") {
// The Go runtime creates pipes on demand for splice(), which
// creates spurious test failures. Ignore all pipes.
// Also get rid of the "eventpoll" fd that is always there and not
// interesting.
hidden++
continue
}
out = append(out, n+"="+target)
}
out = append(out, fmt.Sprintf("(hidden:%d)", hidden))
return out
}