-
-
Notifications
You must be signed in to change notification settings - Fork 3k
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 channel output #499
Changes from all commits
fd40702
c3c0b24
cf1e770
0452a5f
5d3bc65
71838ad
abd390b
981f793
ecc2248
77e5742
69999bd
6236ef7
c2f46b6
0419ce1
97a3688
2816ed0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package commands | ||
|
||
import "io" | ||
|
||
type ChannelMarshaler struct { | ||
Channel <-chan interface{} | ||
Marshaler func(interface{}) (io.Reader, error) | ||
|
||
reader io.Reader | ||
} | ||
|
||
func (cr *ChannelMarshaler) Read(p []byte) (int, error) { | ||
if cr.reader == nil { | ||
val, more := <-cr.Channel | ||
if !more { | ||
return 0, io.EOF | ||
} | ||
|
||
r, err := cr.Marshaler(val) | ||
if err != nil { | ||
return 0, err | ||
} | ||
cr.reader = r | ||
} | ||
|
||
n, err := cr.reader.Read(p) | ||
if err != nil && err != io.EOF { | ||
return n, err | ||
} | ||
if n == 0 { | ||
cr.reader = nil | ||
} | ||
return n, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ package commands | |
import ( | ||
"errors" | ||
"fmt" | ||
"io" | ||
"reflect" | ||
"strings" | ||
|
||
|
@@ -15,9 +16,9 @@ var log = u.Logger("command") | |
// It reads from the Request, and writes results to the Response. | ||
type Function func(Request) (interface{}, error) | ||
|
||
// Marshaler is a function that takes in a Response, and returns a marshalled []byte | ||
// Marshaler is a function that takes in a Response, and returns an io.Reader | ||
// (or an error on failure) | ||
type Marshaler func(Response) ([]byte, error) | ||
type Marshaler func(Response) (io.Reader, error) | ||
|
||
// MarshalerMap is a map of Marshaler functions, keyed by EncodingType | ||
// (or an error on failure) | ||
|
@@ -113,25 +114,27 @@ func (c *Command) Call(req Request) Response { | |
return res | ||
} | ||
|
||
isChan := false | ||
actualType := reflect.TypeOf(output) | ||
if actualType != nil { | ||
if actualType.Kind() == reflect.Ptr { | ||
actualType = actualType.Elem() | ||
} | ||
|
||
// test if output is a channel | ||
isChan = actualType.Kind() == reflect.Chan | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cool idea, fancy! |
||
|
||
// If the command specified an output type, ensure the actual value returned is of that type | ||
if cmd.Type != nil { | ||
definedType := reflect.ValueOf(cmd.Type).Type() | ||
actualType := reflect.ValueOf(output).Type() | ||
if cmd.Type != nil && !isChan { | ||
expectedType := reflect.TypeOf(cmd.Type) | ||
|
||
if definedType != actualType { | ||
if actualType != expectedType { | ||
res.SetError(ErrIncorrectType, ErrNormal) | ||
return res | ||
} | ||
} | ||
|
||
// clean up the request (close the readers, e.g. fileargs) | ||
// NOTE: this means commands can't expect to keep reading after cmd.Run returns (in a goroutine) | ||
err = req.Cleanup() | ||
if err != nil { | ||
res.SetError(err, ErrNormal) | ||
return res | ||
} | ||
|
||
res.SetOutput(output) | ||
return res | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package http | |
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
|
@@ -20,8 +21,11 @@ type Handler struct { | |
var ErrNotFound = errors.New("404 page not found") | ||
|
||
const ( | ||
streamHeader = "X-Stream-Output" | ||
contentTypeHeader = "Content-Type" | ||
streamHeader = "X-Stream-Output" | ||
channelHeader = "X-Chunked-Output" | ||
contentTypeHeader = "Content-Type" | ||
contentLengthHeader = "Content-Length" | ||
transferEncodingHeader = "Transfer-Encoding" | ||
) | ||
|
||
var mimeTypes = map[string]string{ | ||
|
@@ -97,5 +101,67 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
return | ||
} | ||
|
||
// if output is a channel and user requested streaming channels, | ||
// use chunk copier for the output | ||
_, isChan := res.Output().(chan interface{}) | ||
streamChans, _, _ := req.Option("stream-channels").Bool() | ||
if isChan && streamChans { | ||
err = copyChunks(w, out) | ||
if err != nil { | ||
log.Error(err) | ||
} | ||
return | ||
} | ||
|
||
io.Copy(w, out) | ||
} | ||
|
||
// Copies from an io.Reader to a http.ResponseWriter. | ||
// Flushes chunks over HTTP stream as they are read (if supported by transport). | ||
func copyChunks(w http.ResponseWriter, out io.Reader) error { | ||
hijacker, ok := w.(http.Hijacker) | ||
if !ok { | ||
return errors.New("Could not create hijacker") | ||
} | ||
conn, writer, err := hijacker.Hijack() | ||
if err != nil { | ||
return err | ||
} | ||
defer conn.Close() | ||
|
||
writer.WriteString("HTTP/1.1 200 OK\r\n") | ||
writer.WriteString(contentTypeHeader + ": application/json\r\n") | ||
writer.WriteString(transferEncodingHeader + ": chunked\r\n") | ||
writer.WriteString(channelHeader + ": 1\r\n\r\n") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice :) love how simple http is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was probably chosen just so Tim Berners-Lee could test out his server via telnet :P There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah. he didn't have protobufs. :] |
||
|
||
buf := make([]byte, 32*1024) | ||
|
||
for { | ||
n, err := out.Read(buf) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dont think so, but just to check: so you mean to just read whatever There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct, it can be any |
||
|
||
if n > 0 { | ||
length := fmt.Sprintf("%x\r\n", n) | ||
writer.WriteString(length) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this just http-specific framing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have to do this as a hack around the stdlib's http.ResponseWriter :( The built-in HTTP code doesn't let us start writing the response while the client is still writing the request (which is necessary for So to get around it we have to hijack the underlying TCP connection for the request and handle the HTTP stuff manually. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sad. Yeah makes sense. I was just wondering whether the length there was some chunk framing part of http, or framing on our end. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, that's HTTP chunked encoding, there's a hex length before each chunk. |
||
|
||
_, err := writer.Write(buf[0:n]) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
writer.WriteString("\r\n") | ||
writer.Flush() | ||
} | ||
|
||
if err != nil && err != io.EOF { | ||
return err | ||
} | ||
if err == io.EOF { | ||
break | ||
} | ||
} | ||
|
||
writer.WriteString("0\r\n\r\n") | ||
writer.Flush() | ||
|
||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
io.ReadFull
just in case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure? It looks like that would error if
len(p)
is greater than the length of one of the readers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll stop on
io.EOF
and returnio.ErrUnexpectedEOF
(which we can ignore). The thing i'm worried about is thatRead
call not reading all it's supposed to. there's no guarantee the network won't just give it a few bytes and return.Looking closer at this function, looks like if there are any bytes read, we keep the reader, so we just read again. if so then that may be fine (i thought we lost the reader right away)