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

import (
	"fmt"
	"io"
Jan Winkelmann's avatar
Jan Winkelmann committed
6
	"os"
Jan Winkelmann's avatar
Jan Winkelmann committed
7
	"runtime/debug"
8
	"sync"
Jan Winkelmann's avatar
Jan Winkelmann committed
9

keks's avatar
keks committed
10
	"github.com/ipfs/go-ipfs-cmdkit"
keks's avatar
keks committed
11
	"github.com/ipfs/go-ipfs-cmds"
Jan Winkelmann's avatar
Jan Winkelmann committed
12 13
)

14 15
var _ ResponseEmitter = &responseEmitter{}

Jan Winkelmann's avatar
Jan Winkelmann committed
16 17 18 19
type ErrSet struct {
	error
}

20 21
func NewResponseEmitter(stdout, stderr io.Writer, enc func(cmds.Request) func(io.Writer) cmds.Encoder, req cmds.Request) (cmds.ResponseEmitter, <-chan int) {
	ch := make(chan int)
22
	encType := cmds.GetEncoding(req)
Jan Winkelmann's avatar
Jan Winkelmann committed
23

Jan Winkelmann's avatar
Jan Winkelmann committed
24
	if enc == nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
25
		enc = func(cmds.Request) func(io.Writer) cmds.Encoder {
Jan Winkelmann's avatar
Jan Winkelmann committed
26 27 28 29
			return func(io.Writer) cmds.Encoder {
				return nil
			}
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
30 31
	}

32
	return &responseEmitter{stdout: stdout, stderr: stderr, encType: encType, enc: enc(req)(stdout), ch: ch}, ch
Jan Winkelmann's avatar
Jan Winkelmann committed
33 34
}

35 36 37 38
// ResponseEmitter extends cmds.ResponseEmitter to give better control over the command line
type ResponseEmitter interface {
	cmds.ResponseEmitter

39 40
	Stdout() io.Writer
	Stderr() io.Writer
41 42 43
	Exit(int)
}

Jan Winkelmann's avatar
Jan Winkelmann committed
44
type responseEmitter struct {
45 46 47
	wLock  sync.Mutex
	stdout io.Writer
	stderr io.Writer
Jan Winkelmann's avatar
Jan Winkelmann committed
48

49
	length  uint64
50
	err     *cmdkit.Error
51 52
	enc     cmds.Encoder
	encType cmds.EncodingType
53 54
	exit    int
	closed  bool
Jan Winkelmann's avatar
Jan Winkelmann committed
55

56 57 58
	errOccurred bool

	ch chan<- int
Jan Winkelmann's avatar
Jan Winkelmann committed
59 60
}

61 62 63 64 65
func (re *responseEmitter) SetLength(l uint64) {
	re.length = l
}

func (re *responseEmitter) SetEncoder(enc func(io.Writer) cmds.Encoder) {
66
	re.enc = enc(re.stdout)
67 68
}

69
func (re *responseEmitter) SetError(v interface{}, errType cmdkit.ErrorType) {
70 71
	re.errOccurred = true

72
	err := re.Emit(&cmdkit.Error{Message: fmt.Sprint(v), Code: errType})
73 74 75 76 77
	if err != nil {
		panic(err)
	}
}

78
func (re *responseEmitter) isClosed() bool {
79 80 81
	re.wLock.Lock()
	defer re.wLock.Unlock()

82 83 84 85 86 87 88
	return re.closed
}

func (re *responseEmitter) Close() error {
	log.Debugf("err=%v exit=%v\nStack:\n%s", re.err, re.exit, debug.Stack())

	if re.isClosed() {
89 90 91
		return nil
	}

92 93 94 95
	re.wLock.Lock()
	defer re.wLock.Unlock()

	re.ch <- re.exit
96
	close(re.ch)
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

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

	if re.stdout == nil || re.stderr == nil {
		log.Warning("more than one call to RespEm.Close!")
	}

	if f, ok := re.stderr.(*os.File); ok {
		err := f.Sync()
		if err != nil {
			return err
		}
	}
	if f, ok := re.stdout.(*os.File); ok {
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
		err := f.Sync()
		if err != nil {
			return err
		}
	}

	return nil
}

// Head returns the current head.
// TODO: maybe it makes sense to make these pointers to shared memory?
//   might not be so clever though...concurrency and stuff
func (re *responseEmitter) Head() cmds.Head {
	return cmds.Head{
		Len: re.length,
		Err: re.err,
	}
}

Jan Winkelmann's avatar
Jan Winkelmann committed
134
func (re *responseEmitter) Emit(v interface{}) error {
135 136 137 138
	if ch, ok := v.(chan interface{}); ok {
		v = (<-chan interface{})(ch)
	}

139 140 141 142 143 144 145 146 147 148 149 150
	log.Debugf("%T:%v, enc:%p", v, v, re.enc)

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

151
	if ch, isChan := v.(<-chan interface{}); isChan {
152
		log.Debug("iterating over chan...", ch)
153 154 155 156 157 158 159 160 161
		for v = range ch {
			err := re.Emit(v)
			if err != nil {
				return err
			}
		}
		return nil
	}

162
	if re.isClosed() {
Jan Winkelmann's avatar
Jan Winkelmann committed
163 164 165
		return io.ErrClosedPipe
	}

166
	if err, ok := v.(cmdkit.Error); ok {
Jan Winkelmann's avatar
Jan Winkelmann committed
167 168 169 170
		log.Warningf("fixerr %s", debug.Stack())
		v = &err
	}

Jan Winkelmann's avatar
Jan Winkelmann committed
171 172 173
	var err error

	switch t := v.(type) {
174
	case *cmdkit.Error:
175
		_, err = fmt.Fprintln(re.stderr, "Error:", t.Message)
176
		if t.Code == cmdkit.ErrFatal {
177 178 179 180 181 182
			defer re.Close()
		}
		re.wLock.Lock()
		defer re.wLock.Unlock()
		re.exit = 1

183 184
		return nil

Jan Winkelmann's avatar
Jan Winkelmann committed
185
	case io.Reader:
186
		_, err = io.Copy(re.stdout, t)
187
		if err != nil {
188
			re.SetError(err, cmdkit.ErrNormal)
189 190
			err = nil
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
191 192 193 194
	default:
		if re.enc != nil {
			err = re.enc.Encode(v)
		} else {
195
			_, err = fmt.Fprintln(re.stdout, t)
Jan Winkelmann's avatar
Jan Winkelmann committed
196 197 198 199 200
		}
	}

	return err
}
201

202 203 204
// Stderr returns the ResponseWriter's stderr
func (re *responseEmitter) Stderr() io.Writer {
	return re.stderr
205 206
}

207 208 209
// Stdout returns the ResponseWriter's stdout
func (re *responseEmitter) Stdout() io.Writer {
	return re.stdout
210 211
}

212
// Exit sends code to the channel that was returned by NewResponseEmitter, so main() can pass it to os.Exit()
213
func (re *responseEmitter) Exit(code int) {
214 215 216 217 218
	defer re.Close()

	re.wLock.Lock()
	defer re.wLock.Unlock()
	re.exit = code
219
}