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