| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package main |
| 6 |
| 7 import ( |
| 8 "bytes" |
| 9 "fmt" |
| 10 "io" |
| 11 "net/http" |
| 12 "net/url" |
| 13 "os" |
| 14 "sort" |
| 15 "strings" |
| 16 |
| 17 "github.com/maruel/subcommands" |
| 18 "golang.org/x/net/context" |
| 19 |
| 20 "github.com/luci/luci-go/common/logging" |
| 21 ) |
| 22 |
| 23 type format string |
| 24 |
| 25 const ( |
| 26 formatBinary format = "binary" |
| 27 formatJSON format = "json" |
| 28 formatText format = "text" |
| 29 ) |
| 30 |
| 31 // TODO(nodir): support specifying message fields with options, e.g. |
| 32 // rpc call :8080 helloworld.Greeter -name Lucy |
| 33 // It will be the default request format. |
| 34 |
| 35 var cmdCall = &subcommands.Command{ |
| 36 UsageLine: `call [flags] <server> <service>.<method> |
| 37 |
| 38 server: host ("example.com") or port for localhost (":8080"). |
| 39 service: full name of a service, e.g. "pkg.service" |
| 40 method: name of the method. |
| 41 |
| 42 Flags:`, |
| 43 ShortDesc: "calls a service method.", |
| 44 LongDesc: "Calls a service method.", |
| 45 CommandRun: func() subcommands.CommandRun { |
| 46 c := &callRun{} |
| 47 c.registerBaseFlags() |
| 48 c.Flags.StringVar(&c.format, "format", string(formatJSON), fmt.S
printf( |
| 49 "Request format, read from stdin. Valid values: %q, %q,
%q.", |
| 50 formatJSON, formatBinary, formatText, |
| 51 )) |
| 52 return c |
| 53 }, |
| 54 } |
| 55 |
| 56 // callRun implements "call" subcommand. |
| 57 type callRun struct { |
| 58 cmdRun |
| 59 format string |
| 60 message string |
| 61 } |
| 62 |
| 63 func (r *callRun) Run(a subcommands.Application, args []string) int { |
| 64 if len(args) != 2 { |
| 65 return r.argErr("") |
| 66 } |
| 67 var req request |
| 68 var err error |
| 69 req.server, err = parseServer(args[0]) |
| 70 if err != nil { |
| 71 return r.argErr("server: %s", err) |
| 72 } |
| 73 req.service, req.method, err = splitServiceAndMethod(args[1]) |
| 74 if err != nil { |
| 75 return r.argErr("%s", err) |
| 76 } |
| 77 |
| 78 switch f := format(r.format); f { |
| 79 case formatJSON, formatBinary, formatText: |
| 80 req.format = f |
| 81 default: |
| 82 return r.argErr("invalid format %q", f) |
| 83 } |
| 84 |
| 85 var msg bytes.Buffer |
| 86 if _, err := io.Copy(&msg, os.Stdin); err != nil { |
| 87 return r.done(fmt.Errorf("could not read message from stdin: %s"
, err)) |
| 88 } |
| 89 req.message = msg.Bytes() |
| 90 return r.done(call(r.initContext(), &req, os.Stdout)) |
| 91 } |
| 92 |
| 93 func splitServiceAndMethod(fullName string) (service string, method string, err
error) { |
| 94 lastDot := strings.LastIndex(fullName, ".") |
| 95 if lastDot < 0 { |
| 96 return "", "", fmt.Errorf("invalid full method name %q. It must
contain a '.'", fullName) |
| 97 } |
| 98 service = fullName[:lastDot] |
| 99 method = fullName[lastDot+1:] |
| 100 return |
| 101 } |
| 102 |
| 103 // request is an RPC request. |
| 104 type request struct { |
| 105 server *url.URL |
| 106 service string |
| 107 method string |
| 108 format format |
| 109 message []byte |
| 110 } |
| 111 |
| 112 // call makes an RPC and writes response to out. |
| 113 func call(c context.Context, req *request, out io.Writer) error { |
| 114 if req.format == "" { |
| 115 return fmt.Errorf("format is not set") |
| 116 } |
| 117 |
| 118 // Create HTTP request. |
| 119 methodURL := *req.server |
| 120 methodURL.Path = fmt.Sprintf("/prpc/%s/%s", req.service, req.method) |
| 121 hr, err := http.NewRequest("POST", methodURL.String(), bytes.NewBuffer(r
eq.message)) |
| 122 if err != nil { |
| 123 return err |
| 124 } |
| 125 |
| 126 // Set headers. |
| 127 mediaType := "application/prpc; encoding=" + string(req.format) |
| 128 if len(req.message) > 0 { |
| 129 hr.Header.Set("Content-Type", mediaType) |
| 130 } |
| 131 hr.Header.Set("Accept", mediaType) |
| 132 hr.Header.Set("User-Agent", userAgent) |
| 133 hr.Header.Set("Content-Length", fmt.Sprintf("%d", len(req.message))) |
| 134 // TODO(nodir): add "Accept-Encoding: gzip" when pRPC server supports it
. |
| 135 |
| 136 // Log request in curl style. |
| 137 logRequest(c, hr) |
| 138 logging.Infof(c, ">") |
| 139 |
| 140 // Send the request. |
| 141 var client http.Client |
| 142 res, err := client.Do(hr) |
| 143 if err != nil { |
| 144 return fmt.Errorf("failed to send request: %s", err) |
| 145 } |
| 146 defer res.Body.Close() |
| 147 |
| 148 // Log response in curl style. |
| 149 logResponse(c, res) |
| 150 logging.Infof(c, "<") |
| 151 |
| 152 if res.StatusCode != http.StatusOK { |
| 153 return fmt.Errorf("%s", res.Status) |
| 154 } |
| 155 |
| 156 // Read response. |
| 157 if _, err := io.Copy(out, res.Body); err != nil { |
| 158 return fmt.Errorf("failed to read response: %s", err) |
| 159 } |
| 160 return err |
| 161 } |
| 162 |
| 163 // logRequest logs an HTTP request in curl style. |
| 164 func logRequest(c context.Context, r *http.Request) { |
| 165 logging.Infof(c, "> %s %s %s", r.Method, r.URL.Path, r.Proto) |
| 166 logging.Infof(c, "> Host: %s", r.URL.Host) |
| 167 logHeaders(c, r.Header, "> ") |
| 168 } |
| 169 |
| 170 // logResponse logs an HTTP response in curl style. |
| 171 func logResponse(c context.Context, r *http.Response) { |
| 172 logging.Infof(c, "< %s %s", r.Proto, r.Status) |
| 173 logHeaders(c, r.Header, "< ") |
| 174 |
| 175 } |
| 176 |
| 177 // logHeaders logs all headers sorted. |
| 178 func logHeaders(c context.Context, header http.Header, prefix string) { |
| 179 var names []string |
| 180 for n := range header { |
| 181 names = append(names, n) |
| 182 } |
| 183 sort.Strings(names) |
| 184 for _, name := range names { |
| 185 for _, v := range header[name] { |
| 186 logging.Infof(c, "%s%s: %s", prefix, name, v) |
| 187 } |
| 188 } |
| 189 } |
| OLD | NEW |