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