github.com/mutagen-io/mutagen@v0.18.0-rc1/cmd/mutagen/project/pause.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  // pauseMain is the entry point for the pause command.
    25  func pauseMain(_ *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 pauseConfiguration.projectFile != "" {
    33  		var directory string
    34  		directory, configurationFileName = filepath.Split(pauseConfiguration.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-pause commands.
   106  	for _, command := range configuration.BeforePause {
   107  		fmt.Println(">", command)
   108  		if err := runInShell(command); err != nil {
   109  			return fmt.Errorf("pre-pause 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 pause sessions.
   121  	selection := &selection.Selection{
   122  		LabelSelector: fmt.Sprintf("%s=%s", project.LabelKey, projectIdentifier),
   123  	}
   124  
   125  	// Pause forwarding sessions.
   126  	if err := forward.PauseWithSelection(daemonConnection, selection); err != nil {
   127  		return fmt.Errorf("unable to pause forwarding session(s): %w", err)
   128  	}
   129  
   130  	// Pause synchronization sessions.
   131  	if err := sync.PauseWithSelection(daemonConnection, selection); err != nil {
   132  		return fmt.Errorf("unable to pause synchronization session(s): %w", err)
   133  	}
   134  
   135  	// Perform post-pause commands.
   136  	for _, command := range configuration.AfterPause {
   137  		fmt.Println(">", command)
   138  		if err := runInShell(command); err != nil {
   139  			return fmt.Errorf("post-pause command failed: %w", err)
   140  		}
   141  	}
   142  
   143  	// Success.
   144  	return nil
   145  }
   146  
   147  // pauseCommand is the pause command.
   148  var pauseCommand = &cobra.Command{
   149  	Use:          "pause",
   150  	Short:        "Pause project sessions",
   151  	Args:         cmd.DisallowArguments,
   152  	RunE:         pauseMain,
   153  	SilenceUsage: true,
   154  }
   155  
   156  // pauseConfiguration stores configuration for the pause command.
   157  var pauseConfiguration 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 := pauseCommand.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(&pauseConfiguration.help, "help", "h", false, "Show help information")
   174  
   175  	// Wire up project file flags.
   176  	flags.StringVarP(&pauseConfiguration.projectFile, "project-file", "f", "", "Specify project file")
   177  }