Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Command verbosity. #49

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 43 additions & 35 deletions command/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"context"
"fmt"
"os/exec"
"strings"

hub "github.com/konveyor/tackle2-hub/addon"
)
Expand All @@ -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
}

//
Expand All @@ -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
}

Expand All @@ -64,41 +83,30 @@ 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 = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be -1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

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
}

//
// Options are CLI options.
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...))
}
119 changes: 119 additions & 0 deletions command/reporter.go
Original file line number Diff line number Diff line change
@@ -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
}
99 changes: 99 additions & 0 deletions command/writer.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading