github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/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/powerman/golang-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  
   136  	// If ModFlag is set, the go command is invoked with -mod=ModFlag.
   137  	ModFlag string
   138  
   139  	// If ModFile is set, the go command is invoked with -modfile=ModFile.
   140  	ModFile string
   141  
   142  	// If Overlay is set, the go command is invoked with -overlay=Overlay.
   143  	Overlay string
   144  
   145  	// If CleanEnv is set, the invocation will run only with the environment
   146  	// in Env, not starting with os.Environ.
   147  	CleanEnv   bool
   148  	Env        []string
   149  	WorkingDir string
   150  	Logf       func(format string, args ...interface{})
   151  }
   152  
   153  func (i *Invocation) runWithFriendlyError(ctx context.Context, stdout, stderr io.Writer) (friendlyError error, rawError error) {
   154  	rawError = i.run(ctx, stdout, stderr)
   155  	if rawError != nil {
   156  		friendlyError = rawError
   157  		// Check for 'go' executable not being found.
   158  		if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
   159  			friendlyError = fmt.Errorf("go command required, not found: %v", ee)
   160  		}
   161  		if ctx.Err() != nil {
   162  			friendlyError = ctx.Err()
   163  		}
   164  		friendlyError = fmt.Errorf("err: %v: stderr: %s", friendlyError, stderr)
   165  	}
   166  	return
   167  }
   168  
   169  func (i *Invocation) run(ctx context.Context, stdout, stderr io.Writer) error {
   170  	log := i.Logf
   171  	if log == nil {
   172  		log = func(string, ...interface{}) {}
   173  	}
   174  
   175  	goArgs := []string{i.Verb}
   176  
   177  	appendModFile := func() {
   178  		if i.ModFile != "" {
   179  			goArgs = append(goArgs, "-modfile="+i.ModFile)
   180  		}
   181  	}
   182  	appendModFlag := func() {
   183  		if i.ModFlag != "" {
   184  			goArgs = append(goArgs, "-mod="+i.ModFlag)
   185  		}
   186  	}
   187  	appendOverlayFlag := func() {
   188  		if i.Overlay != "" {
   189  			goArgs = append(goArgs, "-overlay="+i.Overlay)
   190  		}
   191  	}
   192  
   193  	switch i.Verb {
   194  	case "env", "version":
   195  		goArgs = append(goArgs, i.Args...)
   196  	case "mod":
   197  		// mod needs the sub-verb before flags.
   198  		goArgs = append(goArgs, i.Args[0])
   199  		appendModFile()
   200  		goArgs = append(goArgs, i.Args[1:]...)
   201  	case "get":
   202  		goArgs = append(goArgs, i.BuildFlags...)
   203  		appendModFile()
   204  		goArgs = append(goArgs, i.Args...)
   205  
   206  	default: // notably list and build.
   207  		goArgs = append(goArgs, i.BuildFlags...)
   208  		appendModFile()
   209  		appendModFlag()
   210  		appendOverlayFlag()
   211  		goArgs = append(goArgs, i.Args...)
   212  	}
   213  	cmd := exec.Command("go", goArgs...)
   214  	cmd.Stdout = stdout
   215  	cmd.Stderr = stderr
   216  	// On darwin the cwd gets resolved to the real path, which breaks anything that
   217  	// expects the working directory to keep the original path, including the
   218  	// go command when dealing with modules.
   219  	// The Go stdlib has a special feature where if the cwd and the PWD are the
   220  	// same node then it trusts the PWD, so by setting it in the env for the child
   221  	// process we fix up all the paths returned by the go command.
   222  	if !i.CleanEnv {
   223  		cmd.Env = os.Environ()
   224  	}
   225  	cmd.Env = append(cmd.Env, i.Env...)
   226  	if i.WorkingDir != "" {
   227  		cmd.Env = append(cmd.Env, "PWD="+i.WorkingDir)
   228  		cmd.Dir = i.WorkingDir
   229  	}
   230  	defer func(start time.Time) { log("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
   231  
   232  	return runCmdContext(ctx, cmd)
   233  }
   234  
   235  // runCmdContext is like exec.CommandContext except it sends os.Interrupt
   236  // before os.Kill.
   237  func runCmdContext(ctx context.Context, cmd *exec.Cmd) error {
   238  	if err := cmd.Start(); err != nil {
   239  		return err
   240  	}
   241  	resChan := make(chan error, 1)
   242  	go func() {
   243  		resChan <- cmd.Wait()
   244  	}()
   245  
   246  	select {
   247  	case err := <-resChan:
   248  		return err
   249  	case <-ctx.Done():
   250  	}
   251  	// Cancelled. Interrupt and see if it ends voluntarily.
   252  	cmd.Process.Signal(os.Interrupt)
   253  	select {
   254  	case err := <-resChan:
   255  		return err
   256  	case <-time.After(time.Second):
   257  	}
   258  	// Didn't shut down in response to interrupt. Kill it hard.
   259  	cmd.Process.Kill()
   260  	return <-resChan
   261  }
   262  
   263  func cmdDebugStr(cmd *exec.Cmd) string {
   264  	env := make(map[string]string)
   265  	for _, kv := range cmd.Env {
   266  		split := strings.SplitN(kv, "=", 2)
   267  		k, v := split[0], split[1]
   268  		env[k] = v
   269  	}
   270  
   271  	var args []string
   272  	for _, arg := range cmd.Args {
   273  		quoted := strconv.Quote(arg)
   274  		if quoted[1:len(quoted)-1] != arg || strings.Contains(arg, " ") {
   275  			args = append(args, quoted)
   276  		} else {
   277  			args = append(args, arg)
   278  		}
   279  	}
   280  	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, " "))
   281  }