package utils
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"golang.org/x/sys/unix"
)
const (
startupDeadlineSeconds = 10
shutdownDeadlineSeconds = 5
)
type runState struct {
cmd *exec.Cmd
out bytes.Buffer
err bytes.Buffer
}
func setRustEnv(cmd *exec.Cmd) {
if cmd.Env == nil {
cmd.Env = os.Environ()
}
cmd.Env = append(cmd.Env, "RUST_BACKTRACE=full")
cmd.Env = append(cmd.Env, "RUST_LOG=info")
}
func run(arg ...string) (*runState, error) {
bin := GetConfig().SandboxfsBinary
var state runState
state.cmd = exec.Command(bin, arg...)
state.cmd.Stdout = &state.out
state.cmd.Stderr = &state.err
setRustEnv(state.cmd)
if err := state.cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start %s with arguments %v: %v", bin, arg, err)
}
return &state, nil
}
func wait(state *runState, wantExitStatus int) (string, string, error) {
err := state.cmd.Wait()
if wantExitStatus == 0 {
if err != nil {
return state.out.String(), state.err.String(), fmt.Errorf("got %v; want sandboxfs to exit with status 0", err)
}
} else {
if err == nil {
return state.out.String(), state.err.String(), fmt.Errorf("got 0; want sandboxfs to exit with status %d", wantExitStatus)
}
status := err.(*exec.ExitError).ProcessState.Sys().(syscall.WaitStatus)
if wantExitStatus != status.ExitStatus() {
return state.out.String(), state.err.String(), fmt.Errorf("got %v; want sandboxfs to exit with status %d", status.ExitStatus(), wantExitStatus)
}
}
return state.out.String(), state.err.String(), nil
}
func RunAndWait(wantExitStatus int, arg ...string) (string, string, error) {
state, err := run(arg...)
if err != nil {
return "", "", err
}
return wait(state, wantExitStatus)
}
func retry(action func() error, message string, deadlineSeconds int) error {
var lastErr error
for tries := 0; tries < deadlineSeconds*10; tries++ {
lastErr = action()
if lastErr == nil {
return nil
}
if tries > 10 {
fmt.Fprintf(os.Stderr, "In retry attempt %d: %s: %v\n", tries, message, lastErr)
}
time.Sleep(100 * time.Millisecond)
}
return lastErr
}
func startBackground(cookie string, stdout io.Writer, stderr io.Writer, user *UnixUser, args ...string) (*exec.Cmd, io.WriteCloser, error) {
bin := GetConfig().SandboxfsBinary
mountPoint := args[len(args)-1]
cmd := exec.Command(bin, args...)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, nil, fmt.Errorf("failed to create stdin pipe: %v", err)
}
cmd.Stdout = stdout
cmd.Stderr = stderr
SetCredential(cmd, user)
setRustEnv(cmd)
if err := cmd.Start(); err != nil {
return nil, nil, fmt.Errorf("failed to start %s with arguments %v: %v", bin, args, err)
}
if cookie != "" {
cookiePath := filepath.Join(mountPoint, cookie)
waitForCookie := func() error { return FileExistsAsUser(cookiePath, user) }
if err := retry(waitForCookie, "waiting for cookie to appear in mount point", startupDeadlineSeconds); err != nil {
stdin.Close()
cmd.Process.Kill()
cmd.Wait()
Unmount(mountPoint)
return nil, nil, fmt.Errorf("file system failed to come up: %s not found", cookiePath)
}
}
return cmd, stdin, nil
}
type MountState struct {
Cmd *exec.Cmd
Stdin io.WriteCloser
stdout *bytes.Buffer
stderr *bytes.Buffer
tempDir string
root string
mountPoint string
oldMask int
}
func (s *MountState) MountPath(arg ...string) string {
return filepath.Join(s.mountPoint, filepath.Join(arg...))
}
func (s *MountState) RootPath(arg ...string) string {
return filepath.Join(s.root, filepath.Join(arg...))
}
func (s *MountState) TempPath(arg ...string) string {
return filepath.Join(s.tempDir, filepath.Join(arg...))
}
func createDirsRequiredByMappings(root string, args ...string) error {
for _, arg := range args {
if !strings.HasPrefix(arg, "--mapping=") {
continue }
fields := strings.Split(arg, ":")
if len(fields) != 3 {
panic(fmt.Sprintf("recognized a mapping but found more fields than expected: %v", fields))
}
dir := fields[2]
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to mkdir %s: %v", dir, err)
}
}
return nil
}
func hasRootMapping(args ...string) bool {
for _, arg := range args {
if strings.HasPrefix(arg, "--mapping=ro:/:") || strings.HasPrefix(arg, "--mapping=rw:/:") {
return true
}
}
return false
}
func MountSetup(t *testing.T, args ...string) *MountState {
t.Helper()
return mountSetupFull(t, os.Stdout, os.Stderr, nil, nil, args...)
}
func MountSetupWithRootSetup(t *testing.T, rootSetup func(string) error, args ...string) *MountState {
t.Helper()
return mountSetupFull(t, os.Stdout, os.Stderr, nil, rootSetup, args...)
}
func MountSetupWithOutputs(t *testing.T, stdout io.Writer, stderr io.Writer, args ...string) *MountState {
t.Helper()
return mountSetupFull(t, stdout, stderr, nil, nil, args...)
}
func MountSetupWithUser(t *testing.T, user *UnixUser, args ...string) *MountState {
t.Helper()
return mountSetupFull(t, os.Stdout, os.Stderr, user, nil, args...)
}
func mountSetupFull(t *testing.T, stdout io.Writer, stderr io.Writer, user *UnixUser, rootSetup func(string) error, args ...string) *MountState {
t.Helper()
success := false
oldMask := unix.Umask(0)
defer func() {
if !success {
unix.Umask(oldMask)
}
}()
tempDir, err := ioutil.TempDir("", "test")
if err != nil {
t.Fatalf("Failed to create temporary directory: %v", err)
}
defer func() {
if !success {
os.RemoveAll(tempDir)
}
}()
root := filepath.Join(tempDir, "root")
mountPoint := filepath.Join(tempDir, "mnt")
MustMkdirAll(t, root, 0755)
MustMkdirAll(t, mountPoint, 0755)
if user != nil {
if err := os.Chmod(tempDir, 0755); err != nil {
t.Fatalf("Failed to change permissions of %s", tempDir)
}
if err := os.Chown(mountPoint, user.UID, user.GID); err != nil {
t.Fatalf("Failed to change ownership of %s", mountPoint)
}
}
realArgs := make([]string, 0, len(args)+1)
for _, arg := range args {
realArgs = append(realArgs, strings.Replace(arg, "%ROOT%", root, -1))
}
realArgs = append(realArgs, mountPoint)
if err := createDirsRequiredByMappings(root, realArgs...); err != nil {
t.Fatalf("Failed to create directories required by mappings: %v", err)
}
if rootSetup != nil {
if err := rootSetup(root); err != nil {
t.Fatalf("Failed to run custom rootSetup hook on %s: %v", root, err)
}
}
var storedStdout *bytes.Buffer
if stdout == os.Stdout {
storedStdout = new(bytes.Buffer)
stdout = storedStdout
}
var storedStderr *bytes.Buffer
if stderr == os.Stderr {
storedStderr = new(bytes.Buffer)
stderr = storedStderr
}
var cmd *exec.Cmd
var stdin io.WriteCloser
if !hasRootMapping(realArgs...) {
cmd, stdin, err = startBackground("", stdout, stderr, user, realArgs...)
} else {
MustWriteFile(t, filepath.Join(root, ".cookie"), 0444, "")
cmd, stdin, err = startBackground(".cookie", stdout, stderr, user, realArgs...)
if err := os.Remove(filepath.Join(root, ".cookie")); err != nil {
t.Errorf("Failed to delete the startup cookie file: %v", err)
}
if err != nil {
t.Fatalf("Failed to start sandboxfs: %v", err)
}
}
success = true
state := &MountState{
Cmd: cmd,
Stdin: stdin,
stdout: storedStdout,
stderr: storedStderr,
tempDir: tempDir,
root: root,
mountPoint: mountPoint,
oldMask: oldMask,
}
return state
}
func (s *MountState) TearDown(t *testing.T) error {
t.Helper()
var firstErr error
setFirstErr := func(err error) {
if firstErr == nil {
firstErr = err
}
}
unix.Umask(s.oldMask)
if s.Stdin != nil {
if err := s.Stdin.Close(); err != nil {
t.Errorf("Failed to close sandboxfs's stdin pipe: %v", err)
setFirstErr(err)
}
s.Stdin = nil
}
if s.Cmd != nil {
unmount := func() error { return Unmount(s.mountPoint) }
if err := retry(unmount, "waiting for file system to be unmounted", shutdownDeadlineSeconds); err != nil {
t.Errorf("Failed to unmount sandboxfs instance during teardown: %v", err)
setFirstErr(err)
}
timer := time.AfterFunc(shutdownDeadlineSeconds*time.Second, func() {
s.Cmd.Process.Kill()
})
err := s.Cmd.Wait()
timer.Stop()
if err != nil {
t.Errorf("sandboxfs did not exit successfully during teardown: %v", err)
setFirstErr(err)
}
s.Cmd = nil
}
if t.Failed() {
if s.stdout != nil {
fmt.Fprintf(os.Stderr, "sandboxfs stdout was:\n%s", s.stdout.String())
}
if s.stderr != nil {
fmt.Fprintf(os.Stderr, "sandboxfs stderr was:\n%s", s.stderr.String())
}
}
if err := os.RemoveAll(s.tempDir); err != nil {
t.Errorf("Failed to remove temporary directory %s during teardown: %v", s.tempDir, err)
setFirstErr(err)
}
return firstErr
}