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