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 }