github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/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  	"log"
    14  	"os"
    15  	"regexp"
    16  	"runtime"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	exec "golang.org/x/sys/execabs"
    23  
    24  	"golang.org/x/tools/internal/event"
    25  )
    26  
    27  // An Runner will run go command invocations and serialize
    28  // them if it sees a concurrency error.
    29  type Runner struct {
    30  	// once guards the runner initialization.
    31  	once sync.Once
    32  
    33  	// inFlight tracks available workers.
    34  	inFlight chan struct{}
    35  
    36  	// serialized guards the ability to run a go command serially,
    37  	// to avoid deadlocks when claiming workers.
    38  	serialized chan struct{}
    39  }
    40  
    41  const maxInFlight = 10
    42  
    43  func (runner *Runner) initialize() {
    44  	runner.once.Do(func() {
    45  		runner.inFlight = make(chan struct{}, maxInFlight)
    46  		runner.serialized = make(chan struct{}, 1)
    47  	})
    48  }
    49  
    50  // 1.13: go: updates to go.mod needed, but contents have changed
    51  // 1.14: go: updating go.mod: existing contents have changed since last read
    52  var modConcurrencyError = regexp.MustCompile(`go:.*go.mod.*contents have changed`)
    53  
    54  // Run is a convenience wrapper around RunRaw.
    55  // It returns only stdout and a "friendly" error.
    56  func (runner *Runner) Run(ctx context.Context, inv Invocation) (*bytes.Buffer, error) {
    57  	stdout, _, friendly, _ := runner.RunRaw(ctx, inv)
    58  	return stdout, friendly
    59  }
    60  
    61  // RunPiped runs the invocation serially, always waiting for any concurrent
    62  // invocations to complete first.
    63  func (runner *Runner) RunPiped(ctx context.Context, inv Invocation, stdout, stderr io.Writer) error {
    64  	_, err := runner.runPiped(ctx, inv, stdout, stderr)
    65  	return err
    66  }
    67  
    68  // RunRaw runs the invocation, serializing requests only if they fight over
    69  // go.mod changes.
    70  func (runner *Runner) RunRaw(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) {
    71  	// Make sure the runner is always initialized.
    72  	runner.initialize()
    73  
    74  	// First, try to run the go command concurrently.
    75  	stdout, stderr, friendlyErr, err := runner.runConcurrent(ctx, inv)
    76  
    77  	// If we encounter a load concurrency error, we need to retry serially.
    78  	if friendlyErr == nil || !modConcurrencyError.MatchString(friendlyErr.Error()) {
    79  		return stdout, stderr, friendlyErr, err
    80  	}
    81  	event.Error(ctx, "Load concurrency error, will retry serially", err)
    82  
    83  	// Run serially by calling runPiped.
    84  	stdout.Reset()
    85  	stderr.Reset()
    86  	friendlyErr, err = runner.runPiped(ctx, inv, stdout, stderr)
    87  	return stdout, stderr, friendlyErr, err
    88  }
    89  
    90  func (runner *Runner) runConcurrent(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) {
    91  	// Wait for 1 worker to become available.
    92  	select {
    93  	case <-ctx.Done():
    94  		return nil, nil, nil, ctx.Err()
    95  	case runner.inFlight <- struct{}{}:
    96  		defer func() { <-runner.inFlight }()
    97  	}
    98  
    99  	stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
   100  	friendlyErr, err := inv.runWithFriendlyError(ctx, stdout, stderr)
   101  	return stdout, stderr, friendlyErr, err
   102  }
   103  
   104  func (runner *Runner) runPiped(ctx context.Context, inv Invocation, stdout, stderr io.Writer) (error, error) {
   105  	// Make sure the runner is always initialized.
   106  	runner.initialize()
   107  
   108  	// Acquire the serialization lock. This avoids deadlocks between two
   109  	// runPiped commands.
   110  	select {
   111  	case <-ctx.Done():
   112  		return nil, ctx.Err()
   113  	case runner.serialized <- struct{}{}:
   114  		defer func() { <-runner.serialized }()
   115  	}
   116  
   117  	// Wait for all in-progress go commands to return before proceeding,
   118  	// to avoid load concurrency errors.
   119  	for i := 0; i < maxInFlight; i++ {
   120  		select {
   121  		case <-ctx.Done():
   122  			return nil, ctx.Err()
   123  		case runner.inFlight <- struct{}{}:
   124  			// Make sure we always "return" any workers we took.
   125  			defer func() { <-runner.inFlight }()
   126  		}
   127  	}
   128  
   129  	return inv.runWithFriendlyError(ctx, stdout, stderr)
   130  }
   131  
   132  // An Invocation represents a call to the go command.
   133  type Invocation struct {
   134  	Verb       string
   135  	Args       []string
   136  	BuildFlags []string
   137  
   138  	// If ModFlag is set, the go command is invoked with -mod=ModFlag.
   139  	ModFlag string
   140  
   141  	// If ModFile is set, the go command is invoked with -modfile=ModFile.
   142  	ModFile string
   143  
   144  	// If Overlay is set, the go command is invoked with -overlay=Overlay.
   145  	Overlay string
   146  
   147  	// If CleanEnv is set, the invocation will run only with the environment
   148  	// in Env, not starting with os.Environ.
   149  	CleanEnv   bool
   150  	Env        []string
   151  	WorkingDir string
   152  	Logf       func(format string, args ...interface{})
   153  }
   154  
   155  func (i *Invocation) runWithFriendlyError(ctx context.Context, stdout, stderr io.Writer) (friendlyError error, rawError error) {
   156  	rawError = i.run(ctx, stdout, stderr)
   157  	if rawError != nil {
   158  		friendlyError = rawError
   159  		// Check for 'go' executable not being found.
   160  		if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
   161  			friendlyError = fmt.Errorf("go command required, not found: %v", ee)
   162  		}
   163  		if ctx.Err() != nil {
   164  			friendlyError = ctx.Err()
   165  		}
   166  		friendlyError = fmt.Errorf("err: %v: stderr: %s", friendlyError, stderr)
   167  	}
   168  	return
   169  }
   170  
   171  func (i *Invocation) run(ctx context.Context, stdout, stderr io.Writer) error {
   172  	log := i.Logf
   173  	if log == nil {
   174  		log = func(string, ...interface{}) {}
   175  	}
   176  
   177  	goArgs := []string{i.Verb}
   178  
   179  	appendModFile := func() {
   180  		if i.ModFile != "" {
   181  			goArgs = append(goArgs, "-modfile="+i.ModFile)
   182  		}
   183  	}
   184  	appendModFlag := func() {
   185  		if i.ModFlag != "" {
   186  			goArgs = append(goArgs, "-mod="+i.ModFlag)
   187  		}
   188  	}
   189  	appendOverlayFlag := func() {
   190  		if i.Overlay != "" {
   191  			goArgs = append(goArgs, "-overlay="+i.Overlay)
   192  		}
   193  	}
   194  
   195  	switch i.Verb {
   196  	case "env", "version":
   197  		goArgs = append(goArgs, i.Args...)
   198  	case "mod":
   199  		// mod needs the sub-verb before flags.
   200  		goArgs = append(goArgs, i.Args[0])
   201  		appendModFile()
   202  		goArgs = append(goArgs, i.Args[1:]...)
   203  	case "get":
   204  		goArgs = append(goArgs, i.BuildFlags...)
   205  		appendModFile()
   206  		goArgs = append(goArgs, i.Args...)
   207  
   208  	default: // notably list and build.
   209  		goArgs = append(goArgs, i.BuildFlags...)
   210  		appendModFile()
   211  		appendModFlag()
   212  		appendOverlayFlag()
   213  		goArgs = append(goArgs, i.Args...)
   214  	}
   215  	cmd := exec.Command("go", goArgs...)
   216  	cmd.Stdout = stdout
   217  	cmd.Stderr = stderr
   218  	// On darwin the cwd gets resolved to the real path, which breaks anything that
   219  	// expects the working directory to keep the original path, including the
   220  	// go command when dealing with modules.
   221  	// The Go stdlib has a special feature where if the cwd and the PWD are the
   222  	// same node then it trusts the PWD, so by setting it in the env for the child
   223  	// process we fix up all the paths returned by the go command.
   224  	if !i.CleanEnv {
   225  		cmd.Env = os.Environ()
   226  	}
   227  	cmd.Env = append(cmd.Env, i.Env...)
   228  	if i.WorkingDir != "" {
   229  		cmd.Env = append(cmd.Env, "PWD="+i.WorkingDir)
   230  		cmd.Dir = i.WorkingDir
   231  	}
   232  	defer func(start time.Time) { log("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
   233  
   234  	return runCmdContext(ctx, cmd)
   235  }
   236  
   237  // DebugHangingGoCommands may be set by tests to enable additional
   238  // instrumentation (including panics) for debugging hanging Go commands.
   239  //
   240  // See golang/go#54461 for details.
   241  var DebugHangingGoCommands = false
   242  
   243  // runCmdContext is like exec.CommandContext except it sends os.Interrupt
   244  // before os.Kill.
   245  func runCmdContext(ctx context.Context, cmd *exec.Cmd) error {
   246  	if err := cmd.Start(); err != nil {
   247  		return err
   248  	}
   249  	resChan := make(chan error, 1)
   250  	go func() {
   251  		resChan <- cmd.Wait()
   252  	}()
   253  
   254  	// If we're interested in debugging hanging Go commands, stop waiting after a
   255  	// minute and panic with interesting information.
   256  	if DebugHangingGoCommands {
   257  		select {
   258  		case err := <-resChan:
   259  			return err
   260  		case <-time.After(1 * time.Minute):
   261  			HandleHangingGoCommand(cmd.Process)
   262  		case <-ctx.Done():
   263  		}
   264  	} else {
   265  		select {
   266  		case err := <-resChan:
   267  			return err
   268  		case <-ctx.Done():
   269  		}
   270  	}
   271  
   272  	// Cancelled. Interrupt and see if it ends voluntarily.
   273  	cmd.Process.Signal(os.Interrupt)
   274  	select {
   275  	case err := <-resChan:
   276  		return err
   277  	case <-time.After(time.Second):
   278  	}
   279  
   280  	// Didn't shut down in response to interrupt. Kill it hard.
   281  	// TODO(rfindley): per advice from bcmills@, it may be better to send SIGQUIT
   282  	// on certain platforms, such as unix.
   283  	if err := cmd.Process.Kill(); err != nil && DebugHangingGoCommands {
   284  		// Don't panic here as this reliably fails on windows with EINVAL.
   285  		log.Printf("error killing the Go command: %v", err)
   286  	}
   287  
   288  	// See above: don't wait indefinitely if we're debugging hanging Go commands.
   289  	if DebugHangingGoCommands {
   290  		select {
   291  		case err := <-resChan:
   292  			return err
   293  		case <-time.After(10 * time.Second): // a shorter wait as resChan should return quickly following Kill
   294  			HandleHangingGoCommand(cmd.Process)
   295  		}
   296  	}
   297  	return <-resChan
   298  }
   299  
   300  func HandleHangingGoCommand(proc *os.Process) {
   301  	switch runtime.GOOS {
   302  	case "linux", "darwin", "freebsd", "netbsd":
   303  		fmt.Fprintln(os.Stderr, `DETECTED A HANGING GO COMMAND
   304  
   305  The gopls test runner has detected a hanging go command. In order to debug
   306  this, the output of ps and lsof/fstat is printed below.
   307  
   308  See golang/go#54461 for more details.`)
   309  
   310  		fmt.Fprintln(os.Stderr, "\nps axo ppid,pid,command:")
   311  		fmt.Fprintln(os.Stderr, "-------------------------")
   312  		psCmd := exec.Command("ps", "axo", "ppid,pid,command")
   313  		psCmd.Stdout = os.Stderr
   314  		psCmd.Stderr = os.Stderr
   315  		if err := psCmd.Run(); err != nil {
   316  			panic(fmt.Sprintf("running ps: %v", err))
   317  		}
   318  
   319  		listFiles := "lsof"
   320  		if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" {
   321  			listFiles = "fstat"
   322  		}
   323  
   324  		fmt.Fprintln(os.Stderr, "\n"+listFiles+":")
   325  		fmt.Fprintln(os.Stderr, "-----")
   326  		listFilesCmd := exec.Command(listFiles)
   327  		listFilesCmd.Stdout = os.Stderr
   328  		listFilesCmd.Stderr = os.Stderr
   329  		if err := listFilesCmd.Run(); err != nil {
   330  			panic(fmt.Sprintf("running %s: %v", listFiles, err))
   331  		}
   332  	}
   333  	panic(fmt.Sprintf("detected hanging go command (pid %d): see golang/go#54461 for more details", proc.Pid))
   334  }
   335  
   336  func cmdDebugStr(cmd *exec.Cmd) string {
   337  	env := make(map[string]string)
   338  	for _, kv := range cmd.Env {
   339  		split := strings.SplitN(kv, "=", 2)
   340  		if len(split) == 2 {
   341  			k, v := split[0], split[1]
   342  			env[k] = v
   343  		}
   344  	}
   345  
   346  	var args []string
   347  	for _, arg := range cmd.Args {
   348  		quoted := strconv.Quote(arg)
   349  		if quoted[1:len(quoted)-1] != arg || strings.Contains(arg, " ") {
   350  			args = append(args, quoted)
   351  		} else {
   352  			args = append(args, arg)
   353  		}
   354  	}
   355  	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, " "))
   356  }