rclone/vfs/vfstest/submount.go
Nick Craig-Wood 4a382c09ec mount: run tests in a subprocess to fix deadlock - fixes #3259
Before this change we ran the tests and the mount in the same process.
This could cause deadlocks and often did, and made the mount tests
very unreliable.

This fixes the problem by running the mount in a seperate process and
commanding it via a pipe over stdin/stdout.
2022-06-16 16:48:09 +01:00

277 lines
6.6 KiB
Go

package vfstest
import (
"bufio"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/rclone/rclone/cmd/mountlib"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/cache"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/lib/file"
"github.com/rclone/rclone/vfs"
"github.com/rclone/rclone/vfs/vfscommon"
)
// Functions to run and control the mount subprocess
var (
runMount = flag.String("run-mount", "", "If set, run the mount subprocess with the options (internal use only)")
)
// Options for the mount sub processes passed with the -run-mount flag
type runMountOpt struct {
MountPoint string
MountOpt mountlib.Options
VFSOpt vfscommon.Options
Remote string
}
// Start the mount subprocess and wait for it to start
func (r *Run) startMountSubProcess() {
// If testing the VFS we don't start a subprocess, we just use
// the VFS directly
if r.useVFS {
vfs := vfs.New(r.fremote, r.vfsOpt)
r.os = vfsOs{vfs}
return
}
r.os = realOs{}
r.mountPath = findMountPath()
log.Printf("startMountSubProcess %q (%q) %q", r.fremote, r.fremoteName, r.mountPath)
opt := runMountOpt{
MountPoint: r.mountPath,
MountOpt: mountlib.Opt,
VFSOpt: *r.vfsOpt,
Remote: r.fremoteName,
}
opts, err := json.Marshal(&opt)
if err != nil {
log.Fatal(err)
}
// Re-run this executable with a new option -run-mount
args := append(os.Args, "-run-mount", string(opts))
r.cmd = exec.Command(args[0], args[1:]...)
r.cmd.Stderr = os.Stderr
r.out, err = r.cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
r.in, err = r.cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
err = r.cmd.Start()
if err != nil {
log.Fatal("startMountSubProcess failed", err)
}
r.scanner = bufio.NewScanner(r.in)
// Wait it for startup
log.Print("Waiting for mount to start")
for r.scanner.Scan() {
rx := strings.TrimSpace(r.scanner.Text())
if rx == "STARTED" {
break
}
log.Printf("..Mount said: %s", rx)
}
if r.scanner.Err() != nil {
log.Printf("scanner err %v", r.scanner.Err())
}
log.Printf("startMountSubProcess: end")
}
// Find a free path to run the mount on
func findMountPath() string {
if runtime.GOOS != "windows" {
mountPath, err := ioutil.TempDir("", "rclonefs-mount")
if err != nil {
log.Fatalf("Failed to create mount dir: %v", err)
}
return mountPath
}
// Find a free drive letter
letter := file.FindUnusedDriveLetter()
drive := ""
if letter == 0 {
log.Fatalf("Couldn't find free drive letter for test")
} else {
drive = string(letter) + ":"
}
return drive
}
// Return true if we are running as a subprocess to run the mount
func isSubProcess() bool {
return *runMount != ""
}
// Run the mount - this is running in a subprocesses and the config
// is passed JSON encoded as the -run-mount parameter
//
// It reads commands from standard input and writes results to
// standard output.
func startMount(mountFn mountlib.MountFn, useVFS bool, opts string) {
log.Print("startMount")
ctx := context.Background()
var opt runMountOpt
err := json.Unmarshal([]byte(opts), &opt)
if err != nil {
log.Fatalf("Unmarshal failed: %v", err)
}
fstest.Initialise()
f, err := cache.Get(ctx, opt.Remote)
if err != nil {
log.Fatalf("Failed to open remote %q: %v", opt.Remote, err)
}
err = f.Mkdir(ctx, "")
if err != nil {
log.Fatalf("Failed to mkdir %q: %v", opt.Remote, err)
}
log.Printf("startMount: Mounting %q on %q with %q", opt.Remote, opt.MountPoint, opt.VFSOpt.CacheMode)
mnt := mountlib.NewMountPoint(mountFn, opt.MountPoint, f, &opt.MountOpt, &opt.VFSOpt)
_, err = mnt.Mount()
if err != nil {
log.Fatalf("mount FAILED %q: %v", opt.Remote, err)
}
defer umount(mnt)
log.Printf("startMount: mount OK")
fmt.Println("STARTED") // signal to parent all is good
// Read commands from stdin
scanner := bufio.NewScanner(os.Stdin)
exit := false
for !exit && scanner.Scan() {
rx := strings.Trim(scanner.Text(), "\r\n")
var tx string
tx, exit = doMountCommand(mnt.VFS, rx)
fmt.Println(tx)
}
err = scanner.Err()
if err != nil {
log.Fatalf("scanner failed %q: %v", opt.Remote, err)
}
}
// Do a mount command which is a line read from stdin and return a
// line to send to stdout with an exit flag.
//
// The format of the lines is
// command \t parameter (optional)
// The response should be
// OK|ERR \t result (optional)
func doMountCommand(vfs *vfs.VFS, rx string) (tx string, exit bool) {
command := strings.Split(rx, "\t")
// log.Printf("doMountCommand: %q received", command)
var out = []string{"OK", ""}
switch command[0] {
case "waitForWriters":
vfs.WaitForWriters(waitForWritersDelay)
case "forget":
root, err := vfs.Root()
if err != nil {
out = []string{"ERR", err.Error()}
} else {
root.ForgetPath(command[1], fs.EntryDirectory)
}
case "exit":
exit = true
default:
out = []string{"ERR", "command not found"}
}
return strings.Join(out, "\t"), exit
}
// Send a command to the mount subprocess and await a response
func (r *Run) sendMountCommand(args ...string) {
r.cmdMu.Lock()
defer r.cmdMu.Unlock()
tx := strings.Join(args, "\t")
// log.Printf("Send mount command: %q", tx)
var rx string
if r.useVFS {
// if using VFS do the VFS command directly
rx, _ = doMountCommand(r.os.(vfsOs).VFS, tx)
} else {
_, err := io.WriteString(r.out, tx+"\n")
if err != nil {
log.Fatalf("WriteString err %v", err)
}
if !r.scanner.Scan() {
log.Fatalf("Mount has gone away")
}
rx = strings.Trim(r.scanner.Text(), "\r\n")
}
in := strings.Split(rx, "\t")
// log.Printf("Answer is %q", in)
if in[0] != "OK" {
log.Fatalf("Error from mount: %q", in[1:])
}
}
// wait for any files being written to be released by fuse
func (r *Run) waitForWriters() {
r.sendMountCommand("waitForWriters")
}
// forget the directory passed in
func (r *Run) forget(dir string) {
r.sendMountCommand("forget", dir)
}
// Unmount the mount
func umount(mnt *mountlib.MountPoint) {
/*
log.Printf("Calling fusermount -u %q", mountPath)
err := exec.Command("fusermount", "-u", mountPath).Run()
if err != nil {
log.Printf("fusermount failed: %v", err)
}
*/
log.Printf("Unmounting %q", mnt.MountPoint)
err := mnt.Unmount()
if err != nil {
log.Printf("signal to umount failed - retrying: %v", err)
time.Sleep(3 * time.Second)
err = mnt.Unmount()
}
if err != nil {
log.Fatalf("signal to umount failed: %v", err)
}
log.Printf("Waiting for umount")
err = <-mnt.ErrChan
if err != nil {
log.Fatalf("umount failed: %v", err)
}
// Cleanup the VFS cache - umount has called Shutdown
err = mnt.VFS.CleanUp()
if err != nil {
log.Printf("Failed to cleanup the VFS cache: %v", err)
}
}