responseemitter.go 3.67 KB
Newer Older
Jan Winkelmann's avatar
Jan Winkelmann committed
1 2 3
package cli

import (
4
	"context"
Jan Winkelmann's avatar
Jan Winkelmann committed
5 6
	"fmt"
	"io"
Jan Winkelmann's avatar
Jan Winkelmann committed
7
	"os"
8
	"sync"
Jan Winkelmann's avatar
Jan Winkelmann committed
9

tavit ohanian's avatar
tavit ohanian committed
10
	cmds "gitlab.dms3.io/dms3/public/go-dms3-cmds"
Jan Winkelmann's avatar
Jan Winkelmann committed
11 12
)

13 14
var _ ResponseEmitter = &responseEmitter{}

15 16
// NewResponseEmitter constructs a new response emitter that writes results to
// the console.
Steven Allen's avatar
Steven Allen committed
17
func NewResponseEmitter(stdout, stderr io.Writer, req *cmds.Request) (ResponseEmitter, error) {
Steven Allen's avatar
Steven Allen committed
18
	encType, enc, err := cmds.GetEncoder(req, stdout, cmds.TextNewline)
Jan Winkelmann's avatar
Jan Winkelmann committed
19

keks's avatar
keks committed
20 21 22 23
	return &responseEmitter{
		stdout:  stdout,
		stderr:  stderr,
		encType: encType,
Steven Allen's avatar
Steven Allen committed
24
		enc:     enc,
Steven Allen's avatar
Steven Allen committed
25
	}, err
Jan Winkelmann's avatar
Jan Winkelmann committed
26 27
}

28 29 30 31
// ResponseEmitter extends cmds.ResponseEmitter to give better control over the command line
type ResponseEmitter interface {
	cmds.ResponseEmitter

32 33
	Stdout() io.Writer
	Stderr() io.Writer
Steven Allen's avatar
Steven Allen committed
34 35 36 37 38 39

	// SetStatus sets the exit status for this command.
	SetStatus(int)

	// Status returns the exit status for the command.
	Status() int
40 41
}

Jan Winkelmann's avatar
Jan Winkelmann committed
42
type responseEmitter struct {
keks's avatar
keks committed
43
	l      sync.Mutex
44 45
	stdout io.Writer
	stderr io.Writer
Jan Winkelmann's avatar
Jan Winkelmann committed
46

47 48 49
	length  uint64
	enc     cmds.Encoder
	encType cmds.EncodingType
50 51
	exit    int
	closed  bool
Jan Winkelmann's avatar
Jan Winkelmann committed
52 53
}

keks's avatar
keks committed
54 55 56 57
func (re *responseEmitter) Type() cmds.PostRunType {
	return cmds.CLI
}

58 59 60 61
func (re *responseEmitter) SetLength(l uint64) {
	re.length = l
}

62
func (re *responseEmitter) isClosed() bool {
keks's avatar
keks committed
63 64
	re.l.Lock()
	defer re.l.Unlock()
65

66 67 68 69
	return re.closed
}

func (re *responseEmitter) Close() error {
Steven Allen's avatar
Steven Allen committed
70 71 72 73
	return re.CloseWithError(nil)
}

