github.com/mutagen-io/mutagen@v0.18.0-rc1/cmd/mutagen/daemon/connect.go (about)

     1  package daemon
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"time"
     9  
    10  	"google.golang.org/grpc"
    11  
    12  	"github.com/mutagen-io/mutagen/cmd"
    13  	"github.com/mutagen-io/mutagen/cmd/external"
    14  
    15  	"github.com/mutagen-io/mutagen/pkg/daemon"
    16  	"github.com/mutagen-io/mutagen/pkg/grpcutil"
    17  	"github.com/mutagen-io/mutagen/pkg/ipc"
    18  	"github.com/mutagen-io/mutagen/pkg/mutagen"
    19  	daemonsvc "github.com/mutagen-io/mutagen/pkg/service/daemon"
    20  )
    21  
    22  const (
    23  	// dialTimeout is the timeout to use when attempting to connect to the
    24  	// daemon IPC endpoint.
    25  	dialTimeout = 500 * time.Millisecond
    26  	// autostartWaitInterval is the wait period between reconnect attempts after
    27  	// autostarting the daemon.
    28  	autostartWaitInterval = 100 * time.Millisecond
    29  	// autostartRetryCount is the number of times to try reconnecting after
    30  	// autostarting the daemon.
    31  	autostartRetryCount = 10
    32  )
    33  
    34  // Connect creates a new daemon client connection and optionally verifies that
    35  // the daemon version matches the current process' version.
    36  func Connect(autostart, enforceVersionMatch bool) (*grpc.ClientConn, error) {
    37  	// Compute the path to the daemon IPC endpoint.
    38  	endpoint, err := daemon.EndpointPath()
    39  	if err != nil {
    40  		return nil, fmt.Errorf("unable to compute endpoint path: %w", err)
    41  	}
    42  
    43  	// Check if autostart has been disabled programmatically or by an
    44  	// environment variable.
    45  	if external.DisableDaemonAutostart || os.Getenv("MUTAGEN_DISABLE_AUTOSTART") == "1" {
    46  		autostart = false
    47  	}
    48  
    49  	// Create a status line printer and defer a clear.
    50  	statusLinePrinter := &cmd.StatusLinePrinter{UseStandardError: true}
    51  	defer statusLinePrinter.BreakIfPopulated()
    52  
    53  	// Perform dialing in a loop until failure or success.
    54  	remainingPostAutostatAttempts := autostartRetryCount
    55  	invokedStart := false
    56  	var connection *grpc.ClientConn
    57  	for {
    58  		// Create a context to timeout the dial.
    59  		ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
    60  
    61  		// Attempt to dial.
    62  		connection, err = grpc.DialContext(
    63  			ctx, endpoint,
    64  			grpc.WithInsecure(),
    65  			grpc.WithContextDialer(ipc.DialContext),
    66  			grpc.WithBlock(),
    67  			grpc.WithDefaultCallOptions(
    68  				grpc.MaxCallSendMsgSize(grpcutil.MaximumMessageSize),
    69  				grpc.MaxCallRecvMsgSize(grpcutil.MaximumMessageSize),
    70  			),
    71  		)
    72  
    73  		// Cancel the dialing context. If the dialing operation has already
    74  		// succeeded, this has no effect, but it is necessary to clean up the
    75  		// Goroutine that backs the context.
    76  		cancel()
    77  
    78  		// Check for errors.
    79  		if err != nil {
    80  			// Handle failure due to timeouts.
    81  			if err == context.DeadlineExceeded {
    82  				// If autostart is enabled, and we have attempts remaining, then
    83  				// try autostarting, waiting, and retrying.
    84  				if autostart && remainingPostAutostatAttempts > 0 {
    85  					if !invokedStart {
    86  						statusLinePrinter.Print("Attempting to start Mutagen daemon...")
    87  						startMain(nil, nil)
    88  						invokedStart = true
    89  					}
    90  					time.Sleep(autostartWaitInterval)
    91  					remainingPostAutostatAttempts--
    92  					continue
    93  				}
    94  
    95  				// Otherwise just fail due to the timeout.
    96  				return nil, errors.New("connection timed out (is the daemon running?)")
    97  			}
    98  
    99  			// If we failed for any other reason, then bail.
   100  			return nil, err
   101  		}
   102  
   103  		// Print a notice if we started the daemon.
   104  		if invokedStart {
   105  			statusLinePrinter.Clear()
   106  			statusLinePrinter.Print("Started Mutagen daemon in background (terminate with \"mutagen daemon stop\")")
   107  		}
   108  
   109  		// We've successfully dialed, so break out of the dialing loop.
   110  		break
   111  	}
   112  
   113  	// If requested, verify that the daemon version matches the current process'
   114  	// version.
   115  	if enforceVersionMatch {
   116  		daemonService := daemonsvc.NewDaemonClient(connection)
   117  		version, err := daemonService.Version(context.Background(), &daemonsvc.VersionRequest{})
   118  		if err != nil {
   119  			connection.Close()
   120  			return nil, fmt.Errorf("unable to query daemon version: %w", err)
   121  		}
   122  		versionMatch := version.Major == mutagen.VersionMajor &&
   123  			version.Minor == mutagen.VersionMinor &&
   124  			version.Patch == mutagen.VersionPatch &&
   125  			version.Tag == mutagen.VersionTag
   126  		if !versionMatch {
   127  			connection.Close()
   128  			return nil, errors.New("client/daemon version mismatch (daemon restart recommended)")
   129  		}
   130  	}
   131  
   132  	// Success.
   133  	return connection, nil
   134  }