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 with task report files. #50

Merged
merged 12 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
76 changes: 41 additions & 35 deletions command/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,30 @@ import (
"context"
"fmt"
"os/exec"
"strings"

hub "github.com/konveyor/tackle2-hub/addon"
"path"
)

var (
addon = hub.Addon
)

//
// New returns a command.
func New(path string) (cmd *Command) {
cmd = &Command{Path: path}
return
}

//
// Command execution.
type Command struct {
Options Options
Path string
Dir string
Output []byte
Options Options
Path string
Dir string
Reporter Reporter
Writer Writer
}

//
Expand All @@ -40,22 +48,31 @@ 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
output := path.Base(r.Path) + ".output"
r.Reporter.file, err = addon.File.Touch(output)
if err != nil {
return
}
r.Reporter.Run(r.Path, r.Options)
addon.Attach(r.Reporter.file)
defer func() {
r.Writer.End()
if err != nil {
r.Reporter.Error(r.Path, err, r.Writer.buffer)
} else {
r.Reporter.Succeeded(r.Path, r.Writer.buffer)
}
}()
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 +81,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 = 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
}

//
// 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...))
}
116 changes: 116 additions & 0 deletions command/reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package command

import (
"strings"
"github.com/konveyor/tackle2-hub/api"
)

//
// 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
)

//
// Reporter activity reporter.
type Reporter struct {
Verbosity int
file *api.File
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, output []byte) {
switch r.Verbosity {
case Disabled:
case Error:
case Default:
addon.Activity(
"[CMD] %s succeeded.",
path)
r.append(output)
case 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",
path,
err.Error())
r.append(output)
case LiveOutput:
addon.Activity(
"[CMD] %s failed: %s.",
path,
err.Error())
}
}

//
// Output reports command output.
func (r *Reporter) Output(buffer []byte) (reported int) {
switch r.Verbosity {
case Disabled:
case Error:
case Default:
case LiveOutput:
if r.index >= len(buffer) {
return
}
batch := buffer[r.index:]
reported = len(batch)
if reported > 0 {
r.index += reported
r.append(batch)
}
}
return
}

//
// append output.
func (r *Reporter) append(batch []byte) {
if r.file == nil {
return
}
err := addon.File.Patch(r.file.ID, batch)
if err != nil {
panic(err)
}
}
89 changes: 89 additions & 0 deletions command/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package command

import (
"time"
)

const (
// Backoff rate increment.
Backoff = time.Millisecond * 100
// MaxBackoff max backoff.
MaxBackoff = 10 * Backoff
// MinBackoff minimum backoff.
MinBackoff = Backoff
)

//
// Writer records command output.
type Writer struct {
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) {
n = len(p)
w.buffer = append(w.buffer, p...)
switch w.reporter.Verbosity {
case LiveOutput:
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)
w.adjustBackoff(n)
if ended && n == 0 {
break
}
}
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
}
}
}
16 changes: 8 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ go 1.18

require (
github.com/clbanning/mxj v1.8.4
github.com/jortel/go-utils v0.1.1
github.com/konveyor/tackle2-hub v0.2.2-0.20230731153407-22bf2d68128a
github.com/jortel/go-utils v0.1.2
github.com/konveyor/tackle2-hub v0.3.0-rc.2.0.20231219211826-f09d0b24c0e6
)

require (
Expand Down Expand Up @@ -43,7 +43,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.3 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
Expand All @@ -64,12 +64,12 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.5.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
Expand Down
Loading