manual geth v1.11.1 merge
This commit is contained in:
+39
-11
@@ -22,6 +22,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
@@ -100,7 +101,7 @@ type Client struct {
|
||||
reqTimeout chan *requestOp // removes response IDs when call timeout expires
|
||||
}
|
||||
|
||||
type reconnectFunc func(ctx context.Context) (ServerCodec, error)
|
||||
type reconnectFunc func(context.Context) (ServerCodec, error)
|
||||
|
||||
type clientContextKey struct{}
|
||||
|
||||
@@ -154,14 +155,16 @@ func (op *requestOp) wait(ctx context.Context, c *Client) (*jsonrpcMessage, erro
|
||||
//
|
||||
// The currently supported URL schemes are "http", "https", "ws" and "wss". If rawurl is a
|
||||
// file name with no URL scheme, a local socket connection is established using UNIX
|
||||
// domain sockets on supported platforms and named pipes on Windows. If you want to
|
||||
// configure transport options, use DialHTTP, DialWebsocket or DialIPC instead.
|
||||
// domain sockets on supported platforms and named pipes on Windows.
|
||||
//
|
||||
// If you want to further configure the transport, use DialOptions instead of this
|
||||
// function.
|
||||
//
|
||||
// For websocket connections, the origin is set to the local host name.
|
||||
//
|
||||
// The client reconnects automatically if the connection is lost.
|
||||
// The client reconnects automatically when the connection is lost.
|
||||
func Dial(rawurl string) (*Client, error) {
|
||||
return DialContext(context.Background(), rawurl)
|
||||
return DialOptions(context.Background(), rawurl)
|
||||
}
|
||||
|
||||
// DialContext creates a new RPC client, just like Dial.
|
||||
@@ -169,22 +172,46 @@ func Dial(rawurl string) (*Client, error) {
|
||||
// The context is used to cancel or time out the initial connection establishment. It does
|
||||
// not affect subsequent interactions with the client.
|
||||
func DialContext(ctx context.Context, rawurl string) (*Client, error) {
|
||||
return DialOptions(ctx, rawurl)
|
||||
}
|
||||
|
||||
// DialOptions creates a new RPC client for the given URL. You can supply any of the
|
||||
// pre-defined client options to configure the underlying transport.
|
||||
//
|
||||
// The context is used to cancel or time out the initial connection establishment. It does
|
||||
// not affect subsequent interactions with the client.
|
||||
//
|
||||
// The client reconnects automatically when the connection is lost.
|
||||
func DialOptions(ctx context.Context, rawurl string, options ...ClientOption) (*Client, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := new(clientConfig)
|
||||
for _, opt := range options {
|
||||
opt.applyOption(cfg)
|
||||
}
|
||||
|
||||
var reconnect reconnectFunc
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
return DialHTTP(rawurl)
|
||||
reconnect = newClientTransportHTTP(rawurl, cfg)
|
||||
case "ws", "wss":
|
||||
return DialWebsocket(ctx, rawurl, "")
|
||||
rc, err := newClientTransportWS(rawurl, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reconnect = rc
|
||||
case "stdio":
|
||||
return DialStdIO(ctx)
|
||||
reconnect = newClientTransportIO(os.Stdin, os.Stdout)
|
||||
case "":
|
||||
return DialIPC(ctx, rawurl)
|
||||
reconnect = newClientTransportIPC(rawurl)
|
||||
default:
|
||||
return nil, fmt.Errorf("no known transport for URL scheme %q", u.Scheme)
|
||||
}
|
||||
|
||||
return newClient(ctx, reconnect)
|
||||
}
|
||||
|
||||
// ClientFromContext retrieves the client from the context, if any. This can be used to perform
|
||||
@@ -500,7 +527,7 @@ func (c *Client) write(ctx context.Context, msg interface{}, retry bool) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := c.writeConn.writeJSON(ctx, msg)
|
||||
err := c.writeConn.writeJSON(ctx, msg, false)
|
||||
if err != nil {
|
||||
c.writeConn = nil
|
||||
if !retry {
|
||||
@@ -633,7 +660,8 @@ func (c *Client) read(codec ServerCodec) {
|
||||
for {
|
||||
msgs, batch, err := codec.readBatch()
|
||||
if _, ok := err.(*json.SyntaxError); ok {
|
||||
codec.writeJSON(context.Background(), errorMessage(&parseError{err.Error()}))
|
||||
msg := errorMessage(&parseError{err.Error()})
|
||||
codec.writeJSON(context.Background(), msg, true)
|
||||
}
|
||||
if err != nil {
|
||||
c.readErr <- err
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright 2022 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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ClientOption is a configuration option for the RPC client.
|
||||
type ClientOption interface {
|
||||
applyOption(*clientConfig)
|
||||
}
|
||||
|
||||
type clientConfig struct {
|
||||
httpClient *http.Client
|
||||
httpHeaders http.Header
|
||||
httpAuth HTTPAuth
|
||||
|
||||
wsDialer *websocket.Dialer
|
||||
}
|
||||
|
||||
func (cfg *clientConfig) initHeaders() {
|
||||
if cfg.httpHeaders == nil {
|
||||
cfg.httpHeaders = make(http.Header)
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *clientConfig) setHeader(key, value string) {
|
||||
cfg.initHeaders()
|
||||
cfg.httpHeaders.Set(key, value)
|
||||
}
|
||||
|
||||
type optionFunc func(*clientConfig)
|
||||
|
||||
func (fn optionFunc) applyOption(opt *clientConfig) {
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
// WithWebsocketDialer configures the websocket.Dialer used by the RPC client.
|
||||
func WithWebsocketDialer(dialer websocket.Dialer) ClientOption {
|
||||
return optionFunc(func(cfg *clientConfig) {
|
||||
cfg.wsDialer = &dialer
|
||||
})
|
||||
}
|
||||
|
||||
// WithHeader configures HTTP headers set by the RPC client. Headers set using this option
|
||||
// will be used for both HTTP and WebSocket connections.
|
||||
func WithHeader(key, value string) ClientOption {
|
||||
return optionFunc(func(cfg *clientConfig) {
|
||||
cfg.initHeaders()
|
||||
cfg.httpHeaders.Set(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
// WithHeaders configures HTTP headers set by the RPC client. Headers set using this
|
||||
// option will be used for both HTTP and WebSocket connections.
|
||||
func WithHeaders(headers http.Header) ClientOption {
|
||||
return optionFunc(func(cfg *clientConfig) {
|
||||
cfg.initHeaders()
|
||||
for k, vs := range headers {
|
||||
cfg.httpHeaders[k] = vs
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithHTTPClient configures the http.Client used by the RPC client.
|
||||
func WithHTTPClient(c *http.Client) ClientOption {
|
||||
return optionFunc(func(cfg *clientConfig) {
|
||||
cfg.httpClient = c
|
||||
})
|
||||
}
|
||||
|
||||
// WithHTTPAuth configures HTTP request authentication. The given provider will be called
|
||||
// whenever a request is made. Note that only one authentication provider can be active at
|
||||
// any time.
|
||||
func WithHTTPAuth(a HTTPAuth) ClientOption {
|
||||
if a == nil {
|
||||
panic("nil auth")
|
||||
}
|
||||
return optionFunc(func(cfg *clientConfig) {
|
||||
cfg.httpAuth = a
|
||||
})
|
||||
}
|
||||
|
||||
// A HTTPAuth function is called by the client whenever a HTTP request is sent.
|
||||
// The function must be safe for concurrent use.
|
||||
//
|
||||
// Usually, HTTPAuth functions will call h.Set("authorization", "...") to add
|
||||
// auth information to the request.
|
||||
type HTTPAuth func(h http.Header) error
|
||||
@@ -0,0 +1,25 @@
|
||||
package rpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// This example configures a HTTP-based RPC client with two options - one setting the
|
||||
// overall request timeout, the other adding a custom HTTP header to all requests.
|
||||
func ExampleDialOptions() {
|
||||
tokenHeader := rpc.WithHeader("x-token", "foo")
|
||||
httpClient := rpc.WithHTTPClient(&http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := rpc.DialOptions(ctx, "http://rpc.example.com", httpClient, tokenHeader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
+6
-2
@@ -83,11 +83,15 @@ func TestClientErrorData(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check code.
|
||||
// The method handler returns an error value which implements the rpc.Error
|
||||
// interface, i.e. it has a custom error code. The server returns this error code.
|
||||
expectedCode := testError{}.ErrorCode()
|
||||
if e, ok := err.(Error); !ok {
|
||||
t.Fatalf("client did not return rpc.Error, got %#v", e)
|
||||
} else if e.ErrorCode() != (testError{}.ErrorCode()) {
|
||||
t.Fatalf("wrong error code %d, want %d", e.ErrorCode(), testError{}.ErrorCode())
|
||||
} else if e.ErrorCode() != expectedCode {
|
||||
t.Fatalf("wrong error code %d, want %d", e.ErrorCode(), expectedCode)
|
||||
}
|
||||
|
||||
// Check data.
|
||||
if e, ok := err.(DataError); !ok {
|
||||
t.Fatalf("client did not return rpc.DataError, got %#v", e)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2022 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"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type mdHeaderKey struct{}
|
||||
|
||||
// NewContextWithHeaders wraps the given context, adding HTTP headers. These headers will
|
||||
// be applied by Client when making a request using the returned context.
|
||||
func NewContextWithHeaders(ctx context.Context, h http.Header) context.Context {
|
||||
if len(h) == 0 {
|
||||
// This check ensures the header map set in context will never be nil.
|
||||
return ctx
|
||||
}
|
||||
|
||||
var ctxh http.Header
|
||||
prev, ok := ctx.Value(mdHeaderKey{}).(http.Header)
|
||||
if ok {
|
||||
ctxh = setHeaders(prev.Clone(), h)
|
||||
} else {
|
||||
ctxh = h.Clone()
|
||||
}
|
||||
return context.WithValue(ctx, mdHeaderKey{}, ctxh)
|
||||
}
|
||||
|
||||
// headersFromContext is used to extract http.Header from context.
|
||||
func headersFromContext(ctx context.Context) http.Header {
|
||||
source, _ := ctx.Value(mdHeaderKey{}).(http.Header)
|
||||
return source
|
||||
}
|
||||
|
||||
// setHeaders sets all headers from src in dst.
|
||||
func setHeaders(dst http.Header, src http.Header) http.Header {
|
||||
for key, values := range src {
|
||||
dst[http.CanonicalHeaderKey(key)] = values
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+28
-29
@@ -15,7 +15,6 @@
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/*
|
||||
|
||||
Package rpc implements bi-directional JSON-RPC 2.0 on multiple transports.
|
||||
|
||||
It provides access to the exported methods of an object across a network or other I/O
|
||||
@@ -23,16 +22,16 @@ connection. After creating a server or client instance, objects can be registere
|
||||
them visible as 'services'. Exported methods that follow specific conventions can be
|
||||
called remotely. It also has support for the publish/subscribe pattern.
|
||||
|
||||
RPC Methods
|
||||
# RPC Methods
|
||||
|
||||
Methods that satisfy the following criteria are made available for remote access:
|
||||
|
||||
- method must be exported
|
||||
- method returns 0, 1 (response or error) or 2 (response and error) values
|
||||
- method must be exported
|
||||
- method returns 0, 1 (response or error) or 2 (response and error) values
|
||||
|
||||
An example method:
|
||||
|
||||
func (s *CalcService) Add(a, b int) (int, error)
|
||||
func (s *CalcService) Add(a, b int) (int, error)
|
||||
|
||||
When the returned error isn't nil the returned integer is ignored and the error is sent
|
||||
back to the client. Otherwise the returned integer is sent back to the client.
|
||||
@@ -41,7 +40,7 @@ Optional arguments are supported by accepting pointer values as arguments. E.g.
|
||||
to do the addition in an optional finite field we can accept a mod argument as pointer
|
||||
value.
|
||||
|
||||
func (s *CalcService) Add(a, b int, mod *int) (int, error)
|
||||
func (s *CalcService) Add(a, b int, mod *int) (int, error)
|
||||
|
||||
This RPC method can be called with 2 integers and a null value as third argument. In that
|
||||
case the mod argument will be nil. Or it can be called with 3 integers, in that case mod
|
||||
@@ -56,40 +55,40 @@ to the client out of order.
|
||||
|
||||
An example server which uses the JSON codec:
|
||||
|
||||
type CalculatorService struct {}
|
||||
type CalculatorService struct {}
|
||||
|
||||
func (s *CalculatorService) Add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
func (s *CalculatorService) Add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
func (s *CalculatorService) Div(a, b int) (int, error) {
|
||||
if b == 0 {
|
||||
return 0, errors.New("divide by zero")
|
||||
}
|
||||
return a/b, nil
|
||||
}
|
||||
func (s *CalculatorService) Div(a, b int) (int, error) {
|
||||
if b == 0 {
|
||||
return 0, errors.New("divide by zero")
|
||||
}
|
||||
return a/b, nil
|
||||
}
|
||||
|
||||
calculator := new(CalculatorService)
|
||||
server := NewServer()
|
||||
server.RegisterName("calculator", calculator)
|
||||
l, _ := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "/tmp/calculator.sock"})
|
||||
server.ServeListener(l)
|
||||
calculator := new(CalculatorService)
|
||||
server := NewServer()
|
||||
server.RegisterName("calculator", calculator)
|
||||
l, _ := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "/tmp/calculator.sock"})
|
||||
server.ServeListener(l)
|
||||
|
||||
Subscriptions
|
||||
# Subscriptions
|
||||
|
||||
The package also supports the publish subscribe pattern through the use of subscriptions.
|
||||
A method that is considered eligible for notifications must satisfy the following
|
||||
criteria:
|
||||
|
||||
- method must be exported
|
||||
- first method argument type must be context.Context
|
||||
- method must have return types (rpc.Subscription, error)
|
||||
- method must be exported
|
||||
- first method argument type must be context.Context
|
||||
- method must have return types (rpc.Subscription, error)
|
||||
|
||||
An example method:
|
||||
|
||||
func (s *BlockChainService) NewBlocks(ctx context.Context) (rpc.Subscription, error) {
|
||||
...
|
||||
}
|
||||
func (s *BlockChainService) NewBlocks(ctx context.Context) (rpc.Subscription, error) {
|
||||
...
|
||||
}
|
||||
|
||||
When the service containing the subscription method is registered to the server, for
|
||||
example under the "blockchain" namespace, a subscription is created by calling the
|
||||
@@ -101,7 +100,7 @@ the client and server. The server will close the connection for any write error.
|
||||
|
||||
For more information about subscriptions, see https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB.
|
||||
|
||||
Reverse Calls
|
||||
# Reverse Calls
|
||||
|
||||
In any method handler, an instance of rpc.Client can be accessed through the
|
||||
ClientFromContext method. Using this client instance, server-to-client method calls can be
|
||||
|
||||
+22
-1
@@ -54,9 +54,20 @@ var (
|
||||
_ Error = new(invalidRequestError)
|
||||
_ Error = new(invalidMessageError)
|
||||
_ Error = new(invalidParamsError)
|
||||
_ Error = new(internalServerError)
|
||||
)
|
||||
|
||||
const defaultErrorCode = -32000
|
||||
const (
|
||||
errcodeDefault = -32000
|
||||
errcodeNotificationsUnsupported = -32001
|
||||
errcodeTimeout = -32002
|
||||
errcodePanic = -32603
|
||||
errcodeMarshalError = -32603
|
||||
)
|
||||
|
||||
const (
|
||||
errMsgTimeout = "request timed out"
|
||||
)
|
||||
|
||||
type methodNotFoundError struct{ method string }
|
||||
|
||||
@@ -101,3 +112,13 @@ type invalidParamsError struct{ message string }
|
||||
func (e *invalidParamsError) ErrorCode() int { return -32602 }
|
||||
|
||||
func (e *invalidParamsError) Error() string { return e.message }
|
||||
|
||||
// internalServerError is used for server errors during request processing.
|
||||
type internalServerError struct {
|
||||
code int
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *internalServerError) ErrorCode() int { return e.code }
|
||||
|
||||
func (e *internalServerError) Error() string { return e.message }
|
||||
|
||||
+143
-19
@@ -34,21 +34,20 @@ import (
|
||||
//
|
||||
// The entry points for incoming messages are:
|
||||
//
|
||||
// h.handleMsg(message)
|
||||
// h.handleBatch(message)
|
||||
// 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)
|
||||
// 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.
|
||||
// }
|
||||
//
|
||||
// if err := op.wait(...); err != nil {
|
||||
// h.removeRequestOp(op) // timeout, etc.
|
||||
// }
|
||||
type handler struct {
|
||||
reg *serviceRegistry
|
||||
unsubscribeCb *callback
|
||||
@@ -92,12 +91,83 @@ func newHandler(connCtx context.Context, conn jsonWriter, idgen func() ID, reg *
|
||||
return h
|
||||
}
|
||||
|
||||
// batchCallBuffer manages in progress call messages and their responses during a batch
|
||||
// call. Calls need to be synchronized between the processing and timeout-triggering
|
||||
// goroutines.
|
||||
type batchCallBuffer struct {
|
||||
mutex sync.Mutex
|
||||
calls []*jsonrpcMessage
|
||||
resp []*jsonrpcMessage
|
||||
wrote bool
|
||||
}
|
||||
|
||||
// nextCall returns the next unprocessed message.
|
||||
func (b *batchCallBuffer) nextCall() *jsonrpcMessage {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
if len(b.calls) == 0 {
|
||||
return nil
|
||||
}
|
||||
// The popping happens in `pushAnswer`. The in progress call is kept
|
||||
// so we can return an error for it in case of timeout.
|
||||
msg := b.calls[0]
|
||||
return msg
|
||||
}
|
||||
|
||||
// pushResponse adds the response to last call returned by nextCall.
|
||||
func (b *batchCallBuffer) pushResponse(answer *jsonrpcMessage) {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
if answer != nil {
|
||||
b.resp = append(b.resp, answer)
|
||||
}
|
||||
b.calls = b.calls[1:]
|
||||
}
|
||||
|
||||
// write sends the responses.
|
||||
func (b *batchCallBuffer) write(ctx context.Context, conn jsonWriter) {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
b.doWrite(ctx, conn, false)
|
||||
}
|
||||
|
||||
// timeout sends the responses added so far. For the remaining unanswered call
|
||||
// messages, it sends a timeout error response.
|
||||
func (b *batchCallBuffer) timeout(ctx context.Context, conn jsonWriter) {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
for _, msg := range b.calls {
|
||||
if !msg.isNotification() {
|
||||
resp := msg.errorResponse(&internalServerError{errcodeTimeout, errMsgTimeout})
|
||||
b.resp = append(b.resp, resp)
|
||||
}
|
||||
}
|
||||
b.doWrite(ctx, conn, true)
|
||||
}
|
||||
|
||||
// doWrite actually writes the response.
|
||||
// This assumes b.mutex is held.
|
||||
func (b *batchCallBuffer) doWrite(ctx context.Context, conn jsonWriter, isErrorResponse bool) {
|
||||
if b.wrote {
|
||||
return
|
||||
}
|
||||
b.wrote = true // can only write once
|
||||
if len(b.resp) > 0 {
|
||||
conn.writeJSON(ctx, b.resp, isErrorResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.writeJSON(cp.ctx, errorMessage(&invalidRequestError{"empty batch"}))
|
||||
resp := errorMessage(&invalidRequestError{"empty batch"})
|
||||
h.conn.writeJSON(cp.ctx, resp, true)
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -114,16 +184,42 @@ func (h *handler) handleBatch(msgs []*jsonrpcMessage) {
|
||||
}
|
||||
// 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)
|
||||
var (
|
||||
timer *time.Timer
|
||||
cancel context.CancelFunc
|
||||
callBuffer = &batchCallBuffer{calls: calls, resp: make([]*jsonrpcMessage, 0, len(calls))}
|
||||
)
|
||||
|
||||
cp.ctx, cancel = context.WithCancel(cp.ctx)
|
||||
defer cancel()
|
||||
|
||||
// Cancel the request context after timeout and send an error response. Since the
|
||||
// currently-running method might not return immediately on timeout, we must wait
|
||||
// for the timeout concurrently with processing the request.
|
||||
if timeout, ok := ContextRequestTimeout(cp.ctx); ok {
|
||||
timer = time.AfterFunc(timeout, func() {
|
||||
cancel()
|
||||
callBuffer.timeout(cp.ctx, h.conn)
|
||||
})
|
||||
}
|
||||
|
||||
for {
|
||||
// No need to handle rest of calls if timed out.
|
||||
if cp.ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
msg := callBuffer.nextCall()
|
||||
if msg == nil {
|
||||
break
|
||||
}
|
||||
resp := h.handleCallMsg(cp, msg)
|
||||
callBuffer.pushResponse(resp)
|
||||
}
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
callBuffer.write(cp.ctx, h.conn)
|
||||
h.addSubscriptions(cp.notifiers)
|
||||
if len(answers) > 0 {
|
||||
h.conn.writeJSON(cp.ctx, answers)
|
||||
}
|
||||
for _, n := range cp.notifiers {
|
||||
n.activate()
|
||||
}
|
||||
@@ -136,10 +232,36 @@ func (h *handler) handleMsg(msg *jsonrpcMessage) {
|
||||
return
|
||||
}
|
||||
h.startCallProc(func(cp *callProc) {
|
||||
var (
|
||||
responded sync.Once
|
||||
timer *time.Timer
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
cp.ctx, cancel = context.WithCancel(cp.ctx)
|
||||
defer cancel()
|
||||
|
||||
// Cancel the request context after timeout and send an error response. Since the
|
||||
// running method might not return immediately on timeout, we must wait for the
|
||||
// timeout concurrently with processing the request.
|
||||
if timeout, ok := ContextRequestTimeout(cp.ctx); ok {
|
||||
timer = time.AfterFunc(timeout, func() {
|
||||
cancel()
|
||||
responded.Do(func() {
|
||||
resp := msg.errorResponse(&internalServerError{errcodeTimeout, errMsgTimeout})
|
||||
h.conn.writeJSON(cp.ctx, resp, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
answer := h.handleCallMsg(cp, msg)
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
h.addSubscriptions(cp.notifiers)
|
||||
if answer != nil {
|
||||
h.conn.writeJSON(cp.ctx, answer)
|
||||
responded.Do(func() {
|
||||
h.conn.writeJSON(cp.ctx, answer, false)
|
||||
})
|
||||
}
|
||||
for _, n := range cp.notifiers {
|
||||
n.activate()
|
||||
@@ -339,7 +461,6 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage
|
||||
//begin PluGeth code injection
|
||||
start := time.Now()
|
||||
answer := h.runMethod(cp.ctx, msg, callb, args)
|
||||
|
||||
// Collect the statistics for RPC calls if metrics is enabled.
|
||||
// We only care about pure rpc call. Filter out subscription.
|
||||
if callb != h.unsubscribeCb {
|
||||
@@ -358,7 +479,10 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage
|
||||
// handleSubscribe processes *_subscribe method calls.
|
||||
func (h *handler) handleSubscribe(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
|
||||
if !h.allowSubscribe {
|
||||
return msg.errorResponse(ErrNotificationsUnsupported)
|
||||
return msg.errorResponse(&internalServerError{
|
||||
code: errcodeNotificationsUnsupported,
|
||||
message: ErrNotificationsUnsupported.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Subscription method name is first argument.
|
||||
|
||||
+115
-19
@@ -23,9 +23,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -45,13 +47,14 @@ type httpConn struct {
|
||||
closeCh chan interface{}
|
||||
mu sync.Mutex // protects headers
|
||||
headers http.Header
|
||||
auth HTTPAuth
|
||||
}
|
||||
|
||||
// httpConn implements ServerCodec, but it is treated specially by Client
|
||||
// and some methods don't work. The panic() stubs here exist to ensure
|
||||
// this special treatment is correct.
|
||||
|
||||
func (hc *httpConn) writeJSON(context.Context, interface{}) error {
|
||||
func (hc *httpConn) writeJSON(context.Context, interface{}, bool) error {
|
||||
panic("writeJSON called on httpConn")
|
||||
}
|
||||
|
||||
@@ -117,8 +120,15 @@ var DefaultHTTPTimeouts = HTTPTimeouts{
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
// DialHTTP creates a new RPC client that connects to an RPC server over HTTP.
|
||||
func DialHTTP(endpoint string) (*Client, error) {
|
||||
return DialHTTPWithClient(endpoint, new(http.Client))
|
||||
}
|
||||
|
||||
// DialHTTPWithClient creates a new RPC client that connects to an RPC server over HTTP
|
||||
// using the provided HTTP Client.
|
||||
//
|
||||
// Deprecated: use DialOptions and the WithHTTPClient option.
|
||||
func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) {
|
||||
// Sanity check URL so we don't end up with a client that will fail every request.
|
||||
_, err := url.Parse(endpoint)
|
||||
@@ -126,24 +136,36 @@ func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
initctx := context.Background()
|
||||
headers := make(http.Header, 2)
|
||||
headers.Set("accept", contentType)
|
||||
headers.Set("content-type", contentType)
|
||||
return newClient(initctx, func(context.Context) (ServerCodec, error) {
|
||||
hc := &httpConn{
|
||||
client: client,
|
||||
headers: headers,
|
||||
url: endpoint,
|
||||
closeCh: make(chan interface{}),
|
||||
}
|
||||
return hc, nil
|
||||
})
|
||||
var cfg clientConfig
|
||||
cfg.httpClient = client
|
||||
fn := newClientTransportHTTP(endpoint, &cfg)
|
||||
return newClient(context.Background(), fn)
|
||||
}
|
||||
|
||||
// DialHTTP creates a new RPC client that connects to an RPC server over HTTP.
|
||||
func DialHTTP(endpoint string) (*Client, error) {
|
||||
return DialHTTPWithClient(endpoint, new(http.Client))
|
||||
func newClientTransportHTTP(endpoint string, cfg *clientConfig) reconnectFunc {
|
||||
headers := make(http.Header, 2+len(cfg.httpHeaders))
|
||||
headers.Set("accept", contentType)
|
||||
headers.Set("content-type", contentType)
|
||||
for key, values := range cfg.httpHeaders {
|
||||
headers[key] = values
|
||||
}
|
||||
|
||||
client := cfg.httpClient
|
||||
if client == nil {
|
||||
client = new(http.Client)
|
||||
}
|
||||
|
||||
hc := &httpConn{
|
||||
client: client,
|
||||
headers: headers,
|
||||
url: endpoint,
|
||||
auth: cfg.httpAuth,
|
||||
closeCh: make(chan interface{}),
|
||||
}
|
||||
|
||||
return func(ctx context.Context) (ServerCodec, error) {
|
||||
return hc, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) error {
|
||||
@@ -187,7 +209,7 @@ func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadClos
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", hc.url, io.NopCloser(bytes.NewReader(body)))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, hc.url, io.NopCloser(bytes.NewReader(body)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -198,6 +220,13 @@ func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadClos
|
||||
hc.mu.Lock()
|
||||
req.Header = hc.headers.Clone()
|
||||
hc.mu.Unlock()
|
||||
setHeaders(req.Header, headersFromContext(ctx))
|
||||
|
||||
if hc.auth != nil {
|
||||
if err := hc.auth(req.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// do request
|
||||
resp, err := hc.client.Do(req)
|
||||
@@ -230,7 +259,42 @@ type httpServerConn struct {
|
||||
func newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec {
|
||||
body := io.LimitReader(r.Body, maxRequestContentLength)
|
||||
conn := &httpServerConn{Reader: body, Writer: w, r: r}
|
||||
return NewCodec(conn)
|
||||
|
||||
encoder := func(v any, isErrorResponse bool) error {
|
||||
if !isErrorResponse {
|
||||
return json.NewEncoder(conn).Encode(v)
|
||||
}
|
||||
|
||||
// It's an error response and requires special treatment.
|
||||
//
|
||||
// In case of a timeout error, the response must be written before the HTTP
|
||||
// server's write timeout occurs. So we need to flush the response. The
|
||||
// Content-Length header also needs to be set to ensure the client knows
|
||||
// when it has the full response.
|
||||
encdata, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("content-length", strconv.Itoa(len(encdata)))
|
||||
|
||||
// If this request is wrapped in a handler that might remove Content-Length (such
|
||||
// as the automatic gzip we do in package node), we need to ensure the HTTP server
|
||||
// doesn't perform chunked encoding. In case WriteTimeout is reached, the chunked
|
||||
// encoding might not be finished correctly, and some clients do not like it when
|
||||
// the final chunk is missing.
|
||||
w.Header().Set("transfer-encoding", "identity")
|
||||
|
||||
_, err = w.Write(encdata)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(conn)
|
||||
dec.UseNumber()
|
||||
|
||||
return NewFuncCodec(conn, encoder, dec.Decode)
|
||||
}
|
||||
|
||||
// Close does nothing and always returns nil.
|
||||
@@ -300,3 +364,35 @@ func validateRequest(r *http.Request) (int, error) {
|
||||
err := fmt.Errorf("invalid content type, only %s is supported", contentType)
|
||||
return http.StatusUnsupportedMediaType, err
|
||||
}
|
||||
|
||||
// ContextRequestTimeout returns the request timeout derived from the given context.
|
||||
func ContextRequestTimeout(ctx context.Context) (time.Duration, bool) {
|
||||
timeout := time.Duration(math.MaxInt64)
|
||||
hasTimeout := false
|
||||
setTimeout := func(d time.Duration) {
|
||||
if d < timeout {
|
||||
timeout = d
|
||||
hasTimeout = true
|
||||
}
|
||||
}
|
||||
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
setTimeout(time.Until(deadline))
|
||||
}
|
||||
|
||||
// If the context is an HTTP request context, use the server's WriteTimeout.
|
||||
httpSrv, ok := ctx.Value(http.ServerContextKey).(*http.Server)
|
||||
if ok && httpSrv.WriteTimeout > 0 {
|
||||
wt := httpSrv.WriteTimeout
|
||||
// When a write timeout is configured, we need to send the response message before
|
||||
// the HTTP server cuts connection. So our internal timeout must be earlier than
|
||||
// the server's true timeout.
|
||||
//
|
||||
// Note: Timeouts are sanitized to be a minimum of 1 second.
|
||||
// Also see issue: https://github.com/golang/go/issues/47229
|
||||
wt -= 100 * time.Millisecond
|
||||
setTimeout(wt)
|
||||
}
|
||||
|
||||
return timeout, hasTimeout
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -198,3 +200,43 @@ func TestHTTPPeerInfo(t *testing.T) {
|
||||
t.Errorf("wrong HTTP.Origin %q", info.HTTP.UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewContextWithHeaders(t *testing.T) {
|
||||
expectedHeaders := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
for i := 0; i < expectedHeaders; i++ {
|
||||
key, want := fmt.Sprintf("key-%d", i), fmt.Sprintf("val-%d", i)
|
||||
if have := request.Header.Get(key); have != want {
|
||||
t.Errorf("wrong request headers for %s, want: %s, have: %s", key, want, have)
|
||||
}
|
||||
}
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := Dial(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %s", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
newHdr := func(k, v string) http.Header {
|
||||
header := http.Header{}
|
||||
header.Set(k, v)
|
||||
return header
|
||||
}
|
||||
ctx1 := NewContextWithHeaders(context.Background(), newHdr("key-0", "val-0"))
|
||||
ctx2 := NewContextWithHeaders(ctx1, newHdr("key-1", "val-1"))
|
||||
ctx3 := NewContextWithHeaders(ctx2, newHdr("key-2", "val-2"))
|
||||
|
||||
expectedHeaders = 3
|
||||
if err := client.CallContext(ctx3, nil, "test"); err != ErrNoResult {
|
||||
t.Error("call failed", err)
|
||||
}
|
||||
|
||||
expectedHeaders = 2
|
||||
if err := client.CallContext(ctx2, nil, "test"); err != ErrNoResult {
|
||||
t.Error("call failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -46,11 +46,15 @@ func (s *Server) ServeListener(l net.Listener) error {
|
||||
// The context is used for the initial connection establishment. It does not
|
||||
// affect subsequent interactions with the client.
|
||||
func DialIPC(ctx context.Context, endpoint string) (*Client, error) {
|
||||
return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
|
||||
return newClient(ctx, newClientTransportIPC(endpoint))
|
||||
}
|
||||
|
||||
func newClientTransportIPC(endpoint string) reconnectFunc {
|
||||
return func(ctx context.Context) (ServerCodec, error) {
|
||||
conn, err := newIPCConnection(ctx, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewCodec(conn), err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -31,8 +31,9 @@ import (
|
||||
|
||||
// ipcListen will create a Unix socket on the given endpoint.
|
||||
func ipcListen(endpoint string) (net.Listener, error) {
|
||||
if len(endpoint) > int(max_path_size) {
|
||||
log.Warn(fmt.Sprintf("The ipc endpoint is longer than %d characters. ", max_path_size),
|
||||
// account for null-terminator too
|
||||
if len(endpoint)+1 > int(max_path_size) {
|
||||
log.Warn(fmt.Sprintf("The ipc endpoint is longer than %d characters. ", max_path_size-1),
|
||||
"endpoint", endpoint)
|
||||
}
|
||||
|
||||
|
||||
+26
-15
@@ -58,21 +58,25 @@ type jsonrpcMessage struct {
|
||||
}
|
||||
|
||||
func (msg *jsonrpcMessage) isNotification() bool {
|
||||
return msg.ID == nil && msg.Method != ""
|
||||
return msg.hasValidVersion() && msg.ID == nil && msg.Method != ""
|
||||
}
|
||||
|
||||
func (msg *jsonrpcMessage) isCall() bool {
|
||||
return msg.hasValidID() && msg.Method != ""
|
||||
return msg.hasValidVersion() && msg.hasValidID() && msg.Method != ""
|
||||
}
|
||||
|
||||
func (msg *jsonrpcMessage) isResponse() bool {
|
||||
return msg.hasValidID() && msg.Method == "" && msg.Params == nil && (msg.Result != nil || msg.Error != nil)
|
||||
return msg.hasValidVersion() && msg.hasValidID() && msg.Method == "" && msg.Params == nil && (msg.Result != nil || msg.Error != nil)
|
||||
}
|
||||
|
||||
func (msg *jsonrpcMessage) hasValidID() bool {
|
||||
return len(msg.ID) > 0 && msg.ID[0] != '{' && msg.ID[0] != '['
|
||||
}
|
||||
|
||||
func (msg *jsonrpcMessage) hasValidVersion() bool {
|
||||
return msg.Version == vsn
|
||||
}
|
||||
|
||||
func (msg *jsonrpcMessage) isSubscribe() bool {
|
||||
return strings.HasSuffix(msg.Method, subscribeMethodSuffix)
|
||||
}
|
||||
@@ -100,15 +104,14 @@ func (msg *jsonrpcMessage) errorResponse(err error) *jsonrpcMessage {
|
||||
func (msg *jsonrpcMessage) response(result interface{}) *jsonrpcMessage {
|
||||
enc, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
// TODO: wrap with 'internal server error'
|
||||
return msg.errorResponse(err)
|
||||
return msg.errorResponse(&internalServerError{errcodeMarshalError, err.Error()})
|
||||
}
|
||||
return &jsonrpcMessage{Version: vsn, ID: msg.ID, Result: enc}
|
||||
}
|
||||
|
||||
func errorMessage(err error) *jsonrpcMessage {
|
||||
msg := &jsonrpcMessage{Version: vsn, ID: null, Error: &jsonError{
|
||||
Code: defaultErrorCode,
|
||||
Code: errcodeDefault,
|
||||
Message: err.Error(),
|
||||
}}
|
||||
ec, ok := err.(Error)
|
||||
@@ -165,18 +168,22 @@ type ConnRemoteAddr interface {
|
||||
// support for parsing arguments and serializing (result) objects.
|
||||
type jsonCodec struct {
|
||||
remote string
|
||||
closer sync.Once // close closed channel once
|
||||
closeCh chan interface{} // closed on Close
|
||||
decode func(v interface{}) error // decoder to allow multiple transports
|
||||
encMu sync.Mutex // guards the encoder
|
||||
encode func(v interface{}) error // encoder to allow multiple transports
|
||||
closer sync.Once // close closed channel once
|
||||
closeCh chan interface{} // closed on Close
|
||||
decode decodeFunc // decoder to allow multiple transports
|
||||
encMu sync.Mutex // guards the encoder
|
||||
encode encodeFunc // encoder to allow multiple transports
|
||||
conn deadlineCloser
|
||||
}
|
||||
|
||||
type encodeFunc = func(v interface{}, isErrorResponse bool) error
|
||||
|
||||
type decodeFunc = func(v interface{}) error
|
||||
|
||||
// NewFuncCodec creates a codec which uses the given functions to read and write. If conn
|
||||
// implements ConnRemoteAddr, log messages will use it to include the remote address of
|
||||
// the connection.
|
||||
func NewFuncCodec(conn deadlineCloser, encode, decode func(v interface{}) error) ServerCodec {
|
||||
func NewFuncCodec(conn deadlineCloser, encode encodeFunc, decode decodeFunc) ServerCodec {
|
||||
codec := &jsonCodec{
|
||||
closeCh: make(chan interface{}),
|
||||
encode: encode,
|
||||
@@ -195,7 +202,11 @@ func NewCodec(conn Conn) ServerCodec {
|
||||
enc := json.NewEncoder(conn)
|
||||
dec := json.NewDecoder(conn)
|
||||
dec.UseNumber()
|
||||
return NewFuncCodec(conn, enc.Encode, dec.Decode)
|
||||
|
||||
encode := func(v interface{}, isErrorResponse bool) error {
|
||||
return enc.Encode(v)
|
||||
}
|
||||
return NewFuncCodec(conn, encode, dec.Decode)
|
||||
}
|
||||
|
||||
func (c *jsonCodec) peerInfo() PeerInfo {
|
||||
@@ -225,7 +236,7 @@ func (c *jsonCodec) readBatch() (messages []*jsonrpcMessage, batch bool, err err
|
||||
return messages, batch, nil
|
||||
}
|
||||
|
||||
func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}) error {
|
||||
func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}, isErrorResponse bool) error {
|
||||
c.encMu.Lock()
|
||||
defer c.encMu.Unlock()
|
||||
|
||||
@@ -234,7 +245,7 @@ func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}) error {
|
||||
deadline = time.Now().Add(defaultWriteTimeout)
|
||||
}
|
||||
c.conn.SetWriteDeadline(deadline)
|
||||
return c.encode(v)
|
||||
return c.encode(v, isErrorResponse)
|
||||
}
|
||||
|
||||
func (c *jsonCodec) close() {
|
||||
|
||||
+38
-15
@@ -19,9 +19,9 @@ package rpc
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
mapset "github.com/deckarep/golang-set"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
@@ -45,13 +45,19 @@ const (
|
||||
type Server struct {
|
||||
services serviceRegistry
|
||||
idgen func() ID
|
||||
run int32
|
||||
codecs mapset.Set
|
||||
|
||||
mutex sync.Mutex
|
||||
codecs map[ServerCodec]struct{}
|
||||
run int32
|
||||
}
|
||||
|
||||
// NewServer creates a new server instance with no registered handlers.
|
||||
func NewServer() *Server {
|
||||
server := &Server{idgen: randomIDGenerator(), codecs: mapset.NewSet(), run: 1}
|
||||
server := &Server{
|
||||
idgen: randomIDGenerator(),
|
||||
codecs: make(map[ServerCodec]struct{}),
|
||||
run: 1,
|
||||
}
|
||||
// Register the default service providing meta information about the RPC service such
|
||||
// as the services and methods it offers.
|
||||
rpcService := &RPCService{server}
|
||||
@@ -75,20 +81,34 @@ func (s *Server) RegisterName(name string, receiver interface{}) error {
|
||||
func (s *Server) ServeCodec(codec ServerCodec, options CodecOption) {
|
||||
defer codec.close()
|
||||
|
||||
// Don't serve if server is stopped.
|
||||
if atomic.LoadInt32(&s.run) == 0 {
|
||||
if !s.trackCodec(codec) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add the codec to the set so it can be closed by Stop.
|
||||
s.codecs.Add(codec)
|
||||
defer s.codecs.Remove(codec)
|
||||
defer s.untrackCodec(codec)
|
||||
|
||||
c := initClient(codec, s.idgen, &s.services)
|
||||
<-codec.closed()
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func (s *Server) trackCodec(codec ServerCodec) bool {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if atomic.LoadInt32(&s.run) == 0 {
|
||||
return false // Don't serve if server is stopped.
|
||||
}
|
||||
s.codecs[codec] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) untrackCodec(codec ServerCodec) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
delete(s.codecs, codec)
|
||||
}
|
||||
|
||||
// serveSingleRequest reads and processes a single RPC request from the given codec. This
|
||||
// is used to serve HTTP connections. Subscriptions and reverse calls are not allowed in
|
||||
// this mode.
|
||||
@@ -105,7 +125,8 @@ func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
|
||||
reqs, batch, err := codec.readBatch()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
codec.writeJSON(ctx, errorMessage(&invalidMessageError{"parse error"}))
|
||||
resp := errorMessage(&invalidMessageError{"parse error"})
|
||||
codec.writeJSON(ctx, resp, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -120,12 +141,14 @@ func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
|
||||
// requests to finish, then closes all codecs which will cancel pending requests and
|
||||
// subscriptions.
|
||||
func (s *Server) Stop() {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if atomic.CompareAndSwapInt32(&s.run, 1, 0) {
|
||||
log.Debug("RPC server shutting down")
|
||||
s.codecs.Each(func(c interface{}) bool {
|
||||
c.(ServerCodec).close()
|
||||
return true
|
||||
})
|
||||
for codec := range s.codecs {
|
||||
codec.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ func TestServerRegisterName(t *testing.T) {
|
||||
t.Fatalf("Expected service calc to be registered")
|
||||
}
|
||||
|
||||
wantCallbacks := 10
|
||||
wantCallbacks := 12
|
||||
if len(svc.callbacks) != wantCallbacks {
|
||||
t.Errorf("Expected %d callbacks for service 'service', got %d", wantCallbacks, len(svc.callbacks))
|
||||
}
|
||||
|
||||
+2
-6
@@ -18,7 +18,6 @@ package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@@ -64,9 +63,6 @@ func (r *serviceRegistry) registerName(name string, rcvr interface{}) error {
|
||||
return fmt.Errorf("no service name for type %s", rcvrVal.Type().String())
|
||||
}
|
||||
callbacks := suitableCallbacks(rcvrVal)
|
||||
// begin PluGeth code injection
|
||||
pluginExtendedCallbacks(callbacks, rcvrVal)
|
||||
// end PluGeth code injection
|
||||
if len(callbacks) == 0 {
|
||||
return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr)
|
||||
}
|
||||
@@ -201,8 +197,8 @@ func (c *callback) call(ctx context.Context, method string, args []reflect.Value
|
||||
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), "args", fullargs)
|
||||
errRes = errors.New("method handler crashed")
|
||||
log.Error("RPC method " + method + " crashed: " + fmt.Sprintf("%v\n%s", err, buf))
|
||||
errRes = &internalServerError{errcodePanic, "method handler crashed"}
|
||||
}
|
||||
}()
|
||||
// Run the callback.
|
||||
|
||||
+6
-2
@@ -32,12 +32,16 @@ func DialStdIO(ctx context.Context) (*Client, error) {
|
||||
|
||||
// DialIO creates a client which uses the given IO channels
|
||||
func DialIO(ctx context.Context, in io.Reader, out io.Writer) (*Client, error) {
|
||||
return newClient(ctx, func(_ context.Context) (ServerCodec, error) {
|
||||
return newClient(ctx, newClientTransportIO(in, out))
|
||||
}
|
||||
|
||||
func newClientTransportIO(in io.Reader, out io.Writer) reconnectFunc {
|
||||
return func(context.Context) (ServerCodec, error) {
|
||||
return NewCodec(stdioConn{
|
||||
in: in,
|
||||
out: out,
|
||||
}), nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stdioConn struct {
|
||||
|
||||
+4
-2
@@ -175,11 +175,13 @@ func (n *Notifier) activate() error {
|
||||
func (n *Notifier) send(sub *Subscription, data json.RawMessage) error {
|
||||
params, _ := json.Marshal(&subscriptionResult{ID: string(sub.ID), Result: data})
|
||||
ctx := context.Background()
|
||||
return n.h.conn.writeJSON(ctx, &jsonrpcMessage{
|
||||
|
||||
msg := &jsonrpcMessage{
|
||||
Version: vsn,
|
||||
Method: n.namespace + notificationMethodSuffix,
|
||||
Params: params,
|
||||
})
|
||||
}
|
||||
return n.h.conn.writeJSON(ctx, msg, false)
|
||||
}
|
||||
|
||||
// A Subscription is created by a notifier and tied to that notifier. The client can use
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestSubscriptions(t *testing.T) {
|
||||
request := map[string]interface{}{
|
||||
"id": i,
|
||||
"method": fmt.Sprintf("%s_subscribe", namespace),
|
||||
"version": "2.0",
|
||||
"jsonrpc": "2.0",
|
||||
"params": []interface{}{"someSubscription", notificationCount, i},
|
||||
}
|
||||
if err := out.Encode(&request); err != nil {
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
// These tests trigger various 'internal error' conditions.
|
||||
|
||||
--> {"jsonrpc":"2.0","id":1,"method":"test_marshalError","params": []}
|
||||
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"json: error calling MarshalText for type *rpc.MarshalErrObj: marshal error"}}
|
||||
|
||||
--> {"jsonrpc":"2.0","id":2,"method":"test_panic","params": []}
|
||||
<-- {"jsonrpc":"2.0","id":2,"error":{"code":-32603,"message":"method handler crashed"}}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
// This test checks processing of messages with invalid Version.
|
||||
|
||||
--> {"jsonrpc":"2.0","id":1,"method":"test_echo","params":["x", 3]}
|
||||
<-- {"jsonrpc":"2.0","id":1,"result":{"String":"x","Int":3,"Args":null}}
|
||||
|
||||
--> {"jsonrpc":"2.1","id":1,"method":"test_echo","params":["x", 3]}
|
||||
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
|
||||
|
||||
--> {"jsonrpc":"go-ethereum","id":1,"method":"test_echo","params":["x", 3]}
|
||||
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
|
||||
|
||||
--> {"jsonrpc":1,"id":1,"method":"test_echo","params":["x", 3]}
|
||||
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
|
||||
|
||||
--> {"jsonrpc":2.0,"id":1,"method":"test_echo","params":["x", 3]}
|
||||
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
|
||||
|
||||
--> {"id":1,"method":"test_echo","params":["x", 3]}
|
||||
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
|
||||
@@ -70,6 +70,12 @@ func (testError) Error() string { return "testError" }
|
||||
func (testError) ErrorCode() int { return 444 }
|
||||
func (testError) ErrorData() interface{} { return "testError data" }
|
||||
|
||||
type MarshalErrObj struct{}
|
||||
|
||||
func (o *MarshalErrObj) MarshalText() ([]byte, error) {
|
||||
return nil, errors.New("marshal error")
|
||||
}
|
||||
|
||||
func (s *testService) NoArgsRets() {}
|
||||
|
||||
func (s *testService) Echo(str string, i int, args *echoArgs) echoResult {
|
||||
@@ -114,6 +120,14 @@ func (s *testService) ReturnError() error {
|
||||
return testError{}
|
||||
}
|
||||
|
||||
func (s *testService) MarshalError() *MarshalErrObj {
|
||||
return &MarshalErrObj{}
|
||||
}
|
||||
|
||||
func (s *testService) Panic() string {
|
||||
panic("service panic")
|
||||
}
|
||||
|
||||
func (s *testService) CallMeBack(ctx context.Context, method string, args []interface{}) (interface{}, error) {
|
||||
c, ok := ClientFromContext(ctx)
|
||||
if !ok {
|
||||
|
||||
+5
-24
@@ -51,7 +51,9 @@ type ServerCodec interface {
|
||||
// jsonWriter can write JSON messages to its underlying connection.
|
||||
// Implementations must be safe for concurrent use.
|
||||
type jsonWriter interface {
|
||||
writeJSON(context.Context, interface{}) error
|
||||
// writeJSON writes a message to the connection.
|
||||
writeJSON(ctx context.Context, msg interface{}, isError bool) error
|
||||
|
||||
// Closed returns a channel which is closed when the connection is closed.
|
||||
closed() <-chan interface{}
|
||||
// RemoteAddr returns the peer address of the connection.
|
||||
@@ -69,7 +71,7 @@ const (
|
||||
)
|
||||
|
||||
// UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports:
|
||||
// - "latest", "earliest" or "pending" as string arguments
|
||||
// - "safe", "finalized", "latest", "earliest" or "pending" as string arguments
|
||||
// - the block number
|
||||
// Returned errors:
|
||||
// - an invalid block number error when the given argument isn't a known strings
|
||||
@@ -110,7 +112,7 @@ func (bn *BlockNumber) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler. It marshals:
|
||||
// - "latest", "earliest" or "pending" as strings
|
||||
// - "safe", "finalized", "latest", "earliest" or "pending" as strings
|
||||
// - other numbers as hex
|
||||
func (bn BlockNumber) MarshalText() ([]byte, error) {
|
||||
switch bn {
|
||||
@@ -241,24 +243,3 @@ func BlockNumberOrHashWithHash(hash common.Hash, canonical bool) BlockNumberOrHa
|
||||
RequireCanonical: canonical,
|
||||
}
|
||||
}
|
||||
|
||||
// DecimalOrHex unmarshals a non-negative decimal or hex parameter into a uint64.
|
||||
type DecimalOrHex uint64
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (dh *DecimalOrHex) UnmarshalJSON(data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
|
||||
input = input[1 : len(input)-1]
|
||||
}
|
||||
|
||||
value, err := strconv.ParseUint(input, 10, 64)
|
||||
if err != nil {
|
||||
value, err = hexutil.DecodeUint64(input)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*dh = DecimalOrHex(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
+71
-27
@@ -27,7 +27,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
mapset "github.com/deckarep/golang-set"
|
||||
mapset "github.com/deckarep/golang-set/v2"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
const (
|
||||
wsReadBuffer = 1024
|
||||
wsWriteBuffer = 1024
|
||||
wsPingInterval = 60 * time.Second
|
||||
wsPingInterval = 30 * time.Second
|
||||
wsPingWriteTimeout = 5 * time.Second
|
||||
wsPongTimeout = 30 * time.Second
|
||||
wsMessageSizeLimit = 15 * 1024 * 1024
|
||||
@@ -69,7 +69,7 @@ func (s *Server) WebsocketHandler(allowedOrigins []string) http.Handler {
|
||||
// websocket upgrade process. When a '*' is specified as an allowed origins all
|
||||
// connections are accepted.
|
||||
func wsHandshakeValidator(allowedOrigins []string) func(*http.Request) bool {
|
||||
origins := mapset.NewSet()
|
||||
origins := mapset.NewSet[string]()
|
||||
allowAllOrigins := false
|
||||
|
||||
for _, origin := range allowedOrigins {
|
||||
@@ -122,10 +122,10 @@ func (e wsHandshakeError) Error() string {
|
||||
return s
|
||||
}
|
||||
|
||||
func originIsAllowed(allowedOrigins mapset.Set, browserOrigin string) bool {
|
||||
func originIsAllowed(allowedOrigins mapset.Set[string], browserOrigin string) bool {
|
||||
it := allowedOrigins.Iterator()
|
||||
for origin := range it.C {
|
||||
if ruleAllowsOrigin(origin.(string), browserOrigin) {
|
||||
if ruleAllowsOrigin(origin, browserOrigin) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -181,24 +181,23 @@ func parseOriginURL(origin string) (string, string, string, error) {
|
||||
return scheme, hostname, port, nil
|
||||
}
|
||||
|
||||
// DialWebsocketWithDialer creates a new RPC client that communicates with a JSON-RPC server
|
||||
// that is listening on the given endpoint using the provided dialer.
|
||||
// DialWebsocketWithDialer creates a new RPC client using WebSocket.
|
||||
//
|
||||
// The context is used for the initial connection establishment. It does not
|
||||
// affect subsequent interactions with the client.
|
||||
//
|
||||
// Deprecated: use DialOptions and the WithWebsocketDialer option.
|
||||
func DialWebsocketWithDialer(ctx context.Context, endpoint, origin string, dialer websocket.Dialer) (*Client, error) {
|
||||
endpoint, header, err := wsClientHeaders(endpoint, origin)
|
||||
cfg := new(clientConfig)
|
||||
cfg.wsDialer = &dialer
|
||||
if origin != "" {
|
||||
cfg.setHeader("origin", origin)
|
||||
}
|
||||
connect, err := newClientTransportWS(endpoint, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
|
||||
conn, resp, err := dialer.DialContext(ctx, endpoint, header)
|
||||
if err != nil {
|
||||
hErr := wsHandshakeError{err: err}
|
||||
if resp != nil {
|
||||
hErr.status = resp.Status
|
||||
}
|
||||
return nil, hErr
|
||||
}
|
||||
return newWebsocketCodec(conn, endpoint, header), nil
|
||||
})
|
||||
return newClient(ctx, connect)
|
||||
}
|
||||
|
||||
// DialWebsocket creates a new RPC client that communicates with a JSON-RPC server
|
||||
@@ -207,12 +206,53 @@ func DialWebsocketWithDialer(ctx context.Context, endpoint, origin string, diale
|
||||
// The context is used for the initial connection establishment. It does not
|
||||
// affect subsequent interactions with the client.
|
||||
func DialWebsocket(ctx context.Context, endpoint, origin string) (*Client, error) {
|
||||
dialer := websocket.Dialer{
|
||||
ReadBufferSize: wsReadBuffer,
|
||||
WriteBufferSize: wsWriteBuffer,
|
||||
WriteBufferPool: wsBufferPool,
|
||||
cfg := new(clientConfig)
|
||||
if origin != "" {
|
||||
cfg.setHeader("origin", origin)
|
||||
}
|
||||
return DialWebsocketWithDialer(ctx, endpoint, origin, dialer)
|
||||
connect, err := newClientTransportWS(endpoint, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newClient(ctx, connect)
|
||||
}
|
||||
|
||||
func newClientTransportWS(endpoint string, cfg *clientConfig) (reconnectFunc, error) {
|
||||
dialer := cfg.wsDialer
|
||||
if dialer == nil {
|
||||
dialer = &websocket.Dialer{
|
||||
ReadBufferSize: wsReadBuffer,
|
||||
WriteBufferSize: wsWriteBuffer,
|
||||
WriteBufferPool: wsBufferPool,
|
||||
}
|
||||
}
|
||||
|
||||
dialURL, header, err := wsClientHeaders(endpoint, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, values := range cfg.httpHeaders {
|
||||
header[key] = values
|
||||
}
|
||||
|
||||
connect := func(ctx context.Context) (ServerCodec, error) {
|
||||
header := header.Clone()
|
||||
if cfg.httpAuth != nil {
|
||||
if err := cfg.httpAuth(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
conn, resp, err := dialer.DialContext(ctx, dialURL, header)
|
||||
if err != nil {
|
||||
hErr := wsHandshakeError{err: err}
|
||||
if resp != nil {
|
||||
hErr.status = resp.Status
|
||||
}
|
||||
return nil, hErr
|
||||
}
|
||||
return newWebsocketCodec(conn, dialURL, header), nil
|
||||
}
|
||||
return connect, nil
|
||||
}
|
||||
|
||||
func wsClientHeaders(endpoint, origin string) (string, http.Header, error) {
|
||||
@@ -247,8 +287,12 @@ func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header) Serve
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
return nil
|
||||
})
|
||||
|
||||
encode := func(v interface{}, isErrorResponse bool) error {
|
||||
return conn.WriteJSON(v)
|
||||
}
|
||||
wc := &websocketCodec{
|
||||
jsonCodec: NewFuncCodec(conn, conn.WriteJSON, conn.ReadJSON).(*jsonCodec),
|
||||
jsonCodec: NewFuncCodec(conn, encode, conn.ReadJSON).(*jsonCodec),
|
||||
conn: conn,
|
||||
pingReset: make(chan struct{}, 1),
|
||||
info: PeerInfo{
|
||||
@@ -275,8 +319,8 @@ func (wc *websocketCodec) peerInfo() PeerInfo {
|
||||
return wc.info
|
||||
}
|
||||
|
||||
func (wc *websocketCodec) writeJSON(ctx context.Context, v interface{}) error {
|
||||
err := wc.jsonCodec.writeJSON(ctx, v)
|
||||
func (wc *websocketCodec) writeJSON(ctx context.Context, v interface{}, isError bool) error {
|
||||
err := wc.jsonCodec.writeJSON(ctx, v, isError)
|
||||
if err == nil {
|
||||
// Notify pingLoop to delay the next idle ping.
|
||||
select {
|
||||
|
||||
@@ -19,14 +19,10 @@ package rpc
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -227,63 +223,6 @@ func TestClientWebsocketLargeMessage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientWebsocketSevered(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
server = wsPingTestServer(t, nil)
|
||||
ctx = context.Background()
|
||||
)
|
||||
defer server.Shutdown(ctx)
|
||||
|
||||
u, err := url.Parse("http://" + server.Addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rproxy := httputil.NewSingleHostReverseProxy(u)
|
||||
var severable *severableReadWriteCloser
|
||||
rproxy.ModifyResponse = func(response *http.Response) error {
|
||||
severable = &severableReadWriteCloser{ReadWriteCloser: response.Body.(io.ReadWriteCloser)}
|
||||
response.Body = severable
|
||||
return nil
|
||||
}
|
||||
frontendProxy := httptest.NewServer(rproxy)
|
||||
defer frontendProxy.Close()
|
||||
|
||||
wsURL := "ws:" + strings.TrimPrefix(frontendProxy.URL, "http:")
|
||||
client, err := DialWebsocket(ctx, wsURL, "")
|
||||
if err != nil {
|
||||
t.Fatalf("client dial error: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
resultChan := make(chan int)
|
||||
sub, err := client.EthSubscribe(ctx, resultChan, "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("client subscribe error: %v", err)
|
||||
}
|
||||
|
||||
// sever the connection
|
||||
severable.Sever()
|
||||
|
||||
// Wait for subscription error.
|
||||
timeout := time.NewTimer(3 * wsPingInterval)
|
||||
defer timeout.Stop()
|
||||
for {
|
||||
select {
|
||||
case err := <-sub.Err():
|
||||
t.Log("client subscription error:", err)
|
||||
return
|
||||
case result := <-resultChan:
|
||||
t.Error("unexpected result:", result)
|
||||
return
|
||||
case <-timeout.C:
|
||||
t.Error("didn't get any error within the test timeout")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wsPingTestServer runs a WebSocket server which accepts a single subscription request.
|
||||
// When a value arrives on sendPing, the server sends a ping frame, waits for a matching
|
||||
// pong and finally delivers a single subscription result.
|
||||
@@ -386,31 +325,3 @@ func wsPingTestHandler(t *testing.T, conn *websocket.Conn, shutdown, sendPing <-
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// severableReadWriteCloser wraps an io.ReadWriteCloser and provides a Sever() method to drop writes and read empty.
|
||||
type severableReadWriteCloser struct {
|
||||
io.ReadWriteCloser
|
||||
severed int32 // atomic
|
||||
}
|
||||
|
||||
func (s *severableReadWriteCloser) Sever() {
|
||||
atomic.StoreInt32(&s.severed, 1)
|
||||
}
|
||||
|
||||
func (s *severableReadWriteCloser) Read(p []byte) (n int, err error) {
|
||||
if atomic.LoadInt32(&s.severed) > 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return s.ReadWriteCloser.Read(p)
|
||||
}
|
||||
|
||||
func (s *severableReadWriteCloser) Write(p []byte) (n int, err error) {
|
||||
if atomic.LoadInt32(&s.severed) > 0 {
|
||||
return len(p), nil
|
||||
}
|
||||
return s.ReadWriteCloser.Write(p)
|
||||
}
|
||||
|
||||
func (s *severableReadWriteCloser) Close() error {
|
||||
return s.ReadWriteCloser.Close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user