github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/cmd/jiri/runp.go (about) 1 // Copyright 2015 The Vanadium 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 main 6 7 import ( 8 "bufio" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "os/exec" 14 "os/signal" 15 "regexp" 16 "sort" 17 "strings" 18 "sync" 19 20 "github.com/btwiuse/jiri" 21 "github.com/btwiuse/jiri/cmdline" 22 "github.com/btwiuse/jiri/envvar" 23 "github.com/btwiuse/jiri/project" 24 "github.com/btwiuse/jiri/simplemr" 25 "github.com/btwiuse/jiri/tool" 26 ) 27 28 var runpFlags struct { 29 projectKeys string 30 verbose bool 31 interactive bool 32 uncommitted bool 33 noUncommitted bool 34 untracked bool 35 noUntracked bool 36 showNamePrefix bool 37 showPathPrefix bool 38 showKeyPrefix bool 39 exitOnError bool 40 collateOutput bool 41 branch string 42 remote string 43 } 44 45 var cmdRunP = &cmdline.Command{ 46 Runner: jiri.RunnerFunc(runRunp), 47 Name: "runp", 48 Short: "Run a command in parallel across jiri projects", 49 Long: `Run a command in parallel across one or more jiri projects. Commands are run 50 using the shell specified by the users $SHELL environment variable, or "sh" 51 if that's not set. Thus commands are run as $SHELL -c "args..." 52 `, 53 ArgsName: "<command line>", 54 ArgsLong: `A command line to be run in each project specified by the supplied command 55 line flags. Any environment variables intended to be evaluated when the 56 command line is run must be quoted to avoid expansion before being passed to 57 runp by the shell. 58 `, 59 } 60 61 func init() { 62 cmdRunP.Flags.BoolVar(&runpFlags.verbose, "v", false, "Print verbose logging information") 63 cmdRunP.Flags.StringVar(&runpFlags.projectKeys, "projects", "", "A Regular expression specifying project keys to run commands in. By default, runp will use projects that have the same branch checked as the current project unless it is run from outside of a project in which case it will default to using all projects.") 64 cmdRunP.Flags.BoolVar(&runpFlags.uncommitted, "uncommitted", false, "Match projects that have uncommitted changes") 65 cmdRunP.Flags.BoolVar(&runpFlags.noUncommitted, "no-uncommitted", false, "Match projects that have no uncommitted changes") 66 cmdRunP.Flags.BoolVar(&runpFlags.untracked, "untracked", false, "Match projects that have untracked files") 67 cmdRunP.Flags.BoolVar(&runpFlags.noUntracked, "no-untracked", false, "Match projects that have no untracked files") 68 cmdRunP.Flags.BoolVar(&runpFlags.interactive, "interactive", false, "If set, the command to be run is interactive and should not have its stdout/stderr manipulated. This flag cannot be used with -show-name-prefix, -show-key-prefix or -collate-stdout.") 69 cmdRunP.Flags.BoolVar(&runpFlags.showNamePrefix, "show-name-prefix", false, "If set, each line of output from each project will begin with the name of the project followed by a colon. This is intended for use with long running commands where the output needs to be streamed. Stdout and stderr are spliced apart. This flag cannot be used with -interactive, -show-path-prefix, -show-key-prefix or -collate-stdout.") 70 cmdRunP.Flags.BoolVar(&runpFlags.showPathPrefix, "show-path-prefix", false, "If set, each line of output from each project will begin with the path of the project followed by a colon. This is intended for use with long running commands where the output needs to be streamed. Stdout and stderr are spliced apart. This flag cannot be used with -interactive, -show-name-prefix, -show-key-prefix or -collate-stdout.") 71 cmdRunP.Flags.BoolVar(&runpFlags.showKeyPrefix, "show-key-prefix", false, "If set, each line of output from each project will begin with the key of the project followed by a colon. This is intended for use with long running commands where the output needs to be streamed. Stdout and stderr are spliced apart. This flag cannot be used with -interactive, -show-name-prefix, -show-path-prefix or -collate-stdout") 72 cmdRunP.Flags.BoolVar(&runpFlags.collateOutput, "collate-stdout", true, "Collate all stdout output from each parallel invocation and display it as if had been generated sequentially. This flag cannot be used with -show-name-prefix, -show-key-prefix or -interactive.") 73 cmdRunP.Flags.BoolVar(&runpFlags.exitOnError, "exit-on-error", false, "If set, all commands will killed as soon as one reports an error, otherwise, each will run to completion.") 74 cmdRunP.Flags.StringVar(&runpFlags.branch, "branch", "", "A regular expression specifying branch names to use in matching projects. A project will match if the specified branch exists, even if it is not checked out.") 75 cmdRunP.Flags.StringVar(&runpFlags.remote, "remote", "", "A Regular expression specifying projects to run commands in by matching against their remote URLs.") 76 } 77 78 type mapInput struct { 79 project.Project 80 key project.ProjectKey 81 jirix *jiri.X 82 index, total int 83 result error 84 } 85 86 func newmapInput(jirix *jiri.X, project project.Project, key project.ProjectKey, index, total int) *mapInput { 87 return &mapInput{ 88 Project: project, 89 key: key, 90 jirix: jirix.Clone(tool.ContextOpts{}), 91 index: index, 92 total: total, 93 } 94 } 95 96 func projectNames(mapInputs map[project.ProjectKey]*mapInput) []string { 97 n := []string{} 98 for _, mi := range mapInputs { 99 n = append(n, mi.Project.Name) 100 } 101 sort.Strings(n) 102 return n 103 } 104 105 func projectKeys(mapInputs map[project.ProjectKey]*mapInput) []string { 106 n := []string{} 107 for key := range mapInputs { 108 n = append(n, string(key)) 109 } 110 sort.Strings(n) 111 return n 112 } 113 114 type runner struct { 115 args []string 116 serializedWriterLock sync.Mutex 117 collatedOutputLock sync.Mutex 118 } 119 120 func (r *runner) serializedWriter(w io.Writer) io.Writer { 121 return &sharedLockWriter{&r.serializedWriterLock, w} 122 } 123 124 type sharedLockWriter struct { 125 mu *sync.Mutex 126 f io.Writer 127 } 128 129 func (lw *sharedLockWriter) Write(d []byte) (int, error) { 130 lw.mu.Lock() 131 defer lw.mu.Unlock() 132 return lw.f.Write(d) 133 } 134 135 func copyWithPrefix(prefix string, w io.Writer, r io.Reader) { 136 reader := bufio.NewReader(r) 137 for { 138 line, err := reader.ReadString('\n') 139 if err != nil { 140 if line != "" { 141 fmt.Fprintf(w, "%v: %v\n", prefix, line) 142 } 143 break 144 } 145 fmt.Fprintf(w, "%v: %v", prefix, line) 146 } 147 } 148 149 type mapOutput struct { 150 mi *mapInput 151 outputFilename string 152 key string 153 err error 154 } 155 156 func (r *runner) Map(mr *simplemr.MR, key string, val interface{}) error { 157 mi := val.(*mapInput) 158 output := &mapOutput{ 159 key: key, 160 mi: mi} 161 jirix := mi.jirix 162 path := os.Getenv("SHELL") 163 if path == "" { 164 path = "sh" 165 } 166 var wg sync.WaitGroup 167 cmd := exec.Command(path, "-c", strings.Join(r.args, " ")) 168 cmd.Env = envvar.MapToSlice(jirix.Env()) 169 cmd.Dir = mi.Project.Path 170 cmd.Stdin = mi.jirix.Stdin() 171 var stdoutCloser, stderrCloser io.Closer 172 if runpFlags.interactive { 173 cmd.Stdout = os.Stdout 174 cmd.Stderr = os.Stderr 175 } else { 176 var stdout io.Writer 177 stderr := r.serializedWriter(jirix.Stderr()) 178 var cleanup func() 179 if runpFlags.collateOutput { 180 // Write standard output to a file, stderr 181 // is not collated. 182 f, err := ioutil.TempFile("", "jiri-runp-") 183 if err != nil { 184 return err 185 } 186 stdout = f 187 output.outputFilename = f.Name() 188 cleanup = func() { 189 os.Remove(output.outputFilename) 190 } 191 // The child process will have exited by the 192 // time this method returns so it's safe to close the file 193 // here. 194 defer f.Close() 195 } else { 196 stdout = r.serializedWriter(os.Stdout) 197 cleanup = func() {} 198 } 199 if !runpFlags.showNamePrefix && !runpFlags.showKeyPrefix && !runpFlags.showPathPrefix { 200 // write directly to stdout, stderr if there's no prefix 201 cmd.Stdout = stdout 202 cmd.Stderr = stderr 203 } else { 204 stdoutReader, stdoutWriter, err := os.Pipe() 205 if err != nil { 206 cleanup() 207 return err 208 } 209 stderrReader, stderrWriter, err := os.Pipe() 210 if err != nil { 211 cleanup() 212 stdoutReader.Close() 213 stdoutWriter.Close() 214 return err 215 } 216 cmd.Stdout = stdoutWriter 217 cmd.Stderr = stderrWriter 218 // Record the write end of the pipe so that it can be closed 219 // after the child has exited, this ensures that all goroutines 220 // will finish. 221 stdoutCloser = stdoutWriter 222 stderrCloser = stderrWriter 223 prefix := key 224 if runpFlags.showNamePrefix { 225 prefix = mi.Project.Name 226 } 227 if runpFlags.showPathPrefix { 228 prefix = mi.Project.Path 229 } 230 wg.Add(2) 231 go func() { copyWithPrefix(prefix, stdout, stdoutReader); wg.Done() }() 232 go func() { copyWithPrefix(prefix, stderr, stderrReader); wg.Done() }() 233 234 } 235 } 236 if err := cmd.Start(); err != nil { 237 mi.result = err 238 } 239 done := make(chan error) 240 go func() { 241 done <- cmd.Wait() 242 }() 243 select { 244 case output.err = <-done: 245 if output.err != nil && runpFlags.exitOnError { 246 mr.Cancel() 247 } 248 case <-mr.CancelCh(): 249 output.err = cmd.Process.Kill() 250 } 251 for _, closer := range []io.Closer{stdoutCloser, stderrCloser} { 252 if closer != nil { 253 closer.Close() 254 } 255 } 256 wg.Wait() 257 mr.MapOut(key, output) 258 return nil 259 } 260 261 func (r *runner) Reduce(mr *simplemr.MR, key string, values []interface{}) error { 262 for _, v := range values { 263 mo := v.(*mapOutput) 264 if mo.err != nil { 265 fmt.Fprintf(os.Stdout, "FAILED: %v: %s %v\n", mo.key, strings.Join(r.args, " "), mo.err) 266 return nil 267 } else { 268 if runpFlags.collateOutput { 269 r.collatedOutputLock.Lock() 270 defer r.collatedOutputLock.Unlock() 271 defer os.Remove(mo.outputFilename) 272 if fi, err := os.Open(mo.outputFilename); err == nil { 273 io.Copy(os.Stdout, fi) 274 fi.Close() 275 } else { 276 return err 277 } 278 } 279 } 280 } 281 return nil 282 } 283 284 func runRunp(jirix *jiri.X, args []string) error { 285 if runpFlags.interactive { 286 runpFlags.collateOutput = false 287 } 288 289 var keysRE, branchRE, remoteRE *regexp.Regexp 290 var err error 291 292 if runpFlags.projectKeys != "" { 293 re := "" 294 for _, pre := range strings.Split(runpFlags.projectKeys, ",") { 295 re += pre + "|" 296 } 297 re = strings.TrimRight(re, "|") 298 keysRE, err = regexp.Compile(re) 299 if err != nil { 300 return fmt.Errorf("failed to compile projects regexp: %q: %v", runpFlags.projectKeys, err) 301 } 302 } 303 304 if runpFlags.branch != "" { 305 branchRE, err = regexp.Compile(runpFlags.branch) 306 if err != nil { 307 return fmt.Errorf("failed to compile has-branch regexp: %q: %v", runpFlags.branch, err) 308 } 309 } 310 311 if runpFlags.remote != "" { 312 remoteRE, err = regexp.Compile(runpFlags.remote) 313 if err != nil { 314 return fmt.Errorf("failed to compile remotes regexp: %q: %v", runpFlags.remote, err) 315 } 316 } 317 318 if (runpFlags.showKeyPrefix || runpFlags.showNamePrefix || runpFlags.showPathPrefix) && runpFlags.interactive { 319 fmt.Fprintf(jirix.Stderr(), "WARNING: interactive mode being disabled because show-key-prefix or show-name-prefix or show-path-prefix was set\n") 320 runpFlags.interactive = false 321 runpFlags.collateOutput = true 322 } 323 324 dir, err := os.Getwd() 325 if err != nil { 326 return fmt.Errorf("os.Getwd() failed: %v", err) 327 } 328 if dir == jirix.Root || err != nil { 329 // jiri was run from outside of a project. Let's assume we'll 330 // use all projects if none have been specified via the projects flag. 331 if keysRE == nil { 332 keysRE = regexp.MustCompile(".*") 333 } 334 } 335 projects, err := project.LocalProjects(jirix, project.FastScan) 336 if err != nil { 337 return err 338 } 339 340 projectStateRequired := branchRE != nil || runpFlags.untracked || runpFlags.noUntracked || runpFlags.uncommitted || runpFlags.noUncommitted 341 var states map[project.ProjectKey]*project.ProjectState 342 if projectStateRequired { 343 var err error 344 states, err = project.GetProjectStates(jirix, projects, runpFlags.untracked || runpFlags.noUntracked || runpFlags.uncommitted || runpFlags.noUncommitted) 345 if err != nil { 346 return err 347 } 348 } 349 mapInputs := map[project.ProjectKey]*mapInput{} 350 var keys project.ProjectKeys 351 for _, localProject := range projects { 352 key := localProject.Key() 353 if keysRE != nil { 354 if !keysRE.MatchString(string(key)) { 355 continue 356 } 357 } 358 state := states[key] 359 if branchRE != nil { 360 found := false 361 for _, br := range state.Branches { 362 if branchRE.MatchString(br.Name) { 363 found = true 364 break 365 } 366 } 367 if !found { 368 continue 369 } 370 } 371 if remoteRE != nil && !remoteRE.MatchString(localProject.Remote) { 372 continue 373 } 374 if (runpFlags.untracked && !state.HasUntracked) || (runpFlags.noUntracked && state.HasUntracked) { 375 continue 376 } 377 if (runpFlags.uncommitted && !state.HasUncommitted) || (runpFlags.noUncommitted && state.HasUncommitted) { 378 continue 379 } 380 mapInputs[key] = &mapInput{ 381 Project: localProject, 382 jirix: jirix, 383 key: key, 384 } 385 keys = append(keys, key) 386 } 387 388 total := len(mapInputs) 389 index := 1 390 for _, mi := range mapInputs { 391 mi.index = index 392 mi.total = total 393 index++ 394 } 395 396 if runpFlags.verbose { 397 fmt.Fprintf(os.Stdout, "Project Names: %s\n", strings.Join(projectNames(mapInputs), " ")) 398 fmt.Fprintf(os.Stdout, "Project Keys: %s\n", strings.Join(projectKeys(mapInputs), " ")) 399 } 400 401 runner := &runner{ 402 args: args, 403 } 404 mr := simplemr.MR{} 405 if runpFlags.interactive { 406 // Run one mapper at a time. 407 mr.NumMappers = 1 408 sort.Sort(keys) 409 } else { 410 mr.NumMappers = int(jirix.Jobs) 411 } 412 in, out := make(chan *simplemr.Record, len(mapInputs)), make(chan *simplemr.Record, len(mapInputs)) 413 sigch := make(chan os.Signal) 414 signal.Notify(sigch, os.Interrupt) 415 jirix.TimerPush("Map and Reduce") 416 go func() { <-sigch; mr.Cancel() }() 417 go mr.Run(in, out, runner, runner) 418 for _, key := range keys { 419 in <- &simplemr.Record{string(key), []interface{}{mapInputs[key]}} 420 } 421 close(in) 422 <-out 423 jirix.TimerPop() 424 return mr.Error() 425 }