diff --git a/command/cmd.go b/command/cmd.go index dfb47e0..06780c9 100644 --- a/command/cmd.go +++ b/command/cmd.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "os/exec" - "strings" hub "github.com/konveyor/tackle2-hub/addon" ) @@ -17,13 +16,30 @@ var ( addon = hub.Addon ) +// +// New returns a command. +func New(path string) (cmd *Command) { + cmd = &Command{Path: path} + cmd.Reporter.Filter = func(in string) (out string) { + out = in + return + } + cmd.Writer.Filter = func(in []byte) (out []byte) { + out = in + return + } + return +} + // // Command execution. type Command struct { - Options Options - Path string - Dir string - Output []byte + Verbosity int + Options Options + Path string + Dir string + Reporter Reporter + Writer Writer } // @@ -40,22 +56,25 @@ func (r *Command) Run() (err error) { // The command and output are both reported in // task Report.Activity. func (r *Command) RunWith(ctx context.Context) (err error) { - addon.Activity( - "[CMD] Running: %s %s", - r.Path, - strings.Join(r.Options, " ")) + r.Writer.reporter = &r.Reporter + r.Reporter.Run(r.Path, r.Options) + defer func() { + r.Writer.End() + if err != nil { + r.Reporter.Error(r.Path, err, r.Writer.buffer) + } else { + r.Reporter.Succeeded(r.Path) + } + }() cmd := exec.CommandContext(ctx, r.Path, r.Options...) cmd.Dir = r.Dir - r.Output, err = cmd.CombinedOutput() + cmd.Stdout = &r.Writer + cmd.Stderr = &r.Writer + err = cmd.Start() if err != nil { - addon.Activity( - "[CMD] %s failed: %s.\n%s", - r.Path, - err.Error(), - string(r.Output)) - } else { - addon.Activity("[CMD] succeeded.") + return } + err = cmd.Wait() return } @@ -64,26 +83,15 @@ func (r *Command) RunWith(ctx context.Context) (err error) { // On error: The command (without arguments) and output are // reported in task Report.Activity func (r *Command) RunSilent() (err error) { - err = r.RunSilentWith(context.TODO()) + r.Reporter.Verbosity = Error + err = r.RunWith(context.TODO()) return } // -// RunSilentWith executes the command with context. -// On error: The command (without arguments) and output are -// reported in task Report.Activity -func (r *Command) RunSilentWith(ctx context.Context) (err error) { - cmd := exec.CommandContext(ctx, r.Path, r.Options...) - cmd.Dir = r.Dir - r.Output, err = cmd.CombinedOutput() - if err != nil { - addon.Activity( - "[CMD] %s failed: %s.\n%s", - r.Path, - err.Error(), - string(r.Output)) - } - return +// Output returns the command output. +func (r *Command) Output() (b []byte) { + return r.Writer.buffer } // @@ -91,14 +99,14 @@ func (r *Command) RunSilentWith(ctx context.Context) (err error) { type Options []string // -// add +// Add option. func (a *Options) Add(option string, s ...string) { *a = append(*a, option) *a = append(*a, s...) } // -// add +// Addf option. func (a *Options) Addf(option string, x ...interface{}) { *a = append(*a, fmt.Sprintf(option, x...)) } diff --git a/command/reporter.go b/command/reporter.go new file mode 100644 index 0000000..237d888 --- /dev/null +++ b/command/reporter.go @@ -0,0 +1,119 @@ +package command + +import ( + "strings" +) + +// +// Verbosity. +const ( + // Disabled reports: NOTHING. + Disabled = -2 + // Error reports: error. + Error = -1 + // Default reports: error, started, succeeded. + Default = 0 + // LiveOutput reports: error, started, succeeded, output (live). + LiveOutput = 1 +) + +// +// ReportFilter filter reported output. +type ReportFilter func(in string) (out string) + +// +// Reporter activity reporter. +type Reporter struct { + Filter ReportFilter + Verbosity int + index int +} + +// +// Run reports command started in task Report.Activity. +func (r *Reporter) Run(path string, options Options) { + switch r.Verbosity { + case Disabled: + case Error: + case Default, + LiveOutput: + addon.Activity( + "[CMD] Running: %s %s", + path, + strings.Join(options, " ")) + } +} + +// +// Succeeded reports command succeeded in task Report.Activity. +func (r *Reporter) Succeeded(path string) { + switch r.Verbosity { + case Disabled: + case Error: + case Default, + LiveOutput: + addon.Activity("[CMD] %s succeeded.", path) + } +} + +// +// Error reports command failed in task Report.Activity. +func (r *Reporter) Error(path string, err error, output []byte) { + if len(output) == 0 { + return + } + switch r.Verbosity { + case Disabled: + case Error, + Default: + addon.Activity( + "[CMD] %s failed: %s.\n%s", + path, + err.Error(), + output) + case LiveOutput: + addon.Activity( + "[CMD] %s failed: %s.", + path, + err.Error()) + } +} + +// +// Output reports command output in task Report.Activity. +// Returns the number of bytes reported. +func (r *Reporter) Output(buffer []byte, delimited bool) (reported int) { + if r.Filter == nil { + r.Filter = func(in string) (out string) { + out = in + return + } + } + switch r.Verbosity { + case Disabled: + case Error: + case Default: + case LiveOutput: + if r.index >= len(buffer) { + return + } + batch := string(buffer[r.index:]) + if delimited { + end := strings.LastIndex(batch, "\n") + if end != -1 { + batch = batch[:end] + output := r.Filter(batch) + addon.Activity("> %s", output) + reported = len(output) + r.index += len(batch) + r.index++ + } + } else { + output := r.Filter(batch) + addon.Activity("> %s", output) + reported = len(batch) + r.index = len(buffer) + } + } + return +} diff --git a/command/writer.go b/command/writer.go new file mode 100644 index 0000000..a80c523 --- /dev/null +++ b/command/writer.go @@ -0,0 +1,99 @@ +package command + +import ( + "time" +) + +const ( + // Backoff rate increment. + Backoff = time.Millisecond * 100 + // MaxBackoff max backoff. + MaxBackoff = 10 * Backoff + // MinBackoff minimum backoff. + MinBackoff = Backoff +) + +// +// OutputFilter filter output. +type OutputFilter func(in []byte) (out []byte) + +// +// Writer records command output. +type Writer struct { + Filter OutputFilter + reporter *Reporter + buffer []byte + backoff time.Duration + end chan any + ended chan any +} + +// +// Write command output. +func (w *Writer) Write(p []byte) (n int, err error) { + if w.Filter == nil { + w.Filter = func(in []byte) (out []byte) { + out = in + return + } + } + n = len(p) + p = w.Filter(p) + w.buffer = append(w.buffer, p...) + if w.ended == nil { + w.end = make(chan any) + w.ended = make(chan any) + go w.report() + } + return +} + +// +// End of writing. +func (w *Writer) End() { + if w.end == nil { + return + } + close(w.end) + <-w.ended + close(w.ended) + w.end = nil +} + +// +// report in task Report.Activity. +// Rate limited. +func (w *Writer) report() { + w.backoff = MinBackoff + ended := false + for { + select { + case <-w.end: + ended = true + case <-time.After(w.backoff): + } + n := w.reporter.Output(w.buffer, true) + w.adjustBackoff(n) + if ended { + break + } + } + w.reporter.Output(w.buffer, false) + w.ended <- true +} + +// +// adjustBackoff adjust the backoff as needed. +// incremented when output reported. +// decremented when no outstanding output reported. +func (w *Writer) adjustBackoff(reported int) { + if reported > 0 { + if w.backoff < MaxBackoff { + w.backoff += Backoff + } + } else { + if w.backoff > MinBackoff { + w.backoff -= Backoff + } + } +} diff --git a/repository/git.go b/repository/git.go index e9a1293..f1892eb 100644 --- a/repository/git.go +++ b/repository/git.go @@ -71,7 +71,7 @@ func (r *Git) Fetch() (err error) { if err != nil { return } - cmd := command.Command{Path: "/usr/bin/git"} + cmd := command.New("/usr/bin/git") cmd.Options.Add("clone", url.String(), r.Path) err = cmd.Run() if err != nil { @@ -83,12 +83,12 @@ func (r *Git) Fetch() (err error) { // Branch creates a branch with the given name if not exist and switch to it. func (r *Git) Branch(name string) (err error) { - cmd := command.Command{Path: "/usr/bin/git"} + cmd := command.New("/usr/bin/git") cmd.Dir = r.Path cmd.Options.Add("checkout", name) err = cmd.Run() if err != nil { - cmd = command.Command{Path: "/usr/bin/git"} + cmd = command.New("/usr/bin/git") cmd.Dir = r.Path cmd.Options.Add("checkout", "-b", name) } @@ -98,7 +98,7 @@ func (r *Git) Branch(name string) (err error) { // addFiles adds files to staging area. func (r *Git) addFiles(files []string) (err error) { - cmd := command.Command{Path: "/usr/bin/git"} + cmd := command.New("/usr/bin/git") cmd.Dir = r.Path cmd.Options.Add("add", files...) return cmd.Run() @@ -110,7 +110,7 @@ func (r *Git) Commit(files []string, msg string) (err error) { if err != nil { return err } - cmd := command.Command{Path: "/usr/bin/git"} + cmd := command.New("/usr/bin/git") cmd.Dir = r.Path cmd.Options.Add("commit") cmd.Options.Add("--message", msg) @@ -123,7 +123,7 @@ func (r *Git) Commit(files []string, msg string) (err error) { // push changes to remote. func (r *Git) push() (err error) { - cmd := command.Command{Path: "/usr/bin/git"} + cmd := command.New("/usr/bin/git") cmd.Dir = r.Path cmd.Options.Add("push", "--set-upstream", "origin", r.Remote.Branch) return cmd.Run() @@ -292,7 +292,7 @@ func (r *Git) checkout() (err error) { _ = os.Chdir(dir) }() _ = os.Chdir(r.Path) - cmd := command.Command{Path: "/usr/bin/git"} + cmd := command.New("/usr/bin/git") cmd.Options.Add("checkout", branch) err = cmd.Run() return diff --git a/repository/maven.go b/repository/maven.go index b427066..7ce51f0 100644 --- a/repository/maven.go +++ b/repository/maven.go @@ -178,7 +178,7 @@ func (r *Maven) run(options command.Options) (err error) { if err != nil { return } - cmd := command.Command{Path: "/usr/bin/mvn"} + cmd := command.New("/usr/bin/svn") cmd.Options = options cmd.Options.Addf("-DoutputDirectory=%s", r.BinDir) cmd.Options.Addf("-Dmaven.repo.local=%s", r.M2Dir) diff --git a/repository/subversion.go b/repository/subversion.go index 796c8f8..afbf115 100644 --- a/repository/subversion.go +++ b/repository/subversion.go @@ -81,7 +81,7 @@ func (r *Subversion) checkout(branch string) (err error) { if err != nil { return } - cmd := command.Command{Path: "/usr/bin/svn"} + cmd := command.New("/usr/bin/svn") cmd.Options.Add("--non-interactive") if insecure { cmd.Options.Add("--trust-server-cert") @@ -105,7 +105,7 @@ func (r *Subversion) Branch(name string) error { // createBranch creates a branch with the given name func (r *Subversion) createBranch(name string) (err error) { url := *r.URL() - cmd := command.Command{Path: "/usr/bin/svn"} + cmd := command.New("/usr/bin/svn") cmd.Options.Add("--non-interactive") branchUrl := url @@ -121,7 +121,7 @@ func (r *Subversion) createBranch(name string) (err error) { // addFiles adds files to staging area func (r *Subversion) addFiles(files []string) (err error) { - cmd := command.Command{Path: "/usr/bin/svn"} + cmd := command.New("/usr/bin/svn") cmd.Dir = r.Path cmd.Options.Add("add") cmd.Options.Add("--force", files...) @@ -135,7 +135,7 @@ func (r *Subversion) Commit(files []string, msg string) (err error) { if err != nil { return } - cmd := command.Command{Path: "/usr/bin/svn"} + cmd := command.New("/usr/bin/svn") cmd.Dir = r.Path cmd.Options.Add("commit", "-m", msg) err = cmd.Run() @@ -199,7 +199,7 @@ func (r *Subversion) writePassword(id *api.Identity) (err error) { return } - cmd := command.Command{Path: "/usr/bin/svn"} + cmd := command.New("/usr/bin/svn") cmd.Options.Add("--non-interactive") cmd.Options.Add("--username") cmd.Options.Add(id.User) diff --git a/ssh/ssh.go b/ssh/ssh.go index 2d543a2..a5b23be 100644 --- a/ssh/ssh.go +++ b/ssh/ssh.go @@ -38,7 +38,7 @@ type Agent struct { func (r *Agent) Start() (err error) { pid := os.Getpid() socket := fmt.Sprintf("/tmp/agent.%d", pid) - cmd := command.Command{Path: "/usr/bin/ssh-agent"} + cmd := command.New("/usr/bin/ssh-agent") cmd.Options.Add("-a", socket) err = cmd.Run() if err != nil { @@ -97,13 +97,13 @@ func (r *Agent) Add(id *api.Identity, host string) (err error) { context.TODO(), time.Second) defer fn() - cmd := command.Command{Path: "/usr/bin/ssh-add"} + cmd := command.New("/usr/bin/ssh-add") cmd.Options.Add(path) err = cmd.RunWith(ctx) if err != nil { return } - cmd = command.Command{Path: "/usr/bin/ssh-keyscan"} + cmd = command.New("/usr/bin/ssh-keyscan") cmd.Options.Add(host) err = cmd.Run() if err != nil { @@ -120,7 +120,7 @@ func (r *Agent) Add(id *api.Identity, host string) (err error) { path) return } - _, err = f.Write(cmd.Output) + _, err = f.Write(cmd.Output()) if err != nil { err = liberr.Wrap( err,