github.com/mutagen-io/mutagen@v0.18.0-rc1/cmd/mutagen/project/run.go (about)

     1  package project
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  
    11  	"github.com/spf13/cobra"
    12  
    13  	"github.com/mutagen-io/mutagen/pkg/filesystem/locking"
    14  	"github.com/mutagen-io/mutagen/pkg/identifier"
    15  	"github.com/mutagen-io/mutagen/pkg/project"
    16  )
    17  
    18  // runMain is the entry point for the run command.
    19  func runMain(_ *cobra.Command, arguments []string) error {
    20  	// Validate arguments.
    21  	var commandName string
    22  	if len(arguments) == 0 {
    23  		return errors.New("missing command name")
    24  	} else if len(arguments) > 1 {
    25  		return errors.New("invalid number of arguments")
    26  	} else {
    27  		commandName = arguments[0]
    28  	}
    29  
    30  	// Compute the name of the configuration file and ensure that our working
    31  	// directory is that in which the file resides. This is required for
    32  	// relative paths (including relative synchronization paths and relative
    33  	// Unix Domain Socket paths) to be resolved relative to the project
    34  	// configuration file.
    35  	configurationFileName := project.DefaultConfigurationFileName
    36  	if runConfiguration.projectFile != "" {
    37  		var directory string
    38  		directory, configurationFileName = filepath.Split(runConfiguration.projectFile)
    39  		if directory != "" {
    40  			if err := os.Chdir(directory); err != nil {
    41  				return fmt.Errorf("unable to switch to target directory: %w", err)
    42  			}
    43  		}
    44  	}
    45  
    46  	// Compute the lock path.
    47  	lockPath := configurationFileName + project.LockFileExtension
    48  
    49  	// Track whether or not we should remove the lock file on return.
    50  	var removeLockFileOnReturn bool
    51  
    52  	// Create a locker and defer its closure and potential removal. On Windows
    53  	// systems, we have to handle this removal after the file is closed.
    54  	locker, err := locking.NewLocker(lockPath, 0600)
    55  	if err != nil {
    56  		return fmt.Errorf("unable to create project locker: %w", err)
    57  	}
    58  	defer func() {
    59  		locker.Close()
    60  		if removeLockFileOnReturn && runtime.GOOS == "windows" {
    61  			os.Remove(lockPath)
    62  		}
    63  	}()
    64  
    65  	// Acquire the project lock and defer its release and potential removal. On
    66  	// Windows systems, we can't remove the lock file if it's locked or even
    67  	// just opened, so we handle removal for Windows systems after we close the
    68  	// lock file (see above). In this case, we truncate the lock file before
    69  	// releasing it to ensure that any other process that opens or acquires the
    70  	// lock file before we manage to remove it will simply see an empty lock
    71  	// file, which it will ignore or attempt to remove.
    72  	if err := locker.Lock(true); err != nil {
    73  		return fmt.Errorf("unable to acquire project lock: %w", err)
    74  	}
    75  	defer func() {
    76  		if removeLockFileOnReturn {
    77  			if runtime.GOOS == "windows" {
    78  				locker.Truncate(0)
    79  			} else {
    80  				os.Remove(lockPath)
    81  			}
    82  		}
    83  		locker.Unlock()
    84  	}()
    85  
    86  	// Read the project identifier from the lock file. If the lock file is
    87  	// empty, then we can assume that we created it when we created the lock and
    88  	// just remove it.
    89  	buffer := &bytes.Buffer{}
    90  	if length, err := buffer.ReadFrom(locker); err != nil {
    91  		return fmt.Errorf("unable to read project lock: %w", err)
    92  	} else if length == 0 {
    93  		removeLockFileOnReturn = true
    94  		return errors.New("project not running")
    95  	}
    96  	projectIdentifier := buffer.String()
    97  
    98  	// Ensure that the project identifier is valid.
    99  	if !identifier.IsValid(projectIdentifier) {
   100  		return errors.New("invalid project identifier found in project lock")
   101  	}
   102  
   103  	// Load the configuration file.
   104  	configuration, err := project.LoadConfiguration(configurationFileName)
   105  	if err != nil {
   106  		return fmt.Errorf("unable to load configuration file: %w", err)
   107  	}
   108  
   109  	// Look up the command.
   110  	command, ok := configuration.Commands[commandName]
   111  	if !ok {
   112  		return fmt.Errorf("unable to find command: '%s'", commandName)
   113  	}
   114  
   115  	// Execute the command.
   116  	return runInShell(command)
   117  }
   118  
   119  // runCommand is the run command.
   120  var runCommand = &cobra.Command{
   121  	Use:          "run <command-name>",
   122  	Short:        "Run a project command",
   123  	RunE:         runMain,
   124  	SilenceUsage: true,
   125  }
   126  
   127  // runConfiguration stores configuration for the run command.
   128  var runConfiguration struct {
   129  	// help indicates whether or not to show help information and exit.
   130  	help bool
   131  	// projectFile is the path to the project file, if non-default.
   132  	projectFile string
   133  }
   134  
   135  func init() {
   136  	// Grab a handle for the command line flags.
   137  	flags := runCommand.Flags()
   138  
   139  	// Disable alphabetical sorting of flags in help output.
   140  	flags.SortFlags = false
   141  
   142  	// Manually add a help flag to override the default message. Cobra will
   143  	// still implement its logic automatically.
   144  	flags.BoolVarP(&runConfiguration.help, "help", "h", false, "Show help information")
   145  
   146  	// Wire up project file flags.
   147  	flags.StringVarP(&runConfiguration.projectFile, "project-file", "f", "", "Specify project file")
   148  }