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 }