github.com/scaleoutsean/fusego@v0.0.0-20220224074057-4a6429e46bb8/samples/subprocess.go (about)

     1  // Copyright 2015 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package samples
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"flag"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"log"
    25  	"os"
    26  	"os/exec"
    27  	"path"
    28  	"sync"
    29  
    30  	"github.com/jacobsa/ogletest"
    31  )
    32  
    33  var fToolPath = flag.String(
    34  	"mount_sample",
    35  	"",
    36  	"Path to the mount_sample tool. If unset, we will compile it.")
    37  
    38  var fDebug = flag.Bool("debug", false, "If true, print fuse debug info.")
    39  
    40  // A struct that implements common behavior needed by tests in the samples/
    41  // directory where the file system is mounted by a subprocess. Use it as an
    42  // embedded field in your test fixture, calling its SetUp method from your
    43  // SetUp method after setting the MountType and MountFlags fields.
    44  type SubprocessTest struct {
    45  	// The type of the file system to mount. Must be recognized by mount_sample.
    46  	MountType string
    47  
    48  	// Additional flags to be passed to the mount_sample tool.
    49  	MountFlags []string
    50  
    51  	// A list of files to pass to mount_sample. The given string flag will be
    52  	// used to pass the file descriptor number.
    53  	MountFiles map[string]*os.File
    54  
    55  	// A context object that can be used for long-running operations.
    56  	Ctx context.Context
    57  
    58  	// The directory at which the file system is mounted.
    59  	Dir string
    60  
    61  	// Anothing non-nil in this slice will be closed by TearDown. The test will
    62  	// fail if closing fails.
    63  	ToClose []io.Closer
    64  
    65  	mountSampleErr <-chan error
    66  }
    67  
    68  // Mount the file system and initialize the other exported fields of the
    69  // struct. Panics on error.
    70  func (t *SubprocessTest) SetUp(ti *ogletest.TestInfo) {
    71  	err := t.initialize(ti.Ctx)
    72  	if err != nil {
    73  		panic(err)
    74  	}
    75  }
    76  
    77  // Private state for getToolPath.
    78  var getToolContents_Contents []byte
    79  var getToolContents_Err error
    80  var getToolContents_Once sync.Once
    81  
    82  // Implementation detail of getToolPath.
    83  func getToolContentsImpl() ([]byte, error) {
    84  	// Fast path: has the user set the flag?
    85  	if *fToolPath != "" {
    86  		contents, err := ioutil.ReadFile(*fToolPath)
    87  		if err != nil {
    88  			return nil, fmt.Errorf("Reading mount_sample contents: %v", err)
    89  		}
    90  
    91  		return contents, err
    92  	}
    93  
    94  	// Create a temporary directory into which we will compile the tool.
    95  	tempDir, err := ioutil.TempDir("", "sample_test")
    96  	if err != nil {
    97  		return nil, fmt.Errorf("TempDir: %v", err)
    98  	}
    99  
   100  	toolPath := path.Join(tempDir, "mount_sample")
   101  
   102  	// Ensure that we kill the temporary directory when we're finished here.
   103  	defer os.RemoveAll(tempDir)
   104  
   105  	// Run "go build".
   106  	cmd := exec.Command(
   107  		"go",
   108  		"build",
   109  		"-o",
   110  		toolPath,
   111  		"github.com/scaleoutsean/fusego/samples/mount_sample")
   112  
   113  	output, err := cmd.CombinedOutput()
   114  	if err != nil {
   115  		return nil, fmt.Errorf(
   116  			"mount_sample exited with %v, output:\n%s",
   117  			err,
   118  			string(output))
   119  	}
   120  
   121  	// Slurp the tool contents.
   122  	contents, err := ioutil.ReadFile(toolPath)
   123  	if err != nil {
   124  		return nil, fmt.Errorf("ReadFile: %v", err)
   125  	}
   126  
   127  	return contents, nil
   128  }
   129  
   130  // Build the mount_sample tool if it has not yet been built for this process.
   131  // Return its contents.
   132  func getToolContents() ([]byte, error) {
   133  	// Get hold of the binary contents, if we haven't yet.
   134  	getToolContents_Once.Do(func() {
   135  		getToolContents_Contents, getToolContents_Err = getToolContentsImpl()
   136  	})
   137  
   138  	return getToolContents_Contents, getToolContents_Err
   139  }
   140  
   141  func waitForMountSample(
   142  	cmd *exec.Cmd,
   143  	errChan chan<- error,
   144  	stderr *bytes.Buffer) {
   145  	// However we exit, write the error to the channel.
   146  	var err error
   147  	defer func() {
   148  		errChan <- err
   149  	}()
   150  
   151  	// Wait for the command.
   152  	err = cmd.Wait()
   153  	if err == nil {
   154  		return
   155  	}
   156  
   157  	// Make exit errors nicer.
   158  	if exitErr, ok := err.(*exec.ExitError); ok {
   159  		err = fmt.Errorf(
   160  			"mount_sample exited with %v. Stderr:\n%s",
   161  			exitErr,
   162  			stderr.String())
   163  
   164  		return
   165  	}
   166  
   167  	err = fmt.Errorf("Waiting for mount_sample: %v", err)
   168  }
   169  
   170  func waitForReady(readyReader *os.File, c chan<- struct{}) {
   171  	_, err := readyReader.Read(make([]byte, 1))
   172  	if err != nil {
   173  		log.Printf("Readying from ready pipe: %v", err)
   174  		return
   175  	}
   176  
   177  	c <- struct{}{}
   178  }
   179  
   180  // Like SetUp, but doens't panic.
   181  func (t *SubprocessTest) initialize(ctx context.Context) error {
   182  	// Initialize the context.
   183  	t.Ctx = ctx
   184  
   185  	// Set up a temporary directory.
   186  	var err error
   187  	t.Dir, err = ioutil.TempDir("", "sample_test")
   188  	if err != nil {
   189  		return fmt.Errorf("TempDir: %v", err)
   190  	}
   191  
   192  	// Build/read the mount_sample tool.
   193  	toolContents, err := getToolContents()
   194  	if err != nil {
   195  		return fmt.Errorf("getTooltoolContents: %v", err)
   196  	}
   197  
   198  	// Create a temporary file to hold the contents of the tool.
   199  	toolFile, err := ioutil.TempFile("", "sample_test")
   200  	if err != nil {
   201  		return fmt.Errorf("TempFile: %v", err)
   202  	}
   203  
   204  	defer toolFile.Close()
   205  
   206  	// Ensure that it is deleted when we leave.
   207  	toolPath := toolFile.Name()
   208  	defer os.Remove(toolPath)
   209  
   210  	// Write out the tool contents and make them executable.
   211  	if _, err = toolFile.Write(toolContents); err != nil {
   212  		return fmt.Errorf("toolFile.Write: %v", err)
   213  	}
   214  
   215  	if err = toolFile.Chmod(0500); err != nil {
   216  		return fmt.Errorf("toolFile.Chmod: %v", err)
   217  	}
   218  
   219  	// Close the tool file to prevent "text file busy" errors below.
   220  	err = toolFile.Close()
   221  	toolFile = nil
   222  	if err != nil {
   223  		return fmt.Errorf("toolFile.Close: %v", err)
   224  	}
   225  
   226  	// Set up basic args for the subprocess.
   227  	args := []string{
   228  		"--type",
   229  		t.MountType,
   230  		"--mount_point",
   231  		t.Dir,
   232  	}
   233  
   234  	args = append(args, t.MountFlags...)
   235  
   236  	// Set up a pipe for the "ready" status.
   237  	readyReader, readyWriter, err := os.Pipe()
   238  	if err != nil {
   239  		return fmt.Errorf("Pipe: %v", err)
   240  	}
   241  
   242  	defer readyReader.Close()
   243  	defer readyWriter.Close()
   244  
   245  	t.MountFiles["ready_file"] = readyWriter
   246  
   247  	// Set up inherited files and appropriate flags.
   248  	var extraFiles []*os.File
   249  	for flag, file := range t.MountFiles {
   250  		// Cf. os/exec.Cmd.ExtraFiles
   251  		fd := 3 + len(extraFiles)
   252  
   253  		extraFiles = append(extraFiles, file)
   254  		args = append(args, "--"+flag)
   255  		args = append(args, fmt.Sprintf("%d", fd))
   256  	}
   257  
   258  	// Set up a command.
   259  	var stderr bytes.Buffer
   260  	mountCmd := exec.Command(toolPath, args...)
   261  	mountCmd.Stderr = &stderr
   262  	mountCmd.ExtraFiles = extraFiles
   263  
   264  	// Handle debug mode.
   265  	if *fDebug {
   266  		mountCmd.Stderr = os.Stderr
   267  		mountCmd.Args = append(mountCmd.Args, "--debug")
   268  	}
   269  
   270  	// Start the command.
   271  	if err := mountCmd.Start(); err != nil {
   272  		return fmt.Errorf("mountCmd.Start: %v", err)
   273  	}
   274  
   275  	// Launch a goroutine that waits for it and returns its status.
   276  	mountSampleErr := make(chan error, 1)
   277  	go waitForMountSample(mountCmd, mountSampleErr, &stderr)
   278  
   279  	// Wait for the tool to say the file system is ready. In parallel, watch for
   280  	// the tool to fail.
   281  	readyChan := make(chan struct{}, 1)
   282  	go waitForReady(readyReader, readyChan)
   283  
   284  	select {
   285  	case <-readyChan:
   286  	case err := <-mountSampleErr:
   287  		return err
   288  	}
   289  
   290  	// TearDown is no responsible for joining.
   291  	t.mountSampleErr = mountSampleErr
   292  
   293  	return nil
   294  }
   295  
   296  // Unmount the file system and clean up. Panics on error.
   297  func (t *SubprocessTest) TearDown() {
   298  	err := t.destroy()
   299  	if err != nil {
   300  		panic(err)
   301  	}
   302  }
   303  
   304  // Like TearDown, but doesn't panic.
   305  func (t *SubprocessTest) destroy() (err error) {
   306  	// Make sure we clean up after ourselves after everything else below.
   307  
   308  	// Close what is necessary.
   309  	for _, c := range t.ToClose {
   310  		if c == nil {
   311  			continue
   312  		}
   313  
   314  		ogletest.ExpectEq(nil, c.Close())
   315  	}
   316  
   317  	// If we didn't try to mount the file system, there's nothing further to do.
   318  	if t.mountSampleErr == nil {
   319  		return nil
   320  	}
   321  
   322  	// In the background, initiate an unmount.
   323  	unmountErrChan := make(chan error)
   324  	go func() {
   325  		unmountErrChan <- unmount(t.Dir)
   326  	}()
   327  
   328  	// Make sure we wait for the unmount, even if we've already returned early in
   329  	// error. Return its error if we haven't seen any other error.
   330  	defer func() {
   331  		// Wait.
   332  		unmountErr := <-unmountErrChan
   333  		if unmountErr != nil {
   334  			if err != nil {
   335  				log.Println("unmount:", unmountErr)
   336  				return
   337  			}
   338  
   339  			err = fmt.Errorf("unmount: %v", unmountErr)
   340  			return
   341  		}
   342  
   343  		// Clean up.
   344  		ogletest.ExpectEq(nil, os.Remove(t.Dir))
   345  	}()
   346  
   347  	// Wait for the subprocess.
   348  	if err := <-t.mountSampleErr; err != nil {
   349  		return err
   350  	}
   351  
   352  	return nil
   353  }