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