├── .gitignore ├── History.md ├── Makefile ├── Readme.md ├── _examples ├── http │ └── main.go └── tcp │ └── main.go ├── cmd └── rpc │ └── main.go ├── internal └── rpc │ └── rpc.go └── vendor └── vendor.json /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.sw[a-z] 3 | bin/* 4 | *.deb 5 | vendor/*/ 6 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | v1.0.3 / 2018-09-24 3 | =================== 4 | 5 | * fix(bin): add closing parenthesis to user-agent 6 | 7 | v1.0.2 / 2018-09-24 8 | =================== 9 | 10 | * fix(cmd): use correct version ref 11 | 12 | v1.0.1 / 2018-09-24 13 | =================== 14 | 15 | * fix(build): add ldflags to set version in binary 16 | 17 | v1.0.0 / 2018-09-24 18 | =================== 19 | 20 | * fix(cmd): add '-legacy' to avoid conflicting with a newer cli 21 | * feat: add User-Agent header (#12) 22 | * dev: suffix -legacy for package and republish 23 | * Use govendor; add Makefile to produce pkg 24 | * feat: use json number in decoder to support large numbers better (#10) 25 | * print application-level errors when using HTTP (#9) 26 | * ocd 27 | * rpc: detect stdin, fixes #4 28 | * internal/rpc: handle empty inputs 29 | * lazy license 30 | * ocd 31 | * Initial commit 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | GOVENDOR := $(shell command -v govendor) 4 | 5 | GIT_DIRTY := $(shell test -n "`git status --porcelain`" && echo "-CHANGES" || true) 6 | GIT_DESCRIBE := $(shell git describe --tags --always) 7 | 8 | VERSION := $(patsubst v%,%,$(GIT_DESCRIBE)$(GIT_DIRTY)) 9 | 10 | LDFLAGS := "-X main.version=$(VERSION)" 11 | 12 | DEBFILE := segment-rpc-legacy_$(VERSION)_amd64.deb 13 | 14 | bin/rpc: dep 15 | mkdir -p bin 16 | go build -o bin/rpc ./cmd/rpc 17 | 18 | bin/rpc-linux-amd64: dep 19 | mkdir -p bin 20 | env GOOS=linux GOARCH=amd64 go build -ldflags $(LDFLAGS) -o bin/rpc-linux-amd64 ./cmd/rpc 21 | 22 | $(DEBFILE): bin/rpc-linux-amd64 23 | fpm \ 24 | -s dir \ 25 | -t deb \ 26 | -n segment-rpc-legacy \ 27 | -v $(VERSION) \ 28 | -m sre-team@segment.com \ 29 | --vendor "Segment.io, Inc." \ 30 | ./bin/rpc-linux-amd64=/usr/bin/rpc-legacy 31 | 32 | deb: $(DEBFILE) 33 | 34 | upload-deb: $(DEBFILE) 35 | package_cloud push segment/infra/ubuntu/xenial $(DEBFILE) 36 | 37 | dep: 38 | ifndef GOVENDOR 39 | go get -u github.com/kardianos/govendor 40 | endif 41 | govendor fetch +outside 42 | govendor sync 43 | 44 | clean: 45 | rm -f bin/* *.deb 46 | 47 | .PHONY: deb upload-deb clean 48 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | `rpc(1)` ⋅ a simple RPC CLI. 3 | 4 | ## Features 5 | 6 | - Interactive mode 7 | - Consumes input from stdin 8 | - Nice UX (`rpc :3000 sum [2,2]`) 9 | - HTTP / TCP (`rpc http://localhost:3000`) 10 | 11 | ## Installation 12 | 13 | ```go 14 | $ go install github.com/segmentio/rpc-cli/cmd/rpc@latest 15 | ``` 16 | 17 | ## Codecs 18 | 19 | - jsonrpc 20 | 21 | ## Usage 22 | 23 | ```bash 24 | $ rpc :3000 25 | :3000> Service.Echo foo=baz 26 | { 27 | foo: "baz" 28 | } 29 | :3000> Service.Sum [2,2] 30 | 4 31 | ``` 32 | 33 | ```bash 34 | $ echo '{"name":"{{ name }}"}' | phony | rpc :3000 Service.Echo 35 | { 36 | "name": "Dyan Patterson" 37 | } 38 | .... 39 | ``` 40 | 41 | ```bash 42 | $ rpc :3000 Service.Echo foo=baz 43 | { 44 | "foo": "baz" 45 | } 46 | ``` 47 | 48 | ```bash 49 | $ rpc :3000 Service.Sum [2,2] 50 | 4 51 | ``` 52 | 53 | ## License 54 | 55 | MIT 56 | -------------------------------------------------------------------------------- /_examples/http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/rpc/v2" 8 | "github.com/gorilla/rpc/v2/json" 9 | ) 10 | 11 | func main() { 12 | srv := rpc.NewServer() 13 | srv.RegisterCodec(json.NewCodec(), "application/json") 14 | srv.RegisterService(Service{}, "Service") 15 | http.Handle("/rpc", srv) 16 | err := http.ListenAndServe(":3000", nil) 17 | if err != nil { 18 | log.Fatalf("listen: %s", err) 19 | } 20 | } 21 | 22 | type Service struct{} 23 | 24 | func (_ Service) Sum(_ *http.Request, req *[]int, sum *int) error { 25 | for _, n := range *req { 26 | *sum += n 27 | } 28 | return nil 29 | } 30 | 31 | func (_ Service) Echo(_ *http.Request, req, res *interface{}) error { 32 | *res = *req 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /_examples/tcp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net" 7 | "net/rpc" 8 | "net/rpc/jsonrpc" 9 | ) 10 | 11 | func main() { 12 | srv := rpc.NewServer() 13 | srv.Register(Service{}) 14 | listen(srv) 15 | } 16 | 17 | func listen(srv *rpc.Server) { 18 | l, err := net.Listen("tcp", ":3000") 19 | if err != nil { 20 | log.Fatalf("listen: %s", err) 21 | } 22 | 23 | for { 24 | sock, err := l.Accept() 25 | if err != nil { 26 | log.Fatalf("accept: %s", err) 27 | } 28 | 29 | go srv.ServeCodec(jsonrpc.NewServerCodec(sock)) 30 | } 31 | } 32 | 33 | type Service struct{} 34 | 35 | func (_ Service) Sum(req *[]int, sum *int) error { 36 | for _, n := range *req { 37 | *sum += n 38 | } 39 | return nil 40 | } 41 | 42 | func (_ Service) Echo(req, res *json.RawMessage) error { 43 | *res = *req 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /cmd/rpc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/segmentio/rpc-cli/internal/rpc" 10 | "github.com/tj/docopt" 11 | ) 12 | 13 | var version string 14 | 15 | const usage = ` 16 | Usage: 17 | rpc ... 18 | rpc 19 | rpc 20 | 21 | rpc -h | --help 22 | rpc -v | --version 23 | 24 | Options: 25 | -h, --help show help information 26 | -v, --version show version information 27 | 28 | ` 29 | 30 | func main() { 31 | args, err := docopt.Parse(usage, nil, true, version, false) 32 | check(err) 33 | 34 | addr := args[""].(string) 35 | 36 | var input io.Reader 37 | 38 | if s, _ := os.Stdin.Stat(); s.Mode()&os.ModeCharDevice == 0 { 39 | input = os.Stdin 40 | } 41 | 42 | cmd := rpc.New() 43 | cmd.HTTP = strings.HasPrefix(addr, "http") 44 | cmd.Input = input 45 | cmd.Output = os.Stdout 46 | cmd.Addr = addr 47 | cmd.Method, _ = args[""].(string) 48 | cmd.Args, _ = args[""].([]string) 49 | cmd.UserAgent = fmt.Sprintf("Segment (rpc-cli-legacy/%s)", version) 50 | 51 | check(cmd.Run()) 52 | } 53 | 54 | func check(err error) { 55 | if err != nil { 56 | fmt.Fprintf(os.Stderr, "rpc: %s\n", err) 57 | os.Exit(1) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/rpc" 12 | "net/rpc/jsonrpc" 13 | "strings" 14 | 15 | "github.com/mattn/go-shellwords" 16 | "gopkg.in/readline.v1" 17 | ) 18 | 19 | type Command struct { 20 | Addr string 21 | Method string 22 | Args []string 23 | UserAgent string 24 | HTTP bool 25 | Interactive bool 26 | Input io.Reader 27 | Output io.Writer 28 | sock net.Conn 29 | client *rpc.Client 30 | id int 31 | } 32 | 33 | func New() *Command { 34 | return new(Command) 35 | } 36 | 37 | func (c *Command) Run() error { 38 | if !c.HTTP { 39 | err := c.connect() 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | if c.Method == "" { 46 | return c.interactive() 47 | } 48 | 49 | if args := c.Args; len(args) > 0 { 50 | req := request(args) 51 | return c.call(c.Method, req) 52 | } 53 | 54 | if c.Input == nil { 55 | return c.call(c.Method, nil) 56 | } 57 | 58 | dec := json.NewDecoder(c.Input) 59 | dec.UseNumber() 60 | 61 | for { 62 | var req interface{} 63 | err := dec.Decode(&req) 64 | 65 | if err == io.EOF { 66 | break 67 | } 68 | 69 | if err != nil { 70 | return err 71 | } 72 | 73 | err = c.call(c.Method, req) 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func (c *Command) connect() (err error) { 83 | sock, err := net.Dial("tcp", c.Addr) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | c.sock = sock 89 | c.client = jsonrpc.NewClient(sock) 90 | return err 91 | } 92 | 93 | func (c *Command) interactive() error { 94 | rl, err := readline.New(c.Addr + "> ") 95 | if err != nil { 96 | return err 97 | } 98 | 99 | defer func() { 100 | cerr := rl.Close() 101 | if err == nil { 102 | err = cerr 103 | } 104 | }() 105 | 106 | for { 107 | l, err := rl.Readline() 108 | 109 | if err == readline.ErrInterrupt { 110 | break 111 | } 112 | 113 | if err != nil { 114 | return err 115 | } 116 | 117 | args, err := shellwords.Parse(l) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | if len(args) == 0 { 123 | continue 124 | } 125 | 126 | method := args[0] 127 | req := request(args[1:]) 128 | err = c.call(method, req) 129 | if err != nil { 130 | return err 131 | } 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func (c *Command) call(method string, req interface{}) error { 138 | if c.HTTP { 139 | return c.post(method, req) 140 | } 141 | 142 | var reply *json.RawMessage 143 | err := c.client.Call(method, req, &reply) 144 | 145 | if err == rpc.ErrShutdown { 146 | err = c.connect() 147 | if err != nil { 148 | return fmt.Errorf("cannot re-connect %s", err) 149 | } 150 | 151 | err = c.client.Call(method, req, &reply) 152 | } 153 | 154 | if err != nil { 155 | return err 156 | } 157 | 158 | buf, err := json.MarshalIndent(reply, "", " ") 159 | if err != nil { 160 | return err 161 | } 162 | 163 | fmt.Fprintln(c.Output, string(buf)) 164 | return nil 165 | } 166 | 167 | func (c *Command) post(method string, req interface{}) error { 168 | r, err := c.request(method, req) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | r.Header.Set("Content-Type", "application/json") 174 | if c.UserAgent != "" { 175 | r.Header.Set("User-Agent", c.UserAgent) 176 | } 177 | 178 | resp, err := http.DefaultClient.Do(r) 179 | if resp != nil { 180 | defer resp.Body.Close() 181 | } 182 | 183 | if err != nil { 184 | return err 185 | } 186 | 187 | if resp.StatusCode >= 400 { 188 | out, _ := httputil.DumpResponse(resp, true) 189 | return fmt.Errorf("received non json response\n%s", out) 190 | } 191 | 192 | res := struct { 193 | Result interface{} `json:"result"` 194 | Error interface{} `json:"error"` 195 | }{} 196 | 197 | dec := json.NewDecoder(resp.Body) 198 | dec.UseNumber() 199 | err = dec.Decode(&res) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | if res.Error != nil { 205 | fmt.Printf("%+v\n", res.Error) 206 | return nil 207 | } 208 | 209 | buf, err := json.MarshalIndent(res.Result, "", " ") 210 | if err != nil { 211 | return err 212 | } 213 | 214 | fmt.Fprintln(c.Output, string(buf)) 215 | return nil 216 | } 217 | 218 | func (c *Command) request(method string, req interface{}) (*http.Request, error) { 219 | c.id++ 220 | 221 | buf, err := json.Marshal(map[string]interface{}{ 222 | "id": c.id, 223 | "method": method, 224 | "params": []interface{}{req}, 225 | }) 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | return http.NewRequest("POST", c.Addr, bytes.NewReader(buf)) 231 | } 232 | 233 | func request(args []string) interface{} { 234 | if len(args) == 1 { 235 | var ret interface{} 236 | err := json.Unmarshal([]byte(args[0]), &ret) 237 | if err == nil { 238 | return ret 239 | } 240 | } 241 | 242 | ret := make(map[string]interface{}) 243 | 244 | for _, arg := range args { 245 | parts := strings.SplitN(arg, "=", 2) 246 | ret[parts[0]] = coerce(parts[1]) 247 | } 248 | 249 | return ret 250 | } 251 | 252 | func coerce(s string) interface{} { 253 | var v interface{} 254 | 255 | err := json.Unmarshal([]byte(s), &v) 256 | if err != nil { 257 | v = s 258 | } 259 | 260 | return v 261 | } 262 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "path": "=M", 7 | "revision": "" 8 | }, 9 | { 10 | "checksumSHA1": "jWJJlpvg/cqbClaKqZoAnNP6HFc=", 11 | "path": "github.com/mattn/go-shellwords", 12 | "revision": "f8471b0a71ded0ab910825ee2cf230f25de000f1", 13 | "revisionTime": "2018-06-05T04:17:37Z" 14 | }, 15 | { 16 | "checksumSHA1": "LfLTyk4ox5wH0YL4sD5rcbrVWBw=", 17 | "path": "github.com/tj/docopt", 18 | "revision": "c8470e45692f168e8b380c5d625327e756d7d0a9", 19 | "revisionTime": "2014-08-07T22:00:17Z" 20 | }, 21 | { 22 | "checksumSHA1": "VAPpB3iZ9bfjWqNDoX7qXqfRcQU=", 23 | "path": "gopkg.in/readline.v1", 24 | "revision": "62c6fe6193755f722b8b8788aa7357be55a50ff1", 25 | "revisionTime": "2016-07-26T13:51:17Z" 26 | } 27 | ], 28 | "rootPath": "github.com/segmentio/rpc-cli" 29 | } 30 | --------------------------------------------------------------------------------