cuelang.org/go@v0.10.1/internal/golangorgx/tools/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  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"log"
    15  	"os"
    16  	"os/exec"
    17  	"reflect"
    18  	"regexp"
    19  	"runtime"
    20  	"strconv"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"cuelang.org/go/internal/golangorgx/tools/event"
    26  	"cuelang.org/go/internal/golangorgx/tools/event/keys"
    27  	"cuelang.org/go/internal/golangorgx/tools/event/label"
    28  	"cuelang.org/go/internal/golangorgx/tools/event/tag"
    29  )
    30  
    31  // An Runner will run go command invocations and serialize
    32  // them if it sees a concurrency error.
    33  type Runner struct {
    34  	// once guards the runner initialization.
    35  	once sync.Once
    36  
    37  	// inFlight tracks available workers.
    38  	inFlight chan struct{}
    39  
    40  	// serialized guards the ability to run a go command serially,
    41  	// to avoid deadlocks when claiming workers.
    42  	serialized chan struct{}
    43  }
    44  
    45  const maxInFlight = 10
    46  
    47  func (runner *Runner) initialize() {
    48  	runner.once.Do(func() {
    49  		runner.inFlight = make(chan struct{}, maxInFlight)
    50  		runner.serialized = make(chan struct{}, 1)
    51  	})
    52  }
    53  
    54  // 1.13: go: updates to go.mod needed, but contents have changed
    55  // 1.14: go: updating go.mod: existing contents have changed since last read
    56  var modConcurrencyError = regexp.MustCompile(`go:.*go.mod.*contents have changed`)
    57  
    58  // verb is an event label for the go command verb.
    59  var verb = keys.NewString("verb", "go command verb")
    60  
    61  func invLabels(inv Invocation) []label.Label {
    62  	return []label.Label{verb.Of(inv.Verb), tag.Directory.Of(inv.WorkingDir)}
    63  }
    64  
    65  // Run is a convenience wrapper around RunRaw.
    66  // It returns only stdout and a "friendly" error.
    67  func (runner *Runner) Run(ctx context.Context, inv Invocation) (*bytes.Buffer, error) {
    68  	ctx, done := event.Start(ctx, "gocommand.Runner.Run", invLabels(inv)...)
    69  	defer done()
    70  
    71  	stdout, _, friendly, _ := runner.RunRaw(ctx, inv)
    72  	return stdout, friendly
    73  }
    74  
    75  // RunPiped runs the invocation serially, always waiting for any concurrent
    76  // invocations to complete first.
    77  func (runner *Runner) RunPiped(ctx context.Context, inv Invocation, stdout, stderr io.Writer) error {
    78  	ctx, done := event.Start(ctx, "gocommand.Runner.RunPiped", invLabels(inv)...)
    79  	defer done()
    80  
    81  	_, err := runner.runPiped(ctx, inv, stdout, stderr)
    82  	return err
    83  }
    84  
    85  // RunRaw runs the invocation, serializing requests only if they fight over
    86  // go.mod changes.
    87  // Postcondition: both error results have same nilness.
    88  func (runner *Runner) RunRaw(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) {
    89  	ctx, done := event.Start(ctx, "gocommand.Runner.RunRaw", invLabels(inv)...)
    90  	defer done()
    91  	// Make sure the runner is always initialized.
    92  	runner.initialize()
    93  
    94  	// First, try to run the go command concurrently.
    95  	stdout, stderr, friendlyErr, err := runner.runConcurrent(ctx, inv)
    96  
    97  	// If we encounter a load concurrency error, we need to retry serially.
    98  	if friendlyErr != nil && modConcurrencyError.MatchString(friendlyErr.Error()) {
    99  		event.Error(ctx, "Load concurrency error, will retry serially", err)
   100  
   101  		// Run serially by calling runPiped.
   102  		stdout.Reset()
   103  		stderr.Reset()
   104  		friendlyErr, err = runner.runPiped(ctx, inv, stdout, stderr)
   105  	}
   106  
   107  	return stdout, stderr, friendlyErr, err
   108  }
   109  
   110  // Postcondition: both error results have same nilness.
   111  func (runner *Runner) runConcurrent(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) {
   112  	// Wait for 1 worker to become available.
   113  	select {
   114  	case <-ctx.Done():
   115  		return nil, nil, ctx.Err(), ctx.Err()
   116  	case runner.inFlight <- struct{}{}:
   117  		defer func() { <-runner.inFlight }()
   118  	}
   119  
   120  	stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
   121  	friendlyErr, err := inv.runWithFriendlyError(ctx, stdout, stderr)
   122  	return stdout, stderr, friendlyErr, err
   123  }
   124  
   125  // Postcondition: both error results have same nilness.
   126  func (runner *Runner) runPiped(ctx context.Context, inv Invocation, stdout, stderr io.Writer) (error, error) {
   127  	// Make sure the runner is always initialized.
   128  	runner.initialize()
   129  
   130  	// Acquire the serialization lock. This avoids deadlocks between two
   131  	// runPiped commands.
   132  	select {
   133  	case <-ctx.Done():
   134  		return ctx.Err(), ctx.Err()
   135  	case runner.serialized <- struct{}{}:
   136  		defer func() { <-runner.serialized }()
   137  	}
   138  
   139  	// Wait for all in-progress go commands to return before proceeding,
   140  	// to avoid load concurrency errors.
   141  	for i := 0; i < maxInFlight; i++ {
   142  		select {
   143  		case <-ctx.Done():
   144  			return ctx.Err(), ctx.Err()
   145  		case runner.inFlight <- struct{}{}:
   146  			// Make sure we always "return" any workers we took.
   147  			defer func() { <-runner.inFlight }()
   148  		}
   149  	}
   150  
   151  	return inv.runWithFriendlyError(ctx, stdout, stderr)
   152  }
   153  
   154  // An Invocation represents a call to the go command.
   155  type Invocation struct {
   156  	Verb       string
   157  	Args       []string
   158  	BuildFlags []string
   159  
   160  	// If ModFlag is set, the go command is invoked with -mod=ModFlag.
   161  	ModFlag string
   162  
   163  	// If ModFile is set, the go command is invoked with -modfile=ModFile.
   164  	ModFile string
   165  
   166  	// If Overlay is set, the go command is invoked with -overlay=Overlay.
   167  	Overlay string
   168  
   169  	// If CleanEnv is set, the invocation will run only with the environment
   170  	// in Env, not starting with os.Environ.
   171  	CleanEnv   bool
   172  	Env        []string
   173  	WorkingDir string
   174  	Logf       func(format string, args ...interface{})
   175  }
   176  
   177  // Postcondition: both error results have same nilness.
   178  func (i *Invocation) runWithFriendlyError(ctx context.Context, stdout, stderr io.Writer) (friendlyError error, rawError error) {
   179  	rawError = i.run(ctx, stdout, stderr)
   180  	if rawError != nil {
   181  		friendlyError = rawError
   182  		// Check for 'go' executable not being found.
   183  		if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
   184  			friendlyError = fmt.Errorf("go command required, not found: %v", ee)
   185  		}
   186  		if ctx.Err() != nil {
   187  			friendlyError = ctx.Err()
   188  		}
   189  		friendlyError = fmt.Errorf("err: %v: stderr: %s", friendlyError, stderr)
   190  	}
   191  	return
   192  }
   193  
   194  func (i *Invocation) run(ctx context.Context, stdout, stderr io.Writer) error {
   195  	log := i.Logf
   196  	if log == nil {
   197  		log = func(string, ...interface{}) {}
   198  	}
   199  
   200  	goArgs := []string{i.Verb}
   201  
   202  	appendModFile := func() {
   203  		if i.ModFile != "" {
   204  			goArgs = append(goArgs, "-modfile="+i.ModFile)
   205  		}
   206  	}
   207  	appendModFlag := func() {
   208  		if i.ModFlag != "" {
   209  			goArgs = append(goArgs, "-mod="+i.ModFlag)
   210  		}
   211  	}
   212  	appendOverlayFlag := func() {
   213  		if i.Overlay != "" {
   214  			goArgs = append(goArgs, "-overlay="+i.Overlay)
   215  		}
   216  	}
   217  
   218  	switch i.Verb {
   219  	case "env", "version":
   220  		goArgs = append(goArgs, i.Args...)
   221  	case "mod":
   222  		// mod needs the sub-verb before flags.
   223  		goArgs = append(goArgs, i.Args[0])
   224  		appendModFile()
   225  		goArgs = append(goArgs, i.Args[1:]...)
   226  	case "get":
   227  		goArgs = append(goArgs, i.BuildFlags...)
   228  		appendModFile()
   229  		goArgs = append(goArgs, i.Args...)
   230  
   231  	default: // notably list and build.
   232  		goArgs = append(goArgs, i.BuildFlags...)
   233  		appendModFile()
   234  		appendModFlag()
   235  		appendOverlayFlag()
   236  		goArgs = append(goArgs, i.Args...)
   237  	}
   238  	cmd := exec.Command("go", goArgs...)
   239  	cmd.Stdout = stdout
   240  	cmd.Stderr = stderr
   241  
   242  	// cmd.WaitDelay was added only in go1.20 (see #50436).
   243  	if waitDelay := reflect.ValueOf(cmd).Elem().FieldByName("WaitDelay"); waitDelay.IsValid() {
   244  		// https://go.dev/issue/59541: don't wait forever copying stderr
   245  		// after the command has exited.
   246  		// After CL 484741 we copy stdout manually, so we we'll stop reading that as
   247  		// soon as ctx is done. However, we also don't want to wait around forever
   248  		// for stderr. Give a much-longer-than-reasonable delay and then assume that
   249  		// something has wedged in the kernel or runtime.
   250  		waitDelay.Set(reflect.ValueOf(30 * time.Second))
   251  	}
   252  
   253  	// On darwin the cwd gets resolved to the real path, which breaks anything that
   254  	// expects the working directory to keep the original path, including the
   255  	// go command when dealing with modules.
   256  	// The Go stdlib has a special feature where if the cwd and the PWD are the
   257  	// same node then it trusts the PWD, so by setting it in the env for the child
   258  	// process we fix up all the paths returned by the go command.
   259  	if !i.CleanEnv {
   260  		cmd.Env = os.Environ()
   261  	}
   262  	cmd.Env = append(cmd.Env, i.Env...)
   263  	if i.WorkingDir != "" {
   264  		cmd.Env = append(cmd.Env, "PWD="+i.WorkingDir)
   265  		cmd.Dir = i.WorkingDir
   266  	}
   267  
   268  	defer func(start time.Time) { log("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
   269  
   270  	return runCmdContext(ctx, cmd)
   271  }
   272  
   273  // DebugHangingGoCommands may be set by tests to enable additional
   274  // instrumentation (including panics) for debugging hanging Go commands.
   275  //
   276  // See golang/go#54461 for details.
   277  var DebugHangingGoCommands = false
   278  
   279  // runCmdContext is like exec.CommandContext except it sends os.Interrupt
   280  // before os.Kill.
   281  func runCmdContext(ctx context.Context, cmd *exec.Cmd) (err error) {
   282  	// If cmd.Stdout is not an *os.File, the exec package will create a pipe and
   283  	// copy it to the Writer in a goroutine until the process has finished and
   284  	// either the pipe reaches EOF or command's WaitDelay expires.
   285  	//
   286  	// However, the output from 'go list' can be quite large, and we don't want to
   287  	// keep reading (and allocating buffers) if we've already decided we don't
   288  	// care about the output. We don't want to wait for the process to finish, and
   289  	// we don't wait to wait for the WaitDelay to expire either.
   290  	//
   291  	// Instead, if cmd.Stdout requires a copying goroutine we explicitly replace
   292  	// it with a pipe (which is an *os.File), which we can close in order to stop
   293  	// copying output as soon as we realize we don't care about it.
   294  	var stdoutW *os.File
   295  	if cmd.Stdout != nil {
   296  		if _, ok := cmd.Stdout.(*os.File); !ok {
   297  			var stdoutR *os.File
   298  			stdoutR, stdoutW, err = os.Pipe()
   299  			if err != nil {
   300  				return err
   301  			}
   302  			prevStdout := cmd.Stdout
   303  			cmd.Stdout = stdoutW
   304  
   305  			stdoutErr := make(chan error, 1)
   306  			go func() {
   307  				_, err := io.Copy(prevStdout, stdoutR)
   308  				if err != nil {
   309  					err = fmt.Errorf("copying stdout: %w", err)
   310  				}
   311  				stdoutErr <- err
   312  			}()
   313  			defer func() {
   314  				// We started a goroutine to copy a stdout pipe.
   315  				// Wait for it to finish, or terminate it if need be.
   316  				var err2 error
   317  				select {
   318  				case err2 = <-stdoutErr:
   319  					stdoutR.Close()
   320  				case <-ctx.Done():
   321  					stdoutR.Close()
   322  					// Per https://pkg.go.dev/os#File.Close, the call to stdoutR.Close
   323  					// should cause the Read call in io.Copy to unblock and return
   324  					// immediately, but we still need to receive from stdoutErr to confirm
   325  					// that it has happened.
   326  					<-stdoutErr
   327  					err2 = ctx.Err()
   328  				}
   329  				if err == nil {
   330  					err = err2
   331  				}
   332  			}()
   333  
   334  			// Per https://pkg.go.dev/os/exec#Cmd, “If Stdout and Stderr are the
   335  			// same writer, and have a type that can be compared with ==, at most
   336  			// one goroutine at a time will call Write.”
   337  			//
   338  			// Since we're starting a goroutine that writes to cmd.Stdout, we must
   339  			// also update cmd.Stderr so that it still holds.
   340  			func() {
   341  				defer func() { recover() }()
   342  				if cmd.Stderr == prevStdout {
   343  					cmd.Stderr = cmd.Stdout
   344  				}
   345  			}()
   346  		}
   347  	}
   348  
   349  	err = cmd.Start()
   350  	if stdoutW != nil {
   351  		// The child process has inherited the pipe file,
   352  		// so close the copy held in this process.
   353  		stdoutW.Close()
   354  		stdoutW = nil
   355  	}
   356  	if err != nil {
   357  		return err
   358  	}
   359  
   360  	resChan := make(chan error, 1)
   361  	go func() {
   362  		resChan <- cmd.Wait()
   363  	}()
   364  
   365  	// If we're interested in debugging hanging Go commands, stop waiting after a
   366  	// minute and panic with interesting information.
   367  	debug := DebugHangingGoCommands
   368  	if debug {
   369  		timer := time.NewTimer(1 * time.Minute)
   370  		defer timer.Stop()
   371  		select {
   372  		case err := <-resChan:
   373  			return err
   374  		case <-timer.C:
   375  			HandleHangingGoCommand(cmd.Process)
   376  		case <-ctx.Done():
   377  		}
   378  	} else {
   379  		select {
   380  		case err := <-resChan:
   381  			return err
   382  		case <-ctx.Done():
   383  		}
   384  	}
   385  
   386  	// Cancelled. Interrupt and see if it ends voluntarily.
   387  	if err := cmd.Process.Signal(os.Interrupt); err == nil {
   388  		// (We used to wait only 1s but this proved
   389  		// fragile on loaded builder machines.)
   390  		timer := time.NewTimer(5 * time.Second)
   391  		defer timer.Stop()
   392  		select {
   393  		case err := <-resChan:
   394  			return err
   395  		case <-timer.C:
   396  		}
   397  	}
   398  
   399  	// Didn't shut down in response to interrupt. Kill it hard.
   400  	// TODO(rfindley): per advice from bcmills@, it may be better to send SIGQUIT
   401  	// on certain platforms, such as unix.
   402  	if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) && debug {
   403  		log.Printf("error killing the Go command: %v", err)
   404  	}
   405  
   406  	return <-resChan
   407  }
   408  
   409  func HandleHangingGoCommand(proc *os.Process) {
   410  	switch runtime.GOOS {
   411  	case "linux", "darwin", "freebsd", "netbsd":
   412  		fmt.Fprintln(os.Stderr, `DETECTED A HANGING GO COMMAND
   413  
   414  The gopls test runner has detected a hanging go command. In order to debug
   415  this, the output of ps and lsof/fstat is printed below.
   416  
   417  See golang/go#54461 for more details.`)
   418  
   419  		fmt.Fprintln(os.Stderr, "\nps axo ppid,pid,command:")
   420  		fmt.Fprintln(os.Stderr, "-------------------------")
   421  		psCmd := exec.Command("ps", "axo", "ppid,pid,command")
   422  		psCmd.Stdout = os.Stderr
   423  		psCmd.Stderr = os.Stderr
   424  		if err := psCmd.Run(); err != nil {
   425  			panic(fmt.Sprintf("running ps: %v", err))
   426  		}
   427  
   428  		listFiles := "lsof"
   429  		if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" {
   430  			listFiles = "fstat"
   431  		}
   432  
   433  		fmt.Fprintln(os.Stderr, "\n"+listFiles+":")
   434  		fmt.Fprintln(os.Stderr, "-----")
   435  		listFilesCmd := exec.Command(listFiles)
   436  		listFilesCmd.Stdout = os.Stderr
   437  		listFilesCmd.Stderr = os.Stderr
   438  		if err := listFilesCmd.Run(); err != nil {
   439  			panic(fmt.Sprintf("running %s: %v", listFiles, err))
   440  		}
   441  	}
   442  	panic(fmt.Sprintf("detected hanging go command (pid %d): see golang/go#54461 for more details", proc.Pid))
   443  }
   444  
   445  func cmdDebugStr(cmd *exec.Cmd) string {
   446  	env := make(map[string]string)
   447  	for _, kv := range cmd.Env {
   448  		split := strings.SplitN(kv, "=", 2)
   449  		if len(split) == 2 {
   450  			k, v := split[0], split[1]
   451  			env[k] = v
   452  		}
   453  	}
   454  
   455  	var args []string
   456  	for _, arg := range cmd.Args {
   457  		quoted := strconv.Quote(arg)
   458  		if quoted[1:len(quoted)-1] != arg || strings.Contains(arg, " ") {
   459  			args = append(args, quoted)
   460  		} else {
   461  			args = append(args, arg)
   462  		}
   463  	}
   464  	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, " "))
   465  }