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

Use a non-interactive progress writer in non-TTY environments #405

Merged
merged 2 commits into from
Sep 15, 2021
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ require (
github.com/google/jsonapi v0.0.0-20201022225600-f822737867f6 // indirect
github.com/klauspost/compress v1.13.5 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/nwaples/rardecode v1.1.2 // indirect
github.com/pierrec/lz4/v4 v4.1.8 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE=
Expand Down Expand Up @@ -135,6 +137,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
Expand Down
7 changes: 1 addition & 6 deletions pkg/commands/compute/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,7 @@ func NewBuildCommand(parent cmd.Registerer, client api.HTTPClient, globals *conf

// Exec implements the command interface.
func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) {
var progress text.Progress
if c.Globals.Verbose() {
progress = text.NewVerboseProgress(out)
} else {
progress = text.NewQuietProgress(out)
}
progress := text.NewProgress(out, c.Globals.Verbose())

defer func(errLog errors.LogInterface) {
if err != nil {
Expand Down
16 changes: 2 additions & 14 deletions pkg/commands/compute/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,7 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) {

// RESOURCE CREATION...

var progress text.Progress
if verbose {
progress = text.NewVerboseProgress(out)
} else {
progress = text.NewQuietProgress(out)
}

progress := text.NewProgress(out, c.Globals.Verbose())
undoStack := undo.NewStack()

defer func(errLog errors.LogInterface, progress text.Progress) {
Expand Down Expand Up @@ -418,13 +412,7 @@ func manageNoServiceIDFlow(
text.Break(out)
}

var progress text.Progress

if verbose {
progress = text.NewVerboseProgress(out)
} else {
progress = text.NewQuietProgress(out)
}
progress := text.NewProgress(out, verbose)

// There is no service and so we'll do a one time creation of the service
//
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/compute/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func (c *InitCommand) Exec(in io.Reader, out io.Writer) (err error) {
text.Break(out)

if !c.Globals.Verbose() {
progress = text.NewQuietProgress(out)
progress = text.NewProgress(out, false)
}

_, err = exec.LookPath("git")
Expand Down
7 changes: 1 addition & 6 deletions pkg/commands/compute/pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,7 @@ func NewPackCommand(parent cmd.Registerer, globals *config.Data) *PackCommand {

// Exec implements the command interface.
func (c *PackCommand) Exec(in io.Reader, out io.Writer) (err error) {
var progress text.Progress
if c.Globals.Verbose() {
progress = text.NewVerboseProgress(out)
} else {
progress = text.NewQuietProgress(out)
}
progress := text.NewProgress(out, c.Globals.Verbose())

defer func(errLog errors.LogInterface) {
if err != nil {
Expand Down
7 changes: 1 addition & 6 deletions pkg/commands/compute/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,7 @@ func (c *ServeCommand) Exec(in io.Reader, out io.Writer) (err error) {
text.Break(out)
}

var progress text.Progress
if c.Globals.Verbose() {
progress = text.NewVerboseProgress(out)
} else {
progress = text.NewQuietProgress(out)
}
progress := text.NewProgress(out, c.Globals.Verbose())

bin, err := getViceroy(progress, out, c.viceroyVersioner)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/compute/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) (err error) {
return err
}

progress := text.NewQuietProgress(out)
progress := text.NewProgress(out, c.Globals.Verbose())
defer func() {
if err != nil {
c.Globals.ErrLog.AddWithContext(err, map[string]interface{}{
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/configure/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) (err error) {

text.Break(out)

progress := text.NewQuietProgress(out)
progress := text.NewProgress(out, c.Globals.Verbose())
defer func() {
if err != nil {
c.Globals.ErrLog.Add(err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/update/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error {
text.Output(out, "Latest version: %s", latest)
text.Break(out)

progress := text.NewQuietProgress(out)
progress := text.NewProgress(out, c.Globals.Verbose())
progress.Step("Updating versioning information...")

err = c.Globals.File.Load(c.Globals.File.CLI.RemoteConfig, c.client, config.ConfigRequestTimeout, config.FilePath)
Expand Down
95 changes: 79 additions & 16 deletions pkg/text/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (
"encoding/hex"
"fmt"
"io"
"os"
"runtime"
"strings"
"sync"
"time"
"unicode/utf8"

"github.com/mattn/go-isatty"
)

// Progress is a producer contract, abstracting over the quiet and verbose
Expand All @@ -27,6 +30,20 @@ type Progress interface {
Fail()
}

// NewProgress returns a Progress based on the given verbosity level or whether
// the current process is running in a terminal environment.
func NewProgress(output io.Writer, verbose bool) Progress {
var progress Progress
if verbose {
progress = NewVerboseProgress(output)
} else if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
progress = NewInteractiveProgress(output)
} else {
progress = NewQuietProgress(output)
}
return progress
}

// Ticker is a small consumer contract for the Spin function,
// capturing part of the Progress interface.
type Ticker interface {
Expand All @@ -52,10 +69,10 @@ func Spin(ctx context.Context, frames []rune, interval time.Duration, target Tic
}
}

// QuietProgress is an implementation of Progress that includes a spinner at the
// InteractiveProgress is an implementation of Progress that includes a spinner at the
// beginning of each Step, and where newline-delimited lines written via Write
// overwrite the current step line in the output.
type QuietProgress struct {
type InteractiveProgress struct {
mtx sync.Mutex
output io.Writer

Expand All @@ -68,9 +85,9 @@ type QuietProgress struct {
done <-chan struct{} // wait for Spin to stop
}

// NewQuietProgress returns a QuietProgress outputting to the writer.
func NewQuietProgress(output io.Writer) *QuietProgress {
p := &QuietProgress{
// NewInteractiveProgress returns a InteractiveProgress outputting to the writer.
func NewInteractiveProgress(output io.Writer) *InteractiveProgress {
p := &InteractiveProgress{
output: output,
stepHeader: "Initializing...",
}
Expand All @@ -89,7 +106,7 @@ func NewQuietProgress(output io.Writer) *QuietProgress {
return p
}

func (p *QuietProgress) replaceLine(format string, args ...interface{}) {
func (p *InteractiveProgress) replaceLine(format string, args ...interface{}) {
// Clear the current line.
n := utf8.RuneCountInString(p.currentOutput)
switch runtime.GOOS {
Expand All @@ -107,15 +124,15 @@ func (p *QuietProgress) replaceLine(format string, args ...interface{}) {
fmt.Fprint(p.output, p.currentOutput)
}

func (p *QuietProgress) getStatus() string {
func (p *InteractiveProgress) getStatus() string {
if p.lastBufferLine != "" {
return p.lastBufferLine // takes precedence
}
return p.stepHeader
}

// Tick implements the Progress interface.
func (p *QuietProgress) Tick(r rune) {
func (p *InteractiveProgress) Tick(r rune) {
p.mtx.Lock()
defer p.mtx.Unlock()

Expand All @@ -124,7 +141,7 @@ func (p *QuietProgress) Tick(r rune) {

// Write implements the Progress interface, emitting each incoming byte slice
// to the internal buffer to be written to the terminal on the next tick.
func (p *QuietProgress) Write(buf []byte) (int, error) {
func (p *InteractiveProgress) Write(buf []byte) (int, error) {
p.mtx.Lock()
defer p.mtx.Unlock()

Expand All @@ -135,7 +152,7 @@ func (p *QuietProgress) Write(buf []byte) (int, error) {
}

// Step implements the Progress interface.
func (p *QuietProgress) Step(msg string) {
func (p *InteractiveProgress) Step(msg string) {
msg = strings.TrimSpace(msg)

p.mtx.Lock()
Expand All @@ -156,7 +173,7 @@ func (p *QuietProgress) Step(msg string) {
}

// Done implements the Progress interface.
func (p *QuietProgress) Done() {
func (p *InteractiveProgress) Done() {
// It's important to cancel the Spin goroutine before taking the lock,
// because otherwise it's possible to generate a deadlock if the output
// io.Writer is also synchronized.
Expand All @@ -171,7 +188,7 @@ func (p *QuietProgress) Done() {
}

// Fail implements the Progress interface.
func (p *QuietProgress) Fail() {
func (p *InteractiveProgress) Fail() {
p.cancel()
<-p.done

Expand Down Expand Up @@ -202,6 +219,48 @@ func LastFullLine(s string) string {
//
//

// QuietProgress is an implementation of Progress that attempts to be quiet in
// it's output. I.e. it only prints each Step as it progresses and discards any
// intermediary writes between steps. No spinners are used, therefore it's
// useful for non-TTY environiments, such as CI.
type QuietProgress struct {
output io.Writer
nullWriter io.Writer
}

// NewQuietProgress returns a QuietProgress outputting to the writer.
func NewQuietProgress(output io.Writer) *QuietProgress {
qp := &QuietProgress{
output: output,
nullWriter: io.Discard,
}
qp.Step("Initializing...")
return qp
}

// Tick implements the Progress interface. It's a no-op.
func (p *QuietProgress) Tick(r rune) {}

// Tick implements the Progress interface.
func (p *QuietProgress) Write(buf []byte) (int, error) {
return p.nullWriter.Write(buf)
}

// Step implements the Progress interface.
func (p *QuietProgress) Step(msg string) {
fmt.Fprintln(p.output, strings.TrimSpace(msg))
}

// Done implements the Progress interface. It's a no-op.
func (p *QuietProgress) Done() {}

// Fail implements the Progress interface. It's a no-op.
func (p *QuietProgress) Fail() {}

//
//
//

// VerboseProgress is an implementation of Progress that treats Step and Write
// more or less the same: it simply pipes all output to the provided Writer. No
// spinners are used.
Expand Down Expand Up @@ -241,19 +300,23 @@ func (p *VerboseProgress) Fail() {}

// NullProgress is an implementation of Progress which discards everything
// written into it and produces no output.
type NullProgress struct{}
type NullProgress struct {
output io.Writer
}

// NewNullProgress returns a NullProgress.
func NewNullProgress() *NullProgress {
return &NullProgress{}
return &NullProgress{
output: io.Discard,
}
}

// Tick implements the Progress interface. It's a no-op.
// Tick implements the Progress interface. It's a no-opt
func (p *NullProgress) Tick(r rune) {}

// Tick implements the Progress interface.
func (p *NullProgress) Write(buf []byte) (int, error) {
return 0, nil
return p.output.Write(buf)
}

// Step implements the Progress interface.
Expand Down