github.com/mutagen-io/mutagen@v0.18.0-rc1/cmd/mutagen/project/terminate.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 // terminateMain is the entry point for the terminate command. 25 func terminateMain(_ *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 terminateConfiguration.projectFile != "" { 33 var directory string 34 directory, configurationFileName = filepath.Split(terminateConfiguration.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-termination commands. 106 for _, command := range configuration.BeforeTerminate { 107 fmt.Println(">", command) 108 if err := runInShell(command); err != nil { 109 return fmt.Errorf("pre-terminate 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 terminate sessions. 121 selection := &selection.Selection{ 122 LabelSelector: fmt.Sprintf("%s=%s", project.LabelKey, projectIdentifier), 123 } 124 125 // Terminate forwarding sessions. 126 if err := forward.TerminateWithSelection(daemonConnection, selection); err != nil { 127 return fmt.Errorf("unable to terminate forwarding session(s): %w", err) 128 } 129 130 // Terminate synchronization sessions. 131 if err := sync.TerminateWithSelection(daemonConnection, selection); err != nil { 132 return fmt.Errorf("unable to terminate synchronization session(s): %w", err) 133 } 134 135 // Perform post-termination commands. 136 for _, command := range configuration.AfterTerminate { 137 fmt.Println(">", command) 138 if err := runInShell(command); err != nil { 139 return fmt.Errorf("post-terminate command failed: %w", err) 140 } 141 } 142 143 // Schedule the project lock for removal. 144 removeLockFileOnReturn = true 145 146 // Success. 147 return nil 148 } 149 150 // terminateCommand is the terminate command. 151 var terminateCommand = &cobra.Command{ 152 Use: "terminate", 153 Short: "Terminate project sessions", 154 Args: cmd.DisallowArguments, 155 RunE: terminateMain, 156 SilenceUsage: true, 157 } 158 159 // terminateConfiguration stores configuration for the terminate command. 160 var terminateConfiguration struct { 161 // help indicates whether or not to show help information and exit. 162 help bool 163 // projectFile is the path to the project file, if non-default. 164 projectFile string 165 } 166 167 func init() { 168 // Grab a handle for the command line flags. 169 flags := terminateCommand.Flags() 170 171 // Disable alphabetical sorting of flags in help output. 172 flags.SortFlags = false 173 174 // Manually add a help flag to override the default message. Cobra will 175 // still implement its logic automatically. 176 flags.BoolVarP(&terminateConfiguration.help, "help", "h", false, "Show help information") 177 178 // Wire up project file flags. 179 flags.StringVarP(&terminateConfiguration.projectFile, "project-file", "f", "", "Specify project file") 180 }