github.com/v2fly/tools@v0.100.0/internal/gocommand/invoke.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package gocommand is a helper for calling the go command.
     6  package gocommand
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	exec "golang.org/x/sys/execabs"
    21  
    22  	"github.com/v2fly/tools/internal/event"
    23  )
    24  
    25  // An Runner will run go command invocations and serialize
    26  // them if it sees a concurrency error.
    27  type Runner struct {
    28  	// once guards the runner initialization.
    29  	once sync.Once
    30  
    31  	// inFlight tracks available workers.
    32  	inFlight chan struct{}
    33  
    34  	// serialized guards the ability to run a go command serially,
    35  	// to avoid deadlocks when claiming workers.
    36  	serialized chan struct{}
    37  }
    38  
    39  const maxInFlight = 10
    40  
    41  func (runner *Runner) initialize() {
    42  	runner.once.Do(func() {
    43  		runner.inFlight = make(chan struct{}, maxInFlight)
    44  		runner.serialized = make(chan struct{}, 1)
    45  	})
    46  }
    47  
    48  // 1.13: go: updates to go.mod needed, but contents have changed
    49  // 1.14: go: updating go.mod: existing contents have changed since last read
    50  var modConcurrencyError = regexp.MustCompile(`go:.*go.mod.*contents have changed`)
    51  
    52  // Run is a convenience wrapper around RunRaw.
    53  // It returns only stdout and a "friendly" error.
    54  func (runner *Runner) Run(ctx context.Context, inv Invocation) (*bytes.Buffer, error) {
    55  	stdout, _, friendly, _ := runner.RunRaw(ctx, inv)
    56  	return stdout, friendly
    57  }
    58  
    59  // RunPiped runs the invocation serially, always waiting for any concurrent
    60  // invocations to complete first.
    61  func (runner *Runner) RunPiped(ctx context.Context, inv Invocation, stdout, stderr io.Writer) error {
    62  	_, err := runner.runPiped(ctx, inv, stdout, stderr)
    63  	return err
    64  }
    65  
    66  // RunRaw runs the invocation, serializing requests only if they fight over
    67  // go.mod changes.
    68  func (runner *Runner) RunRaw(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) {
    69  	// Make sure the runner is always initialized.
    70  	runner.initialize()
    71  
    72  	// First, try to run the go command concurrently.
    73  	stdout, stderr, friendlyErr, err := runner.runConcurrent(ctx, inv)
    74  
    75  	// If we encounter a load concurrency error, we need to retry serially.
    76  	if friendlyErr == nil || !modConcurrencyError.MatchString(friendlyErr.Error()) {
    77  		return stdout, stderr, friendlyErr, err
    78  	}
    79  	event.Error(ctx, "Load concurrency error, will retry serially", err)
    80  
    81  	// Run serially by calling runPiped.
    82  	stdout.Reset()
    83  	stderr.Reset()
    84  	friendlyErr, err = runner.runPiped(ctx, inv, stdout, stderr)
    85  	return stdout, stderr, friendlyErr, err
    86  }
    87  
    88  func (runner *Runner) runConcurrent(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) {
    89  	// Wait for 1 worker to become available.
    90  	select {
    91  	case <-ctx.Done():
    92  		return nil, nil, nil, ctx.Err()
    93  	case runner.inFlight <- struct{}{}:
    94  		defer func() { <-runner.inFlight }()
    95  	}
    96  
    97  	stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
    98  	friendlyErr, err := inv.runWithFriendlyError(ctx, stdout, stderr)
    99  	return stdout, stderr, friendlyErr, err
   100  }
   101  
   102  func (runner *Runner) runPiped(ctx context.Context, inv Invocation, stdout, stderr io.Writer) (error, error) {
   103  	// Make sure the runner is always initialized.
   104  	runner.initialize()
   105  
   106  	// Acquire the serialization lock. This avoids deadlocks between two
   107  	// runPiped commands.
   108  	select {
   109  	case <-ctx.Done():
   110  		return nil, ctx.Err()
   111  	case runner.serialized <- struct{}{}:
   112  		defer func() { <-runner.serialized }()
   113  	}
   114  
   115  	// Wait for all in-progress go commands to return before proceeding,
   116  	// to avoid load concurrency errors.
   117  	for i := 0; i < maxInFlight; i++ {
   118  		select {
   119  		case <-ctx.Done():
   120  			return nil, ctx.Err()
   121  		case runner.inFlight <- struct{}{}:
   122  			// Make sure we always "return" any workers we took.
   123  			defer func() { <-runner.inFlight }()
   124  		}
   125  	}
   126  
   127  	return inv.runWithFriendlyError(ctx, stdout, stderr)
   128  }
   129  
   130  // An Invocation represents a call to the go command.
   131  type Invocation struct {
   132  	Verb       string
   133  	Args       []string
   134  	BuildFlags []string
   135  	ModFlag    string
   136  	ModFile    string
   137  	Overlay    string
   138  	// If CleanEnv is set, the invocation will run only with the environment
   139  	// in Env, not starting with os.Environ.
   140  	CleanEnv   bool
   141  	Env        []string
   142  	WorkingDir string
   143  	Logf       func(format string, args ...interface{})
   144  }
   145  
   146  func (i *Invocation) runWithFriendlyError(ctx context.Context, stdout, stderr io.Writer) (friendlyError error, rawError error) {
   147  	rawError = i.run(ctx, stdout, stderr)
   148  	if rawError != nil {
   149  		friendlyError = rawError
   150  		// Check for 'go' executable not being found.
   151  		if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
   152  			friendlyError = fmt.Errorf("go command required, not found: %v", ee)
   153  		}
   154  		if ctx.Err() != nil {
   155  			friendlyError = ctx.Err()
   156  		}
   157  		friendlyError = fmt.Errorf("err: %v: stderr: %s", friendlyError, stderr)
   158  	}
   159  	return
   160  }
   161  
   162  func (i *Invocation) run(ctx context.Context, stdout, stderr io.Writer) error {
   163  	log := i.Logf
   164  	if log == nil {
   165  		log = func(string, ...interface{}) {}
   166  	}
   167  
   168  	goArgs := []string{i.Verb}
   169  
   170  	appendModFile := func() {
   171  		if i.ModFile != "" {
   172  			goArgs = append(goArgs, "-modfile="+i.ModFile)
   173  		}
   174  	}
   175  	appendModFlag := func() {
   176  		if i.ModFlag != "" {
   177  			goArgs = append(goArgs, "-mod="+i.ModFlag)
   178  		}
   179  	}
   180  	appendOverlayFlag := func() {
   181  		if i.Overlay != "" {
   182  			goArgs = append(goArgs, "-overlay="+i.Overlay)
   183  		}
   184  	}
   185  
   186  	switch i.Verb {
   187  	case "env", "version":
   188  		goArgs = append(goArgs, i.Args...)
   189  	case "mod":
   190  		// mod needs the sub-verb before flags.
   191  		goArgs = append(goArgs, i.Args[0])
   192  		appendModFile()
   193  		goArgs = append(goArgs, i.Args[1:]...)
   194  	case "get":
   195  		goArgs = append(goArgs, i.BuildFlags...)
   196  		appendModFile()
   197  		goArgs = append(goArgs, i.Args...)
   198  
   199  	default: // notably list and build.
   200  		goArgs = append(goArgs, i.BuildFlags...)
   201  		appendModFile()
   202  		appendModFlag()
   203  		appendOverlayFlag()
   204  		goArgs = append(goArgs, i.Args...)
   205  	}
   206  	cmd := exec.Command("go", goArgs...)
   207  	cmd.Stdout = stdout
   208  	cmd.Stderr = stderr
   209  	// On darwin the cwd gets resolved to the real path, which breaks anything that
   210  	// expects the working directory to keep the original path, including the
   211  	// go command when dealing with modules.
   212  	// The Go stdlib has a special feature where if the cwd and the PWD are the
   213  	// same node then it trusts the PWD, so by setting it in the env for the child
   214  	// process we fix up all the paths returned by the go command.
   215  	if !i.CleanEnv {
   216  		cmd.Env = os.Environ()
   217  	}
   218  	cmd.Env = append(cmd.Env, i.Env...)
   219  	if i.WorkingDir != "" {
   220  		cmd.Env = append(cmd.Env, "PWD="+i.WorkingDir)
   221  		cmd.Dir = i.WorkingDir
   222  	}
   223  	defer func(start time.Time) { log("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
   224  
   225  	return runCmdContext(ctx, cmd)
   226  }
   227  
   228  // runCmdContext is like exec.CommandContext except it sends os.Interrupt
   229  // before os.Kill.
   230  func runCmdContext(ctx context.Context, cmd *exec.Cmd) error {
   231  	if err := cmd.Start(); err != nil {
   232  		return err
   233  	}
   234  	resChan := make(chan error, 1)
   235  	go func() {
   236  		resChan <- cmd.Wait()
   237  	}()
   238  
   239  	select {
   240  	case err := <-resChan:
   241  		return err
   242  	case <-ctx.Done():
   243  	}
   244  	// Cancelled. Interrupt and see if it ends voluntarily.
   245  	cmd.Process.Signal(os.Interrupt)
   246  	select {
   247  	case err := <-resChan:
   248  		return err
   249  	case <-time.After(time.Second):
   250  	}
   251  	// Didn't shut down in response to interrupt. Kill it hard.
   252  	cmd.Process.Kill()
   253  	return <-resChan
   254  }
   255  
   256  func cmdDebugStr(cmd *exec.Cmd) string {
   257  	env := make(map[string]string)
   258  	for _, kv := range cmd.Env {
   259  		split := strings.SplitN(kv, "=", 2)
   260  		k, v := split[0], split[1]
   261  		env[k] = v
   262  	}
   263  
   264  	var args []string
   265  	for _, arg := range cmd.Args {
   266  		quoted := strconv.Quote(arg)
   267  		if quoted[1:len(quoted)-1] != arg || strings.Contains(arg, " ") {
   268  			args = append(args, quoted)
   269  		} else {
   270  			args = append(args, arg)
   271  		}
   272  	}
   273  	return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], strings.Join(args, " "))
   274  }