func (re *responseEmitter) CloseWithError(err error) error {
keks's avatar
keks committed
74 75 76
	re.l.Lock()
	defer re.l.Unlock()

77
	if re.closed {
keks's avatar
keks committed
78
		return cmds.ErrClosingClosedEmitter
79
	}
Steven Allen's avatar
Steven Allen committed
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
	re.closed = true

	var msg string
	if err != nil {
		if re.exit == 0 {
			// Default "error" exit code.
			re.exit = 1
		}
		switch err {
		case context.Canceled:
			msg = "canceled"
		case context.DeadlineExceeded:
			msg = "timed out"
		default:
			msg = err.Error()
		}
96

Steven Allen's avatar
Steven Allen committed
97 98
		fmt.Fprintln(re.stderr, "Error:", msg)
	}
99 100 101 102 103 104

	defer func() {
		re.stdout = nil
		re.stderr = nil
	}()

Steven Allen's avatar
Steven Allen committed
105
	var errStderr, errStdout error
106
	if f, ok := re.stderr.(*os.File); ok {
Steven Allen's avatar
Steven Allen committed
107
		errStderr = f.Sync()
108 109
	}
	if f, ok := re.stdout.(*os.File); ok {
Steven Allen's avatar
Steven Allen committed
110 111
		errStdout = f.Sync()
	}
112 113 114

	// ignore error if the operating system doesn't support syncing std{out,err}
	if errStderr != nil && !isSyncNotSupportedErr(errStderr) {
Steven Allen's avatar
Steven Allen committed
115 116
		return errStderr
	}
117
	if errStdout != nil && !isSyncNotSupportedErr(errStdout) {
Steven Allen's avatar
Steven Allen committed
118
		return errStdout
119 120 121 122
	}
	return nil
}

Jan Winkelmann's avatar
Jan Winkelmann committed
123
func (re *responseEmitter) Emit(v interface{}) error {
keks's avatar
keks committed
124
	var isSingle bool
125 126 127
	// unwrap
	if val, ok := v.(cmds.Single); ok {
		v = val.Value
keks's avatar
keks committed
128
		isSingle = true
129 130
	}

keks's avatar
keks committed
131
	// channel emission iteration
132 133 134
	if ch, ok := v.(chan interface{}); ok {
		v = (<-chan interface{})(ch)
	}
keks's avatar
keks committed
135 136 137
	if ch, isChan := v.(<-chan interface{}); isChan {
		return cmds.EmitChan(re, ch)
	}
138

139 140 141 142 143 144 145 146 147 148 149
	// TODO find a better solution for this.
	// Idea: use the actual cmd.Type and not *cmd.Type
	// would need to fix all commands though
	switch c := v.(type) {
	case *string:
		v = *c
	case *int:
		v = *c
	}

	if re.isClosed() {
keks's avatar
keks committed
150
		return cmds.ErrClosedEmitter
Jan Winkelmann's avatar
Jan Winkelmann committed
151 152
	}

Jan Winkelmann's avatar
Jan Winkelmann committed
153 154 155 156
	var err error

	switch t := v.(type) {
	case io.Reader:
157
		_, err = io.Copy(re.stdout, t)
158
		if err != nil {
keks's avatar
keks committed
159
			return err
160
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
161 162 163 164
	default:
		if re.enc != nil {
			err = re.enc.Encode(v)
		} else {
165
			_, err = fmt.Fprintln(re.stdout, t)
Jan Winkelmann's avatar
Jan Winkelmann committed
166 167 168
		}
	}

keks's avatar
keks committed
169
	if isSingle {
keks's avatar
keks committed
170
		return re.CloseWithError(err)
keks's avatar
keks committed
171 172
	}

Jan Winkelmann's avatar
Jan Winkelmann committed
173 174
	return err
}
175

176 177 178
// Stderr returns the ResponseWriter's stderr
func (re *responseEmitter) Stderr() io.Writer {
	return re.stderr
179 180
}

181 182 183
// Stdout returns the ResponseWriter's stdout
func (re *responseEmitter) Stdout() io.Writer {
	return re.stdout
184 185
}

Steven Allen's avatar
Steven Allen committed
186 187
// SetStatus sets the exit status of the command.
func (re *responseEmitter) SetStatus(code int) {
keks's avatar
keks committed
188 189
	re.l.Lock()
	defer re.l.Unlock()
190
	re.exit = code
191
}
Steven Allen's avatar
Steven Allen committed
192 193 194 195 196 197 198

// Status _returns_ the exit status of the command.
func (re *responseEmitter) Status() int {
	re.l.Lock()
	defer re.l.Unlock()
	return re.exit
}