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 }