mirror of https://github.com/ethereum/go-ethereum
rpc: implement full bi-directional communication (#18471)
New APIs added: client.RegisterName(namespace, service) // makes service available to server client.Notify(ctx, method, args...) // sends a notification ClientFromContext(ctx) // to get a client in handler method This is essentially a rewrite of the server-side code. JSON-RPC processing code is now the same on both server and client side. Many minor issues were fixed in the process and there is a new test suite for JSON-RPC spec compliance (and non-compliance in some cases). List of behavior changes: - Method handlers are now called with a per-request context instead of a per-connection context. The context is canceled right after the method returns. - Subscription error channels are always closed when the connection ends. There is no need to also wait on the Notifier's Closed channel to detect whether the subscription has ended. - Client now omits "params" instead of sending "params": null when there are no arguments to a call. The previous behavior was not compliant with the spec. The server still accepts "params": null. - Floating point numbers are allowed as "id". The spec doesn't allow them, but we handle request "id" as json.RawMessage and guarantee that the same number will be sent back. - Logging is improved significantly. There is now a message at DEBUG level for each RPC call served.pull/18989/head
parent
ec3432bccb
commit
245f3146c2
@ -0,0 +1,397 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rpc |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"reflect" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
// handler handles JSON-RPC messages. There is one handler per connection. Note that
|
||||
// handler is not safe for concurrent use. Message handling never blocks indefinitely
|
||||
// because RPCs are processed on background goroutines launched by handler.
|
||||
//
|
||||
// The entry points for incoming messages are:
|
||||
//
|
||||
// h.handleMsg(message)
|
||||
// h.handleBatch(message)
|
||||
//
|
||||
// Outgoing calls use the requestOp struct. Register the request before sending it
|
||||
// on the connection:
|
||||
//
|
||||
// op := &requestOp{ids: ...}
|
||||
// h.addRequestOp(op)
|
||||
//
|
||||
// Now send the request, then wait for the reply to be delivered through handleMsg:
|
||||
//
|
||||
// if err := op.wait(...); err != nil {
|
||||
// h.removeRequestOp(op) // timeout, etc.
|
||||
// }
|
||||
//
|
||||
type handler struct { |
||||
reg *serviceRegistry |
||||
unsubscribeCb *callback |
||||
idgen func() ID // subscription ID generator
|
||||
respWait map[string]*requestOp // active client requests
|
||||
clientSubs map[string]*ClientSubscription // active client subscriptions
|
||||
callWG sync.WaitGroup // pending call goroutines
|
||||
rootCtx context.Context // canceled by close()
|
||||
cancelRoot func() // cancel function for rootCtx
|
||||
conn jsonWriter // where responses will be sent
|
||||
log log.Logger |
||||
allowSubscribe bool |
||||
|
||||
subLock sync.Mutex |
||||
serverSubs map[ID]*Subscription |
||||
} |
||||
|
||||
type callProc struct { |
||||
ctx context.Context |
||||
notifiers []*Notifier |
||||
} |
||||
|
||||
func newHandler(connCtx context.Context, conn jsonWriter, idgen func() ID, reg *serviceRegistry) *handler { |
||||
rootCtx, cancelRoot := context.WithCancel(connCtx) |
||||
h := &handler{ |
||||
reg: reg, |
||||
idgen: idgen, |
||||
conn: conn, |
||||
respWait: make(map[string]*requestOp), |
||||
clientSubs: make(map[string]*ClientSubscription), |
||||
rootCtx: rootCtx, |
||||
cancelRoot: cancelRoot, |
||||
allowSubscribe: true, |
||||
serverSubs: make(map[ID]*Subscription), |
||||
log: log.Root(), |
||||
} |
||||
if conn.RemoteAddr() != "" { |
||||
h.log = h.log.New("conn", conn.RemoteAddr()) |
||||
} |
||||
h.unsubscribeCb = newCallback(reflect.Value{}, reflect.ValueOf(h.unsubscribe)) |
||||
return h |
||||
} |
||||
|
||||
// handleBatch executes all messages in a batch and returns the responses.
|
||||
func (h *handler) handleBatch(msgs []*jsonrpcMessage) { |
||||
// Emit error response for empty batches:
|
||||
if len(msgs) == 0 { |
||||
h.startCallProc(func(cp *callProc) { |
||||
h.conn.Write(cp.ctx, errorMessage(&invalidRequestError{"empty batch"})) |
||||
}) |
||||
return |
||||
} |
||||
|
||||
// Handle non-call messages first:
|
||||
calls := make([]*jsonrpcMessage, 0, len(msgs)) |
||||
for _, msg := range msgs { |
||||
if handled := h.handleImmediate(msg); !handled { |
||||
calls = append(calls, msg) |
||||
} |
||||
} |
||||
if len(calls) == 0 { |
||||
return |
||||
} |
||||
// Process calls on a goroutine because they may block indefinitely:
|
||||
h.startCallProc(func(cp *callProc) { |
||||
answers := make([]*jsonrpcMessage, 0, len(msgs)) |
||||
for _, msg := range calls { |
||||
if answer := h.handleCallMsg(cp, msg); answer != nil { |
||||
answers = append(answers, answer) |
||||
} |
||||
} |
||||
h.addSubscriptions(cp.notifiers) |
||||
if len(answers) > 0 { |
||||
h.conn.Write(cp.ctx, answers) |
||||
} |
||||
for _, n := range cp.notifiers { |
||||
n.activate() |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// handleMsg handles a single message.
|
||||
func (h *handler) handleMsg(msg *jsonrpcMessage) { |
||||
if ok := h.handleImmediate(msg); ok { |
||||
return |
||||
} |
||||
h.startCallProc(func(cp *callProc) { |
||||
answer := h.handleCallMsg(cp, msg) |
||||
h.addSubscriptions(cp.notifiers) |
||||
if answer != nil { |
||||
h.conn.Write(cp.ctx, answer) |
||||
} |
||||
for _, n := range cp.notifiers { |
||||
n.activate() |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// close cancels all requests except for inflightReq and waits for
|
||||
// call goroutines to shut down.
|
||||
func (h *handler) close(err error, inflightReq *requestOp) { |
||||
h.cancelAllRequests(err, inflightReq) |
||||
h.cancelRoot() |
||||
h.callWG.Wait() |
||||
h.cancelServerSubscriptions(err) |
||||
} |
||||
|
||||
// addRequestOp registers a request operation.
|
||||
func (h *handler) addRequestOp(op *requestOp) { |
||||
for _, id := range op.ids { |
||||
h.respWait[string(id)] = op |
||||
} |
||||
} |
||||
|
||||
// removeRequestOps stops waiting for the given request IDs.
|
||||
func (h *handler) removeRequestOp(op *requestOp) { |
||||
for _, id := range op.ids { |
||||
delete(h.respWait, string(id)) |
||||
} |
||||
} |
||||
|
||||
// cancelAllRequests unblocks and removes pending requests and active subscriptions.
|
||||
func (h *handler) cancelAllRequests(err error, inflightReq *requestOp) { |
||||
didClose := make(map[*requestOp]bool) |
||||
if inflightReq != nil { |
||||
didClose[inflightReq] = true |
||||
} |
||||
|
||||
for id, op := range h.respWait { |
||||
// Remove the op so that later calls will not close op.resp again.
|
||||
delete(h.respWait, id) |
||||
|
||||
if !didClose[op] { |
||||
op.err = err |
||||
close(op.resp) |
||||
didClose[op] = true |
||||
} |
||||
} |
||||
for id, sub := range h.clientSubs { |
||||
delete(h.clientSubs, id) |
||||
sub.quitWithError(err, false) |
||||
} |
||||
} |
||||
|
||||
func (h *handler) addSubscriptions(nn []*Notifier) { |
||||
h.subLock.Lock() |
||||
defer h.subLock.Unlock() |
||||
|
||||
for _, n := range nn { |
||||
if sub := n.takeSubscription(); sub != nil { |
||||
h.serverSubs[sub.ID] = sub |
||||
} |
||||
} |
||||
} |
||||
|
||||
// cancelServerSubscriptions removes all subscriptions and closes their error channels.
|
||||
func (h *handler) cancelServerSubscriptions(err error) { |
||||
h.subLock.Lock() |
||||
defer h.subLock.Unlock() |
||||
|
||||
for id, s := range h.serverSubs { |
||||
s.err <- err |
||||
close(s.err) |
||||
delete(h.serverSubs, id) |
||||
} |
||||
} |
||||
|
||||
// startCallProc runs fn in a new goroutine and starts tracking it in the h.calls wait group.
|
||||
func (h *handler) startCallProc(fn func(*callProc)) { |
||||
h.callWG.Add(1) |
||||
go func() { |
||||
ctx, cancel := context.WithCancel(h.rootCtx) |
||||
defer h.callWG.Done() |
||||
defer cancel() |
||||
fn(&callProc{ctx: ctx}) |
||||
}() |
||||
} |
||||
|
||||
// handleImmediate executes non-call messages. It returns false if the message is a
|
||||
// call or requires a reply.
|
||||
func (h *handler) handleImmediate(msg *jsonrpcMessage) bool { |
||||
start := time.Now() |
||||
switch { |
||||
case msg.isNotification(): |
||||
if strings.HasSuffix(msg.Method, notificationMethodSuffix) { |
||||
h.handleSubscriptionResult(msg) |
||||
return true |
||||
} |
||||
return false |
||||
case msg.isResponse(): |
||||
h.handleResponse(msg) |
||||
h.log.Trace("Handled RPC response", "reqid", idForLog{msg.ID}, "t", time.Since(start)) |
||||
return true |
||||
default: |
||||
return false |
||||
} |
||||
} |
||||
|
||||
// handleSubscriptionResult processes subscription notifications.
|
||||
func (h *handler) handleSubscriptionResult(msg *jsonrpcMessage) { |
||||
var result subscriptionResult |
||||
if err := json.Unmarshal(msg.Params, &result); err != nil { |
||||
h.log.Debug("Dropping invalid subscription message") |
||||
return |
||||
} |
||||
if h.clientSubs[result.ID] != nil { |
||||
h.clientSubs[result.ID].deliver(result.Result) |
||||
} |
||||
} |
||||
|
||||
// handleResponse processes method call responses.
|
||||
func (h *handler) handleResponse(msg *jsonrpcMessage) { |
||||
op := h.respWait[string(msg.ID)] |
||||
if op == nil { |
||||
h.log.Debug("Unsolicited RPC response", "reqid", idForLog{msg.ID}) |
||||
return |
||||
} |
||||
delete(h.respWait, string(msg.ID)) |
||||
// For normal responses, just forward the reply to Call/BatchCall.
|
||||
if op.sub == nil { |
||||
op.resp <- msg |
||||
return |
||||
} |
||||
// For subscription responses, start the subscription if the server
|
||||
// indicates success. EthSubscribe gets unblocked in either case through
|
||||
// the op.resp channel.
|
||||
defer close(op.resp) |
||||
if msg.Error != nil { |
||||
op.err = msg.Error |
||||
return |
||||
} |
||||
if op.err = json.Unmarshal(msg.Result, &op.sub.subid); op.err == nil { |
||||
go op.sub.start() |
||||
h.clientSubs[op.sub.subid] = op.sub |
||||
} |
||||
} |
||||
|
||||
// handleCallMsg executes a call message and returns the answer.
|
||||
func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMessage { |
||||
start := time.Now() |
||||
switch { |
||||
case msg.isNotification(): |
||||
h.handleCall(ctx, msg) |
||||
h.log.Debug("Served "+msg.Method, "t", time.Since(start)) |
||||
return nil |
||||
case msg.isCall(): |
||||
resp := h.handleCall(ctx, msg) |
||||
if resp.Error != nil { |
||||
h.log.Info("Served "+msg.Method, "reqid", idForLog{msg.ID}, "t", time.Since(start), "err", resp.Error.Message) |
||||
} else { |
||||
h.log.Debug("Served "+msg.Method, "reqid", idForLog{msg.ID}, "t", time.Since(start)) |
||||
} |
||||
return resp |
||||
case msg.hasValidID(): |
||||
return msg.errorResponse(&invalidRequestError{"invalid request"}) |
||||
default: |
||||
return errorMessage(&invalidRequestError{"invalid request"}) |
||||
} |
||||
} |
||||
|
||||
// handleCall processes method calls.
|
||||
func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage { |
||||
if msg.isSubscribe() { |
||||
return h.handleSubscribe(cp, msg) |
||||
} |
||||
var callb *callback |
||||
if msg.isUnsubscribe() { |
||||
callb = h.unsubscribeCb |
||||
} else { |
||||
callb = h.reg.callback(msg.Method) |
||||
} |
||||
if callb == nil { |
||||
return msg.errorResponse(&methodNotFoundError{method: msg.Method}) |
||||
} |
||||
args, err := parsePositionalArguments(msg.Params, callb.argTypes) |
||||
if err != nil { |
||||
return msg.errorResponse(&invalidParamsError{err.Error()}) |
||||
} |
||||
|
||||
return h.runMethod(cp.ctx, msg, callb, args) |
||||
} |
||||
|
||||
// handleSubscribe processes *_subscribe method calls.
|
||||
func (h *handler) handleSubscribe(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage { |
||||
if !h.allowSubscribe { |
||||
return msg.errorResponse(ErrNotificationsUnsupported) |
||||
} |
||||
|
||||
// Subscription method name is first argument.
|
||||
name, err := parseSubscriptionName(msg.Params) |
||||
if err != nil { |
||||
return msg.errorResponse(&invalidParamsError{err.Error()}) |
||||
} |
||||
namespace := msg.namespace() |
||||
callb := h.reg.subscription(namespace, name) |
||||
if callb == nil { |
||||
return msg.errorResponse(&subscriptionNotFoundError{namespace, name}) |
||||
} |
||||
|
||||
// Parse subscription name arg too, but remove it before calling the callback.
|
||||
argTypes := append([]reflect.Type{stringType}, callb.argTypes...) |
||||
args, err := parsePositionalArguments(msg.Params, argTypes) |
||||
if err != nil { |
||||
return msg.errorResponse(&invalidParamsError{err.Error()}) |
||||
} |
||||
args = args[1:] |
||||
|
||||
// Install notifier in context so the subscription handler can find it.
|
||||
n := &Notifier{h: h, namespace: namespace} |
||||
cp.notifiers = append(cp.notifiers, n) |
||||
ctx := context.WithValue(cp.ctx, notifierKey{}, n) |
||||
|
||||
return h.runMethod(ctx, msg, callb, args) |
||||
} |
||||
|
||||
// runMethod runs the Go callback for an RPC method.
|
||||
func (h *handler) runMethod(ctx context.Context, msg *jsonrpcMessage, callb *callback, args []reflect.Value) *jsonrpcMessage { |
||||
result, err := callb.call(ctx, msg.Method, args) |
||||
if err != nil { |
||||
return msg.errorResponse(err) |
||||
} |
||||
return msg.response(result) |
||||
} |
||||
|
||||
// unsubscribe is the callback function for all *_unsubscribe calls.
|
||||
func (h *handler) unsubscribe(ctx context.Context, id ID) (bool, error) { |
||||
h.subLock.Lock() |
||||
defer h.subLock.Unlock() |
||||
|
||||
s := h.serverSubs[id] |
||||
if s == nil { |
||||
return false, ErrSubscriptionNotFound |
||||
} |
||||
close(s.err) |
||||
delete(h.serverSubs, id) |
||||
return true, nil |
||||
} |
||||
|
||||
type idForLog struct{ json.RawMessage } |
||||
|
||||
func (id idForLog) String() string { |
||||
if s, err := strconv.Unquote(string(id.RawMessage)); err == nil { |
||||
return s |
||||
} |
||||
return string(id.RawMessage) |
||||
} |
@ -1,178 +0,0 @@ |
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rpc |
||||
|
||||
import ( |
||||
"bufio" |
||||
"bytes" |
||||
"encoding/json" |
||||
"reflect" |
||||
"strconv" |
||||
"testing" |
||||
) |
||||
|
||||
type RWC struct { |
||||
*bufio.ReadWriter |
||||
} |
||||
|
||||
func (rwc *RWC) Close() error { |
||||
return nil |
||||
} |
||||
|
||||
func TestJSONRequestParsing(t *testing.T) { |
||||
server := NewServer() |
||||
service := new(Service) |
||||
|
||||
if err := server.RegisterName("calc", service); err != nil { |
||||
t.Fatalf("%v", err) |
||||
} |
||||
|
||||
req := bytes.NewBufferString(`{"id": 1234, "jsonrpc": "2.0", "method": "calc_add", "params": [11, 22]}`) |
||||
var str string |
||||
reply := bytes.NewBufferString(str) |
||||
rw := &RWC{bufio.NewReadWriter(bufio.NewReader(req), bufio.NewWriter(reply))} |
||||
|
||||
codec := NewJSONCodec(rw) |
||||
|
||||
requests, batch, err := codec.ReadRequestHeaders() |
||||
if err != nil { |
||||
t.Fatalf("%v", err) |
||||
} |
||||
|
||||
if batch { |
||||
t.Fatalf("Request isn't a batch") |
||||
} |
||||
|
||||
if len(requests) != 1 { |
||||
t.Fatalf("Expected 1 request but got %d requests - %v", len(requests), requests) |
||||
} |
||||
|
||||
if requests[0].service != "calc" { |
||||
t.Fatalf("Expected service 'calc' but got '%s'", requests[0].service) |
||||
} |
||||
|
||||
if requests[0].method != "add" { |
||||
t.Fatalf("Expected method 'Add' but got '%s'", requests[0].method) |
||||
} |
||||
|
||||
if rawId, ok := requests[0].id.(*json.RawMessage); ok { |
||||
id, e := strconv.ParseInt(string(*rawId), 0, 64) |
||||
if e != nil { |
||||
t.Fatalf("%v", e) |
||||
} |
||||
if id != 1234 { |
||||
t.Fatalf("Expected id 1234 but got %d", id) |
||||
} |
||||
} else { |
||||
t.Fatalf("invalid request, expected *json.RawMesage got %T", requests[0].id) |
||||
} |
||||
|
||||
var arg int |
||||
args := []reflect.Type{reflect.TypeOf(arg), reflect.TypeOf(arg)} |
||||
|
||||
v, err := codec.ParseRequestArguments(args, requests[0].params) |
||||
if err != nil { |
||||
t.Fatalf("%v", err) |
||||
} |
||||
|
||||
if len(v) != 2 { |
||||
t.Fatalf("Expected 2 argument values, got %d", len(v)) |
||||
} |
||||
|
||||
if v[0].Int() != 11 || v[1].Int() != 22 { |
||||
t.Fatalf("expected %d == 11 && %d == 22", v[0].Int(), v[1].Int()) |
||||
} |
||||
} |
||||
|
||||
func TestJSONRequestParamsParsing(t *testing.T) { |
||||
|
||||
var ( |
||||
stringT = reflect.TypeOf("") |
||||
intT = reflect.TypeOf(0) |
||||
intPtrT = reflect.TypeOf(new(int)) |
||||
|
||||
stringV = reflect.ValueOf("abc") |
||||
i = 1 |
||||
intV = reflect.ValueOf(i) |
||||
intPtrV = reflect.ValueOf(&i) |
||||
) |
||||
|
||||
var validTests = []struct { |
||||
input string |
||||
argTypes []reflect.Type |
||||
expected []reflect.Value |
||||
}{ |
||||
{`[]`, []reflect.Type{}, []reflect.Value{}}, |
||||
{`[]`, []reflect.Type{intPtrT}, []reflect.Value{intPtrV}}, |
||||
{`[1]`, []reflect.Type{intT}, []reflect.Value{intV}}, |
||||
{`[1,"abc"]`, []reflect.Type{intT, stringT}, []reflect.Value{intV, stringV}}, |
||||
{`[null]`, []reflect.Type{intPtrT}, []reflect.Value{intPtrV}}, |
||||
{`[null,"abc"]`, []reflect.Type{intPtrT, stringT, intPtrT}, []reflect.Value{intPtrV, stringV, intPtrV}}, |
||||
{`[null,"abc",null]`, []reflect.Type{intPtrT, stringT, intPtrT}, []reflect.Value{intPtrV, stringV, intPtrV}}, |
||||
} |
||||
|
||||
codec := jsonCodec{} |
||||
|
||||
for _, test := range validTests { |
||||
params := (json.RawMessage)([]byte(test.input)) |
||||
args, err := codec.ParseRequestArguments(test.argTypes, params) |
||||
|
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
var match []interface{} |
||||
json.Unmarshal([]byte(test.input), &match) |
||||
|
||||
if len(args) != len(test.argTypes) { |
||||
t.Fatalf("expected %d parsed args, got %d", len(test.argTypes), len(args)) |
||||
} |
||||
|
||||
for i, arg := range args { |
||||
expected := test.expected[i] |
||||
|
||||
if arg.Kind() != expected.Kind() { |
||||
t.Errorf("expected type for param %d in %s", i, test.input) |
||||
} |
||||
|
||||
if arg.Kind() == reflect.Int && arg.Int() != expected.Int() { |
||||
t.Errorf("expected int(%d), got int(%d) in %s", expected.Int(), arg.Int(), test.input) |
||||
} |
||||
|
||||
if arg.Kind() == reflect.String && arg.String() != expected.String() { |
||||
t.Errorf("expected string(%s), got string(%s) in %s", expected.String(), arg.String(), test.input) |
||||
} |
||||
} |
||||
} |
||||
|
||||
var invalidTests = []struct { |
||||
input string |
||||
argTypes []reflect.Type |
||||
}{ |
||||
{`[]`, []reflect.Type{intT}}, |
||||
{`[null]`, []reflect.Type{intT}}, |
||||
{`[1]`, []reflect.Type{stringT}}, |
||||
{`[1,2]`, []reflect.Type{stringT}}, |
||||
{`["abc", null]`, []reflect.Type{stringT, intT}}, |
||||
} |
||||
|
||||
for i, test := range invalidTests { |
||||
if _, err := codec.ParseRequestArguments(test.argTypes, test.input); err == nil { |
||||
t.Errorf("expected test %d - %s to fail", i, test.input) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,285 @@ |
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rpc |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"reflect" |
||||
"runtime" |
||||
"strings" |
||||
"sync" |
||||
"unicode" |
||||
"unicode/utf8" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
var ( |
||||
contextType = reflect.TypeOf((*context.Context)(nil)).Elem() |
||||
errorType = reflect.TypeOf((*error)(nil)).Elem() |
||||
subscriptionType = reflect.TypeOf(Subscription{}) |
||||
stringType = reflect.TypeOf("") |
||||
) |
||||
|
||||
type serviceRegistry struct { |
||||
mu sync.Mutex |
||||
services map[string]service |
||||
} |
||||
|
||||
// service represents a registered object.
|
||||
type service struct { |
||||
name string // name for service
|
||||
callbacks map[string]*callback // registered handlers
|
||||
subscriptions map[string]*callback // available subscriptions/notifications
|
||||
} |
||||
|
||||
// callback is a method callback which was registered in the server
|
||||
type callback struct { |
||||
fn reflect.Value // the function
|
||||
rcvr reflect.Value // receiver object of method, set if fn is method
|
||||
argTypes []reflect.Type // input argument types
|
||||
hasCtx bool // method's first argument is a context (not included in argTypes)
|
||||
errPos int // err return idx, of -1 when method cannot return error
|
||||
isSubscribe bool // true if this is a subscription callback
|
||||
} |
||||
|
||||
func (r *serviceRegistry) registerName(name string, rcvr interface{}) error { |
||||
rcvrVal := reflect.ValueOf(rcvr) |
||||
if name == "" { |
||||
return fmt.Errorf("no service name for type %s", rcvrVal.Type().String()) |
||||
} |
||||
callbacks := suitableCallbacks(rcvrVal) |
||||
if len(callbacks) == 0 { |
||||
return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr) |
||||
} |
||||
|
||||
r.mu.Lock() |
||||
defer r.mu.Unlock() |
||||
if r.services == nil { |
||||
r.services = make(map[string]service) |
||||
} |
||||
svc, ok := r.services[name] |
||||
if !ok { |
||||
svc = service{ |
||||
name: name, |
||||
callbacks: make(map[string]*callback), |
||||
subscriptions: make(map[string]*callback), |
||||
} |
||||
r.services[name] = svc |
||||
} |
||||
for name, cb := range callbacks { |
||||
if cb.isSubscribe { |
||||
svc.subscriptions[name] = cb |
||||
} else { |
||||
svc.callbacks[name] = cb |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// callback returns the callback corresponding to the given RPC method name.
|
||||
func (r *serviceRegistry) callback(method string) *callback { |
||||
elem := strings.SplitN(method, serviceMethodSeparator, 2) |
||||
if len(elem) != 2 { |
||||
return nil |
||||
} |
||||
r.mu.Lock() |
||||
defer r.mu.Unlock() |
||||
return r.services[elem[0]].callbacks[elem[1]] |
||||
} |
||||
|
||||
// subscription returns a subscription callback in the given service.
|
||||
func (r *serviceRegistry) subscription(service, name string) *callback { |
||||
r.mu.Lock() |
||||
defer r.mu.Unlock() |
||||
return r.services[service].subscriptions[name] |
||||
} |
||||
|
||||
// suitableCallbacks iterates over the methods of the given type. It determines if a method
|
||||
// satisfies the criteria for a RPC callback or a subscription callback and adds it to the
|
||||
// collection of callbacks. See server documentation for a summary of these criteria.
|
||||
func suitableCallbacks(receiver reflect.Value) map[string]*callback { |
||||
typ := receiver.Type() |
||||
callbacks := make(map[string]*callback) |
||||
for m := 0; m < typ.NumMethod(); m++ { |
||||
method := typ.Method(m) |
||||
if method.PkgPath != "" { |
||||
continue // method not exported
|
||||
} |
||||
cb := newCallback(receiver, method.Func) |
||||
if cb == nil { |
||||
continue // function invalid
|
||||
} |
||||
name := formatName(method.Name) |
||||
callbacks[name] = cb |
||||
} |
||||
return callbacks |
||||
} |
||||
|
||||
// newCallback turns fn (a function) into a callback object. It returns nil if the function
|
||||
// is unsuitable as an RPC callback.
|
||||
func newCallback(receiver, fn reflect.Value) *callback { |
||||
fntype := fn.Type() |
||||
c := &callback{fn: fn, rcvr: receiver, errPos: -1, isSubscribe: isPubSub(fntype)} |
||||
// Determine parameter types. They must all be exported or builtin types.
|
||||
c.makeArgTypes() |
||||
if !allExportedOrBuiltin(c.argTypes) { |
||||
return nil |
||||
} |
||||
// Verify return types. The function must return at most one error
|
||||
// and/or one other non-error value.
|
||||
outs := make([]reflect.Type, fntype.NumOut()) |
||||
for i := 0; i < fntype.NumOut(); i++ { |
||||
outs[i] = fntype.Out(i) |
||||
} |
||||
if len(outs) > 2 || !allExportedOrBuiltin(outs) { |
||||
return nil |
||||
} |
||||
// If an error is returned, it must be the last returned value.
|
||||
switch { |
||||
case len(outs) == 1 && isErrorType(outs[0]): |
||||
c.errPos = 0 |
||||
case len(outs) == 2: |
||||
if isErrorType(outs[0]) || !isErrorType(outs[1]) { |
||||
return nil |
||||
} |
||||
c.errPos = 1 |
||||
} |
||||
return c |
||||
} |
||||
|
||||
// makeArgTypes composes the argTypes list.
|
||||
func (c *callback) makeArgTypes() { |
||||
fntype := c.fn.Type() |
||||
// Skip receiver and context.Context parameter (if present).
|
||||
firstArg := 0 |
||||
if c.rcvr.IsValid() { |
||||
firstArg++ |
||||
} |
||||
if fntype.NumIn() > firstArg && fntype.In(firstArg) == contextType { |
||||
c.hasCtx = true |
||||
firstArg++ |
||||
} |
||||
// Add all remaining parameters.
|
||||
c.argTypes = make([]reflect.Type, fntype.NumIn()-firstArg) |
||||
for i := firstArg; i < fntype.NumIn(); i++ { |
||||
c.argTypes[i-firstArg] = fntype.In(i) |
||||
} |
||||
} |
||||
|
||||
// call invokes the callback.
|
||||
func (c *callback) call(ctx context.Context, method string, args []reflect.Value) (res interface{}, errRes error) { |
||||
// Create the argument slice.
|
||||
fullargs := make([]reflect.Value, 0, 2+len(args)) |
||||
if c.rcvr.IsValid() { |
||||
fullargs = append(fullargs, c.rcvr) |
||||
} |
||||
if c.hasCtx { |
||||
fullargs = append(fullargs, reflect.ValueOf(ctx)) |
||||
} |
||||
fullargs = append(fullargs, args...) |
||||
|
||||
// Catch panic while running the callback.
|
||||
defer func() { |
||||
if err := recover(); err != nil { |
||||
const size = 64 << 10 |
||||
buf := make([]byte, size) |
||||
buf = buf[:runtime.Stack(buf, false)] |
||||
log.Error("RPC method " + method + " crashed: " + fmt.Sprintf("%v\n%s", err, buf)) |
||||
errRes = errors.New("method handler crashed") |
||||
} |
||||
}() |
||||
// Run the callback.
|
||||
results := c.fn.Call(fullargs) |
||||
if len(results) == 0 { |
||||
return nil, nil |
||||
} |
||||
if c.errPos >= 0 && !results[c.errPos].IsNil() { |
||||
// Method has returned non-nil error value.
|
||||
err := results[c.errPos].Interface().(error) |
||||
return reflect.Value{}, err |
||||
} |
||||
return results[0].Interface(), nil |
||||
} |
||||
|
||||
// Is this an exported - upper case - name?
|
||||
func isExported(name string) bool { |
||||
rune, _ := utf8.DecodeRuneInString(name) |
||||
return unicode.IsUpper(rune) |
||||
} |
||||
|
||||
// Are all those types exported or built-in?
|
||||
func allExportedOrBuiltin(types []reflect.Type) bool { |
||||
for _, typ := range types { |
||||
for typ.Kind() == reflect.Ptr { |
||||
typ = typ.Elem() |
||||
} |
||||
// PkgPath will be non-empty even for an exported type,
|
||||
// so we need to check the type name as well.
|
||||
if !isExported(typ.Name()) && typ.PkgPath() != "" { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// Is t context.Context or *context.Context?
|
||||
func isContextType(t reflect.Type) bool { |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
return t == contextType |
||||
} |
||||
|
||||
// Does t satisfy the error interface?
|
||||
func isErrorType(t reflect.Type) bool { |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
return t.Implements(errorType) |
||||
} |
||||
|
||||
// Is t Subscription or *Subscription?
|
||||
func isSubscriptionType(t reflect.Type) bool { |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
return t == subscriptionType |
||||
} |
||||
|
||||
// isPubSub tests whether the given method has as as first argument a context.Context and
|
||||
// returns the pair (Subscription, error).
|
||||
func isPubSub(methodType reflect.Type) bool { |
||||
// numIn(0) is the receiver type
|
||||
if methodType.NumIn() < 2 || methodType.NumOut() != 2 { |
||||
return false |
||||
} |
||||
return isContextType(methodType.In(1)) && |
||||
isSubscriptionType(methodType.Out(0)) && |
||||
isErrorType(methodType.Out(1)) |
||||
} |
||||
|
||||
// formatName converts to first character of name to lowercase.
|
||||
func formatName(name string) string { |
||||
ret := []rune(name) |
||||
if len(ret) > 0 { |
||||
ret[0] = unicode.ToLower(ret[0]) |
||||
} |
||||
return string(ret) |
||||
} |
@ -0,0 +1,7 @@ |
||||
// This test checks processing of messages with invalid ID.
|
||||
|
||||
--> {"id":[],"method":"test_foo"} |
||||
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}} |
||||
|
||||
--> {"id":{},"method":"test_foo"} |
||||
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}} |
@ -0,0 +1,14 @@ |
||||
// This test checks the behavior of batches with invalid elements.
|
||||
// Empty batches are not allowed. Batches may contain junk.
|
||||
|
||||
--> [] |
||||
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"empty batch"}} |
||||
|
||||
--> [1] |
||||
<-- [{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}] |
||||
|
||||
--> [1,2,3] |
||||
<-- [{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}] |
||||
|
||||
--> [{"jsonrpc":"2.0","id":1,"method":"test_echo","params":["foo",1]},55,{"jsonrpc":"2.0","id":2,"method":"unknown_method"},{"foo":"bar"}] |
||||
<-- [{"jsonrpc":"2.0","id":1,"result":{"String":"foo","Int":1,"Args":null}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"the method unknown_method does not exist/is not available"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}] |
@ -0,0 +1,7 @@ |
||||
// This test checks processing of messages that contain just the ID and nothing else.
|
||||
|
||||
--> {"id":1} |
||||
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}} |
||||
|
||||
--> {"jsonrpc":"2.0","id":1} |
||||
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}} |
@ -0,0 +1,4 @@ |
||||
// This test checks behavior for invalid requests.
|
||||
|
||||
--> 1 |
||||
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}} |
@ -0,0 +1,5 @@ |
||||
// This test checks that an error is written for invalid JSON requests. |
||||
|
||||
--> 'f |
||||
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"invalid character '\\'' looking for beginning of value"}} |
||||
|
@ -0,0 +1,8 @@ |
||||
// There is no response for all-notification batches.
|
||||
|
||||
--> [{"jsonrpc":"2.0","method":"test_echo","params":["x",99]}] |
||||
|
||||
// This test checks regular batch calls.
|
||||
|
||||
--> [{"jsonrpc":"2.0","id":2,"method":"test_echo","params":[]}, {"jsonrpc":"2.0","id": 3,"method":"test_echo","params":["x",3]}] |
||||
<-- [{"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 0"}},{"jsonrpc":"2.0","id":3,"result":{"String":"x","Int":3,"Args":null}}] |
@ -0,0 +1,16 @@ |
||||
// This test calls the test_echo method.
|
||||
|
||||
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": []} |
||||
<-- {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 0"}} |
||||
|
||||
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x"]} |
||||
<-- {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 1"}} |
||||
|
||||
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x", 3]} |
||||
<-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":null}} |
||||
|
||||
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x", 3, {"S": "foo"}]} |
||||
<-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":{"S":"foo"}}} |
||||
|
||||
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echoWithCtx", "params": ["x", 3, {"S": "foo"}]} |
||||
<-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":{"S":"foo"}}} |
@ -0,0 +1,5 @@ |
||||
// This test checks that an error response is sent for calls
|
||||
// with named parameters.
|
||||
|
||||
--> {"jsonrpc":"2.0","method":"test_echo","params":{"int":23},"id":3} |
||||
<-- {"jsonrpc":"2.0","id":3,"error":{"code":-32602,"message":"non-array args"}} |
@ -0,0 +1,4 @@ |
||||
// This test calls the test_noArgsRets method.
|
||||
|
||||
--> {"jsonrpc": "2.0", "id": "foo", "method": "test_noArgsRets", "params": []} |
||||
<-- {"jsonrpc":"2.0","id":"foo","result":null} |
@ -0,0 +1,4 @@ |
||||
// This test calls a method that doesn't exist.
|
||||
|
||||
--> {"jsonrpc": "2.0", "id": 2, "method": "invalid_method", "params": [2, 3]} |
||||
<-- {"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"the method invalid_method does not exist/is not available"}} |
@ -0,0 +1,4 @@ |
||||
// This test checks that calls with no parameters work.
|
||||
|
||||
--> {"jsonrpc":"2.0","method":"test_noArgsRets","id":3} |
||||
<-- {"jsonrpc":"2.0","id":3,"result":null} |
@ -0,0 +1,4 @@ |
||||
// This test checks that calls with "params":null work.
|
||||
|
||||
--> {"jsonrpc":"2.0","method":"test_noArgsRets","params":null,"id":3} |
||||
<-- {"jsonrpc":"2.0","id":3,"result":null} |
@ -0,0 +1,6 @@ |
||||
// This test checks reverse calls.
|
||||
|
||||
--> {"jsonrpc":"2.0","id":2,"method":"test_callMeBack","params":["foo",[1]]} |
||||
<-- {"jsonrpc":"2.0","id":1,"method":"foo","params":[1]} |
||||
--> {"jsonrpc":"2.0","id":1,"result":"my result"} |
||||
<-- {"jsonrpc":"2.0","id":2,"result":"my result"} |
@ -0,0 +1,7 @@ |
||||
// This test checks reverse calls.
|
||||
|
||||
--> {"jsonrpc":"2.0","id":2,"method":"test_callMeBackLater","params":["foo",[1]]} |
||||
<-- {"jsonrpc":"2.0","id":2,"result":null} |
||||
<-- {"jsonrpc":"2.0","id":1,"method":"foo","params":[1]} |
||||
--> {"jsonrpc":"2.0","id":1,"result":"my result"} |
||||
|
@ -0,0 +1,12 @@ |
||||
// This test checks basic subscription support.
|
||||
|
||||
--> {"jsonrpc":"2.0","id":1,"method":"nftest_subscribe","params":["someSubscription",5,1]} |
||||
<-- {"jsonrpc":"2.0","id":1,"result":"0x1"} |
||||
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":1}} |
||||
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":2}} |
||||
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":3}} |
||||
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":4}} |
||||
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":5}} |
||||
|
||||
--> {"jsonrpc":"2.0","id":2,"method":"nftest_echo","params":[11]} |
||||
<-- {"jsonrpc":"2.0","id":2,"result":11} |
@ -0,0 +1,180 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rpc |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/binary" |
||||
"errors" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
func newTestServer() *Server { |
||||
server := NewServer() |
||||
server.idgen = sequentialIDGenerator() |
||||
if err := server.RegisterName("test", new(testService)); err != nil { |
||||
panic(err) |
||||
} |
||||
if err := server.RegisterName("nftest", new(notificationTestService)); err != nil { |
||||
panic(err) |
||||
} |
||||
return server |
||||
} |
||||
|
||||
func sequentialIDGenerator() func() ID { |
||||
var ( |
||||
mu sync.Mutex |
||||
counter uint64 |
||||
) |
||||
return func() ID { |
||||
mu.Lock() |
||||
defer mu.Unlock() |
||||
counter++ |
||||
id := make([]byte, 8) |
||||
binary.BigEndian.PutUint64(id, counter) |
||||
return encodeID(id) |
||||
} |
||||
} |
||||
|
||||
type testService struct{} |
||||
|
||||
type Args struct { |
||||
S string |
||||
} |
||||
|
||||
type Result struct { |
||||
String string |
||||
Int int |
||||
Args *Args |
||||
} |
||||
|
||||
func (s *testService) NoArgsRets() {} |
||||
|
||||
func (s *testService) Echo(str string, i int, args *Args) Result { |
||||
return Result{str, i, args} |
||||
} |
||||
|
||||
func (s *testService) EchoWithCtx(ctx context.Context, str string, i int, args *Args) Result { |
||||
return Result{str, i, args} |
||||
} |
||||
|
||||
func (s *testService) Sleep(ctx context.Context, duration time.Duration) { |
||||
time.Sleep(duration) |
||||
} |
||||
|
||||
func (s *testService) Rets() (string, error) { |
||||
return "", nil |
||||
} |
||||
|
||||
func (s *testService) InvalidRets1() (error, string) { |
||||
return nil, "" |
||||
} |
||||
|
||||
func (s *testService) InvalidRets2() (string, string) { |
||||
return "", "" |
||||
} |
||||
|
||||
func (s *testService) InvalidRets3() (string, string, error) { |
||||
return "", "", nil |
||||
} |
||||
|
||||
func (s *testService) CallMeBack(ctx context.Context, method string, args []interface{}) (interface{}, error) { |
||||
c, ok := ClientFromContext(ctx) |
||||
if !ok { |
||||
return nil, errors.New("no client") |
||||
} |
||||
var result interface{} |
||||
err := c.Call(&result, method, args...) |
||||
return result, err |
||||
} |
||||
|
||||
func (s *testService) CallMeBackLater(ctx context.Context, method string, args []interface{}) error { |
||||
c, ok := ClientFromContext(ctx) |
||||
if !ok { |
||||
return errors.New("no client") |
||||
} |
||||
go func() { |
||||
<-ctx.Done() |
||||
var result interface{} |
||||
c.Call(&result, method, args...) |
||||
}() |
||||
return nil |
||||
} |
||||
|
||||
func (s *testService) Subscription(ctx context.Context) (*Subscription, error) { |
||||
return nil, nil |
||||
} |
||||
|
||||
type notificationTestService struct { |
||||
unsubscribed chan string |
||||
gotHangSubscriptionReq chan struct{} |
||||
unblockHangSubscription chan struct{} |
||||
} |
||||
|
||||
func (s *notificationTestService) Echo(i int) int { |
||||
return i |
||||
} |
||||
|
||||
func (s *notificationTestService) Unsubscribe(subid string) { |
||||
if s.unsubscribed != nil { |
||||
s.unsubscribed <- subid |
||||
} |
||||
} |
||||
|
||||
func (s *notificationTestService) SomeSubscription(ctx context.Context, n, val int) (*Subscription, error) { |
||||
notifier, supported := NotifierFromContext(ctx) |
||||
if !supported { |
||||
return nil, ErrNotificationsUnsupported |
||||
} |
||||
|
||||
// By explicitly creating an subscription we make sure that the subscription id is send
|
||||
// back to the client before the first subscription.Notify is called. Otherwise the
|
||||
// events might be send before the response for the *_subscribe method.
|
||||
subscription := notifier.CreateSubscription() |
||||
go func() { |
||||
for i := 0; i < n; i++ { |
||||
if err := notifier.Notify(subscription.ID, val+i); err != nil { |
||||
return |
||||
} |
||||
} |
||||
select { |
||||
case <-notifier.Closed(): |
||||
case <-subscription.Err(): |
||||
} |
||||
if s.unsubscribed != nil { |
||||
s.unsubscribed <- string(subscription.ID) |
||||
} |
||||
}() |
||||
return subscription, nil |
||||
} |
||||
|
||||
// HangSubscription blocks on s.unblockHangSubscription before sending anything.
|
||||
func (s *notificationTestService) HangSubscription(ctx context.Context, val int) (*Subscription, error) { |
||||
notifier, supported := NotifierFromContext(ctx) |
||||
if !supported { |
||||
return nil, ErrNotificationsUnsupported |
||||
} |
||||
s.gotHangSubscriptionReq <- struct{}{} |
||||
<-s.unblockHangSubscription |
||||
subscription := notifier.CreateSubscription() |
||||
|
||||
go func() { |
||||
notifier.Notify(subscription.ID, val) |
||||
}() |
||||
return subscription, nil |
||||
} |
@ -1,226 +0,0 @@ |
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rpc |
||||
|
||||
import ( |
||||
"bufio" |
||||
"context" |
||||
crand "crypto/rand" |
||||
"encoding/binary" |
||||
"encoding/hex" |
||||
"math/rand" |
||||
"reflect" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
"unicode" |
||||
"unicode/utf8" |
||||
) |
||||
|
||||
var ( |
||||
subscriptionIDGenMu sync.Mutex |
||||
subscriptionIDGen = idGenerator() |
||||
) |
||||
|
||||
// Is this an exported - upper case - name?
|
||||
func isExported(name string) bool { |
||||
rune, _ := utf8.DecodeRuneInString(name) |
||||
return unicode.IsUpper(rune) |
||||
} |
||||
|
||||
// Is this type exported or a builtin?
|
||||
func isExportedOrBuiltinType(t reflect.Type) bool { |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
// PkgPath will be non-empty even for an exported type,
|
||||
// so we need to check the type name as well.
|
||||
return isExported(t.Name()) || t.PkgPath() == "" |
||||
} |
||||
|
||||
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem() |
||||
|
||||
// isContextType returns an indication if the given t is of context.Context or *context.Context type
|
||||
func isContextType(t reflect.Type) bool { |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
return t == contextType |
||||
} |
||||
|
||||
var errorType = reflect.TypeOf((*error)(nil)).Elem() |
||||
|
||||
// Implements this type the error interface
|
||||
func isErrorType(t reflect.Type) bool { |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
return t.Implements(errorType) |
||||
} |
||||
|
||||
var subscriptionType = reflect.TypeOf((*Subscription)(nil)).Elem() |
||||
|
||||
// isSubscriptionType returns an indication if the given t is of Subscription or *Subscription type
|
||||
func isSubscriptionType(t reflect.Type) bool { |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
return t == subscriptionType |
||||
} |
||||
|
||||
// isPubSub tests whether the given method has as as first argument a context.Context
|
||||
// and returns the pair (Subscription, error)
|
||||
func isPubSub(methodType reflect.Type) bool { |
||||
// numIn(0) is the receiver type
|
||||
if methodType.NumIn() < 2 || methodType.NumOut() != 2 { |
||||
return false |
||||
} |
||||
|
||||
return isContextType(methodType.In(1)) && |
||||
isSubscriptionType(methodType.Out(0)) && |
||||
isErrorType(methodType.Out(1)) |
||||
} |
||||
|
||||
// formatName will convert to first character to lower case
|
||||
func formatName(name string) string { |
||||
ret := []rune(name) |
||||
if len(ret) > 0 { |
||||
ret[0] = unicode.ToLower(ret[0]) |
||||
} |
||||
return string(ret) |
||||
} |
||||
|
||||
// suitableCallbacks iterates over the methods of the given type. It will determine if a method satisfies the criteria
|
||||
// for a RPC callback or a subscription callback and adds it to the collection of callbacks or subscriptions. See server
|
||||
// documentation for a summary of these criteria.
|
||||
func suitableCallbacks(rcvr reflect.Value, typ reflect.Type) (callbacks, subscriptions) { |
||||
callbacks := make(callbacks) |
||||
subscriptions := make(subscriptions) |
||||
|
||||
METHODS: |
||||
for m := 0; m < typ.NumMethod(); m++ { |
||||
method := typ.Method(m) |
||||
mtype := method.Type |
||||
mname := formatName(method.Name) |
||||
if method.PkgPath != "" { // method must be exported
|
||||
continue |
||||
} |
||||
|
||||
var h callback |
||||
h.isSubscribe = isPubSub(mtype) |
||||
h.rcvr = rcvr |
||||
h.method = method |
||||
h.errPos = -1 |
||||
|
||||
firstArg := 1 |
||||
numIn := mtype.NumIn() |
||||
if numIn >= 2 && mtype.In(1) == contextType { |
||||
h.hasCtx = true |
||||
firstArg = 2 |
||||
} |
||||
|
||||
if h.isSubscribe { |
||||
h.argTypes = make([]reflect.Type, numIn-firstArg) // skip rcvr type
|
||||
for i := firstArg; i < numIn; i++ { |
||||
argType := mtype.In(i) |
||||
if isExportedOrBuiltinType(argType) { |
||||
h.argTypes[i-firstArg] = argType |
||||
} else { |
||||
continue METHODS |
||||
} |
||||
} |
||||
|
||||
subscriptions[mname] = &h |
||||
continue METHODS |
||||
} |
||||
|
||||
// determine method arguments, ignore first arg since it's the receiver type
|
||||
// Arguments must be exported or builtin types
|
||||
h.argTypes = make([]reflect.Type, numIn-firstArg) |
||||
for i := firstArg; i < numIn; i++ { |
||||
argType := mtype.In(i) |
||||
if !isExportedOrBuiltinType(argType) { |
||||
continue METHODS |
||||
} |
||||
h.argTypes[i-firstArg] = argType |
||||
} |
||||
|
||||
// check that all returned values are exported or builtin types
|
||||
for i := 0; i < mtype.NumOut(); i++ { |
||||
if !isExportedOrBuiltinType(mtype.Out(i)) { |
||||
continue METHODS |
||||
} |
||||
} |
||||
|
||||
// when a method returns an error it must be the last returned value
|
||||
h.errPos = -1 |
||||
for i := 0; i < mtype.NumOut(); i++ { |
||||
if isErrorType(mtype.Out(i)) { |
||||
h.errPos = i |
||||
break |
||||
} |
||||
} |
||||
|
||||
if h.errPos >= 0 && h.errPos != mtype.NumOut()-1 { |
||||
continue METHODS |
||||
} |
||||
|
||||
switch mtype.NumOut() { |
||||
case 0, 1, 2: |
||||
if mtype.NumOut() == 2 && h.errPos == -1 { // method must one return value and 1 error
|
||||
continue METHODS |
||||
} |
||||
callbacks[mname] = &h |
||||
} |
||||
} |
||||
|
||||
return callbacks, subscriptions |
||||
} |
||||
|
||||
// idGenerator helper utility that generates a (pseudo) random sequence of
|
||||
// bytes that are used to generate identifiers.
|
||||
func idGenerator() *rand.Rand { |
||||
if seed, err := binary.ReadVarint(bufio.NewReader(crand.Reader)); err == nil { |
||||
return rand.New(rand.NewSource(seed)) |
||||
} |
||||
return rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) |
||||
} |
||||
|
||||
// NewID generates a identifier that can be used as an identifier in the RPC interface.
|
||||
// e.g. filter and subscription identifier.
|
||||
func NewID() ID { |
||||
subscriptionIDGenMu.Lock() |
||||
defer subscriptionIDGenMu.Unlock() |
||||
|
||||
id := make([]byte, 16) |
||||
for i := 0; i < len(id); i += 7 { |
||||
val := subscriptionIDGen.Int63() |
||||
for j := 0; i+j < len(id) && j < 7; j++ { |
||||
id[i+j] = byte(val) |
||||
val >>= 8 |
||||
} |
||||
} |
||||
|
||||
rpcId := hex.EncodeToString(id) |
||||
// rpc ID's are RPC quantities, no leading zero's and 0 is 0x0
|
||||
rpcId = strings.TrimLeft(rpcId, "0") |
||||
if rpcId == "" { |
||||
rpcId = "0" |
||||
} |
||||
|
||||
return ID("0x" + rpcId) |
||||
} |
@ -1,43 +0,0 @@ |
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rpc |
||||
|
||||
import ( |
||||
"strings" |
||||
"testing" |
||||
) |
||||
|
||||
func TestNewID(t *testing.T) { |
||||
hexchars := "0123456789ABCDEFabcdef" |
||||
for i := 0; i < 100; i++ { |
||||
id := string(NewID()) |
||||
if !strings.HasPrefix(id, "0x") { |
||||
t.Fatalf("invalid ID prefix, want '0x...', got %s", id) |
||||
} |
||||
|
||||
id = id[2:] |
||||
if len(id) == 0 || len(id) > 32 { |
||||
t.Fatalf("invalid ID length, want len(id) > 0 && len(id) <= 32), got %d", len(id)) |
||||
} |
||||
|
||||
for i := 0; i < len(id); i++ { |
||||
if strings.IndexByte(hexchars, id[i]) == -1 { |
||||
t.Fatalf("unexpected byte, want any valid hex char, got %c", id[i]) |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue