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  }