18 Commits

Author SHA1 Message Date
Joshua Humphries
5631bba117 add official Dockerfile (#104) 2019-07-03 15:57:24 -04:00
Joshua Humphries
80425d1b17 use protoreflect 1.4.4 (#109) 2019-07-03 15:56:51 -04:00
Joshua Humphries
7e4045565f update all deps; use new ResolveFilenames method (#103) 2019-05-24 10:26:38 -04:00
Joshua Humphries
e5b4fc6cc0 add API to expose AnyResolver implementations backed by a DescriptorSource (#102) 2019-05-22 21:38:46 -04:00
Joshua Humphries
09c3d1d69e use just-released v1.3.0 protoreflect (#101) 2019-05-22 16:20:41 -04:00
Joshua Humphries
5d6316f470 Adds support for showing error details (#98)
To better support printing of google.protobuf.Any messages (error details), this
also makes a few other changes:
1. Allows printing of unresolvable Any messages using an "@value" field in JSON output
   that has the base64-encoded embedded message data.
2. Improves support for "-format text" to show expanded Any messages if possible.
   (Due to limitations in underlying proto package, this will usually *not* be
   that helpful. But this should greatly improve with v2 of the go protobuf API.)
3. Addresses a TODO in existing AnyResolver code to lazily fetch descriptors
   as needed instead of having to download all files eagerly.
2019-04-09 09:34:39 -04:00
Joshua Humphries
f0723c6273 fix flaky test (#93) 2019-03-25 10:34:32 -04:00
Joshua Humphries
fe97274a1b staticcheck no longer runs on Go 1.10 (#92)
* staticcheck no longer runs on Go 1.10, so run it on Go 1.11 in CI
* be explicit about default make target in .travis.yml
2019-03-22 14:35:01 -04:00
CodeLingo Team
1bbf8dae71 Fix function comments based on best practices from Effective Go (#87)
Signed-off-by: CodeLingo Bot <bot@codelingo.io>
2019-03-13 10:58:13 -04:00
Joshua Humphries
0fcd3253f6 latest protoreflect fixes some bugs in JSON marshaling/unmarshaling (#86) 2019-03-07 13:35:49 -05:00
Joshua Humphries
4c9c82cec3 use custom flagset (#85) 2019-02-28 10:58:53 -05:00
Joshua Humphries
5082a1dc68 add -max-msg-sz flag (#84) 2019-02-28 08:35:50 -05:00
Joshua Humphries
d641a66208 fix flaky test where code can be 'cancelled' unexpectedly, instead of some error code provided by the server (#83) 2019-02-27 20:28:43 -05:00
Joshua Humphries
ce84976d3c fix typo in readme
fixes #79
2019-02-27 18:16:55 -05:00
Joshua Humphries
b292d5aef8 add Go 1.12 to travis config (#82) 2019-02-27 17:31:54 -05:00
Joshua Humphries
5516a45602 update proto and grpc deps (#81)
Fixes the build by updating grpc (and deps) and using new, non-deprecated function
2019-02-27 17:30:44 -05:00
Andrew McCallum
4a329f3b13 add Homebrew installation method to README (#77) 2019-01-23 10:50:39 -05:00
Joshua Humphries
1c6532c060 fix minor issues caught by staticcheck (#76) 2019-01-03 10:09:24 -05:00
15 changed files with 383 additions and 124 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
dist/
VERSION

View File

@@ -5,12 +5,17 @@ matrix:
include:
- go: "1.9"
- go: "1.10"
env: VET=1
- go: "1.11"
env:
- GO111MODULE=off
- VET=1
- go: "1.11"
env: GO111MODULE=on
- go: "1.12"
env: GO111MODULE=off
- go: "1.11"
- go: "1.12"
env: GO111MODULE=on
- go: tip
script:
- if [[ "$VET" = 1 ]]; then make; else make deps test; fi
- if [[ "$VET" = 1 ]]; then make ci; else make deps test; fi

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM golang:1.11.10-alpine as builder
MAINTAINER FullStory Engineering
# currently, a module build requires gcc (so Go tool can build
# module-aware versions of std library; it ships only w/ the
# non-module versions)
RUN apk update && apk add --no-cache ca-certificates git gcc g++ libc-dev
# create non-privileged group and user
RUN addgroup -S grpcurl && adduser -S grpcurl -G grpcurl
WORKDIR /tmp/fullstorydev/grpcurl
# copy just the files/sources we need to build grpcurl
COPY VERSION *.go go.* /tmp/fullstorydev/grpcurl/
COPY cmd /tmp/fullstorydev/grpcurl/cmd
# and build a completely static binary (so we can use
# scratch as basis for the final image)
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
ENV GO111MODULE=on
RUN go build -o /grpcurl \
-ldflags "-w -extldflags \"-static\" -X \"main.version=$(cat VERSION)\"" \
./cmd/grpcurl
# New FROM so we have a nice'n'tiny image
FROM scratch
WORKDIR /
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /grpcurl /bin/grpcurl
USER grpcurl
ENTRYPOINT ["/bin/grpcurl"]

View File

@@ -6,7 +6,7 @@ dev_build_version=$(shell git describe --tags --always --dirty)
# they are just too noisy to be a requirement for a CI -- we don't even *want*
# to fix some of the things they consider to be violations.
.PHONY: ci
ci: deps checkgofmt vet staticcheck unused ineffassign predeclared test
ci: deps checkgofmt vet staticcheck ineffassign predeclared test
.PHONY: deps
deps:
@@ -25,6 +25,12 @@ release:
@GO111MODULE=off go get github.com/goreleaser/goreleaser
goreleaser --rm-dist
.PHONY: docker
docker:
@echo $(dev_build_version) > VERSION
docker build -t fullstorydev/grpcurl:$(dev_build_version) .
@rm VERSION
.PHONY: checkgofmt
checkgofmt:
gofmt -s -l .
@@ -47,12 +53,7 @@ vet:
.PHONY: staticcheck
staticcheck:
@go get honnef.co/go/tools/cmd/staticcheck
staticcheck -ignore github.com/fullstorydev/grpcurl/tls_settings_test.go:SA1019 ./...
.PHONY: unused
unused:
@go get honnef.co/go/tools/cmd/unused
unused ./...
staticcheck ./...
.PHONY: ineffassign
ineffassign:

View File

@@ -49,6 +49,11 @@ files (containing compiled descriptors, produced by `protoc`) to `grpcurl`.
Download the binary from the [releases](https://github.com/fullstorydev/grpcurl/releases) page.
On macOS, `grpcurl` is available via Homebrew:
```shell
brew install grpcurl
```
### From Source
You can use the `go` tool to install `grpcurl`:
```shell
@@ -102,7 +107,7 @@ If you want to include `grpcurl` in a command pipeline, such as when using `jq`
create a request body, you can use `-d @`, which tells `grpcurl` to read the actual
request body from stdin:
```shell
grpcurl -d @ grpc.server.com:443 my.custom.server.Service/Method <<<EOM
grpcurl -d @ grpc.server.com:443 my.custom.server.Service/Method <<EOM
{
"id": 1234,
"tags": [

View File

@@ -8,7 +8,7 @@ import (
"fmt"
"io"
"os"
"strconv"
"path/filepath"
"strings"
"time"
@@ -32,22 +32,24 @@ var (
isUnixSocket func() bool // nil when run on non-unix platform
help = flag.Bool("help", false, prettify(`
flags = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
help = flags.Bool("help", false, prettify(`
Print usage instructions and exit.`))
printVersion = flag.Bool("version", false, prettify(`
printVersion = flags.Bool("version", false, prettify(`
Print version.`))
plaintext = flag.Bool("plaintext", false, prettify(`
plaintext = flags.Bool("plaintext", false, prettify(`
Use plain-text HTTP/2 when connecting to server (no TLS).`))
insecure = flag.Bool("insecure", false, prettify(`
insecure = flags.Bool("insecure", false, prettify(`
Skip server certificate and domain verification. (NOT SECURE!) Not
valid with -plaintext option.`))
cacert = flag.String("cacert", "", prettify(`
cacert = flags.String("cacert", "", prettify(`
File containing trusted root certificates for verifying the server.
Ignored if -insecure is specified.`))
cert = flag.String("cert", "", prettify(`
cert = flags.String("cert", "", prettify(`
File containing client certificate (public key), to present to the
server. Not valid with -plaintext option. Must also provide -key option.`))
key = flag.String("key", "", prettify(`
key = flags.String("key", "", prettify(`
File containing client private key, to present to the server. Not valid
with -plaintext option. Must also provide -cert option.`))
protoset multiString
@@ -56,15 +58,15 @@ var (
addlHeaders multiString
rpcHeaders multiString
reflHeaders multiString
authority = flag.String("authority", "", prettify(`
authority = flags.String("authority", "", prettify(`
Value of :authority pseudo-header to be use with underlying HTTP/2
requests. It defaults to the given address.`))
data = flag.String("d", "", prettify(`
data = flags.String("d", "", prettify(`
Data for request contents. If the value is '@' then the request contents
are read from stdin. For calls that accept a stream of requests, the
contents should include all such request messages concatenated together
(possibly delimited; see -format).`))
format = flag.String("format", "json", prettify(`
format = flags.String("format", "json", prettify(`
The format of request data. The allowed values are 'json' or 'text'. For
'json', the input data must be in JSON format. Multiple request values
may be concatenated (messages with a JSON representation other than
@@ -74,43 +76,46 @@ var (
ASCII character: 0x1E. The stream should not end in a record separator.
If it does, it will be interpreted as a final, blank message after the
separator.`))
connectTimeout = flag.String("connect-timeout", "", prettify(`
connectTimeout = flags.Float64("connect-timeout", 0, prettify(`
The maximum time, in seconds, to wait for connection to be established.
Defaults to 10 seconds.`))
keepaliveTime = flag.String("keepalive-time", "", prettify(`
keepaliveTime = flags.Float64("keepalive-time", 0, prettify(`
If present, the maximum idle time in seconds, after which a keepalive
probe is sent. If the connection remains idle and no keepalive response
is received for this same period then the connection is closed and the
operation fails.`))
maxTime = flag.String("max-time", "", prettify(`
The maximum total time the operation can take. This is useful for
preventing batch jobs that use grpcurl from hanging due to slow or bad
network links or due to incorrect stream method usage.`))
emitDefaults = flag.Bool("emit-defaults", false, prettify(`
maxTime = flags.Float64("max-time", 0, prettify(`
The maximum total time the operation can take, in seconds. This is
useful for preventing batch jobs that use grpcurl from hanging due to
slow or bad network links or due to incorrect stream method usage.`))
maxMsgSz = flags.Int("max-msg-sz", 0, prettify(`
The maximum encoded size of a response message, in bytes, that grpcurl
will accept. If not specified, defaults to 4,194,304 (4 megabytes).`))
emitDefaults = flags.Bool("emit-defaults", false, prettify(`
Emit default values for JSON-encoded responses.`))
msgTemplate = flag.Bool("msg-template", false, prettify(`
msgTemplate = flags.Bool("msg-template", false, prettify(`
When describing messages, show a template of input data.`))
verbose = flag.Bool("v", false, prettify(`
verbose = flags.Bool("v", false, prettify(`
Enable verbose output.`))
serverName = flag.String("servername", "", prettify(`
serverName = flags.String("servername", "", prettify(`
Override server name when validating TLS certificate.`))
)
func init() {
flag.Var(&addlHeaders, "H", prettify(`
flags.Var(&addlHeaders, "H", prettify(`
Additional headers in 'name: value' format. May specify more than one
via multiple flags. These headers will also be included in reflection
requests requests to a server.`))
flag.Var(&rpcHeaders, "rpc-header", prettify(`
flags.Var(&rpcHeaders, "rpc-header", prettify(`
Additional RPC headers in 'name: value' format. May specify more than
one via multiple flags. These headers will *only* be used when invoking
the requested RPC method. They are excluded from reflection requests.`))
flag.Var(&reflHeaders, "reflect-header", prettify(`
flags.Var(&reflHeaders, "reflect-header", prettify(`
Additional reflection headers in 'name: value' format. May specify more
than one via multiple flags. These headers will *only* be used during
reflection requests and will be excluded when invoking the requested RPC
method.`))
flag.Var(&protoset, "protoset", prettify(`
flags.Var(&protoset, "protoset", prettify(`
The name of a file containing an encoded FileDescriptorSet. This file's
contents will be used to determine the RPC schema instead of querying
for it from the remote server via the gRPC reflection API. When set: the
@@ -119,7 +124,7 @@ func init() {
symbols found in the given descriptors. May specify more than one via
multiple -protoset flags. It is an error to use both -protoset and
-proto flags.`))
flag.Var(&protoFiles, "proto", prettify(`
flags.Var(&protoFiles, "proto", prettify(`
The name of a proto source file. Source files given will be used to
determine the RPC schema instead of querying for it from the remote
server via the gRPC reflection API. When set: the 'list' action lists
@@ -129,7 +134,7 @@ func init() {
-proto flags. Imports will be resolved using the given -import-path
flags. Multiple proto files can be specified by specifying multiple
-proto flags. It is an error to use both -protoset and -proto flags.`))
flag.Var(&importPaths, "import-path", prettify(`
flags.Var(&importPaths, "import-path", prettify(`
The path to a directory from which proto sources can be imported, for
use with -proto flags. Multiple import paths can be configured by
specifying multiple -import-path flags. Paths will be searched in the
@@ -150,18 +155,30 @@ func (s *multiString) Set(value string) error {
}
func main() {
flag.CommandLine.Usage = usage
flag.Parse()
flags.Usage = usage
flags.Parse(os.Args[1:])
if *help {
usage()
os.Exit(0)
}
if *printVersion {
fmt.Fprintf(os.Stderr, "%s %s\n", os.Args[0], version)
fmt.Fprintf(os.Stderr, "%s %s\n", filepath.Base(os.Args[0]), version)
os.Exit(0)
}
// Do extra validation on arguments and figure out what user asked us to do.
if *connectTimeout < 0 {
fail(nil, "The -connect-timeout argument must not be negative.")
}
if *keepaliveTime < 0 {
fail(nil, "The -keepalive-time argument must not be negative.")
}
if *maxTime < 0 {
fail(nil, "The -max-time argument must not be negative.")
}
if *maxMsgSz < 0 {
fail(nil, "The -max-msg-sz argument must not be negative.")
}
if *plaintext && *insecure {
fail(nil, "The -plaintext and -insecure arguments are mutually exclusive.")
}
@@ -181,7 +198,7 @@ func main() {
warn("The -emit-defaults is only used when using json format.")
}
args := flag.Args()
args := flags.Args()
if len(args) == 0 {
fail(nil, "Too few arguments.")
@@ -246,38 +263,29 @@ func main() {
}
ctx := context.Background()
if *maxTime != "" {
t, err := strconv.ParseFloat(*maxTime, 64)
if err != nil {
fail(nil, "The -max-time argument must be a valid number.")
}
timeout := time.Duration(t * float64(time.Second))
if *maxTime > 0 {
timeout := time.Duration(*maxTime * float64(time.Second))
ctx, _ = context.WithTimeout(ctx, timeout)
}
dial := func() *grpc.ClientConn {
dialTime := 10 * time.Second
if *connectTimeout != "" {
t, err := strconv.ParseFloat(*connectTimeout, 64)
if err != nil {
fail(nil, "The -connect-timeout argument must be a valid number.")
}
dialTime = time.Duration(t * float64(time.Second))
if *connectTimeout > 0 {
dialTime = time.Duration(*connectTimeout * float64(time.Second))
}
ctx, cancel := context.WithTimeout(ctx, dialTime)
defer cancel()
var opts []grpc.DialOption
if *keepaliveTime != "" {
t, err := strconv.ParseFloat(*keepaliveTime, 64)
if err != nil {
fail(nil, "The -keepalive-time argument must be a valid number.")
}
timeout := time.Duration(t * float64(time.Second))
if *keepaliveTime > 0 {
timeout := time.Duration(*keepaliveTime * float64(time.Second))
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: timeout,
Timeout: timeout,
}))
}
if *maxMsgSz > 0 {
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(*maxMsgSz)))
}
if *authority != "" {
opts = append(opts, grpc.WithAuthority(*authority))
}
@@ -511,7 +519,7 @@ func main() {
fmt.Printf("Sent %d request%s and received %d response%s\n", reqCount, reqSuffix, h.NumResponses, respSuffix)
}
if h.Status.Code() != codes.OK {
fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.Status.Code().String(), h.Status.Message())
grpcurl.PrintStatus(os.Stderr, h.Status, formatter)
exit(1)
}
}
@@ -546,7 +554,7 @@ path to the domain socket.
Available flags:
`, os.Args[0])
flag.PrintDefaults()
flags.PrintDefaults()
}
func prettify(docString string) string {

View File

@@ -2,10 +2,8 @@
package main
import "flag"
var (
unix = flag.Bool("unix", false, prettify(`
unix = flags.Bool("unix", false, prettify(`
Indicates that the server address is the path to a Unix domain socket.`))
)

View File

@@ -58,9 +58,14 @@ func DescriptorSourceFromProtoSets(fileNames ...string) (DescriptorSource, error
// whose contents are Protocol Buffer source files. The given importPaths are used to locate
// any imported files.
func DescriptorSourceFromProtoFiles(importPaths []string, fileNames ...string) (DescriptorSource, error) {
fileNames, err := protoparse.ResolveFilenames(importPaths, fileNames...)
if err != nil {
return nil, err
}
p := protoparse.Parser{
ImportPaths: importPaths,
InferImportPaths: len(importPaths) == 0,
ImportPaths: importPaths,
InferImportPaths: len(importPaths) == 0,
IncludeSourceCodeInfo: true,
}
fds, err := p.ParseFiles(fileNames...)
if err != nil {
@@ -109,7 +114,7 @@ func resolveFileDescriptor(unresolved map[string]*descpb.FileDescriptorProto, re
return result, nil
}
// DescriptorSourceFromFileDescriptorSet creates a DescriptorSource that is backed by the given
// DescriptorSourceFromFileDescriptors creates a DescriptorSource that is backed by the given
// file descriptors
func DescriptorSourceFromFileDescriptors(files ...*desc.FileDescriptor) (DescriptorSource, error) {
fds := map[string]*desc.FileDescriptor{}

216
format.go
View File

@@ -3,14 +3,19 @@ package grpcurl
import (
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
"sync"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/dynamic"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
@@ -142,6 +147,8 @@ type textFormatter struct {
numFormatted int
}
var protoTextMarshaler = proto.TextMarshaler{ExpandAny: true}
func (tf *textFormatter) format(m proto.Message) (string, error) {
var buf bytes.Buffer
if tf.useSeparator && tf.numFormatted > 0 {
@@ -166,7 +173,7 @@ func (tf *textFormatter) format(m proto.Message) (string, error) {
if _, err := buf.Write(b); err != nil {
return "", err
}
} else if err := proto.MarshalText(&buf, m); err != nil {
} else if err := protoTextMarshaler.Marshal(&buf, m); err != nil {
return "", err
}
@@ -188,24 +195,153 @@ const (
FormatText = Format("text")
)
func anyResolver(source DescriptorSource) (jsonpb.AnyResolver, error) {
// TODO: instead of pro-actively downloading file descriptors to
// build a dynamic resolver, it would be better if the resolver
// impl was lazy, and simply downloaded the descriptors as needed
// when asked to resolve a particular type URL
// best effort: build resolver with whatever files we can
// load, ignoring any errors
files, _ := GetAllFiles(source)
var er dynamic.ExtensionRegistry
for _, fd := range files {
er.AddExtensionsFromFile(fd)
}
mf := dynamic.NewMessageFactoryWithExtensionRegistry(&er)
return dynamic.AnyResolver(mf, files...), nil
// AnyResolverFromDescriptorSource returns an AnyResolver that will search for
// types using the given descriptor source.
func AnyResolverFromDescriptorSource(source DescriptorSource) jsonpb.AnyResolver {
return &anyResolver{source: source}
}
// AnyResolverFromDescriptorSourceWithFallback returns an AnyResolver that will
// search for types using the given descriptor source and then fallback to a
// special message if the type is not found. The fallback type will render to
// JSON with a "@type" property, just like an Any message, but also with a
// custom "@value" property that includes the binary encoded payload.
func AnyResolverFromDescriptorSourceWithFallback(source DescriptorSource) jsonpb.AnyResolver {
res := anyResolver{source: source}
return &anyResolverWithFallback{AnyResolver: &res}
}
type anyResolver struct {
source DescriptorSource
er dynamic.ExtensionRegistry
mu sync.RWMutex
mf *dynamic.MessageFactory
resolved map[string]func() proto.Message
}
func (r *anyResolver) Resolve(typeUrl string) (proto.Message, error) {
mname := typeUrl
if slash := strings.LastIndex(mname, "/"); slash >= 0 {
mname = mname[slash+1:]
}
r.mu.RLock()
factory := r.resolved[mname]
r.mu.RUnlock()
// already resolved?
if factory != nil {
return factory(), nil
}
r.mu.Lock()
defer r.mu.Unlock()
// double-check, in case we were racing with another goroutine
// that resolved this one
factory = r.resolved[mname]
if factory != nil {
return factory(), nil
}
// use descriptor source to resolve message type
d, err := r.source.FindSymbol(mname)
if err != nil {
return nil, err
}
md, ok := d.(*desc.MessageDescriptor)
if !ok {
return nil, fmt.Errorf("unknown message: %s", typeUrl)
}
// populate any extensions for this message, too
if exts, err := r.source.AllExtensionsForType(mname); err != nil {
return nil, err
} else if err := r.er.AddExtension(exts...); err != nil {
return nil, err
}
if r.mf == nil {
r.mf = dynamic.NewMessageFactoryWithExtensionRegistry(&r.er)
}
factory = func() proto.Message {
return r.mf.NewMessage(md)
}
if r.resolved == nil {
r.resolved = map[string]func() proto.Message{}
}
r.resolved[mname] = factory
return factory(), nil
}
// anyResolverWithFallback can provide a fallback value for unknown
// messages that will format itself to JSON using an "@value" field
// that has the base64-encoded data for the unknown message value.
type anyResolverWithFallback struct {
jsonpb.AnyResolver
}
func (r anyResolverWithFallback) Resolve(typeUrl string) (proto.Message, error) {
msg, err := r.AnyResolver.Resolve(typeUrl)
if err == nil {
return msg, err
}
// Try "default" resolution logic. This mirrors the default behavior
// of jsonpb, which checks to see if the given message name is registered
// in the proto package.
mname := typeUrl
if slash := strings.LastIndex(mname, "/"); slash >= 0 {
mname = mname[slash+1:]
}
mt := proto.MessageType(mname)
if mt != nil {
return reflect.New(mt.Elem()).Interface().(proto.Message), nil
}
// finally, fallback to a special placeholder that can marshal itself
// to JSON using a special "@value" property to show base64-encoded
// data for the embedded message
return &unknownAny{TypeUrl: typeUrl, Error: fmt.Sprintf("%s is not recognized; see @value for raw binary message data", mname)}, nil
}
type unknownAny struct {
TypeUrl string `json:"@type"`
Error string `json:"@error"`
Value string `json:"@value"`
}
func (a *unknownAny) MarshalJSONPB(jsm *jsonpb.Marshaler) ([]byte, error) {
if jsm.Indent != "" {
return json.MarshalIndent(a, "", jsm.Indent)
}
return json.Marshal(a)
}
func (a *unknownAny) Unmarshal(b []byte) error {
a.Value = base64.StdEncoding.EncodeToString(b)
return nil
}
func (a *unknownAny) Reset() {
a.Value = ""
}
func (a *unknownAny) String() string {
b, err := a.MarshalJSONPB(&jsonpb.Marshaler{})
if err != nil {
return fmt.Sprintf("ERROR: %v", err.Error())
}
return string(b)
}
func (a *unknownAny) ProtoMessage() {
}
var _ proto.Message = (*unknownAny)(nil)
// RequestParserAndFormatterFor returns a request parser and formatter for the
// given format. The given descriptor source may be used for parsing message
// data (if needed by the format). The flags emitJSONDefaultFields and
@@ -214,11 +350,8 @@ func anyResolver(source DescriptorSource) (jsonpb.AnyResolver, error) {
func RequestParserAndFormatterFor(format Format, descSource DescriptorSource, emitJSONDefaultFields, includeTextSeparator bool, in io.Reader) (RequestParser, Formatter, error) {
switch format {
case FormatJSON:
resolver, err := anyResolver(descSource)
if err != nil {
return nil, nil, fmt.Errorf("error creating message resolver: %v", err)
}
return NewJSONRequestParser(in, resolver), NewJSONFormatter(emitJSONDefaultFields, resolver), nil
resolver := AnyResolverFromDescriptorSource(descSource)
return NewJSONRequestParser(in, resolver), NewJSONFormatter(emitJSONDefaultFields, anyResolverWithFallback{AnyResolver: resolver}), nil
case FormatText:
return NewTextRequestParser(in), NewTextFormatter(includeTextSeparator), nil
default:
@@ -295,3 +428,42 @@ func (h *DefaultEventHandler) OnReceiveTrailers(stat *status.Status, md metadata
fmt.Fprintf(h.out, "\nResponse trailers received:\n%s\n", MetadataToString(md))
}
}
// PrintStatus prints details about the given status to the given writer. The given
// formatter is used to print any detail messages that may be included in the status.
// If the given status has a code of OK, "OK" is printed and that is all. Otherwise,
// "ERROR:" is printed along with a line showing the code, one showing the message
// string, and each detail message if any are present. The detail messages will be
// printed as proto text format or JSON, depending on the given formatter.
func PrintStatus(w io.Writer, stat *status.Status, formatter Formatter) {
if stat.Code() == codes.OK {
fmt.Fprintln(w, "OK")
return
}
fmt.Fprintf(w, "ERROR:\n Code: %s\n Message: %s\n", stat.Code().String(), stat.Message())
statpb := stat.Proto()
if len(statpb.Details) > 0 {
fmt.Fprintf(w, " Details:\n")
for i, det := range statpb.Details {
prefix := fmt.Sprintf(" %d)", i+1)
fmt.Fprintf(w, "%s\t", prefix)
prefix = strings.Repeat(" ", len(prefix)) + "\t"
output, err := formatter(det)
if err != nil {
fmt.Fprintf(w, "Error parsing detail message: %v\n", err)
} else {
lines := strings.Split(output, "\n")
for i, line := range lines {
if i == 0 {
// first line is already indented
fmt.Fprintf(w, "%s\n", line)
} else {
fmt.Fprintf(w, "%s%s\n", prefix, line)
}
}
}
}
}
}

8
go.mod
View File

@@ -1,8 +1,8 @@
module github.com/fullstorydev/grpcurl
require (
github.com/golang/protobuf v1.1.0
github.com/jhump/protoreflect v1.1.0
golang.org/x/net v0.0.0-20180530234432-1e491301e022
google.golang.org/grpc v1.12.0
github.com/golang/protobuf v1.3.1
github.com/jhump/protoreflect v1.4.4
golang.org/x/net v0.0.0-20190311183353-d8887717615a
google.golang.org/grpc v1.21.0
)

33
go.sum
View File

@@ -1,13 +1,30 @@
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/jhump/protoreflect v1.1.0 h1:h+zsMrsiq0vIl7yWmeowmd8e8VtnWk75U04GgXA2s6Y=
github.com/jhump/protoreflect v1.1.0/go.mod h1:kG/zRVeS2M91gYaCvvUbPkMjjtFQS4qqjcPFzFkh2zE=
golang.org/x/net v0.0.0-20180530234432-1e491301e022 h1:MVYFTUmVD3/+ERcvRRI+P/C2+WOUimXh+Pd8LVsklZ4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/jhump/protoreflect v1.4.4 h1:kySdALZUh7xRtW6UoZjjHtlR8k7rLzx5EXJFRvsO5UY=
github.com/jhump/protoreflect v1.4.4/go.mod h1:gZ3i/BeD62fjlaIL0VW4UDMT70CTX+3m4pOnAlJ0BX8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/genproto v0.0.0-20170818100345-ee236bd376b0 h1:jgaHBfsPDMBDKsth1hPtI1HcOyecWndWOFSGW21VgaM=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20170818100345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.12.0 h1:Mm8atZtkT+P6R43n/dqNDWkPPu5BwRVu/1rJnJCeZH8=
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -17,7 +17,6 @@ import (
"net"
"sort"
"strings"
"time"
"github.com/golang/protobuf/proto"
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
@@ -574,11 +573,8 @@ func BlockingDial(ctx context.Context, network, address string, creds credential
}
}
dialer := func(address string, timeout time.Duration) (net.Conn, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
conn, err := (&net.Dialer{Cancel: ctx.Done()}).Dial(network, address)
dialer := func(ctx context.Context, address string) (net.Conn, error) {
conn, err := (&net.Dialer{}).DialContext(ctx, network, address)
if err != nil {
writeResult(err)
return nil, err
@@ -601,7 +597,7 @@ func BlockingDial(ctx context.Context, network, address string, creds credential
opts = append(opts,
grpc.WithBlock(),
grpc.FailOnNonTempDialError(true),
grpc.WithDialer(dialer),
grpc.WithContextDialer(dialer),
grpc.WithInsecure(), // we are handling TLS, so tell grpc not to
)
conn, err := grpc.DialContext(ctx, address, opts...)

View File

@@ -311,6 +311,7 @@ func invokeBidi(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescr
}
if err != nil {
err = fmt.Errorf("error getting request data: %v", err)
cancel()
break
}
@@ -321,7 +322,6 @@ func invokeBidi(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescr
if err != nil {
sendErr.Store(err)
cancel()
}
}()
}

View File

@@ -120,7 +120,7 @@ func unaryLogger(ctx context.Context, req interface{}, info *grpc.UnaryServerInf
} else {
code = codes.Unknown
}
grpclog.Infof("completed <%d>: %v (%d) %v\n", i, code, code, time.Now().Sub(start))
grpclog.Infof("completed <%d>: %v (%d) %v\n", i, code, code, time.Since(start))
return rsp, err
}
@@ -135,7 +135,7 @@ func streamLogger(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServer
} else {
code = codes.Unknown
}
grpclog.Infof("completed <%d>: %v(%d) %v\n", i, code, code, time.Now().Sub(start))
grpclog.Infof("completed <%d>: %v(%d) %v\n", i, code, code, time.Since(start))
return err
}

View File

@@ -109,24 +109,42 @@ func TestBrokenTLS_ClientPlainText(t *testing.T) {
}
// client connection (usually) succeeds since client is not waiting for TLS handshake
e, err := createTestServerAndClient(serverCreds, nil)
if err != nil {
if strings.Contains(err.Error(), "deadline exceeded") {
// It is possible that connection never becomes healthy:
// (we try several times, but if we never get a connection and the error message is
// a known/expected possibility, we'll just bail)
var e testEnv
failCount := 0
for {
e, err = createTestServerAndClient(serverCreds, nil)
if err == nil {
// success!
defer e.Close()
break
}
if strings.Contains(err.Error(), "deadline exceeded") ||
strings.Contains(err.Error(), "use of closed network connection") {
// It is possible that the connection never becomes healthy:
// 1) grpc connects successfully
// 2) grpc client tries to send HTTP/2 preface and settings frame
// 3) server, expecting handshake, closes the connection
// 4) in the client, the write fails, so the connection never
// becomes ready
// More often than not, the connection becomes ready (presumably
// the write to the socket succeeds before the server closes the
// connection). But when it does not, it is possible to observe
// timeouts when setting up the connection.
return
// The client will attempt to reconnect on transient errors, so
// may eventually bump into the connect time limit. This used to
// result in a "deadline exceeded" error, but more recent versions
// of the grpc library report any underlying I/O error instead, so
// we also check for "use of closed network connection".
failCount++
if failCount > 5 {
return // bail...
}
// we'll try again
} else {
// some other error occurred, so we'll consider that a test failure
t.Fatalf("failed to setup server and client: %v", err)
}
t.Fatalf("failed to setup server and client: %v", err)
}
defer e.Close()
// but request fails because server closes connection upon seeing request
// bytes that are not a TLS handshake
@@ -285,7 +303,7 @@ func simpleTest(t *testing.T, cc *grpc.ClientConn) {
cl := grpc_testing.NewTestServiceClient(cc)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_, err := cl.UnaryCall(ctx, &grpc_testing.SimpleRequest{}, grpc.FailFast(false))
_, err := cl.UnaryCall(ctx, &grpc_testing.SimpleRequest{}, grpc.WaitForReady(true))
if err != nil {
t.Errorf("simple RPC failed: %v", err)
}