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  }