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 }