github.com/mutagen-io/mutagen@v0.18.0-rc1/cmd/mutagen/project/resume.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/cmd" 14 "github.com/mutagen-io/mutagen/cmd/mutagen/daemon" 15 "github.com/mutagen-io/mutagen/cmd/mutagen/forward" 16 "github.com/mutagen-io/mutagen/cmd/mutagen/sync" 17 18 "github.com/mutagen-io/mutagen/pkg/filesystem/locking" 19 "github.com/mutagen-io/mutagen/pkg/identifier" 20 "github.com/mutagen-io/mutagen/pkg/project" 21 "github.com/mutagen-io/mutagen/pkg/selection" 22 ) 23 24 // resumeMain is the entry point for the resume command. 25 func resumeMain(_ *cobra.Command, _ []string) error { 26 // Compute the name of the configuration file and ensure that our working 27 // directory is that in which the file resides. This is required for 28 // relative paths (including relative synchronization paths and relative 29 // Unix Domain Socket paths) to be resolved relative to the project 30 // configuration file. 31 configurationFileName := project.DefaultConfigurationFileName 32 if resumeConfiguration.projectFile != "" { 33 var directory string 34 directory, configurationFileName = filepath.Split(resumeConfiguration.projectFile) 35 if directory != "" { 36 if err := os.Chdir(directory); err != nil { 37 return fmt.Errorf("unable to switch to target directory: %w", err) 38 } 39 } 40 } 41 42 // Compute the lock path. 43 lockPath := configurationFileName + project.LockFileExtension 44 45 // Track whether or not we should remove the lock file on return. 46 var removeLockFileOnReturn bool 47 48 // Create a locker and defer its closure and potential removal. On Windows 49 // systems, we have to handle this removal after the file is closed. 50 locker, err := locking.NewLocker(lockPath, 0600) 51 if err != nil { 52 return fmt.Errorf("unable to create project locker: %w", err) 53 } 54 defer func() { 55 locker.Close() 56 if removeLockFileOnReturn && runtime.GOOS == "windows" { 57 os.Remove(lockPath) 58 } 59 }() 60 61 // Acquire the project lock and defer its release and potential removal. On 62 // Windows systems, we can't remove the lock file if it's locked or even 63 // just opened, so we handle removal for Windows systems after we close the 64 // lock file (see above). In this case, we truncate the lock file before 65 // releasing it to ensure that any other process that opens or acquires the 66 // lock file before we manage to remove it will simply see an empty lock 67 // file, which it will ignore or attempt to remove. 68 if err := locker.Lock(true); err != nil { 69 return fmt.Errorf("unable to acquire project lock: %w", err) 70 } 71 defer func() { 72 if removeLockFileOnReturn { 73 if runtime.GOOS == "windows" { 74 locker.Truncate(0) 75 } else { 76 os.Remove(lockPath) 77 } 78 } 79 locker.Unlock() 80 }() 81 82 // Read the project identifier from the lock file. If the lock file is 83 // empty, then we can assume that we created it when we created the lock and 84 // just remove it. 85 buffer := &bytes.Buffer{} 86 if length, err := buffer.ReadFrom(locker); err != nil { 87 return fmt.Errorf("unable to read project lock: %w", err) 88 } else if length == 0 { 89 removeLockFileOnReturn = true 90 return errors.New("project not running") 91 } 92 projectIdentifier := buffer.String() 93 94 // Ensure that the project identifier is valid. 95 if !identifier.IsValid(projectIdentifier) { 96 return errors.New("invalid project identifier found in project lock") 97 } 98 99 // Load the configuration file. 100 configuration, err := project.LoadConfiguration(configurationFileName) 101 if err != nil { 102 return fmt.Errorf("unable to load configuration file: %w", err) 103 } 104 105 // Perform pre-resume commands. 106 for _, command := range configuration.BeforeResume { 107 fmt.Println(">", command) 108 if err := runInShell(command); err != nil { 109 return fmt.Errorf("pre-resume command failed: %w", err) 110 } 111 } 112 113 // Connect to the daemon and defer closure of the connection. 114 daemonConnection, err := daemon.Connect(true, true) 115 if err != nil { 116 return fmt.Errorf("unable to connect to daemon: %w", err) 117 } 118 defer daemonConnection.Close() 119 120 // Compute the selection that we're going to use to resume sessions. 121 selection := &selection.Selection{ 122 LabelSelector: fmt.Sprintf("%s=%s", project.LabelKey, projectIdentifier), 123 } 124 125 // Resume forwarding sessions. 126 if err := forward.ResumeWithSelection(daemonConnection, selection); err != nil { 127 return fmt.Errorf("unable to resume forwarding session(s): %w", err) 128 } 129 130 // Resume synchronization sessions. 131 if err := sync.ResumeWithSelection(daemonConnection, selection); err != nil { 132 return fmt.Errorf("unable to resume synchronization session(s): %w", err) 133 } 134 135 // Perform post-resume commands. 136 for _, command := range configuration.AfterResume { 137 fmt.Println(">", command) 138 if err := runInShell(command); err != nil { 139 return fmt.Errorf("post-resume command failed: %w", err) 140 } 141 } 142 143 // Success. 144 return nil 145 } 146 147 // resumeCommand is the resume command. 148 var resumeCommand = &cobra.Command{ 149 Use: "resume", 150 Short: "Resume project sessions", 151 Args: cmd.DisallowArguments, 152 RunE: resumeMain, 153 SilenceUsage: true, 154 } 155 156 // resumeConfiguration stores configuration for the resume command. 157 var resumeConfiguration struct { 158 // help indicates whether or not to show help information and exit. 159 help bool 160 // projectFile is the path to the project file, if non-default. 161 projectFile string 162 } 163 164 func init() { 165 // Grab a handle for the command line flags. 166 flags := resumeCommand.Flags() 167 168 // Disable alphabetical sorting of flags in help output. 169 flags.SortFlags = false 170 171 // Manually add a help flag to override the default message. Cobra will 172 // still implement its logic automatically. 173 flags.BoolVarP(&resumeConfiguration.help, "help", "h", false, "Show help information") 174 175 // Wire up project file flags. 176 flags.StringVarP(&resumeConfiguration.projectFile, "project-file", "f", "", "Specify project file") 177 }