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

pkg/lineprinter: Add a wrapper for Write -> Print #730

Merged
merged 1 commit into from
Dec 1, 2018
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
83 changes: 83 additions & 0 deletions pkg/lineprinter/lineprinter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Package lineprinter wraps a Print implementation to provide an io.WriteCloser.
package lineprinter

import (
"bytes"
"io"
"sync"
)

// Print is a type that can hold fmt.Print and other implementations
// which match that signature. For example, you can use:
//
// trimmer := &lineprinter.Trimmer{WrappedPrint: logrus.StandardLogger().Debug}
// linePrinter := &linePrinter{Print: trimmer.Print}
//
// to connect the line printer to logrus at the debug level.
type Print func(args ...interface{})

// LinePrinter is an io.WriteCloser that buffers written bytes.
// During each Write, newline-terminated lines are removed from the
// buffer and passed to Print. On Close, any content remaining in the
// buffer is also passed to Print.
//
// One use-case is connecting a subprocess's standard streams to a
// logger:
//
// linePrinter := &linePrinter{
// Print: &Trimmer{WrappedPrint: logrus.StandardLogger().Debug}.Print,
// }
// defer linePrinter.Close()
// cmd := exec.Command(...)
// cmd.Stdout = linePrinter
//
// LinePrinter buffers the subcommand's byte stream and splits it into
// lines for the logger. Sometimes we might have a partial line
// written to the buffer. We don't want to push that partial line into
// the logs if the next read attempt will pull in the remainder of the
// line. But we do want to push that partial line into the logs if there
// will never be a next read. So LinePrinter.Write pushes any
// complete lines to the wrapped printer, and LinePrinter.Close pushes
// any remaining partial line.
type LinePrinter struct {
buf bytes.Buffer
Print Print

sync.Mutex
}

// Write writes len(p) bytes from p to an internal buffer. Then it
// retrieves any newline-terminated lines from the internal buffer and
// prints them with lp.Print. Partial lines are left in the internal
wking marked this conversation as resolved.
Show resolved Hide resolved
// buffer.
func (lp *LinePrinter) Write(p []byte) (int, error) {
lp.Lock()
defer lp.Unlock()
n, err := lp.buf.Write(p)
if err != nil {
return n, err
}

for {
line, err := lp.buf.ReadString(byte('\n'))
if err == io.EOF {
_, err = lp.buf.Write([]byte(line))
return n, err
} else if err != nil {
return n, err
}

lp.Print(line)
}
}

// Close prints anything that remains in the buffer.
func (lp *LinePrinter) Close() error {
lp.Lock()
defer lp.Unlock()
line := lp.buf.String()
if len(line) > 0 {
lp.Print(line)
}
return nil
}
49 changes: 49 additions & 0 deletions pkg/lineprinter/lineprinter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package lineprinter

import (
"testing"

"github.com/stretchr/testify/assert"
)

type printer struct {
data [][]interface{}
}

func (p *printer) print(args ...interface{}) {
p.data = append(p.data, args)
}

func TestLinePrinter(t *testing.T) {
print := &printer{}
lp := &LinePrinter{Print: print.print}
data := []byte("Hello\nWorld\nAnd more")
n, err := lp.Write(data)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, len(data), n)
assert.Equal(
t,
[][]interface{}{
{"Hello\n"},
{"World\n"},
},
print.data,
)
print.data = [][]interface{}{}

err = lp.Close()
if err != nil {
t.Fatal(err)
}

assert.Equal(
t,
[][]interface{}{
{"And more"},
},
print.data,
)
}
27 changes: 27 additions & 0 deletions pkg/lineprinter/trimmer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package lineprinter
Copy link
Contributor

Choose a reason for hiding this comment

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

lineprinters is splitting on \n it should be the one trimming it. Why add something new?

Copy link
Member Author

Choose a reason for hiding this comment

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

lineprinters is splitting on \n it should be the one trimming it. Why add something new?

If you hook LinePrinter up to fmt.Print, you do want the trailing newlines:

$ cat test.go
package main

import (
	"fmt"

	"github.com/openshift/installer/pkg/lineprinter"
)

func main() {
	linePrinter := &lineprinter.LinePrinter{
		Print: func (args ...interface{}) {
			fmt.Print(args...)
		},
	}
	linePrinter.Write([]byte("1\n2\n3\n4"))
	linePrinter.Close()
}
$ go run test.go 
1
2
3
4

Copy link
Contributor

Choose a reason for hiding this comment

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

lineprinters is splitting on \n it should be the one trimming it. Why add something new?

If you hook LinePrinter up to fmt.Print, you do want the trailing newlines:

$ cat test.go
package main

import (
	"fmt"

	"github.com/openshift/installer/pkg/lineprinter"
)

func main() {
	linePrinter := &lineprinter.LinePrinter{
		Print: func (args ...interface{}) {
			fmt.Print(args...)
		},
	}
	linePrinter.Write([]byte("1\n2\n3\n4"))
	linePrinter.Close()
}
$ go run test.go 
1
2
3
4

This example makes it seems like lineprinter is not named correctly then.

Copy link
Member Author

Choose a reason for hiding this comment

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

This example makes it seems like lineprinter is not named correctly then.

It reads in a stream and calls the wrapped print on each line. What would you call it instead?


import (
"strings"
)

// Trimmer is a Print wrapper that removes trailing newlines from the
// final argument (if it is a string argument). This is useful for
// connecting a LinePrinter to a logger whose Print-analog does not
// expect trailing newlines.
type Trimmer struct {
WrappedPrint Print
}

// Print removes trailing newlines from the final argument (if it is a
// string argument) and then passes the arguments through to
// WrappedPrint.
func (t *Trimmer) Print(args ...interface{}) {
if len(args) > 0 {
i := len(args) - 1
arg, ok := args[i].(string)
if ok {
args[i] = strings.TrimRight(arg, "\n")
}
}
t.WrappedPrint(args...)
}
22 changes: 22 additions & 0 deletions pkg/lineprinter/trimmer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package lineprinter

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestTrimmer(t *testing.T) {
print := &printer{}
trimmer := &Trimmer{WrappedPrint: print.print}
trimmer.Print("Hello\n", "World\n")
trimmer.Print(123)
assert.Equal(
t,
[][]interface{}{
{"Hello\n", "World"},
{123},
},
print.data,
)
}
24 changes: 12 additions & 12 deletions pkg/terraform/executor.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package terraform

import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/openshift/installer/pkg/lineprinter"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -59,24 +61,22 @@ func (ex *executor) execute(clusterDir string, args ...string) error {
return errors.Errorf("clusterDir is unset. Quitting")
}

trimmer := &lineprinter.Trimmer{WrappedPrint: logrus.StandardLogger().Debug}
linePrinter := &lineprinter.LinePrinter{Print: trimmer.Print}
defer linePrinter.Close()

stderr := &bytes.Buffer{}

cmd := exec.Command(ex.binaryPath, args...)
cmd.Dir = clusterDir
cmd.Stdout = linePrinter
cmd.Stderr = stderr

logrus.Debugf("Running %#v...", cmd)

if logrus.GetLevel() == logrus.DebugLevel {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

output, err := cmd.Output()
err := cmd.Run()
if err != nil {
exitError := err.(*exec.ExitError)
if len(exitError.Stderr) == 0 {
exitError.Stderr = output
}
exitError.Stderr = stderr.Bytes()
}
return err
}
Expand Down