d361f6e35b
The tests check if they leak fds themselves, but we also check if gocryptfs leaks fds. Clarify what is what in the error message.
246 lines
6.5 KiB
Go
246 lines
6.5 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.
|
|
//
|
|
// Contrary to InitFS(), you MUST passt "-extpass=echo test" (or another way for
|
|
// getting the master key) explicitely.
|
|
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(2 * 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.
|
|
//
|
|
// Contrary to InitFS(), you MUST passt "-extpass=echo test" (or another way for
|
|
// getting the master key) explicitely.
|
|
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.
|
|
//
|
|
// Contrary to InitFS(), you MUST passt "-extpass=echo test" (or another way for
|
|
// getting the master key) explicitely.
|
|
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())
|
|
}
|
|
}
|
|
|
|
// gocryptfs may hold up to maxCacheFds open for caching
|
|
// Keep in sync with fusefrontend.dirCacheSize
|
|
// TODO: How to share this constant without causing an import cycle?!
|
|
const maxCacheFds = 3
|
|
|
|
// 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 && runtime.GOOS == "linux" {
|
|
// The FD leak check only works on Linux.
|
|
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 {
|
|
for j := 1; j <= max; j++ {
|
|
// File close on FUSE is asynchronous, closing a socket
|
|
// when testing "-ctlsock" is as well. Wait a little and
|
|
// hope that all close commands get through to the gocryptfs
|
|
// process.
|
|
fdsNow = ListFds(pid)
|
|
if len(fdsNow) <= len(fds)+maxCacheFds {
|
|
break
|
|
}
|
|
fmt.Printf("UnmountErr: fdsOld=%d fdsNow=%d, retrying\n", len(fds), len(fdsNow))
|
|
time.Sleep(10 * 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)+maxCacheFds {
|
|
return fmt.Errorf("fd leak in gocryptfs process? 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 {
|
|
fmt.Printf("ListFds: %v\n", err)
|
|
return nil
|
|
}
|
|
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
|
|
}
|